CracImpl.java

/*
 * Copyright (c) 2019, RTE (http://www.rte-france.com)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

package com.powsybl.openrao.data.crac.impl;

import com.powsybl.contingency.Contingency;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.PhysicalParameter;
import com.powsybl.openrao.data.crac.api.ContingencyAdder;
import com.powsybl.openrao.data.crac.api.Crac;
import com.powsybl.openrao.data.crac.api.Identifiable;
import com.powsybl.openrao.data.crac.api.Instant;
import com.powsybl.openrao.data.crac.api.InstantKind;
import com.powsybl.openrao.data.crac.api.NetworkElement;
import com.powsybl.openrao.data.crac.api.RaUsageLimits;
import com.powsybl.openrao.data.crac.api.RaUsageLimitsAdder;
import com.powsybl.openrao.data.crac.api.RemedialAction;
import com.powsybl.openrao.data.crac.api.State;
import com.powsybl.openrao.data.crac.api.cnec.AngleCnec;
import com.powsybl.openrao.data.crac.api.cnec.AngleCnecAdder;
import com.powsybl.openrao.data.crac.api.cnec.BranchCnec;
import com.powsybl.openrao.data.crac.api.cnec.Cnec;
import com.powsybl.openrao.data.crac.api.cnec.FlowCnec;
import com.powsybl.openrao.data.crac.api.cnec.FlowCnecAdder;
import com.powsybl.openrao.data.crac.api.cnec.VoltageCnec;
import com.powsybl.openrao.data.crac.api.cnec.VoltageCnecAdder;
import com.powsybl.openrao.data.crac.api.rangeaction.CounterTradeRangeAction;
import com.powsybl.openrao.data.crac.api.rangeaction.CounterTradeRangeActionAdder;
import com.powsybl.openrao.data.crac.api.rangeaction.HvdcRangeAction;
import com.powsybl.openrao.data.crac.api.rangeaction.HvdcRangeActionAdder;
import com.powsybl.openrao.data.crac.api.rangeaction.InjectionRangeAction;
import com.powsybl.openrao.data.crac.api.rangeaction.InjectionRangeActionAdder;
import com.powsybl.openrao.data.crac.api.rangeaction.PstRangeAction;
import com.powsybl.openrao.data.crac.api.rangeaction.PstRangeActionAdder;
import com.powsybl.openrao.data.crac.api.rangeaction.RangeAction;
import com.powsybl.openrao.data.crac.api.networkaction.NetworkAction;
import com.powsybl.openrao.data.crac.api.networkaction.NetworkActionAdder;
import com.powsybl.openrao.data.crac.api.usagerule.OnContingencyState;
import com.powsybl.openrao.data.crac.api.usagerule.UsageMethod;

import java.time.OffsetDateTime;
import java.util.*;
import java.util.stream.Collectors;

import static java.lang.String.format;

/**
 * Business object of the CRAC file.
 *
 * @author Viktor Terrier {@literal <viktor.terrier at rte-france.com>}
 */
public class CracImpl extends AbstractIdentifiable<Crac> implements Crac {
    private static final String ADD_ELEMENT_TO_CRAC_ERROR_MESSAGE = "Please add %s to crac first.";
    private static final String SAME_ELEMENT_ID_DIFFERENT_NAME_ERROR_MESSAGE = "A network element with the same ID (%s) but a different name already exists.";

    private final Map<String, NetworkElement> networkElements = new HashMap<>();
    private final Map<String, Contingency> contingencies = new HashMap<>();
    private final Map<String, Instant> instants = new HashMap<>();
    private final Map<String, State> states = new HashMap<>();
    private final Map<String, FlowCnec> flowCnecs = new HashMap<>();
    private final Map<String, AngleCnec> angleCnecs = new HashMap<>();
    private final Map<String, VoltageCnec> voltageCnecs = new HashMap<>();
    private final Map<String, PstRangeAction> pstRangeActions = new HashMap<>();
    private final Map<String, HvdcRangeAction> hvdcRangeActions = new HashMap<>();
    private final Map<String, InjectionRangeAction> injectionRangeActions = new HashMap<>();
    private final Map<String, CounterTradeRangeAction> counterTradeRangeActions = new HashMap<>();
    private final Map<String, NetworkAction> networkActions = new HashMap<>();
    private final Map<Instant, RaUsageLimits> raUsageLimitsPerInstant = new HashMap<>();
    private Instant lastInstantAdded = null;
    private final OffsetDateTime timestamp;

    public CracImpl(String id, String name, OffsetDateTime timestamp) {
        super(id, name);
        this.timestamp = timestamp;
    }

    public CracImpl(String id, String name) {
        this(id, name, null);
    }

