Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import com.powsybl.commons.extensions.Extendable;
import com.powsybl.commons.util.ServiceLoaderCache;
import com.powsybl.iidm.network.TwoSides;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.PhysicalParameter;
import com.powsybl.openrao.commons.Unit;
Expand All @@ -17,9 +18,7 @@
import com.powsybl.openrao.data.crac.api.Instant;
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.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;
Expand Down Expand Up @@ -69,19 +68,6 @@ public interface RaoResult extends Extendable<RaoResult> {
*/
double getFlow(Instant optimizedInstant, FlowCnec flowCnec, TwoSides side, Unit unit);

/**
* It gives the angle on an {@link AngleCnec} at a given {@link Instant} and in a
* given {@link Unit}.
*
* @param optimizedInstant The optimized instant to be studied (set to null to access initial results)
* @param angleCnec The angle cnec to be studied.
* @param unit The unit in which the flow is queried. Only accepted value for now is DEGREE.
* @return The angle on the cnec at the optimization state in the given unit.
*/
default double getAngle(Instant optimizedInstant, AngleCnec angleCnec, Unit unit) {
throw new OpenRaoException("Angle cnecs are not computed in the rao");
}

/**
* It gives the minimum voltage on a {@link VoltageCnec} at a given {@link Instant} and in a
* given {@link Unit}.
Expand Down Expand Up @@ -120,20 +106,6 @@ default double getMaxVoltage(Instant optimizedInstant, VoltageCnec voltageCnec,
*/
double getMargin(Instant optimizedInstant, FlowCnec flowCnec, Unit unit);

/**
* It gives the margin on an {@link AngleCnec} at a given {@link Instant} and in a
* given {@link Unit}. It is basically the difference between the angle and the most constraining threshold in the
* angle direction of the given branch. If it is negative the cnec is under constraint.
*
* @param optimizedInstant The optimized instant to be studied (set to null to access initial results)
* @param angleCnec The angle cnec to be studied.
* @param unit The unit in which the margin is queried. Only accepted for now is DEGREE.
* @return The margin on the angle cnec at the optimization state in the given unit.
*/
default double getMargin(Instant optimizedInstant, AngleCnec angleCnec, Unit unit) {
throw new OpenRaoException("Angle cnecs are not computed in the rao");
}

/**
* It gives the margin on a {@link VoltageCnec} at a given {@link Instant} and in a
* given {@link Unit}. It is basically the difference between the voltage and the most constraining threshold in the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2026, 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.api.extension;

import com.powsybl.commons.extensions.Extension;
import com.powsybl.openrao.data.raoresult.api.RaoResult;

/**
* @author Thomas Bouquet {@literal <thomas.bouquet at rte-france.com>}
*/
public abstract class AbstractRaoResultExtension implements Extension<RaoResult> {
protected RaoResult owner;

@Override
public RaoResult getExtendable() {
return owner;
}

@Override
public void setExtendable(RaoResult raoResult) {
this.owner = raoResult;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Copyright (c) 2026, 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.api.extension;

import com.fasterxml.jackson.core.JsonGenerator;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.Unit;
import com.powsybl.openrao.data.crac.api.Identifiable;
import com.powsybl.openrao.data.crac.api.Instant;
import com.powsybl.openrao.data.crac.api.cnec.AngleCnec;

import java.io.IOException;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
* @author Thomas Bouquet {@literal <thomas.bouquet at rte-france.com>}
*/
public class AngleExtension extends AbstractRaoResultExtension {
private static final String EXTENSION_NAME = "angle-results";

private final Map<AngleCnec, AngleCnecResult> results;

public AngleExtension() {
this.results = new HashMap<>();
}

@Override
public String getName() {
return EXTENSION_NAME;
}

/**
* It gives the angle on an {@link AngleCnec} at a given {@link Instant} and in a
* given {@link Unit}.
*
* @param optimizedInstant The optimized instant to be studied (set to null to access initial results)
* @param angleCnec The angle cnec to be studied.
* @param unit The unit in which the flow is queried. Only accepted value for now is DEGREE.
* @return The angle on the cnec at the optimization state in the given unit.
*/
public double getAngle(Instant optimizedInstant, AngleCnec angleCnec, Unit unit) {
checkUnit(unit);
return results.getOrDefault(angleCnec, AngleCnecResult.of(angleCnec)).getAngle(optimizedInstant).orElse(Double.NaN);
}

public void addAngle(double angle, Instant optimizedInstant, AngleCnec angleCnec, Unit unit) {
checkUnit(unit);
results.computeIfAbsent(angleCnec, k -> AngleCnecResult.of(angleCnec)).addAngleMeasurement(optimizedInstant, angle);
}

/**
* It gives the margin on an {@link AngleCnec} at a given {@link Instant} and in a
* given {@link Unit}. It is basically the difference between the angle and the most constraining threshold in the
* angle direction of the given branch. If it is negative the cnec is under constraint.
*
* @param optimizedInstant The optimized instant to be studied (set to null to access initial results)
* @param angleCnec The angle cnec to be studied.
* @param unit The unit in which the margin is queried. Only accepted for now is DEGREE.
* @return The margin on the angle cnec at the optimization state in the given unit.
*/
public double getMargin(Instant optimizedInstant, AngleCnec angleCnec, Unit unit) {
checkUnit(unit);
return results.getOrDefault(angleCnec, AngleCnecResult.of(angleCnec)).getMargin(optimizedInstant).orElse(Double.NaN);
}

private static void checkUnit(Unit unit) {
if (!Unit.DEGREE.equals(unit)) {
throw new OpenRaoException("AngleCNEC results are only allowed for degrees.");
}
}

// serialization

public void serialize(JsonGenerator jsonGenerator) throws IOException {
jsonGenerator.writeStartArray();
for (AngleCnec angleCnec : results.keySet().stream().sorted(Comparator.comparing(Identifiable::getId)).toList()) {
results.get(angleCnec).serialize(jsonGenerator);
}
jsonGenerator.writeEndArray();
}

// result data model

private static class AngleCnecResult {
private final AngleCnec angleCnec;
private Double initialAngle;
private Double initialMargin;
private final Map<Instant, Double> anglePerInstant;
private final Map<Instant, Double> marginPerInstant;

public AngleCnecResult(AngleCnec angleCnec) {
this.angleCnec = angleCnec;
this.initialAngle = null;
this.initialMargin = null;
this.anglePerInstant = new HashMap<>();
this.marginPerInstant = new HashMap<>();
}

public AngleCnec getAngleCnec() {
return angleCnec;
}

public Optional<Double> getAngle(Instant instant) {
return instant == null ? Optional.ofNullable(initialAngle) : Optional.ofNullable(anglePerInstant.getOrDefault(instant, null));
}

public Optional<Double> getMargin(Instant instant) {
return instant == null ? Optional.ofNullable(initialMargin) : Optional.ofNullable(marginPerInstant.getOrDefault(instant, null));
}

public void addAngleMeasurement(Instant instant, double angle) {
if (instant == null) {
initialAngle = angle;
initialMargin = computeMargin(angle);
} else {
anglePerInstant.put(instant, angle);
marginPerInstant.put(instant, computeMargin(angle));
}
}

private double computeMargin(double angle) {
return Math.min(
angle - angleCnec.getLowerBound(Unit.DEGREE).orElse(-Double.MAX_VALUE),
angleCnec.getUpperBound(Unit.DEGREE).orElse(Double.MAX_VALUE) - angle
);
}

public static AngleCnecResult of(AngleCnec angleCnec) {
return new AngleCnecResult(angleCnec);
}

public void serialize(JsonGenerator jsonGenerator) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("angleCnecId", angleCnec.getId());
jsonGenerator.writeObjectFieldStart("measurements");
jsonGenerator.writeArrayFieldStart(Unit.DEGREE.name().toLowerCase());
serializeInitialResults(jsonGenerator);
for (Instant instant : anglePerInstant.keySet().stream().sorted().toList()) {
serializeMeasurementsForInstant(jsonGenerator, instant);
}
jsonGenerator.writeEndArray();
jsonGenerator.writeEndObject();
jsonGenerator.writeEndObject();
}

private void serializeInitialResults(JsonGenerator jsonGenerator) throws IOException {
if (initialAngle != null && initialMargin != null) {
serializeResults(jsonGenerator, "initial", initialAngle, initialMargin);
}
}

private void serializeMeasurementsForInstant(JsonGenerator jsonGenerator, Instant instant) throws IOException {
serializeResults(jsonGenerator, instant.getId(), anglePerInstant.get(instant), marginPerInstant.get(instant));
}

private static void serializeResults(JsonGenerator jsonGenerator, String instantId, double angle, double margin) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("instant", instantId);
jsonGenerator.writeNumberField("angle", angle);
jsonGenerator.writeNumberField("margin", margin);
jsonGenerator.writeEndObject();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2026, 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.api.extension;

import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.Unit;
import com.powsybl.openrao.data.crac.api.Instant;
import com.powsybl.openrao.data.crac.api.cnec.AngleCnec;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

/**
* @author Thomas Bouquet {@literal <thomas.bouquet at rte-france.com>}
*/
public class AngleExtensionTest {
@Test
void testAngleExtension() {
AngleCnec angleCnec = Mockito.mock(AngleCnec.class);
Mockito.when(angleCnec.getUpperBound(Unit.DEGREE)).thenReturn(Optional.of(30.0));
Mockito.when(angleCnec.getLowerBound(Unit.DEGREE)).thenReturn(Optional.of(0.0));

Instant preventiveInstant = Mockito.mock(Instant.class);
Mockito.when(preventiveInstant.getOrder()).thenReturn(0);
Instant curativeInstant = Mockito.mock(Instant.class);
Mockito.when(curativeInstant.getOrder()).thenReturn(1);

AngleExtension angleExtension = new AngleExtension();
assertEquals("angle-results", angleExtension.getName());

// initial results

assertEquals(Double.NaN, angleExtension.getAngle(null, angleCnec, Unit.DEGREE));
assertEquals(Double.NaN, angleExtension.getMargin(null, angleCnec, Unit.DEGREE));

assertEquals(Double.NaN, angleExtension.getAngle(preventiveInstant, angleCnec, Unit.DEGREE));
assertEquals(Double.NaN, angleExtension.getMargin(preventiveInstant, angleCnec, Unit.DEGREE));

assertEquals(Double.NaN, angleExtension.getAngle(curativeInstant, angleCnec, Unit.DEGREE));
assertEquals(Double.NaN, angleExtension.getMargin(curativeInstant, angleCnec, Unit.DEGREE));

// manually add results

angleExtension.addAngle(25.0, null, angleCnec, Unit.DEGREE);
assertEquals(25.0, angleExtension.getAngle(null, angleCnec, Unit.DEGREE));
assertEquals(5.0, angleExtension.getMargin(null, angleCnec, Unit.DEGREE));

angleExtension.addAngle(17.0, preventiveInstant, angleCnec, Unit.DEGREE);
assertEquals(17.0, angleExtension.getAngle(preventiveInstant, angleCnec, Unit.DEGREE));
assertEquals(13.0, angleExtension.getMargin(preventiveInstant, angleCnec, Unit.DEGREE));

angleExtension.addAngle(-5.0, curativeInstant, angleCnec, Unit.DEGREE);
assertEquals(-5.0, angleExtension.getAngle(curativeInstant, angleCnec, Unit.DEGREE));
assertEquals(-5.0, angleExtension.getMargin(curativeInstant, angleCnec, Unit.DEGREE));

// invalid unit

OpenRaoException exception = assertThrows(OpenRaoException.class, () -> angleExtension.addAngle(25.0, null, angleCnec, Unit.MEGAWATT));
assertEquals("AngleCNEC results are only allowed for degrees.", exception.getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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 com.powsybl.openrao.data.raoresult.api.extension.AngleExtension;

import java.util.*;
import java.util.function.Function;
Expand Down Expand Up @@ -96,11 +97,6 @@ public double getFlow(Instant optimizedInstant, FlowCnec flowCnec, TwoSides side
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 getMinVoltage(Instant optimizedInstant, VoltageCnec voltageCnec, Unit unit) {
return voltageCnecResults.getOrDefault(voltageCnec, DEFAULT_VOLTAGECNEC_RESULT).getResult(optimizedInstant).getMinVoltage(unit);
Expand All @@ -116,11 +112,6 @@ 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);
Expand Down Expand Up @@ -339,16 +330,20 @@ private boolean instantHasNoNegativeMargin(Instant optimizedInstant, PhysicalPar
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))
// TODO: do we want to keep the use of the extension here?
AngleExtension angleExtension = getExtension(AngleExtension.class);
if (angleExtension != null) {
if (crac.getAngleCnecs().stream()
.mapToDouble(cnec -> angleExtension.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 -> angleExtension.getMargin(optimizedInstant, cnec, Unit.DEGREE))
.filter(margin -> !Double.isNaN(margin))
.anyMatch(margin -> margin < 0)) {
return false;
return false;
}
}
}
case FLOW -> {
Expand Down
Loading