RaoResultImpl.java

/*
 * Copyright (c) 2021, 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.raoresult.impl;

import com.powsybl.openrao.commons.MinOrMax;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.PhysicalParameter;
import com.powsybl.openrao.commons.Unit;
import com.powsybl.openrao.data.crac.api.Crac;
import com.powsybl.openrao.data.crac.api.Instant;
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.FlowCnec;
import com.powsybl.iidm.network.TwoSides;
import com.powsybl.openrao.data.crac.api.cnec.VoltageCnec;
import com.powsybl.openrao.data.crac.api.networkaction.NetworkAction;
import com.powsybl.openrao.data.crac.api.rangeaction.PstRangeAction;
import com.powsybl.openrao.data.crac.api.rangeaction.RangeAction;
import com.powsybl.openrao.data.raoresult.api.ComputationStatus;
import com.powsybl.openrao.data.raoresult.api.RaoResult;
import com.powsybl.openrao.data.raoresult.api.OptimizationStepsExecuted;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author Baptiste Seguinot {@literal <baptiste.seguinot at rte-france.com>}
 */
public class RaoResultImpl implements RaoResult {

    private static final FlowCnecResult DEFAULT_FLOWCNEC_RESULT = new FlowCnecResult();
    private static final AngleCnecResult DEFAULT_ANGLECNEC_RESULT = new AngleCnecResult();
    private static final VoltageCnecResult DEFAULT_VOLTAGECNEC_RESULT = new VoltageCnecResult();
    private static final NetworkActionResult DEFAULT_NETWORKACTION_RESULT = new NetworkActionResult();
    private static final RangeActionResult DEFAULT_RANGEACTION_RESULT = new RangeActionResult();
    private static final CostResult DEFAULT_COST_RESULT = new CostResult();

    private final Crac crac;

    private ComputationStatus computationStatus;
    private final Map<State, ComputationStatus> computationStatusPerState = new HashMap<>();
    private final Map<FlowCnec, FlowCnecResult> flowCnecResults = new HashMap<>();
    private final Map<AngleCnec, AngleCnecResult> angleCnecResults = new HashMap<>();
    private final Map<VoltageCnec, VoltageCnecResult> voltageCnecResults = new HashMap<>();
    private final Map<NetworkAction, NetworkActionResult> networkActionResults = new HashMap<>();
    private final Map<RangeAction<?>, RangeActionResult> rangeActionResults = new HashMap<>();
    private final Map<String, CostResult> costResults = new HashMap<>();

    private String executionDetails = OptimizationStepsExecuted.FIRST_PREVENTIVE_ONLY;

    public RaoResultImpl(Crac crac) {
        this.crac = crac;
    }

    public void setComputationStatus(ComputationStatus computationStatus) {
        this.computationStatus = computationStatus;
    }

    public void setComputationStatus(State state, ComputationStatus computationStatus) {
        this.computationStatusPerState.put(state, computationStatus);
    }

    @Override
    public ComputationStatus getComputationStatus() {
        return computationStatus;
    }

    @Override
    public ComputationStatus getComputationStatus(State state) {
        return computationStatusPerState.getOrDefault(state, ComputationStatus.DEFAULT);
    }

    private Instant checkOptimizedInstant(Instant optimizedInstant, FlowCnec flowCnec) {
        if (optimizedInstant == null) {
            return null;
        }
        Instant instant = optimizedInstant;
        if (flowCnec.getState().getInstant().comesBefore(instant)) {
            instant = flowCnec.getState().getInstant();
        }
        if (instant.isOutage()) {
            instant = crac.getPreventiveInstant();
        }
        return instant;
    }

    @Override
    public double getFlow(Instant optimizedInstant, FlowCnec flowCnec, TwoSides side, Unit unit) {
        return flowCnecResults.getOrDefault(flowCnec, DEFAULT_FLOWCNEC_RESULT).getResult(checkOptimizedInstant(optimizedInstant, flowCnec)).getFlow(side, unit);
    }

    @Override
    public double getAngle(Instant optimizedInstant, AngleCnec angleCnec, Unit unit) {
        return angleCnecResults.getOrDefault(angleCnec, DEFAULT_ANGLECNEC_RESULT).getResult(optimizedInstant).getAngle(unit);
    }