    public CracImpl(String id, OffsetDateTime timestamp) {
        this(id, id, timestamp);
    }

    public CracImpl(String id) {
        this(id, id, null);
    }

    public Optional<OffsetDateTime> getTimestamp() {
        return Optional.ofNullable(timestamp);
    }

    // ========================================
    // region NetworkElements management
    // ========================================

    Set<NetworkElement> getNetworkElements() {
        return new HashSet<>(networkElements.values());
    }

    NetworkElement getNetworkElement(String id) {
        return networkElements.getOrDefault(id, null);
    }

    /**
     * Removes NetworkElement objects from the Crac, if they are not used within other objects of the Crac.
     * Only NetworkElement objects that are not referenced are removed.
     * @param networkElementIds: IDs of the network elements to remove
     */
    void safeRemoveNetworkElements(Set<String> networkElementIds) {
        networkElementIds.stream()
                .filter(networkElementId -> !isNetworkElementUsedWithinCrac(networkElementId))
                .forEach(networkElements::remove);
    }

    /**
     * Check if a NetworkElement is referenced in the CRAC (ie in a Contingency, a Cnec or a RemedialAction)
     * @param networkElementId: ID of the NetworkElement
     * @return true if the NetworkElement is referenced in a Contingency, a Cnec or a RemedialAction
     */
    private boolean isNetworkElementUsedWithinCrac(String networkElementId) {
        return getCnecs().stream()
                .map(Cnec::getNetworkElements)
                .flatMap(Set::stream)
                .anyMatch(ne -> ((NetworkElement) ne).getId().equals(networkElementId))
            || getRemedialActions().stream()
                .map(RemedialAction::getNetworkElements)
                .flatMap(Set::stream)
                .anyMatch(ne -> ne.getId().equals(networkElementId));
    }

    /**
     * This method add a network element to the crac internal set and returns a network element of this set.
     * If an element with the same data is already added, the element of the internal set will be returned,
     * otherwise it is created and then returned. An error is thrown when an element with an already
     * existing ID is added with a different name.
     *
     * @param networkElementId: network element ID as in network files
     * @param networkElementName: network element name for more human readable name
     * @return a network element object that is already defined in the crac
     */
    NetworkElement addNetworkElement(String networkElementId, String networkElementName) {
        String name = (networkElementName != null) ? networkElementName : networkElementId;
        NetworkElement cracNetworkElement = getNetworkElement(networkElementId);
        if (cracNetworkElement == null) {
            cracNetworkElement = new NetworkElementImpl(networkElementId, name);
        } else if (!cracNetworkElement.getName().equals(name)) {
            throw new OpenRaoException(format(SAME_ELEMENT_ID_DIFFERENT_NAME_ERROR_MESSAGE, networkElementId));
        }
        networkElements.put(networkElementId, cracNetworkElement);
        return cracNetworkElement;
    }

    //endregion
    // ========================================
    // region Contingencies management
    // ========================================

    @Override
    public ContingencyAdder newContingency() {
        return new ContingencyAdderImpl(this);
    }

    @Override
    public Set<Contingency> getContingencies() {
        return new HashSet<>(contingencies.values());
    }

    @Override
    public Contingency getContingency(String id) {
        return contingencies.get(id);
    }

    @Override
    public void removeContingency(String id) {
        if (isContingencyUsedWithinCrac(id)) {
            throw new OpenRaoException(format("Contingency %s is used within a CNEC or an OnContingencyState UsageRule. Please remove all references to the contingency first.", id));
        } else {
            Contingency contingency = contingencies.get(id);
            if (contingency != null) {
                contingencies.remove(id);
                safeRemoveStates(getStates(contingency).stream().map(State::getId).collect(Collectors.toSet()));
            }
        }
    }

    @Override
    public CracImpl newInstant(String instantId, InstantKind instantKind) {
        if (instants.containsKey(instantId)) {
            throw new OpenRaoException(format("Instant '%s' is already defined", instantId));
        }
        InstantImpl instant = new InstantImpl(instantId, instantKind, lastInstantAdded);
        if (instant.getOrder() == 0 && !instant.isPreventive()) {
            throw new OpenRaoException("The first instant in the CRAC must be preventive");
        }
        if (instant.getOrder() == 1 && !instant.isOutage()) {
            throw new OpenRaoException("The second instant in the CRAC must be an outage");
        }
        lastInstantAdded = instant;
        instants.put(instantId, instant);
        return this;
    }

    @Override
    public Instant getInstant(String instantId) {
        if (!instants.containsKey(instantId)) {
            throw new OpenRaoException(String.format("Instant '%s' has not been defined", instantId));
        }
        return instants.get(instantId);
    }

