diff --git a/iidm/iidm-api/src/main/java/com/powsybl/iidm/network/ReactiveCapabilityCurve.java b/iidm/iidm-api/src/main/java/com/powsybl/iidm/network/ReactiveCapabilityCurve.java index d6706a35b13..b60ab8214d9 100644 --- a/iidm/iidm-api/src/main/java/com/powsybl/iidm/network/ReactiveCapabilityCurve.java +++ b/iidm/iidm-api/src/main/java/com/powsybl/iidm/network/ReactiveCapabilityCurve.java @@ -70,4 +70,15 @@ public interface Point { */ double getMaxP(); + /** + * Get the reactive power minimum value of the curve (with the possibility of extrapolating slope of reactive + * limits outside active limits) + */ + double getMinQ(double p, boolean extrapolateReactiveLimitSlope); + + /** + * Get the reactive power maximum value of the curve (with the possibility of extrapolating slope of reactive + * limits outside active limits) + */ + double getMaxQ(double p, boolean extrapolateReactiveLimitSlope); } diff --git a/iidm/iidm-api/src/main/java/com/powsybl/iidm/network/util/ReactiveCapabilityCurveUtil.java b/iidm/iidm-api/src/main/java/com/powsybl/iidm/network/util/ReactiveCapabilityCurveUtil.java new file mode 100644 index 00000000000..4a5d2821813 --- /dev/null +++ b/iidm/iidm-api/src/main/java/com/powsybl/iidm/network/util/ReactiveCapabilityCurveUtil.java @@ -0,0 +1,66 @@ +/** + * 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.network.util; + +import com.powsybl.iidm.network.ReactiveCapabilityCurve; +import org.apache.commons.lang3.function.TriFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.TreeMap; + +/** + * @author Sylvestre Prabakaran {@literal } + */ +public final class ReactiveCapabilityCurveUtil { + + private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveCapabilityCurveUtil.class); + + private ReactiveCapabilityCurveUtil() { + } + + /** + * Extrapolate reactive limits when p is outside [minP,maxP] using slopes of reactive limits at the crossed limit of p + * (Note that this method throws an exception if p is inside [minP, maxP]) + * @param p Active power value to evaluate the reactive limits + * @param points TreeMap of all points defining the reactive capability curve mapped by their active power values + * @param valuesToReactiveCapabilityPoint TriFunction returning the used implementation of {@link ReactiveCapabilityCurve.Point} interface: (p, minQ, maxQ) -> Point + * @param ownerDescription Description of the ReactiveCapabilityCurve's owner (for logging purpose) + * @return A ReactiveCapabilityCurve.Point of the extrapolated limits at the requested value of p + */ + public static ReactiveCapabilityCurve.Point extrapolateReactiveLimitsSlope(double p, TreeMap points, TriFunction valuesToReactiveCapabilityPoint, String ownerDescription) { + double minQ; + double maxQ; + ReactiveCapabilityCurve.Point pBound; + ReactiveCapabilityCurve.Point pbis; + + if (p < points.firstKey()) { + // Extrapolate reactive limits slope below min active power limit (pBound = min active power limit) + pBound = points.firstEntry().getValue(); + pbis = points.higherEntry(points.firstKey()).getValue(); // p < pBound < pbis + } else if (p > points.lastKey()) { + // Extrapolate reactive limits slope above max active power limit (pBound = max active power limit) + pBound = points.lastEntry().getValue(); + pbis = points.lowerEntry(points.lastKey()).getValue(); // pbis < pBound < p + } else { + throw new IllegalStateException(); + } + double slopeMinQ = (pbis.getMinQ() - pBound.getMinQ()) / (pbis.getP() - pBound.getP()); + double slopeMaxQ = (pbis.getMaxQ() - pBound.getMaxQ()) / (pbis.getP() - pBound.getP()); + minQ = pBound.getMinQ() + slopeMinQ * (p - pBound.getP()); + maxQ = pBound.getMaxQ() + slopeMaxQ * (p - pBound.getP()); + + if (minQ <= maxQ) { + return valuesToReactiveCapabilityPoint.apply(p, minQ, maxQ); + } else { // Corner case of intersecting reactive limits when extrapolated + double limitQ = (minQ + maxQ) / 2; + LOGGER.warn("Extrapolation of reactive capability curve for {} leads to minQ > maxQ, correcting to minQ = maxQ", ownerDescription); // This log message can be over flowing (if called at each iteration), apply filters in logback to avoid it + return valuesToReactiveCapabilityPoint.apply(p, limitQ, limitQ); // Returning the mean as limits minQ and maxQ + } + } +} diff --git a/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/ReactiveCapabilityCurveAdderImpl.java b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/ReactiveCapabilityCurveAdderImpl.java index 05c71b9ee27..a9b3493210c 100644 --- a/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/ReactiveCapabilityCurveAdderImpl.java +++ b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/ReactiveCapabilityCurveAdderImpl.java @@ -109,7 +109,7 @@ public ReactiveCapabilityCurve add() { if (points.size() < 2) { throw new ValidationException(owner, "a reactive capability curve should have at least two points"); } - ReactiveCapabilityCurveImpl curve = new ReactiveCapabilityCurveImpl(points); + ReactiveCapabilityCurveImpl curve = new ReactiveCapabilityCurveImpl(points, owner.getMessageHeader()); owner.setReactiveLimits(curve); return curve; } diff --git a/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/ReactiveCapabilityCurveImpl.java b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/ReactiveCapabilityCurveImpl.java index 283aab9c294..c940c714553 100644 --- a/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/ReactiveCapabilityCurveImpl.java +++ b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/ReactiveCapabilityCurveImpl.java @@ -9,18 +9,20 @@ import com.powsybl.iidm.network.ReactiveCapabilityCurve; import com.powsybl.iidm.network.ReactiveLimitsKind; +import com.powsybl.iidm.network.util.ReactiveCapabilityCurveUtil; import java.util.Collection; import java.util.Collections; -import java.util.Map; import java.util.TreeMap; +import java.util.function.ToDoubleFunction; /** - * * @author Geoffroy Jamgotchian {@literal } */ class ReactiveCapabilityCurveImpl implements ReactiveCapabilityCurve { + private final String ownerDescription; + static class PointImpl implements Point { private double p; @@ -54,11 +56,16 @@ public double getMaxQ() { private final TreeMap points; - ReactiveCapabilityCurveImpl(TreeMap points) { + private static void checkPointsSize(TreeMap points) { if (points.size() < 2) { - throw new IllegalStateException("Points size must be >= 2"); + throw new IllegalStateException("points size should be >= 2"); } + } + + ReactiveCapabilityCurveImpl(TreeMap points, String ownerDescription) { + checkPointsSize(points); this.points = points; + this.ownerDescription = ownerDescription; } @Override @@ -88,51 +95,53 @@ public ReactiveLimitsKind getKind() { @Override public double getMinQ(double p) { - if (points.size() < 2) { - throw new IllegalStateException("points size should be >= 2"); - } - Point pt = points.get(p); - if (pt != null) { - return pt.getMinQ(); - } else { - Map.Entry e1 = points.floorEntry(p); - Map.Entry e2 = points.ceilingEntry(p); - if (e1 == null && e2 != null) { - return e2.getValue().getMinQ(); - } else if (e1 != null && e2 == null) { - return e1.getValue().getMinQ(); - } else if (e1 != null && e2 != null) { - Point p1 = e1.getValue(); - Point p2 = e2.getValue(); - return p1.getMinQ() + (p2.getMinQ() - p1.getMinQ()) / (p2.getP() - p1.getP()) * (p - p1.getP()); - } else { - throw new IllegalStateException(); - } - } + return getMinQ(p, false); } @Override public double getMaxQ(double p) { - if (points.size() < 2) { - throw new IllegalStateException("points size should be >= 2"); - } + return getMaxQ(p, false); + } + + @Override + public double getMinQ(double p, boolean extrapolateReactiveLimitSlope) { + return getReactiveLimit(p, extrapolateReactiveLimitSlope, Point::getMinQ); + } + + @Override + public double getMaxQ(double p, boolean extrapolateReactiveLimitSlope) { + return getReactiveLimit(p, extrapolateReactiveLimitSlope, Point::getMaxQ); + } + + private double getReactiveLimit(double p, boolean extrapolateReactiveLimitSlope, ToDoubleFunction getMinOrMaxQ) { + checkPointsSize(points); + + // First case : searched point is one of the points defining the curve Point pt = points.get(p); if (pt != null) { - return pt.getMaxQ(); + return getMinOrMaxQ.applyAsDouble(pt); + } + + // Second case : searched point is between minP and maxP + if (p >= this.getMinP() && p <= this.getMaxP()) { + Point p1 = points.floorEntry(p).getValue(); + Point p2 = points.ceilingEntry(p).getValue(); + return getMinOrMaxQ.applyAsDouble(p1) + (getMinOrMaxQ.applyAsDouble(p2) - getMinOrMaxQ.applyAsDouble(p1)) / (p2.getP() - p1.getP()) * (p - p1.getP()); + } + + // Third case : searched point is outside minP and maxP + if (extrapolateReactiveLimitSlope) { + Point extrapolatedPoint = ReactiveCapabilityCurveUtil.extrapolateReactiveLimitsSlope(p, points, PointImpl::new, ownerDescription); + return getMinOrMaxQ.applyAsDouble(extrapolatedPoint); } else { - Map.Entry e1 = points.floorEntry(p); - Map.Entry e2 = points.ceilingEntry(p); - if (e1 == null && e2 != null) { - return e2.getValue().getMaxQ(); - } else if (e1 != null && e2 == null) { - return e1.getValue().getMaxQ(); - } else if (e1 != null && e2 != null) { - Point p1 = e1.getValue(); - Point p2 = e2.getValue(); - return p1.getMaxQ() + (p2.getMaxQ() - p1.getMaxQ()) / (p2.getP() - p1.getP()) * (p - p1.getP()); - } else { - throw new IllegalStateException(); + if (p < this.getMinP()) { // p < minP + Point pMin = points.firstEntry().getValue(); + return getMinOrMaxQ.applyAsDouble(pMin); + } else { // p > maxP + Point pMax = points.lastEntry().getValue(); + return getMinOrMaxQ.applyAsDouble(pMax); } } } + } diff --git a/iidm/iidm-impl/src/test/java/com/powsybl/iidm/network/impl/ReactiveCapabilityCurveImplTest.java b/iidm/iidm-impl/src/test/java/com/powsybl/iidm/network/impl/ReactiveCapabilityCurveImplTest.java index b56d2decd2d..bbcd2b81295 100644 --- a/iidm/iidm-impl/src/test/java/com/powsybl/iidm/network/impl/ReactiveCapabilityCurveImplTest.java +++ b/iidm/iidm-impl/src/test/java/com/powsybl/iidm/network/impl/ReactiveCapabilityCurveImplTest.java @@ -10,6 +10,8 @@ import com.powsybl.iidm.network.ReactiveCapabilityCurve.Point; import com.powsybl.iidm.network.impl.ReactiveCapabilityCurveImpl.PointImpl; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.util.TreeMap; @@ -25,11 +27,11 @@ private ReactiveCapabilityCurveImpl createCurve(Point... points) { for (Point pt : points) { map.put(pt.getP(), pt); } - return new ReactiveCapabilityCurveImpl(map); + return new ReactiveCapabilityCurveImpl(map, "ReactiveCapabilityCurve owner"); } @Test - void testInterpolation() { + void testReactiveCapabilityCurve() { ReactiveCapabilityCurveImpl curve = createCurve(new PointImpl(100.0, 200.0, 300.0), new PointImpl(200.0, 300.0, 400.0)); // bounds test @@ -51,4 +53,33 @@ void testInterpolation() { assertEquals(400.0, curve.getMaxQ(1000.0), 0.0); } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testReactiveCapabilityCurveWithReactiveLimitsExtrapolation(boolean extrapolate) { + ReactiveCapabilityCurveImpl curve = createCurve(new PointImpl(100.0, 200.0, 300.0), + new PointImpl(200.0, 300.0, 400.0), + new PointImpl(300.0, 300.0, 400.0), + new PointImpl(400.0, 310.0, 390.0)); + // bounds test + assertEquals(200.0, curve.getMinQ(100.0, extrapolate), 0.0); + assertEquals(300.0, curve.getMaxQ(100.0, extrapolate), 0.0); + assertEquals(300.0, curve.getMinQ(200.0, extrapolate), 0.0); + assertEquals(400.0, curve.getMaxQ(200.0, extrapolate), 0.0); + + // interpolation test + assertEquals(250.0, curve.getMinQ(150.0, extrapolate), 0.0); + assertEquals(350.0, curve.getMaxQ(150.0, extrapolate), 0.0); + assertEquals(210.0, curve.getMinQ(110.0, extrapolate), 0.0); + assertEquals(310.0, curve.getMaxQ(110.0, extrapolate), 0.0); + + // out of bounds test + assertEquals(extrapolate ? 100.0 : 200.0, curve.getMinQ(0.0, extrapolate), 0.0); + assertEquals(extrapolate ? 200.0 : 300.0, curve.getMaxQ(0.0, extrapolate), 0.0); + assertEquals(extrapolate ? 320.0 : 310.0, curve.getMinQ(500.0, extrapolate), 0.0); + assertEquals(extrapolate ? 380.0 : 390.0, curve.getMaxQ(500.0, extrapolate), 0.0); + + // intersecting reactive limits test + assertEquals(extrapolate ? 350.0 : 310.0, curve.getMinQ(1500.0, extrapolate), 0.0); + assertEquals(extrapolate ? 350.0 : 390.0, curve.getMaxQ(1500.0, extrapolate), 0.0); + } }