    @Override
    public double getVoltage(Instant optimizedInstant, VoltageCnec voltageCnec, MinOrMax minOrMax, Unit unit) {
        return voltageCnecResults.getOrDefault(voltageCnec, DEFAULT_VOLTAGECNEC_RESULT).getResult(optimizedInstant).getVoltage(minOrMax, unit);
    }

    @Override
    public double getMargin(Instant optimizedInstant, FlowCnec flowCnec, Unit unit) {
        return flowCnecResults.getOrDefault(flowCnec, DEFAULT_FLOWCNEC_RESULT).getResult(checkOptimizedInstant(optimizedInstant, flowCnec)).getMargin(unit);
    }

    @Override
    public double getMargin(Instant optimizedInstant, AngleCnec angleCnec, Unit unit) {
        return angleCnecResults.getOrDefault(angleCnec, DEFAULT_ANGLECNEC_RESULT).getResult(optimizedInstant).getMargin(unit);
    }

    @Override
    public double getMargin(Instant optimizedInstant, VoltageCnec voltageCnec, Unit unit) {
        return voltageCnecResults.getOrDefault(voltageCnec, DEFAULT_VOLTAGECNEC_RESULT).getResult(optimizedInstant).getMargin(unit);
    }

    @Override
    public double getRelativeMargin(Instant optimizedInstant, FlowCnec flowCnec, Unit unit) {
        return flowCnecResults.getOrDefault(flowCnec, DEFAULT_FLOWCNEC_RESULT).getResult(checkOptimizedInstant(optimizedInstant, flowCnec)).getRelativeMargin(unit);
    }

    @Override
    public double getLoopFlow(Instant optimizedInstant, FlowCnec flowCnec, TwoSides side, Unit unit) {
        return flowCnecResults.getOrDefault(flowCnec, DEFAULT_FLOWCNEC_RESULT).getResult(checkOptimizedInstant(optimizedInstant, flowCnec)).getLoopFlow(side, unit);
    }

    @Override
    public double getCommercialFlow(Instant optimizedInstant, FlowCnec flowCnec, TwoSides side, Unit unit) {
        return flowCnecResults.getOrDefault(flowCnec, DEFAULT_FLOWCNEC_RESULT).getResult(checkOptimizedInstant(optimizedInstant, flowCnec)).getCommercialFlow(side, unit);
    }

    @Override
    public double getPtdfZonalSum(Instant optimizedInstant, FlowCnec flowCnec, TwoSides side) {
        return flowCnecResults.getOrDefault(flowCnec, DEFAULT_FLOWCNEC_RESULT).getResult(checkOptimizedInstant(optimizedInstant, flowCnec)).getPtdfZonalSum(side);
    }

    public FlowCnecResult getAndCreateIfAbsentFlowCnecResult(FlowCnec flowCnec) {
        flowCnecResults.putIfAbsent(flowCnec, new FlowCnecResult());
        return flowCnecResults.get(flowCnec);
    }

    public AngleCnecResult getAndCreateIfAbsentAngleCnecResult(AngleCnec angleCnec) {
        angleCnecResults.putIfAbsent(angleCnec, new AngleCnecResult());
        return angleCnecResults.get(angleCnec);
    }

    public VoltageCnecResult getAndCreateIfAbsentVoltageCnecResult(VoltageCnec voltageCnec) {
        voltageCnecResults.putIfAbsent(voltageCnec, new VoltageCnecResult());
        return voltageCnecResults.get(voltageCnec);
    }

    public CostResult getAndCreateIfAbsentCostResult(String optimizedInstantId) {
        costResults.putIfAbsent(optimizedInstantId, new CostResult());
        return costResults.get(optimizedInstantId);
    }

    @Override
    public double getCost(Instant optimizedInstant) {
        String id = getIdFromNullableInstant(optimizedInstant);
        return costResults.getOrDefault(id, DEFAULT_COST_RESULT).getCost();
    }

    @Override
    public double getFunctionalCost(Instant optimizedInstant) {
        String id = getIdFromNullableInstant(optimizedInstant);
        return costResults.getOrDefault(id, DEFAULT_COST_RESULT).getFunctionalCost();
    }

    @Override
    public double getVirtualCost(Instant optimizedInstant) {
        String id = getIdFromNullableInstant(optimizedInstant);
        return costResults.getOrDefault(id, DEFAULT_COST_RESULT).getVirtualCost();
    }

    @Override
    public Set<String> getVirtualCostNames() {
        return costResults.values().stream().flatMap(c -> c.getVirtualCostNames().stream()).collect(Collectors.toSet());
    }