    @Override
    public List<Instant> getSortedInstants() {
        return instants.values().stream().sorted(Comparator.comparingInt(Instant::getOrder)).toList();
    }

    @Override
    public Instant getInstant(InstantKind instantKind) {
        Set<Instant> instantsOfKind = getInstants(instantKind);
        if (instantsOfKind.size() != 1) {
            throw new OpenRaoException(String.format("Crac does not contain exactly one instant of kind '%s'. It contains %d instants of kind '%s'", instantKind.toString(), instantsOfKind.size(), instantKind));
        }
        return instantsOfKind.stream().findAny().orElseThrow(
            () -> new OpenRaoException(String.format("Should not occur as there is only one '%s' instant", instantKind))
        );
    }

    @Override
    public SortedSet<Instant> getInstants(InstantKind instantKind) {
        SortedSet<Instant> sortedInstants = new TreeSet<>();
        instants.values().stream().filter(instant -> instant.getKind().equals(instantKind)).forEach(sortedInstants::add);
        return sortedInstants;
    }

    @Override
    public Instant getInstantBefore(Instant providedInstant) {
        Objects.requireNonNull(providedInstant);
        checkCracContainsProvidedInstantId(providedInstant);
        checkCracInstantAndProvidedInstantAreTheSame(providedInstant);

        if (providedInstant instanceof InstantImpl instantImpl) {
            return instantImpl.getInstantBefore();
        }
        throw new OpenRaoException("This should not happen thanks to the equality ckeck. " +
            "Method getInstantBefore might not have been defined as a package-private method " +
            "in the implementation of the Instant interface");
    }

    @Override
    public Instant getPreventiveInstant() {
        return getInstant(InstantKind.PREVENTIVE);
    }

    @Override
    public Instant getOutageInstant() {
        return getInstant(InstantKind.OUTAGE);
    }

    @Override
    public Instant getLastInstant() {
        return lastInstantAdded;
    }

    @Override
    public boolean hasAutoInstant() {
        return !getInstants(InstantKind.AUTO).isEmpty();
    }

    private void checkCracContainsProvidedInstantId(Instant providedInstant) {
        if (!instants.containsKey(providedInstant.getId())) {
            throw new OpenRaoException(String.format("Provided instant '%s' is not defined in the CRAC", providedInstant));
        }
    }

    private void checkCracInstantAndProvidedInstantAreTheSame(Instant providedInstant) {
        Instant instantInsideCracWithSameId = getInstant(providedInstant.getId());
        if (!Objects.equals(instantInsideCracWithSameId, providedInstant)) {
            throw new OpenRaoException(String.format(
                "Provided instant {id:'%s', kind:'%s', order:%d} is not the same {id: '%s', kind:'%s', order:%d} in the CRAC",
                providedInstant.getId(), providedInstant.getKind(), providedInstant.getOrder(),
                instantInsideCracWithSameId.getId(), instantInsideCracWithSameId.getKind(), instantInsideCracWithSameId.getOrder()));
        }
    }

    void addContingency(Contingency contingency) {
        contingencies.put(contingency.getId(), contingency);
    }

    /**
     * Check if a Contingency is referenced in the CRAC (ie in a Cnec or in a RemedialAction's UsageRule)
     * @param contingencyId: ID of the Contingency
     * @return true if the Contingency is referenced in a Cnec or in a RemedialAction's UsageRule
     */
    private boolean isContingencyUsedWithinCrac(String contingencyId) {
        return isContingencyUsedForCnecs(contingencyId) || isContingencyUsedForRemedialActions(contingencyId);
    }

    /**
     * Check if a Contingency is referenced in a Cnec
     * @param contingencyId: ID of the Contingency
     * @return true if the Contingency is referenced in a Cnec
     */
    private boolean isContingencyUsedForCnecs(String contingencyId) {
        return getCnecs().stream().anyMatch(cnec ->
            cnec.getState().getContingency().isPresent() && cnec.getState().getContingency().get().getId().equals(contingencyId));
    }

    /**
     * Check if a Contingency is referenced in a RemedialAction's UsageRule
     * @param contingencyId: ID of the Contingency
     * @return true if the Contingency is referenced in a RemedialAction's UsageRule
     */
    private boolean isContingencyUsedForRemedialActions(String contingencyId) {
        return getRemedialActions().stream().map(RemedialAction::getUsageRules).flatMap(Set::stream).anyMatch(usageRule ->
            usageRule instanceof OnContingencyState onContingencyState && onContingencyState.getContingency().getId().equals(contingencyId));
    }

    //endregion
    // ========================================
    // region States management
    // ========================================

