SumMaxPerTimestampCostEvaluatorResult.java
/*
* Copyright (c) 2025, 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.searchtreerao.commons.costevaluatorresult;
import com.powsybl.contingency.Contingency;
import com.powsybl.iidm.network.TwoSides;
import com.powsybl.openrao.commons.Unit;
import com.powsybl.openrao.data.crac.api.State;
import com.powsybl.openrao.data.crac.api.cnec.Cnec;
import com.powsybl.openrao.data.crac.api.cnec.FlowCnec;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.stream.Collectors;
import static com.powsybl.openrao.searchtreerao.commons.objectivefunctionevaluator.CostEvaluatorUtils.groupFlowCnecsPerState;
/**
* @author Thomas Bouquet {@literal <thomas.bouquet at rte-france.com>}
* @author Roxane Chen {@literal <roxane.chen at rte-france.com>}
*/
public class SumMaxPerTimestampCostEvaluatorResult implements CostEvaluatorResult {
private final List<FlowCnec> costlyElements;
private final Map<FlowCnec, Double> marginPerCnec;
private final Unit unit;
private double highestThreshold = Double.NaN;
public SumMaxPerTimestampCostEvaluatorResult(Map<FlowCnec, Double> marginPerCnec, List<FlowCnec> costlyElements, Unit unit) {
this.marginPerCnec = marginPerCnec;
this.costlyElements = costlyElements;
this.unit = unit;
}
@Override
public double getCost(Set<String> contingenciesToExclude, Set<String> cnecsToExclude) {
// exclude cnecs
Map<FlowCnec, Double> filteredCnecs = marginPerCnec.entrySet().stream()
.filter(entry -> !cnecsToExclude.contains(entry.getKey().getId()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
// Compute state wise cost
Map<State, Set<FlowCnec>> flowCnecsPerState = groupFlowCnecsPerState(filteredCnecs.keySet());
Map<State, Double> costPerState = flowCnecsPerState.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> computeCostForState(entry.getValue())));
Map<OffsetDateTime, Set<State>> statesToEvaluatePerTimestamp = new HashMap<>();
Set<State> statesToEvaluateWithoutTimestamp = new HashSet<>();
costPerState.keySet().stream().forEach(state -> {
if (statesContingencyMustBeKept(state, contingenciesToExclude)) {
Optional<OffsetDateTime> timestamp = state.getTimestamp();
if (timestamp.isPresent()) {
statesToEvaluatePerTimestamp.computeIfAbsent(timestamp.get(), s -> new HashSet<>()).add(state);
} else {
statesToEvaluateWithoutTimestamp.add(state);
}
}
});
return statesToEvaluatePerTimestamp.values().stream().mapToDouble(states -> states.stream().mapToDouble(state -> costPerState.get(state)).max().orElse(0)).sum()
+ statesToEvaluateWithoutTimestamp.stream().mapToDouble(state -> costPerState.get(state)).max().orElse(0);
}
private double getHighestThresholdAmongFlowCnecs() {
if (Double.isNaN(highestThreshold)) {
highestThreshold = marginPerCnec.keySet().stream().map(this::getHighestThreshold).max(Double::compareTo).orElse(0.0);
}
return highestThreshold;
}
private double getHighestThreshold(FlowCnec flowCnec) {
return Math.max(
Math.max(
flowCnec.getUpperBound(TwoSides.ONE, unit).orElse(0.0),
flowCnec.getUpperBound(TwoSides.TWO, unit).orElse(0.0)),
Math.max(
-flowCnec.getLowerBound(TwoSides.ONE, unit).orElse(0.0),
-flowCnec.getLowerBound(TwoSides.TWO, unit).orElse(0.0)));
}
protected double computeCostForState(Set<FlowCnec> flowCnecsOfState) {
List<FlowCnec> flowCnecsByMargin = flowCnecsOfState.stream()
.filter(Cnec::isOptimized)
.sorted(Comparator.comparingDouble(marginPerCnec::get))
.toList();
FlowCnec limitingElement;
if (flowCnecsByMargin.isEmpty()) {
limitingElement = null;
} else {
limitingElement = flowCnecsByMargin.get(0);
}
if (limitingElement == null) {
// In case there is no limiting element (may happen in perimeters where only MNECs exist),
// return a finite value, so that the virtual cost is not hidden by the functional cost
// This finite value should only be equal to the highest possible margin, i.e. the highest cnec threshold
return -getHighestThresholdAmongFlowCnecs();
}
double margin = marginPerCnec.get(limitingElement);
if (margin >= Double.MAX_VALUE / 2) {
// In case margin is infinite (may happen in perimeters where only unoptimized CNECs exist, none of which has seen its margin degraded),
// return a finite value, like MNEC case above
return -getHighestThresholdAmongFlowCnecs();
}
return -margin;
}
@Override
public List<FlowCnec> getCostlyElements(Set<String> contingenciesToExclude, Set<String> cnecsToExclude) {
return costlyElements.stream()
.filter(flowCnec -> !cnecsToExclude.contains(flowCnec.getId()))
.filter(flowCnec -> statesContingencyMustBeKept(flowCnec.getState(), contingenciesToExclude))
.toList();
}
private static boolean statesContingencyMustBeKept(State state, Set<String> contingenciesToExclude) {
Optional<Contingency> contingency = state.getContingency();
return contingency.isEmpty() || !contingenciesToExclude.contains(contingency.get().getId());
}
}