    @Override
    public double getVirtualCost(Instant optimizedInstant, String virtualCostName) {
        String id = getIdFromNullableInstant(optimizedInstant);
        return costResults.getOrDefault(id, DEFAULT_COST_RESULT).getVirtualCost(virtualCostName);
    }

    private static String getIdFromNullableInstant(Instant optimizedInstant) {
        return optimizedInstant == null ? INITIAL_INSTANT_ID : optimizedInstant.getId();
    }

    public NetworkActionResult getAndCreateIfAbsentNetworkActionResult(NetworkAction networkAction) {
        networkActionResults.putIfAbsent(networkAction, new NetworkActionResult());
        return networkActionResults.get(networkAction);
    }

    @Override
    public boolean wasActivatedBeforeState(State state, NetworkAction networkAction) {
        if (state.isPreventive() || state.getContingency().isEmpty()) {
            return false;
        }

        // if it is activated in the preventive state, return true
        if (networkActionResults.getOrDefault(networkAction, DEFAULT_NETWORKACTION_RESULT)
                .getStatesWithActivation().stream()
                .anyMatch(State::isPreventive)) {
            return true;
        }

        return networkActionResults.getOrDefault(networkAction, DEFAULT_NETWORKACTION_RESULT)
                .getStatesWithActivation().stream()
                .filter(st -> st.getContingency().isPresent())
                .filter(st -> st.getInstant().getOrder() < state.getInstant().getOrder())
                .anyMatch(st -> st.getContingency().get().getId().equals(state.getContingency().get().getId()));
    }

    @Override
    public boolean isActivatedDuringState(State state, NetworkAction networkAction) {
        return networkActionResults.getOrDefault(networkAction, DEFAULT_NETWORKACTION_RESULT).getStatesWithActivation().contains(state);
    }

    @Override
    public Set<NetworkAction> getActivatedNetworkActionsDuringState(State state) {
        return networkActionResults.entrySet().stream()
                .filter(e -> e.getValue().getStatesWithActivation().contains(state))
                .map(Map.Entry::getKey)
                .collect(Collectors.toSet());
    }

    public RangeActionResult getAndCreateIfAbsentRangeActionResult(RangeAction<?> rangeAction) {
        rangeActionResults.putIfAbsent(rangeAction, new RangeActionResult());
        return rangeActionResults.get(rangeAction);
    }

    @Override
    public boolean isActivatedDuringState(State state, RangeAction<?> rangeAction) {
        return rangeActionResults.getOrDefault(rangeAction, DEFAULT_RANGEACTION_RESULT).isActivatedDuringState(state);
    }

    @Override
    public int getPreOptimizationTapOnState(State state, PstRangeAction pstRangeAction) {
        return pstRangeAction.convertAngleToTap(getPreOptimizationSetPointOnState(state, pstRangeAction));
    }

    @Override
    public int getOptimizedTapOnState(State state, PstRangeAction pstRangeAction) {
        return pstRangeAction.convertAngleToTap(getOptimizedSetPointOnState(state, pstRangeAction));
    }

    @Override
    public double getPreOptimizationSetPointOnState(State state, RangeAction<?> rangeAction) {
        if (state.isPreventive()) {
            return rangeActionResults.getOrDefault(rangeAction, DEFAULT_RANGEACTION_RESULT).getInitialSetpoint();
        } else {
            return getOptimizedSetPointOnState(stateBefore(state), rangeAction);
        }
    }

    @Override
    public double getOptimizedSetPointOnState(State state, RangeAction<?> rangeAction) {
        State stateBefore = state;
        // Search for any RA with same network element that has been activated before given state
        while (Objects.nonNull(stateBefore)) {
            final State finalStateBefore = stateBefore;
            Optional<Map.Entry<RangeAction<?>, RangeActionResult>> activatedRangeAction =
                    rangeActionResults.entrySet().stream().filter(entry ->
                    entry.getKey().getNetworkElements().equals(rangeAction.getNetworkElements())
                            && entry.getValue().isActivatedDuringState(finalStateBefore)).findAny();
            if (activatedRangeAction.isPresent()) {
                return activatedRangeAction.get().getValue().getOptimizedSetpointOnState(stateBefore);
            }
            stateBefore = stateBefore(stateBefore);
        }
        // If no activated RA was found, return initial setpoint
        return getPreOptimizationSetPointOnState(crac.getPreventiveState(), rangeAction);
    }