    @Override
    public final Set<State> getStates() {
        return new HashSet<>(states.values());
    }

    @Override
    public State getPreventiveState() {
        return states.get(StateIdHelper.getStateId(getInstant(InstantKind.PREVENTIVE), timestamp));
    }

    @Override
    public Set<State> getCurativeStates() {
        return states.values().stream()
            .filter(state -> state.getInstant().isCurative())
            .collect(Collectors.toSet());
    }

    @Override
    public SortedSet<State> getStates(Contingency contingency) {
        Objects.requireNonNull(contingency, "Contingency must not be null when getting states.");
        return states.values().stream()
            .filter(state -> state.getContingency().isPresent() && state.getContingency().get().getId().equals(contingency.getId()))
            .collect(Collectors.toCollection(TreeSet::new));
    }

    @Override
    public Set<State> getStates(Instant instant) {
        return states.values().stream()
            .filter(state -> state.getInstant().equals(instant))
            .collect(Collectors.toSet());
    }

    @Override
    public State getState(Contingency contingency, Instant instant) {
        Objects.requireNonNull(contingency, "Contingency must not be null when getting a state.");
        return states.get(StateIdHelper.getStateId(contingency, instant, timestamp));
    }

    State addPreventiveState() {
        if (getPreventiveState() != null) {
            return getPreventiveState();
        } else {
            State state = new PreventiveState(getPreventiveInstant(), timestamp);
            states.put(state.getId(), state);
            return state;
        }
    }

    State addState(Contingency contingency, Instant instant) {
        Objects.requireNonNull(contingency, "Contingency must not be null when adding a state.");
        if (instant.isPreventive()) {
            throw new OpenRaoException("Impossible to add a preventive state with a contingency.");
        }
        if (getState(contingency, instant) != null) {
            return getState(contingency, instant);
        } else {
            if (!contingencies.containsKey(contingency.getId())) {
                throw new OpenRaoException(format(ADD_ELEMENT_TO_CRAC_ERROR_MESSAGE, contingency.getId()));
            }
            State state = new PostContingencyState(getContingency(contingency.getId()), instant, timestamp);
            states.put(state.getId(), state);
            return state;
        }
    }

    /**
     * Removes State objects from the Crac, if they are not used within other objects of the Crac
     * Only State objects that are not referenced are removed.
     * @param stateIds: IDs of the States to remove
     */
    void safeRemoveStates(Set<String> stateIds) {
        stateIds.stream()
                .filter(stateId -> !isStateUsedWithinCrac(stateId))
                .forEach(states::remove);
    }

    /**
     * Check if a State is referenced in the CRAC (ie in a Cnec or a RemedialAction's UsageRule)
     * @param stateId: ID of the State
     * @return true if the State is referenced in a Cnec or a RemedialAction's UsageRule
     */
    private boolean isStateUsedWithinCrac(String stateId) {
        return getCnecs().stream()
            .anyMatch(cnec -> cnec.getState().getId().equals(stateId))
                || getRemedialActions().stream()
                .map(RemedialAction::getUsageRules)
                .flatMap(Set::stream)
                .anyMatch(ur -> ur instanceof OnContingencyState onContingencyState && onContingencyState.getState().getId().equals(stateId));
    }

    //endregion
    // ========================================
    // region Cnec management
    // ========================================

    @Override
    public FlowCnecAdder newFlowCnec() {
        return new FlowCnecAdderImpl(this);
    }

    @Override
    public AngleCnecAdder newAngleCnec() {
        return new AngleCnecAdderImpl(this);
    }

    @Override
    public VoltageCnecAdder newVoltageCnec() {
        return new VoltageCnecAdderImpl(this);
    }

    @Override
    public Set<Cnec> getCnecs() {
        Set<Cnec> cnecs = new HashSet<>();
        cnecs.addAll(getFlowCnecs());
        cnecs.addAll(getAngleCnecs());
        cnecs.addAll(getVoltageCnecs());
        return cnecs;
    }

    @Override
    public Set<Cnec> getCnecs(State state) {
        Set<Cnec> cnecs = new HashSet<>();
        cnecs.addAll(getFlowCnecs(state));
        cnecs.addAll(getAngleCnecs(state));
        cnecs.addAll(getVoltageCnecs(state));
        return cnecs;
    }

    @Override
    public Set<Cnec> getCnecs(PhysicalParameter physicalParameter) {
        return switch (physicalParameter) {
            case ANGLE -> new HashSet<>(getAngleCnecs());
            case VOLTAGE -> new HashSet<>(getVoltageCnecs());
            case FLOW -> new HashSet<>(getFlowCnecs());
        };
    }

    @Override
    public Set<Cnec> getCnecs(PhysicalParameter physicalParameter, State state) {
        return switch (physicalParameter) {
            case ANGLE -> new HashSet<>(getAngleCnecs(state));
            case VOLTAGE -> new HashSet<>(getVoltageCnecs(state));
            case FLOW -> new HashSet<>(getFlowCnecs(state));
        };
    }

    @Override
    public Cnec getCnec(String cnecId) {
        if (flowCnecs.containsKey(cnecId)) {
            return getFlowCnec(cnecId);
        } else if (angleCnecs.containsKey(cnecId)) {
            return getAngleCnec(cnecId);
        } else if (voltageCnecs.containsKey(cnecId)) {
            return getVoltageCnec(cnecId);
        }
        return null;
    }

    /**
     * Find a BranchCnec by its id, returns null if the BranchCnec does not exists
     *
     * @deprecated consider using getCnec() or getFlowCnec() instead
     */
    @Override
    @Deprecated (since = "3.0.0")
    public BranchCnec getBranchCnec(String id) {
        return getFlowCnec(id);
    }

    /**
     * Gather all the BranchCnecs present in the Crac. It returns a set because Cnecs
     * must not be duplicated and there is no defined order for Cnecs.
     *
     * @deprecated consider using getCnecs() or getFlowCnecs() instead
     */
    @Override
    @Deprecated (since = "3.0.0")
    public Set<BranchCnec> getBranchCnecs() {
        return new HashSet<>(flowCnecs.values());
    }

    /**
     * Gather all the BranchCnecs of a specified State. It returns a set because Cnecs
     * must not be duplicated and there is no defined order for Cnecs.
     *
     * @deprecated consider using getCnecs() or getFlowCnecs() instead
     */
    @Override
    @Deprecated (since = "3.0.0")
    public Set<BranchCnec> getBranchCnecs(State state) {
        return new HashSet<>(getFlowCnecs(state));
    }

    @Override
    public FlowCnec getFlowCnec(String flowCnecId) {
        return flowCnecs.get(flowCnecId);
    }

    @Override
    public Set<FlowCnec> getFlowCnecs() {
        return new HashSet<>(flowCnecs.values());
    }

    @Override
    public Set<FlowCnec> getFlowCnecs(State state) {
        return flowCnecs.values().stream()
                .filter(cnec -> cnec.getState().equals(state))
                .collect(Collectors.toSet());
    }

    @Override
    public AngleCnec getAngleCnec(String angleCnecId) {
        return angleCnecs.get(angleCnecId);
    }

    @Override
    public Set<AngleCnec> getAngleCnecs() {
        return new HashSet<>(angleCnecs.values());
    }

    @Override
    public Set<AngleCnec> getAngleCnecs(State state) {
        return angleCnecs.values().stream()
            .filter(cnec -> cnec.getState().equals(state))
            .collect(Collectors.toSet());
    }

    @Override
    public VoltageCnec getVoltageCnec(String voltageCnecId) {
        return voltageCnecs.get(voltageCnecId);
    }

    @Override
    public Set<VoltageCnec> getVoltageCnecs() {
        return new HashSet<>(voltageCnecs.values());
    }

    @Override
    public Set<VoltageCnec> getVoltageCnecs(State state) {
        return voltageCnecs.values().stream()
            .filter(cnec -> cnec.getState().equals(state))
            .collect(Collectors.toSet());
    }

    @Override
    public void removeCnec(String cnecId) {
        removeFlowCnec(cnecId);
        removeAngleCnec(cnecId);
        removeVoltageCnec(cnecId);
    }

    @Override
    public void removeFlowCnec(String flowCnecId) {
        removeFlowCnecs(Collections.singleton(flowCnecId));
    }

    @Override
    public void removeFlowCnecs(Set<String> flowCnecsIds) {
        Set<FlowCnec> flowCnecsToRemove = flowCnecsIds.stream().map(flowCnecs::get).filter(Objects::nonNull).collect(Collectors.toSet());
        Set<String> networkElementsToRemove = flowCnecsToRemove.stream().map(cnec -> cnec.getNetworkElement().getId()).collect(Collectors.toSet());
        Set<String> statesToRemove = flowCnecsToRemove.stream().map(Cnec::getState).map(State::getId).collect(Collectors.toSet());
        flowCnecsToRemove.forEach(flowCnecToRemove ->
            flowCnecs.remove(flowCnecToRemove.getId())
        );
        safeRemoveNetworkElements(networkElementsToRemove);
        safeRemoveStates(statesToRemove);
    }

    @Override
    public void removeAngleCnec(String angleCnecId) {
        removeAngleCnecs(Collections.singleton(angleCnecId));
    }