    @Override
    public Set<RangeAction<?>> getActivatedRangeActionsDuringState(State state) {
        return rangeActionResults.entrySet().stream()
                .filter(e -> e.getValue().isActivatedDuringState(state))
                .map(Map.Entry::getKey)
                .collect(Collectors.toSet());
    }

    @Override
    public Map<PstRangeAction, Integer> getOptimizedTapsOnState(State state) {
        return crac.getPstRangeActions().stream().collect(Collectors.toMap(Function.identity(), pst -> getOptimizedTapOnState(state, pst)));
    }

    @Override
    public Map<RangeAction<?>, Double> getOptimizedSetPointsOnState(State state) {
        return crac.getRangeActions().stream().collect(Collectors.toMap(Function.identity(), ra -> getOptimizedSetPointOnState(state, ra)));
    }

    private State stateBefore(State state) {
        if (state.getContingency().isPresent()) {
            return stateBefore(state.getContingency().orElseThrow().getId(), state.getInstant());
        } else {
            return null;
        }
    }

    private State stateBefore(String contingencyId, Instant instant) {
        if (instant.isOutage()) {
            return crac.getPreventiveState();
        }
        State stateBefore = lookupState(contingencyId, crac.getInstantBefore(instant));
        if (Objects.nonNull(stateBefore)) {
            return stateBefore;
        } else {
            return stateBefore(contingencyId, crac.getInstantBefore(instant));
        }
    }

    private State lookupState(String contingencyId, Instant instant) {
        return crac.getStates(instant).stream()
                .filter(state -> state.getContingency().isPresent() && state.getContingency().get().getId().equals(contingencyId))
                .findAny()
                .orElse(null);
    }

    @Override
    public void setExecutionDetails(String executionDetails) {
        this.executionDetails = executionDetails;
    }

    private boolean instantHasNoNegativeMargin(Instant optimizedInstant, PhysicalParameter... u) {
        for (PhysicalParameter physicalParameter : Set.of(u)) {
            switch (physicalParameter) {
                case ANGLE -> {
                    if (crac.getAngleCnecs().stream()
                        .mapToDouble(cnec -> getMargin(Instant.min(optimizedInstant, cnec.getState().getInstant()), cnec, Unit.DEGREE))
                        .anyMatch(Double::isNaN)) {
                        throw new OpenRaoException("RaoResult does not contain angle values for all AngleCNECs, security status for physical parameter ANGLE is unknown");
                    }
                    if (crac.getAngleCnecs().stream()
                            .mapToDouble(cnec -> getMargin(optimizedInstant, cnec, Unit.DEGREE))
                            .filter(margin -> !Double.isNaN(margin))
                            .anyMatch(margin -> margin < 0)) {
                        return false;
                    }
                }
                case FLOW -> {
                    if (getFunctionalCost(optimizedInstant) > 0) {
                        return false;
                    }
                }
                case VOLTAGE -> {
                    if (crac.getVoltageCnecs().stream()
                        .mapToDouble(cnec -> getMargin(Instant.min(optimizedInstant, cnec.getState().getInstant()), cnec, Unit.KILOVOLT))
                        .anyMatch(Double::isNaN)) {
                        throw new OpenRaoException("RaoResult does not contain voltage values for all VoltageCNECs, security status for physical parameter VOLTAGE is unknown");
                    }
                    if (crac.getVoltageCnecs().stream()
                            .mapToDouble(cnec -> getMargin(optimizedInstant, cnec, Unit.KILOVOLT))
                            .filter(margin -> !Double.isNaN(margin))
                            .anyMatch(margin -> margin < 0)) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    @Override
    public boolean isSecure(Instant optimizedInstant, PhysicalParameter... u) {
        if (ComputationStatus.FAILURE.equals(getComputationStatus())) {
            return false;
        }
        if (computationStatusPerState.keySet().stream().filter(state -> optimizedInstant.equals(state.getInstant()))
                .anyMatch(state -> ComputationStatus.FAILURE.equals(computationStatusPerState.get(state)))) {
            return false;
        }
        return instantHasNoNegativeMargin(optimizedInstant, u);
    }

    @Override
    public boolean isSecure(PhysicalParameter... u) {
        return isSecure(crac.getLastInstant(), u);
    }

    @Override
    public String getExecutionDetails() {
        return executionDetails;
    }
}