    @Override
    public void removeAngleCnecs(Set<String> angleCnecsIds) {
        Set<AngleCnec> angleCnecsToRemove = angleCnecsIds.stream().map(angleCnecs::get).filter(Objects::nonNull).collect(Collectors.toSet());
        Set<String> networkElementsToRemove = angleCnecsToRemove.stream().map(Cnec::getNetworkElements)
            .flatMap(Set::stream).map(Identifiable::getId).collect(Collectors.toSet());
        Set<String> statesToRemove = angleCnecsToRemove.stream().map(Cnec::getState).map(State::getId).collect(Collectors.toSet());
        angleCnecsToRemove.forEach(angleCnecToRemove ->
            angleCnecs.remove(angleCnecToRemove.getId())
        );
        safeRemoveNetworkElements(networkElementsToRemove);
        safeRemoveStates(statesToRemove);
    }

    @Override
    public void removeVoltageCnec(String voltageCnecId) {
        removeVoltageCnecs(Collections.singleton(voltageCnecId));
    }

    @Override
    public void removeVoltageCnecs(Set<String> voltageCnecsIds) {
        Set<VoltageCnec> voltageCnecsToRemove = voltageCnecsIds.stream().map(voltageCnecs::get).filter(Objects::nonNull).collect(Collectors.toSet());
        Set<String> networkElementsToRemove = voltageCnecsToRemove.stream().map(cnec -> cnec.getNetworkElement().getId()).collect(Collectors.toSet());
        Set<String> statesToRemove = voltageCnecsToRemove.stream().map(Cnec::getState).map(State::getId).collect(Collectors.toSet());
        voltageCnecsToRemove.forEach(voltageCnecToRemove ->
            voltageCnecs.remove(voltageCnecToRemove.getId())
        );
        safeRemoveNetworkElements(networkElementsToRemove);
        safeRemoveStates(statesToRemove);
    }

    void addFlowCnec(FlowCnec flowCnec) {
        flowCnecs.put(flowCnec.getId(), flowCnec);
    }

    void addAngleCnec(AngleCnec angleCnec) {
        angleCnecs.put(angleCnec.getId(), angleCnec);
    }

    void addVoltageCnec(VoltageCnec voltageCnec) {
        voltageCnecs.put(voltageCnec.getId(), voltageCnec);
    }

    // endregion
    // ========================================
    // region RemedialActions management
    // ========================================

    @Override
    public Set<RemedialAction<?>> getRemedialActions() {
        Set<RemedialAction<?>> remedialActions = new HashSet<>();
        remedialActions.addAll(pstRangeActions.values());
        remedialActions.addAll(hvdcRangeActions.values());
        remedialActions.addAll(injectionRangeActions.values());
        remedialActions.addAll(networkActions.values());
        return remedialActions;
    }

    @Override
    public RemedialAction<?> getRemedialAction(String remedialActionId) {
        RemedialAction<?> remedialAction = getNetworkAction(remedialActionId);
        if (!Objects.isNull(remedialAction)) {
            return remedialAction;
        } else {
            return getRangeAction(remedialActionId);
        }
    }

    @Override
    public void removeRemedialAction(String remedialActionId) {
        removeRangeAction(remedialActionId);
        removeNetworkAction(remedialActionId);
    }

    private Set<State> getAssociatedStates(RemedialAction<?> remedialAction) {
        return remedialAction.getUsageRules().stream()
                .filter(OnContingencyState.class::isInstance)
                .map(ur -> ((OnContingencyState) ur).getState())
                .collect(Collectors.toSet());
    }

    // endregion
    // ========================================
    // region RangeAction management
    // ========================================

    @Override
    public PstRangeActionAdder newPstRangeAction() {
        return new PstRangeActionAdderImpl(this);
    }

    @Override
    public HvdcRangeActionAdder newHvdcRangeAction() {
        return new HvdcRangeActionAdderImpl(this);
    }

    @Override
    public InjectionRangeActionAdder newInjectionRangeAction() {
        return new InjectionRangeActionAdderImpl(this);
    }

    @Override
    public CounterTradeRangeActionAdder newCounterTradeRangeAction() {
        return new CounterTradeRangeActionAdderImpl(this);
    }

    @Override
    public Set<PstRangeAction> getPstRangeActions() {
        return new HashSet<>(pstRangeActions.values());
    }

    @Override
    public Set<HvdcRangeAction> getHvdcRangeActions() {
        return new HashSet<>(hvdcRangeActions.values());
    }

    @Override
    public Set<InjectionRangeAction> getInjectionRangeActions() {
        return new HashSet<>(injectionRangeActions.values());
    }

    @Override
    public Set<CounterTradeRangeAction> getCounterTradeRangeActions() {
        return new HashSet<>(counterTradeRangeActions.values());
    }

    @Override
    public PstRangeAction getPstRangeAction(String pstRangeActionId) {
        return pstRangeActions.get(pstRangeActionId);
    }

    @Override
    public HvdcRangeAction getHvdcRangeAction(String hvdcRangeActionId) {
        return hvdcRangeActions.get(hvdcRangeActionId);
    }

    @Override
    public InjectionRangeAction getInjectionRangeAction(String injectionRangActionId) {
        return injectionRangeActions.get(injectionRangActionId);
    }

    @Override
    public CounterTradeRangeAction getCounterTradeRangeAction(String counterTradeRangActionId) {
        return counterTradeRangeActions.get(counterTradeRangActionId);
    }

    @Override
    public Set<RangeAction<?>> getRangeActions() {
        Set<RangeAction<?>> rangeActionsSet = new HashSet<>(pstRangeActions.values());
        rangeActionsSet.addAll(hvdcRangeActions.values());
        rangeActionsSet.addAll(injectionRangeActions.values());
        rangeActionsSet.addAll(counterTradeRangeActions.values());
        return rangeActionsSet;
    }

    @Override
    public Set<RangeAction<?>> getRangeActions(State state, UsageMethod... usageMethods) {
        Set<RangeAction<?>> pstRangeActionsSet = pstRangeActions.values().stream()
                .filter(rangeAction -> Arrays.stream(usageMethods).anyMatch(usageMethod -> rangeAction.getUsageMethod(state).equals(usageMethod)))
                .collect(Collectors.toSet());
        Set<RangeAction<?>> hvdcRangeActionsSet = hvdcRangeActions.values().stream()
                .filter(rangeAction -> Arrays.stream(usageMethods).anyMatch(usageMethod -> rangeAction.getUsageMethod(state).equals(usageMethod)))
                .collect(Collectors.toSet());
        Set<RangeAction<?>> injectionRangeActionSet = injectionRangeActions.values().stream()
                .filter(rangeAction -> Arrays.stream(usageMethods).anyMatch(usageMethod -> rangeAction.getUsageMethod(state).equals(usageMethod)))
                .collect(Collectors.toSet());
        Set<RangeAction<?>> counterTradeRangeActionSet = counterTradeRangeActions.values().stream()
            .filter(rangeAction -> Arrays.stream(usageMethods).anyMatch(usageMethod -> rangeAction.getUsageMethod(state).equals(usageMethod)))
            .collect(Collectors.toSet());
        Set<RangeAction<?>> rangeActionsSet = new HashSet<>(pstRangeActionsSet);
        rangeActionsSet.addAll(hvdcRangeActionsSet);
        rangeActionsSet.addAll(injectionRangeActionSet);
        rangeActionsSet.addAll(counterTradeRangeActionSet);
        return rangeActionsSet;
    }

    @Override
    public Set<RangeAction<?>> getPotentiallyAvailableRangeActions(State state) {
        return getRangeActions(state, UsageMethod.AVAILABLE, UsageMethod.FORCED);
    }

    @Override
    public RangeAction<?> getRangeAction(String id) {
        if (pstRangeActions.get(id) != null) {
            return pstRangeActions.get(id);
        } else if (hvdcRangeActions.get(id) != null) {
            return hvdcRangeActions.get(id);
        } else if (injectionRangeActions.get(id) != null) {
            return injectionRangeActions.get(id);
        } else {
            return counterTradeRangeActions.get(id);
        }
    }

    public void removeRangeAction(String id) {
        if (pstRangeActions.get(id) != null) {
            removePstRangeAction(id);
        } else if (hvdcRangeActions.get(id) != null) {
            removeHvdcRangeAction(id);
        } else {
            removeInjectionRangeAction(id);
        }
    }

    @Override
    public void removePstRangeAction(String id) {
        PstRangeAction rangeActionToRemove = pstRangeActions.get(id);
        if (Objects.isNull(rangeActionToRemove)) {
            return;
        }

        Set<String> associatedNetworkElementsIds = rangeActionToRemove.getNetworkElements().stream().map(NetworkElement::getId).collect(Collectors.toSet());
        Set<String> associatedStatesIds = getAssociatedStates(rangeActionToRemove).stream().map(State::getId).collect(Collectors.toSet());

        pstRangeActions.remove(id);

        safeRemoveNetworkElements(associatedNetworkElementsIds);
        safeRemoveStates(associatedStatesIds);
    }

    @Override
    public void removeHvdcRangeAction(String id) {
        HvdcRangeAction rangeActionToRemove = hvdcRangeActions.get(id);
        if (Objects.isNull(rangeActionToRemove)) {
            return;
        }

        Set<String> associatedNetworkElementsIds = rangeActionToRemove.getNetworkElements().stream().map(NetworkElement::getId).collect(Collectors.toSet());
        Set<String> associatedStatesIds = getAssociatedStates(rangeActionToRemove).stream().map(State::getId).collect(Collectors.toSet());

        hvdcRangeActions.remove(id);

        safeRemoveNetworkElements(associatedNetworkElementsIds);
        safeRemoveStates(associatedStatesIds);

    }

    @Override
    public void removeInjectionRangeAction(String id) {
        InjectionRangeAction rangeActionToRemove = injectionRangeActions.get(id);
        if (Objects.isNull(rangeActionToRemove)) {
            return;
        }

        Set<String> associatedNetworkElementsIds = rangeActionToRemove.getNetworkElements().stream().map(NetworkElement::getId).collect(Collectors.toSet());
        Set<String> associatedStatesIds = getAssociatedStates(rangeActionToRemove).stream().map(State::getId).collect(Collectors.toSet());

        injectionRangeActions.remove(id);

        safeRemoveNetworkElements(associatedNetworkElementsIds);
        safeRemoveStates(associatedStatesIds);
    }

    void addPstRangeAction(PstRangeAction pstRangeAction) {
        pstRangeActions.put(pstRangeAction.getId(), pstRangeAction);
    }

    void addHvdcRangeAction(HvdcRangeAction hvdcRangeAction) {
        hvdcRangeActions.put(hvdcRangeAction.getId(), hvdcRangeAction);
    }

    void addInjectionRangeAction(InjectionRangeAction injectionRangeAction) {
        injectionRangeActions.put(injectionRangeAction.getId(), injectionRangeAction);
    }

    void addCounterTradeRangeAction(CounterTradeRangeAction counterTradeRangeAction) {
        counterTradeRangeActions.put(counterTradeRangeAction.getId(), counterTradeRangeAction);
    }

    // endregion
    // ========================================
    // region NetworkAction management
    // ========================================

    @Override
    public Set<NetworkAction> getNetworkActions() {
        return new HashSet<>(networkActions.values());
    }

    @Override
    public Set<NetworkAction> getNetworkActions(State state, UsageMethod... usageMethods) {
        return networkActions.values().stream()
            .filter(networkAction -> Arrays.stream(usageMethods).anyMatch(usageMethod -> networkAction.getUsageMethod(state).equals(usageMethod)))
            .collect(Collectors.toSet());
    }

    @Override
    public Set<NetworkAction> getPotentiallyAvailableNetworkActions(State state) {
        return getNetworkActions(state, UsageMethod.AVAILABLE, UsageMethod.FORCED);
    }

    @Override
    public NetworkAction getNetworkAction(String id) {
        return networkActions.get(id);
    }

    @Override
    public NetworkActionAdder newNetworkAction() {
        return new NetworkActionAdderImpl(this);
    }

    @Override
    public void removeNetworkAction(String id) {
        NetworkAction networkActionToRemove = networkActions.get(id);
        if (Objects.isNull(networkActionToRemove)) {
            return;
        }

        Set<String> associatedNetworkElementsIds = networkActionToRemove.getNetworkElements().stream().map(NetworkElement::getId).collect(Collectors.toSet());
        Set<String> associatedStatesIds = getAssociatedStates(networkActionToRemove).stream().map(State::getId).collect(Collectors.toSet());

        networkActions.remove(id);

        safeRemoveNetworkElements(associatedNetworkElementsIds);
        safeRemoveStates(associatedStatesIds);
    }

    void addNetworkAction(NetworkAction networkAction) {
        networkActions.put(networkAction.getId(), networkAction);
    }
    // endregion

    @Override
    public Map<Instant, RaUsageLimits> getRaUsageLimitsPerInstant() {
        return this.raUsageLimitsPerInstant;
    }

    @Override
    public RaUsageLimits getRaUsageLimits(Instant instant) {
        return this.raUsageLimitsPerInstant.getOrDefault(instant, new RaUsageLimits());
    }

    void addRaUsageLimits(Instant instant, RaUsageLimits raUsageLimits) {
        this.raUsageLimitsPerInstant.put(instant, raUsageLimits);
    }

    @Override
    public RaUsageLimitsAdder newRaUsageLimits(String instantName) {
        return new RaUsageLimitsAdderImpl(this, instantName);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        CracImpl other = (CracImpl) o;
        return getId().equals(other.getId());
    }

    @Override
    public int hashCode() {
        return getId().hashCode();
    }
}