diff --git a/docs/grid_features/loadflow_validation.md b/docs/grid_features/loadflow_validation.md index e8927878fbf..e8bc60582a9 100644 --- a/docs/grid_features/loadflow_validation.md +++ b/docs/grid_features/loadflow_validation.md @@ -124,7 +124,7 @@ in susceptance, so that they are voltage dependent. $targetP = 0$ MW -- If the regulation mode is `OFF`, then $targetQ$ is constant +- If the regulation is disabled, then $targetQ$ is constant - If the regulation mode is `REACTIVE_POWER`, it behaves like a generator without voltage regulation - If the regulation mode is `VOLTAGE`, it behaves like a generator with voltage regulation with the following bounds (dependent on the voltage, which is not the case for generators): $minQ = - Bmax * V^2$ and $maxQ = - Bmin V^2$ diff --git a/docs/user/itools/loadflow-validation.md b/docs/user/itools/loadflow-validation.md index 45d6fb207f1..2ed13e26104 100644 --- a/docs/user/itools/loadflow-validation.md +++ b/docs/user/itools/loadflow-validation.md @@ -156,7 +156,18 @@ incomplete to go through the rest of the validation. In this section, we go into more details about the checks performed by the validation feature of load-flow results available in PowSyBl. ### Buses -If all values are present, or if only one value is missing, the result is considered to be consistent. +The bus active and reactive power balances are considered consistent when: + +$$ +\begin{aligned} +\left| \sum_{load} P + \sum_{injections} P \right| \leq \epsilon \\ +\left| \sum_{load} Q + \sum_{injections} Q \right| \leq \epsilon +\end{aligned} +$$ + +- `P injections` and `Q injections` are the sums of connected injections (generators, batteries, shunts, SVCs, VSC, lines, dangling lines, and transformers) +- `P load` and `Q load` are the sums of connected loads. + Note that if the result contains only the voltages (phase and angle), the PowSyBl validation provides a load-flow results completion feature. It can be used to compute the flows from the voltages to ensure the result consistency, with the run-computation option of the PowSyBl validation. @@ -178,6 +189,8 @@ check more leniently. In case the voltages are available but not the powers, the result completion feature of the PowSyBl validation can be used to recompute them using the validation equations (meaning that the branch validation tests will always be OK, so that it allows performing the bus validation tests). +In case of disconnected branch, $P_i$ and $Q_i$ must be undefined or approximately equal to zero. + ### Three-winding transformers To be implemented, based on a conversion into 3 two-winding transformers. @@ -211,16 +224,18 @@ $$ \end{aligned} $$ -In the PowSyBl validation, there are a few tricks to handle special cases: +In the PowSyBl validation, there are a few tricks to handle special cases before applying the nominal active/reactive/voltage rules +- if `P` or `Q` is missing, validation fails if setpoints are defined and non-zero - if $minQ > maxQ$, then the values are switched to recover a meaningful interval if `noRequirementIfReactiveBoundInversion = false` - in case of a missing value, the corresponding test is OK -- $minQ$ and $maxQ$ are function of $P$. If $targetP$ is outside $[minP, maxP]$, no test is done. +- $minQ$ and $maxQ$ are function of $P$. If $targetP$ is outside $[minP, maxP]$, and `noRequirementIfSetpointOutsidePowerBounds = true`, generator validation checks are skipped. ### Loads To be implemented, with tests similar to generators with voltage regulation. -### Shunts -The two following conditions must be fulfilled in valid results: +### Shunt Compensator +#### Linear model +For connected shunts, the two following conditions must be fulfilled for valid results: $$ \begin{aligned} @@ -229,22 +244,24 @@ $$ \end{aligned} $$ +Additional condition for disconnected shunts: +- If the shunt is disconnected, `Q` must be undefined or equal to `0` + ### Static VAR Compensators -The following conditions must be fulfilled in valid results: -$targetP = 0MW$ -- If the regulation mode is `OFF`, then +The following conditions must be fulfilled for valid results: -$$\left| targetQ - Q \right| < \epsilon$$ +- If the regulation is disabled, then $|Q| <= \epsilon$. +- Static VAR Compensators behave like generators producing zero active power: $|P - targetP| <= \epsilon$, with $targetP = 0$ MW. +- If `P` or `Q` is missing, then reactive power setpoint must be undefined equal to `0` +- If the regulation mode is `REACTIVE_POWER`, same checks as a generator without voltage regulation: -- If the regulation mode is `REACTIVE_POWER`, same checks as a generator without voltage regulation + $|Q - reactivePowerSetpoint| <= \epsilon$. - If the regulation mode is `VOLTAGE`, same checks as a generator with voltage regulation with the following bounds: -$$ -\begin{aligned} -minQ = - B_{max} * V^2 \\ -maxQ = - B_{min} * V^2 -\end{aligned} -$$ + $minQ = -Bmax * V^2$ and $maxQ = -Bmin * V^2$. + - If $V < voltageSetpoint$, then `Q` must match `maxQ`. + - If $V > voltageSetpoint$, then `Q` must match `minQ`. + - If $|V - voltageSetpoint| <= \epsilon$, then `Q` must be within `[minQ, maxQ]`. ### HVDC lines To be done. diff --git a/iidm/iidm-api/src/main/java/com/powsybl/iidm/network/Bus.java b/iidm/iidm-api/src/main/java/com/powsybl/iidm/network/Bus.java index b6a3367a545..410ec3859bf 100644 --- a/iidm/iidm-api/src/main/java/com/powsybl/iidm/network/Bus.java +++ b/iidm/iidm-api/src/main/java/com/powsybl/iidm/network/Bus.java @@ -12,7 +12,7 @@ /** * A bus is a set of equipments connected together through a closed switch. - *

It could be a configured object ot a result of a computation depending of the + *

It could be a configured object ot a result of a computation depending on the * context.

* * @author Geoffroy Jamgotchian {@literal } diff --git a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/BusesValidation.java b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/BusesValidation.java index 5bb0cf293f1..8004235cf16 100644 --- a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/BusesValidation.java +++ b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/BusesValidation.java @@ -22,9 +22,14 @@ import com.powsybl.loadflow.validation.io.ValidationWriter; +import static com.powsybl.loadflow.validation.ValidationUtils.*; + /** * * @author Massimo Ferraro {@literal } + * + * Rule for valid results :
+ * |incomingP + loadP| <= threshold and |incomingQ + loadQ| <= threshold */ public final class BusesValidation { @@ -157,15 +162,11 @@ public boolean checkBuses(String id, double loadP, double loadQ, double genP, do double incomingP = genP + batP + shuntP + svcP + vscCSP + lineP + boundaryLineP + t2wtP + t3wtP; double incomingQ = genQ + batQ + shuntQ + svcQ + vscCSQ + lineQ + boundaryLineQ + t2wtQ + t3wtQ; - if (ValidationUtils.isMainComponent(config, mainComponent)) { - if (ValidationUtils.areNaN(config, incomingP, loadP) || Math.abs(incomingP + loadP) > config.getThreshold()) { - LOGGER.warn("{} {}: {} P {} {}", ValidationType.BUSES, ValidationUtils.VALIDATION_ERROR, id, incomingP, loadP); - validated = false; - } - if (ValidationUtils.areNaN(config, incomingQ, loadQ) || Math.abs(incomingQ + loadQ) > config.getThreshold()) { - LOGGER.warn("{} {}: {} Q {} {}", ValidationType.BUSES, ValidationUtils.VALIDATION_ERROR, id, incomingQ, loadQ); - validated = false; - } + if (isMainComponent(config, mainComponent)) { + // |incomingP + loadP| <= threshold + validated &= validatePowerBalance(id, "P", incomingP, loadP, config); + // |incomingQ + loadQ| <= threshold + validated &= validatePowerBalance(id, "Q", incomingQ, loadQ, config); } try { busesWriter.write(id, incomingP, incomingQ, loadP, loadQ, genP, genQ, batP, batQ, shuntP, shuntQ, svcP, svcQ, vscCSP, vscCSQ, @@ -175,4 +176,17 @@ public boolean checkBuses(String id, double loadP, double loadQ, double genP, do } return validated; } + + private boolean validatePowerBalance(String id, String balanceType, double incomingPower, double loadPower, ValidationConfig config) { + if (!areNaN(config, incomingPower, loadPower) && !isBalanceInconsistent(incomingPower, loadPower, config.getThreshold())) { + return true; + } + LOGGER.warn("{} {}: {} {} {} {}", ValidationType.BUSES, ValidationUtils.VALIDATION_ERROR, id, balanceType, incomingPower, loadPower); + return false; + } + + private static boolean isBalanceInconsistent(double incomingP, double loadP, double threshold) { + return isOutsideTolerance(incomingP, -loadP, threshold); + } + } diff --git a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/FlowsValidation.java b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/FlowsValidation.java index 083dac9df4f..279b9d23b74 100644 --- a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/FlowsValidation.java +++ b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/FlowsValidation.java @@ -27,9 +27,15 @@ import com.powsybl.iidm.network.util.BranchData; import com.powsybl.loadflow.validation.io.ValidationWriter; +import static com.powsybl.loadflow.validation.ValidationUtils.*; + /** * * @author Geoffroy Jamgotchian {@literal } + * + * Rules for valid results:
+ * Rule 1: checks disconnected terminal: P and Q must be undefined or ~0
+ * Rule 2: checks connected terminal: |P - Pcalc| <= ε and |Q - Qcalc| <= ε */ public final class FlowsValidation { @@ -46,7 +52,7 @@ public boolean checkFlows(BranchData branch, ValidationConfig config, Writer wri Objects.requireNonNull(config); Objects.requireNonNull(writer); - try (ValidationWriter flowsWriter = ValidationUtils.createValidationWriter(branch.getId(), config, writer, ValidationType.FLOWS)) { + try (ValidationWriter flowsWriter = createValidationWriter(branch.getId(), config, writer, ValidationType.FLOWS)) { return checkFlows(branch, config, flowsWriter); } catch (IOException e) { throw new UncheckedIOException(e); @@ -61,15 +67,15 @@ public boolean checkFlows(BranchData branch, ValidationConfig config, Validation boolean validated = true; if (!branch.isConnected1()) { - validated &= checkDisconnectedTerminal(branch.getId(), "1", branch.getP1(), branch.getComputedP1(), branch.getQ1(), branch.getComputedQ1(), config); + validated &= checkDisconnectedTerminal(branch.getId(), "1", branch.getP1(), branch.getComputedP1(), branch.getQ1(), branch.getComputedQ1(), config.getThreshold()); } if (!branch.isConnected2()) { - validated &= checkDisconnectedTerminal(branch.getId(), "2", branch.getP2(), branch.getComputedP2(), branch.getQ2(), branch.getComputedQ2(), config); + validated &= checkDisconnectedTerminal(branch.getId(), "2", branch.getP2(), branch.getComputedP2(), branch.getQ2(), branch.getComputedQ2(), config.getThreshold()); } - if (branch.isConnected1() && ValidationUtils.isMainComponent(config, branch.isMainComponent1())) { + if (branch.isConnected1() && isMainComponent(config, branch.isMainComponent1())) { validated &= checkConnectedTerminal(branch.getId(), "1", branch.getP1(), branch.getComputedP1(), branch.getQ1(), branch.getComputedQ1(), config); } - if (branch.isConnected2() && ValidationUtils.isMainComponent(config, branch.isMainComponent2())) { + if (branch.isConnected2() && isMainComponent(config, branch.isMainComponent2())) { validated &= checkConnectedTerminal(branch.getId(), "2", branch.getP2(), branch.getComputedP2(), branch.getQ2(), branch.getComputedQ2(), config); } try { @@ -90,14 +96,14 @@ public boolean checkFlows(BranchData branch, ValidationConfig config, Validation return validated; } - private static boolean checkDisconnectedTerminal(String id, String terminalNumber, double p, double pCalc, double q, double qCalc, ValidationConfig config) { + private static boolean checkDisconnectedTerminal(String id, String terminalNumber, double p, double pCalc, double q, double qCalc, double threshold) { boolean validated = true; - if (!Double.isNaN(p) && Math.abs(p) > config.getThreshold()) { - LOGGER.warn("{} {}: {} disconnected P{} {} {}", ValidationType.FLOWS, ValidationUtils.VALIDATION_ERROR, id, terminalNumber, p, pCalc); + if (!Double.isNaN(p) && isOutsideTolerance(p, 0.0, threshold)) { + LOGGER.warn("{} {}: {} disconnected P{} {} {}", ValidationType.FLOWS, VALIDATION_ERROR, id, terminalNumber, p, pCalc); validated = false; } - if (!Double.isNaN(q) && Math.abs(q) > config.getThreshold()) { - LOGGER.warn("{} {}: {} disconnected Q{} {} {}", ValidationType.FLOWS, ValidationUtils.VALIDATION_ERROR, id, terminalNumber, q, qCalc); + if (!Double.isNaN(q) && isOutsideTolerance(q, 0.0, threshold)) { + LOGGER.warn("{} {}: {} disconnected Q{} {} {}", ValidationType.FLOWS, VALIDATION_ERROR, id, terminalNumber, q, qCalc); validated = false; } return validated; @@ -105,12 +111,12 @@ private static boolean checkDisconnectedTerminal(String id, String terminalNumbe private static boolean checkConnectedTerminal(String id, String terminalNumber, double p, double pCalc, double q, double qCalc, ValidationConfig config) { boolean validated = true; - if (ValidationUtils.areNaN(config, pCalc) || Math.abs(p - pCalc) > config.getThreshold()) { - LOGGER.warn("{} {}: {} P{} {} {}", ValidationType.FLOWS, ValidationUtils.VALIDATION_ERROR, id, terminalNumber, p, pCalc); + if (areNaN(config, pCalc) || isOutsideTolerance(p, pCalc, config.getThreshold())) { + LOGGER.warn("{} {}: {} P{} {} {}", ValidationType.FLOWS, VALIDATION_ERROR, id, terminalNumber, p, pCalc); validated = false; } - if (ValidationUtils.areNaN(config, qCalc) || Math.abs(q - qCalc) > config.getThreshold()) { - LOGGER.warn("{} {}: {} Q{} {} {}", ValidationType.FLOWS, ValidationUtils.VALIDATION_ERROR, id, terminalNumber, q, qCalc); + if (areNaN(config, qCalc) || isOutsideTolerance(q, qCalc, config.getThreshold())) { + LOGGER.warn("{} {}: {} Q{} {} {}", ValidationType.FLOWS, VALIDATION_ERROR, id, terminalNumber, q, qCalc); validated = false; } return validated; @@ -121,7 +127,7 @@ public boolean checkFlows(Line l, ValidationConfig config, Writer writer) { Objects.requireNonNull(config); Objects.requireNonNull(writer); - try (ValidationWriter flowsWriter = ValidationUtils.createValidationWriter(l.getId(), config, writer, ValidationType.FLOWS)) { + try (ValidationWriter flowsWriter = createValidationWriter(l.getId(), config, writer, ValidationType.FLOWS)) { return checkFlows(l, config, flowsWriter); } catch (IOException e) { throw new UncheckedIOException(e); @@ -142,7 +148,7 @@ public boolean checkFlows(TwoWindingsTransformer twt, ValidationConfig config, W Objects.requireNonNull(config); Objects.requireNonNull(writer); - try (ValidationWriter flowsWriter = ValidationUtils.createValidationWriter(twt.getId(), config, writer, ValidationType.FLOWS)) { + try (ValidationWriter flowsWriter = createValidationWriter(twt.getId(), config, writer, ValidationType.FLOWS)) { return checkFlows(twt, config, flowsWriter); } catch (IOException e) { throw new UncheckedIOException(e); @@ -168,7 +174,7 @@ public boolean checkFlows(TieLine tl, ValidationConfig config, Writer writer) { Objects.requireNonNull(config); Objects.requireNonNull(writer); - try (ValidationWriter flowsWriter = ValidationUtils.createValidationWriter(tl.getId(), config, writer, ValidationType.FLOWS)) { + try (ValidationWriter flowsWriter = createValidationWriter(tl.getId(), config, writer, ValidationType.FLOWS)) { return checkFlows(tl, config, flowsWriter); } catch (IOException e) { throw new UncheckedIOException(e); @@ -189,7 +195,7 @@ public boolean checkFlows(Network network, ValidationConfig config, Writer write Objects.requireNonNull(config); Objects.requireNonNull(writer); - try (ValidationWriter flowsWriter = ValidationUtils.createValidationWriter(network.getId(), config, writer, ValidationType.FLOWS)) { + try (ValidationWriter flowsWriter = createValidationWriter(network.getId(), config, writer, ValidationType.FLOWS)) { return checkFlows(network, config, flowsWriter); } catch (IOException e) { throw new UncheckedIOException(e); diff --git a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/GeneratorsValidation.java b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/GeneratorsValidation.java index dbff3aea94d..8698dea5db5 100644 --- a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/GeneratorsValidation.java +++ b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/GeneratorsValidation.java @@ -19,14 +19,27 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.powsybl.iidm.network.Bus; import com.powsybl.iidm.network.Generator; import com.powsybl.iidm.network.Network; import com.powsybl.loadflow.validation.io.ValidationWriter; +import static com.powsybl.loadflow.validation.ValidationUtils.*; + /** * * @author Massimo Ferraro {@literal } + * @author Samir Romdhani {@literal } + * + * Rules for valid results :
+ * Rule 1: A validation error should be detected if there is both a voltage and a target but no p or q
+ * Rule 2: If reactive limits are inverted (`maxQ < minQ`) and noRequirementIfReactiveBoundInversion = true, generator validation OK.
+ * Rule 3: Active setpoint outside bounds, if `targetP` is outside `[minP, maxP]` and noRequirementIfSetpointOutsidePowerBounds = true, generator validation OK
+ * Rule 4: Active power p matches expected setpoint
+ * Rule 5: If voltage regulator is disabled, reactive power Q matches targetQ
+ * Rule 6: If voltage regulator is enabled, reactive power q follow V/targetV logic
+ * - qGen ~ minQ if V > targetV + threshold
+ * - qGen ~ maxQ if V < targetV - threshold
+ * - else qGen within [minQ, maxQ]) */ public final class GeneratorsValidation { @@ -89,7 +102,6 @@ public boolean checkGenerators(Generator gen, ValidationConfig config, Validatio Objects.requireNonNull(generatorsWriter); double p = gen.getTerminal().getP(); double q = gen.getTerminal().getQ(); - Bus bus = gen.getTerminal().getBusView().getBus(); double targetP = gen.getTargetP(); double targetQ = gen.getTargetQ(); double targetV = gen.getTargetV(); @@ -98,11 +110,10 @@ public boolean checkGenerators(Generator gen, ValidationConfig config, Validatio double maxP = gen.getMaxP(); double minQ = gen.getReactiveLimits().getMinQ(targetP); double maxQ = gen.getReactiveLimits().getMaxQ(targetP); - double v = bus != null ? bus.getV() : Double.NaN; - boolean connected = bus != null; - Bus connectableBus = gen.getTerminal().getBusView().getConnectableBus(); - boolean connectableMainComponent = connectableBus != null && connectableBus.isInMainConnectedComponent(); - boolean mainComponent = bus != null ? bus.isInMainConnectedComponent() : connectableMainComponent; + TerminalState terminalState = getTerminalState(gen.getTerminal()); + double v = terminalState.v(); + boolean connected = terminalState.connected(); + boolean mainComponent = terminalState.mainComponent(); return checkGenerators(gen.getId(), p, q, v, targetP, targetQ, targetV, voltageRegulatorOn, minP, maxP, minQ, maxQ, connected, mainComponent, config, generatorsWriter, guesser); } @@ -129,14 +140,13 @@ public boolean checkGenerators(String id, double p, double q, double v, double t Objects.requireNonNull(config); Objects.requireNonNull(generatorsWriter); boolean validated = true; - double expectedP = getExpectedP(guesser, id, p, targetP, minP, maxP, config.getThreshold()); - if (connected && ValidationUtils.isMainComponent(config, mainComponent)) { - if (Double.isNaN(p) || Double.isNaN(q)) { - validated = checkGeneratorsNaNValues(id, p, q, targetP, targetQ); - } else if (checkReactiveBoundInversion(minQ, maxQ, config)) { // when maxQ < minQ if noRequirementIfReactiveBoundInversion return true + if (isConnectedAndMainComponent(connected, mainComponent, config)) { + if (areNaN(p, q)) { + validated = validateMissingPQRule(id, p, q, targetP, targetQ); + } else if (isGenReactiveBoundInverted(minQ, maxQ, config.getThreshold(), config.isNoRequirementIfReactiveBoundInversion())) { validated = true; - } else if (checkSetpointOutsidePowerBounds(targetP, minP, maxP, config)) { // when targetP < minP or targetP > maxP if noRequirementIfSetpointOutsidePowerBounds return true + } else if (isGenSetpointOutsidePowerBounds(targetP, minP, maxP, config.getThreshold(), config.isNoRequirementIfSetpointOutsidePowerBounds())) { validated = true; } else { validated = checkGeneratorsValues(id, p, q, v, expectedP, targetQ, targetV, voltageRegulatorOn, minQ, maxQ, config); @@ -168,59 +178,87 @@ private static double getExpectedP(BalanceTypeGuesser guesser, String id, double } } - private static boolean checkGeneratorsNaNValues(String id, double p, double q, double targetP, double targetQ) { - // a validation error should be detected if there is both a voltage and a target but no p or q - if (!Double.isNaN(targetP) && targetP != 0 - || !Double.isNaN(targetQ) && targetQ != 0) { - LOGGER.warn("{} {}: {}: P={} targetP={} - Q={} targetQ={}", ValidationType.GENERATORS, ValidationUtils.VALIDATION_ERROR, id, p, targetP, q, targetQ); - return false; - } - return true; - } - private static boolean checkGeneratorsValues(String id, double p, double q, double v, double expectedP, double targetQ, double targetV, boolean voltageRegulatorOn, double minQ, double maxQ, ValidationConfig config) { boolean validated = true; - // active power should be equal to setpoint - if (ValidationUtils.areNaN(config, expectedP) || Math.abs(p + expectedP) > config.getThreshold()) { + double threshold = config.getThreshold(); + // Rule 4: Active power p matches expected setpoint + if (areNaN(config, expectedP) || isGenActivePowerInconsistent(p, expectedP, threshold)) { LOGGER.warn("{} {}: {}: P={} expectedP={}", ValidationType.GENERATORS, ValidationUtils.VALIDATION_ERROR, id, p, expectedP); validated = false; } - // if voltageRegulatorOn="false" then reactive power should be equal to setpoint - if (!voltageRegulatorOn && (ValidationUtils.areNaN(config, targetQ) || Math.abs(q + targetQ) > config.getThreshold())) { + + //Rule 5: If voltage regulator is disabled, Reactive power Q matches targetQ + if (!voltageRegulatorOn && (areNaN(config, targetQ) || isGenReactivePowerInconsistent(q, targetQ, threshold))) { LOGGER.warn("{} {}: {}: voltage regulator off - Q={} targetQ={}", ValidationType.GENERATORS, ValidationUtils.VALIDATION_ERROR, id, q, targetQ); validated = false; } - // if voltageRegulatorOn="true" then - // either q is equal to g.getReactiveLimits().getMinQ(p) and V is higher than g.getTargetV() - // or q is equal to g.getReactiveLimits().getMaxQ(p) and V is lower than g.getTargetV() - // or V at the connected bus is equal to g.getTargetV() and the reactive bounds are satisfied + double qGen = -q; - if (voltageRegulatorOn - && (ValidationUtils.areNaN(config, minQ, maxQ, targetV) - || v > targetV + config.getThreshold() && Math.abs(qGen - getMinQ(minQ, maxQ)) > config.getThreshold() - || v < targetV - config.getThreshold() && Math.abs(qGen - getMaxQ(minQ, maxQ)) > config.getThreshold() - || Math.abs(v - targetV) <= config.getThreshold() && !ValidationUtils.boundedWithin(minQ, maxQ, qGen, config.getThreshold()))) { + // Rule 6: If voltage regulator is enabled, reactive power q follow V/targetV logic + // - qGen ~ minQ if V > targetV + threshold + // - qGen ~ maxQ if V < targetV - threshold + // - else qGen within [minQ, maxQ]) + if (voltageRegulatorOn && (ValidationUtils.areNaN(config, minQ, maxQ, targetV) || isGenVoltageRegulationInconsistent(qGen, v, targetV, minQ, maxQ, threshold))) { LOGGER.warn("{} {}: {}: voltage regulator on - Q={} minQ={} maxQ={} - V={} targetV={}", ValidationType.GENERATORS, ValidationUtils.VALIDATION_ERROR, id, qGen, minQ, maxQ, v, targetV); validated = false; } return validated; } - private static double getMaxQ(double minQ, double maxQ) { - return maxQ < minQ ? minQ : maxQ; + /** + * Rule 1: a validation error should be detected if there is both a voltage and a target but no p or q + */ + private static boolean validateMissingPQRule(String id, double p, double q, double targetP, double targetQ) { + if (!Double.isNaN(targetP) && targetP != 0 || !Double.isNaN(targetQ) && targetQ != 0) { + LOGGER.warn("{} {}: {}: P={} targetP={} - Q={} targetQ={}", ValidationType.GENERATORS, ValidationUtils.VALIDATION_ERROR, id, p, targetP, q, targetQ); + return false; + } + return true; + } + + /** + * Rule 2: rule for valid result: if reactive limits are inverted (`maxQ < minQ`) and noRequirementIfReactiveBoundInversion = true, generator validation OK. + */ + private static boolean isGenReactiveBoundInverted(double minQ, double maxQ, double threshold, boolean isNoRequirementIfReactiveBoundInversion) { + return maxQ < minQ - threshold && isNoRequirementIfReactiveBoundInversion; } - private static double getMinQ(double minQ, double maxQ) { - return maxQ < minQ ? maxQ : minQ; + /** + * Rule 3: rule for valid result: active setpoint outside bounds, if `targetP` is outside `[minP, maxP]` and noRequirementIfSetpointOutsidePowerBounds = true, generator validation OK + */ + private static boolean isGenSetpointOutsidePowerBounds(double targetP, double minP, double maxP, double threshold, boolean isNoRequirementIfSetpointOutsidePowerBounds) { + return (targetP < minP - threshold || targetP > maxP + threshold) && isNoRequirementIfSetpointOutsidePowerBounds; } - private static boolean checkReactiveBoundInversion(double minQ, double maxQ, ValidationConfig config) { - return maxQ < minQ - config.getThreshold() && config.isNoRequirementIfReactiveBoundInversion(); + /** + * Rule 4: rule for valid result: Active power p matches expected setpoint + */ + private static boolean isGenActivePowerInconsistent(double p, double expectedP, double threshold) { + return isOutsideTolerance(p, -expectedP, threshold); } - private static boolean checkSetpointOutsidePowerBounds(double targetP, double minP, double maxP, ValidationConfig config) { - return (targetP < minP - config.getThreshold() || targetP > maxP + config.getThreshold()) && config.isNoRequirementIfSetpointOutsidePowerBounds(); + /** + * Rule 5: rule for valid result: Reactive power Q matches targetQ + */ + private static boolean isGenReactivePowerInconsistent(double q, double targetQ, double threshold) { + return isOutsideOrAtTolerance(q, -targetQ, threshold); + } + + /** + * Rule 6: rule for valid result:

+ * targetV - V < threshold && |Q - minQ| <= threshold

+ * V - targetV < threshold && |Q - maxQ| <= threshold

+ * |V - targetV| < threshold && minQ <= Q <= maxQ + */ + private static boolean isGenVoltageRegulationInconsistent(double qGen, double v, double targetV, double minQ, double maxQ, double threshold) { + + // When V is higher than g.getTargetV() then q must equal to g.getReactiveLimits().getMinQ(p) + // When V is lower than g.getTargetV() q must equal to g.getReactiveLimits().getMaxQ(p) + // When V is equal to g.getTargetV() then q (reactive bounds) must satisfy + return v > targetV + threshold && Math.abs(qGen - Math.min(minQ, maxQ)) > threshold + || v < targetV - threshold && Math.abs(qGen - Math.max(minQ, maxQ)) > threshold + || Math.abs(v - targetV) <= threshold && !boundedWithin(minQ, maxQ, qGen, threshold); } } diff --git a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/ShuntCompensatorsValidation.java b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/ShuntCompensatorsValidation.java index c66e4e72824..9d38d896655 100644 --- a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/ShuntCompensatorsValidation.java +++ b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/ShuntCompensatorsValidation.java @@ -23,9 +23,16 @@ import com.powsybl.loadflow.validation.io.ValidationWriter; +import static com.powsybl.loadflow.validation.ValidationUtils.*; + /** * * @author Massimo Ferraro {@literal } + * + * Rules for valid results :
+ * Rule1: |p| < e
+ * Rule2: q must match expectedQ
+ * Rule3: if the shunt is disconnected, q should be undefined or 0 */ public final class ShuntCompensatorsValidation { @@ -96,12 +103,10 @@ public boolean checkShunts(ShuntCompensator shunt, ValidationConfig config, Vali double bPerSection = shunt.getModel(ShuntCompensatorLinearModel.class).getBPerSection(); double nominalV = shunt.getTerminal().getVoltageLevel().getNominalV(); double qMax = bPerSection * maximumSectionCount * nominalV * nominalV; - Bus bus = shunt.getTerminal().getBusView().getBus(); - double v = bus != null ? bus.getV() : Double.NaN; - boolean connected = bus != null; - Bus connectableBus = shunt.getTerminal().getBusView().getConnectableBus(); - boolean connectableMainComponent = connectableBus != null && connectableBus.isInMainConnectedComponent(); - boolean mainComponent = bus != null ? bus.isInMainConnectedComponent() : connectableMainComponent; + TerminalState terminalState = getTerminalState(shunt.getTerminal()); + double v = terminalState.v(); + boolean connected = terminalState.connected(); + boolean mainComponent = terminalState.mainComponent(); return checkShunts(shunt.getId(), p, q, currentSectionCount, maximumSectionCount, bPerSection, v, qMax, nominalV, connected, mainComponent, config, shuntsWriter); } @@ -119,24 +124,31 @@ public boolean checkShunts(String id, double p, double q, int currentSectionCoun } } + /** + * - Rule1: |p| < e
+ * - Rule2: q must match expectedQ
+ * - Rule3: if the shunt is disconnected, q should be NaN or 0 + */ public boolean checkShunts(String id, double p, double q, int currentSectionCount, int maximumSectionCount, double bPerSection, double v, double qMax, double nominalV, boolean connected, boolean mainComponent, ValidationConfig config, ValidationWriter shuntsWriter) { boolean validated = true; - - if (!connected && !Double.isNaN(q) && q != 0) { // if the shunt is disconnected then either “q” is not defined or “q” is 0 + double threshold = config.getThreshold(); + // Rule3: if the shunt is disconnected, q should be undefined or 0 + if (!connected && !isUndefinedOrZero(q, threshold)) { LOGGER.warn("{} {}: {}: disconnected shunt Q {}", ValidationType.SHUNTS, ValidationUtils.VALIDATION_ERROR, id, q); validated = false; } - // “q” = - bPerSection * currentSectionCount * v^2 - double expectedQ = -bPerSection * currentSectionCount * v * v; - if (connected && ValidationUtils.isMainComponent(config, mainComponent)) { - // “p” is always NaN - if (!Double.isNaN(p)) { + // “expectedQ” = - bPerSection * currentSectionCount * v^2 + double expectedQ = computeShuntExpectedQ(bPerSection, currentSectionCount, v); + if (isConnectedAndMainComponent(connected, mainComponent, config)) { + // Rule1: |p| < e + if (!Double.isNaN(p) && Math.abs(p) > threshold) { LOGGER.warn("{} {}: {}: P={}", ValidationType.SHUNTS, ValidationUtils.VALIDATION_ERROR, id, p); validated = false; } - if (ValidationUtils.areNaN(config, q, expectedQ) || Math.abs(q - expectedQ) > config.getThreshold()) { + // Rule2: q must match expectedQ + if (areNaN(config, q, expectedQ) || isOutsideTolerance(q, expectedQ, threshold)) { LOGGER.warn("{} {}: {}: Q {} {}", ValidationType.SHUNTS, ValidationUtils.VALIDATION_ERROR, id, q, expectedQ); validated = false; } @@ -148,4 +160,9 @@ public boolean checkShunts(String id, double p, double q, int currentSectionCoun } return validated; } + + // expectedQ = - #sections * B * v^2 + private static double computeShuntExpectedQ(double bPerSection, int sectionCount, double v) { + return -bPerSection * sectionCount * v * v; + } } diff --git a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/StaticVarCompensatorsValidation.java b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/StaticVarCompensatorsValidation.java index ea552b7a979..47e59d5d081 100644 --- a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/StaticVarCompensatorsValidation.java +++ b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/StaticVarCompensatorsValidation.java @@ -25,9 +25,21 @@ import com.powsybl.iidm.network.StaticVarCompensator.RegulationMode; import com.powsybl.loadflow.validation.io.ValidationWriter; +import static com.powsybl.loadflow.validation.ValidationUtils.*; + /** * * @author Massimo Ferraro {@literal } + * + * Rules for valid results: + * Rule 1: active power should be equal to 0
+ * Rule 2: reactivePowerSetpoint must be 0 if p or q is missing
+ * Rule 3: regulationMode = REACTIVE_POWER, q must match reactivePowerSetpoint
+ * Rule 4: regulationMode = VOLTAGE
+ * - V is lower than voltageSetpoint (within threshold) AND q must match qMax (within threshold)
+ * - V is higher than voltageSetpoint (within threshold) AND q must match Qmin (within threshold)
+ * - V is at the controlled bus (within threshold) AND q is bounded within [Qmin=-bMax*V*V, Qmax=-bMin*V*V]
+ * Rule 5: if regulating is false then reactive power (q) should be equal to 0 */ public final class StaticVarCompensatorsValidation { @@ -97,17 +109,12 @@ public boolean checkSVCs(StaticVarCompensator svc, ValidationConfig config, Vali double bMax = svc.getBmax(); double nominalVcontroller = svc.getTerminal().getVoltageLevel().getNominalV(); double vController = bus != null ? bus.getV() : Double.NaN; - double vControlled; - if (svc.getRegulatingTerminal() != null) { - Bus controlledBus = svc.getRegulatingTerminal().getBusView().getBus(); - vControlled = controlledBus != null ? controlledBus.getV() : Double.NaN; - } else { - vControlled = vController; - } - boolean connected = bus != null; - Bus connectableBus = svc.getTerminal().getBusView().getConnectableBus(); - boolean connectableMainComponent = connectableBus != null && connectableBus.isInMainConnectedComponent(); - boolean mainComponent = bus != null ? bus.isInMainConnectedComponent() : connectableMainComponent; + double vControlled = svc.getRegulatingTerminal() != null + ? getTerminalState(svc.getRegulatingTerminal()).v() + : vController; + TerminalState terminalState = getTerminalState(svc.getTerminal()); + boolean connected = terminalState.connected(); + boolean mainComponent = terminalState.mainComponent(); return checkSVCs(svc.getId(), p, q, vControlled, vController, nominalVcontroller, reactivePowerSetpoint, voltageSetpoint, regulationMode, regulating, bMin, bMax, connected, mainComponent, config, svcsWriter); } @@ -133,9 +140,14 @@ public boolean checkSVCs(String id, double p, double q, double vControlled, doub Objects.requireNonNull(svcsWriter); boolean validated = true; - if (connected && ValidationUtils.isMainComponent(config, mainComponent)) { + if (isConnectedAndMainComponent(connected, mainComponent, config)) { + // Rule2: **reactivePowerSetpoint** must be 0 if p or q is missing (NaN) if (Double.isNaN(p) || Double.isNaN(q)) { - validated = checkSVCsNaNValues(id, p, q, reactivePowerSetpoint); + // a validation error should be detected if there is a setpoint but no p or q + if (!isUndefinedOrZero(reactivePowerSetpoint, 0.0)) { + LOGGER.warn("{} {}: {}: P={} Q={} reactivePowerSetpoint={}", ValidationType.SVCS, ValidationUtils.VALIDATION_ERROR, id, p, q, reactivePowerSetpoint); + validated = false; + } } else { validated = checkSVCsValues(id, p, q, vControlled, vController, nominalVcontroller, reactivePowerSetpoint, voltageSetpoint, regulationMode, regulating, bMin, bMax, config); } @@ -148,32 +160,21 @@ public boolean checkSVCs(String id, double p, double q, double vControlled, doub return validated; } - private static boolean checkSVCsNaNValues(String id, double p, double q, double reactivePowerSetpoint) { - // a validation error should be detected if there is a setpoint but no p or q - if (!Double.isNaN(reactivePowerSetpoint) && reactivePowerSetpoint != 0) { - LOGGER.warn("{} {}: {}: P={} Q={} reactivePowerSetpoint={}", ValidationType.SVCS, ValidationUtils.VALIDATION_ERROR, id, p, q, reactivePowerSetpoint); - return false; - } - return true; - } - private static boolean checkSVCsValues(String id, double p, double q, double vControlled, double vController, double nominalVcontroller, double reactivePowerSetpoint, double voltageSetpoint, RegulationMode regulationMode, boolean regulating, double bMin, double bMax, ValidationConfig config) { boolean validated = true; - // active power should be equal to 0 - if (Math.abs(p) > config.getThreshold()) { + double threshold = config.getThreshold(); + // Rule 1: active power should be equal to 0 + if (isOutsideTolerance(p, 0.0, threshold)) { LOGGER.warn("{} {}: {}: P={}", ValidationType.SVCS, ValidationUtils.VALIDATION_ERROR, id, p); validated = false; } - - double vAux = vController; - if (vAux == 0 || Double.isNaN(vAux)) { - vAux = nominalVcontroller; - } + double vAux = (Double.isNaN(vController) || vController == 0.0) ? nominalVcontroller : vController; double qMin = -bMax * vAux * vAux; double qMax = -bMin * vAux * vAux; + //Rule 3: **regulationMode = REACTIVE_POWER** if (reactivePowerRegulationModeKo(regulationMode, q, qMin, qMax, reactivePowerSetpoint, config)) { LOGGER.warn( "{} {}: {}: regulator mode={} - Q={} qMin={} qMax={} bMin={} bMax={} Vcontroller={} nominalV={} reactivePowerSetpoint={}", @@ -182,6 +183,7 @@ private static boolean checkSVCsValues(String id, double p, double q, double vCo validated = false; } + // Rule 4: **regulationMode = VOLTAGE** if (voltageRegulationModeKo(regulationMode, q, qMin, qMax, vControlled, voltageSetpoint, config)) { LOGGER.warn( "{} {}: {}: regulator mode={} - Q={} qMin={} qMax={} bMin={} bMax={} Vcontroller={} Vcontrolled={} targetV={}", @@ -189,10 +191,9 @@ private static boolean checkSVCsValues(String id, double p, double q, double vCo vController, vControlled, voltageSetpoint); validated = false; } - + // Rule 5: if regulating is false then reactive power (q) should be equal to 0 if (notRegulatingKo(regulating, q, config)) { - LOGGER.warn("{} {}: {}: regulator mode={} - Q={} ", ValidationType.SVCS, ValidationUtils.VALIDATION_ERROR, - id, regulationMode, q); + LOGGER.warn("{} {}: {}: regulator mode={} - Q={} ", ValidationType.SVCS, ValidationUtils.VALIDATION_ERROR, id, regulationMode, q); validated = false; } return validated; @@ -201,14 +202,13 @@ private static boolean checkSVCsValues(String id, double p, double q, double vCo private static boolean reactivePowerRegulationModeKo(RegulationMode regulationMode, double q, double qMin, double qMax, double reactivePowerSetpoint, ValidationConfig config) { // if regulationMode = REACTIVE_POWER, the reactive power must be equal to setpoint - if (regulationMode != RegulationMode.REACTIVE_POWER) { return false; } if (ValidationUtils.areNaN(config, reactivePowerSetpoint, qMin, qMax)) { return true; } - return Math.abs(q - reactivePowerSetpoint) > config.getThreshold(); + return isOutsideTolerance(q, reactivePowerSetpoint, config.getThreshold()); } private static boolean voltageRegulationModeKo(RegulationMode regulationMode, double q, double qMin, @@ -218,28 +218,25 @@ private static boolean voltageRegulationModeKo(RegulationMode regulationMode, do // or q is equal to Qmin = -bMax * V * V and V is higher than voltageSetpoint // or V at the controlled bus is equal to voltageSetpoint and q is bounded // within [Qmin=-bMax*V*V, Qmax=-bMin*V*V] - + double threshold = config.getThreshold(); if (regulationMode != RegulationMode.VOLTAGE) { return false; } if (ValidationUtils.areNaN(config, qMin, qMax, vControlled, voltageSetpoint)) { return true; } - if (vControlled < voltageSetpoint - config.getThreshold() && Math.abs(q - qMax) > config.getThreshold()) { + if (vControlled < voltageSetpoint - threshold && isOutsideTolerance(q, qMax, threshold)) { return true; } - if (vControlled > voltageSetpoint + config.getThreshold() && Math.abs(q - qMin) > config.getThreshold()) { + if (vControlled > voltageSetpoint + threshold && isOutsideTolerance(q, qMin, threshold)) { return true; } - return Math.abs(vControlled - voltageSetpoint) < config.getThreshold() && !ValidationUtils.boundedWithin(qMin, qMax, q, config.getThreshold()); + boolean voltageAtSetpoint = Math.abs(vControlled - voltageSetpoint) < threshold; + return voltageAtSetpoint && !boundedWithin(qMin, qMax, q, threshold); } private static boolean notRegulatingKo(boolean regulating, double q, ValidationConfig config) { // if regulating is false then reactive power should be equal to 0 - - if (regulating) { - return false; - } - return Math.abs(q) > config.getThreshold(); + return !regulating && Math.abs(q) > config.getThreshold(); } } diff --git a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/TransformersValidation.java b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/TransformersValidation.java index f5264d83430..2d94759dff9 100644 --- a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/TransformersValidation.java +++ b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/TransformersValidation.java @@ -21,11 +21,18 @@ import com.powsybl.iidm.network.TwoSides; import com.powsybl.loadflow.validation.io.ValidationWriter; +import static com.powsybl.loadflow.validation.ValidationUtils.getTerminalState; + /** - * Tries to validate that transformers regulating voltage have been correclty simulated. - * + * Tries to validate that transformers regulating voltage have been correctly simulated. + *
* We check that the voltage deviation from the target voltage stays inside a deadband around the target voltage, * taken equal to the maximum possible voltage increase/decrease for a one-tap change. + *
+ * Rules for valid results :
+ * Rule 1 (voltage is lower than target): if voltageDeviation (error) is negative and increase is possible : |deviation| <= downDeadband + threshold
+ * Rule 2 (voltage is higher than target): if voltageDeviation (error) is positive and decrease is possible: deviation < upDeadband + threshold
+ * Rule 3: if no increase/decrease is possible, the check is not applies on the corresponding side. * * @author Massimo Ferraro {@literal } */ @@ -97,12 +104,11 @@ public boolean checkTransformer(TwoWindingsTransformer twt, ValidationConfig con } return true; } - Bus bus = ratioTapChanger.getRegulationTerminal().getBusView().getBus(); - double v = bus != null ? bus.getV() : Double.NaN; - boolean connected = bus != null; - Bus connectableBus = ratioTapChanger.getRegulationTerminal().getBusView().getConnectableBus(); - boolean connectableMainComponent = connectableBus != null && connectableBus.isInMainConnectedComponent(); - boolean mainComponent = bus != null ? bus.isInMainConnectedComponent() : connectableMainComponent; + ValidationUtils.TerminalState terminalState = getTerminalState(ratioTapChanger.getRegulationTerminal()); + double v = terminalState.v(); + boolean connected = terminalState.connected(); + boolean mainComponent = terminalState.mainComponent(); + return checkTransformer(twt.getId(), rho, rhoPreviousStep, rhoNextStep, tapPosition, lowTapPosition, highTapPosition, targetV, regulatedSide, v, connected, mainComponent, config, twtsWriter); } @@ -130,7 +136,7 @@ public boolean checkTransformer(String id, double rho, double rhoPreviousStep, d Objects.requireNonNull(twtsWriter); boolean validated = true; - double error = v - targetV; + double error = v - targetV; // voltageDeviation double upIncrement = Double.isNaN(rhoNextStep) ? Double.NaN : evaluateVoltage(regulatedSide, v, rho, rhoNextStep) - v; double downIncrement = Double.isNaN(rhoPreviousStep) ? Double.NaN : evaluateVoltage(regulatedSide, v, rho, rhoPreviousStep) - v; if (connected && ValidationUtils.isMainComponent(config, mainComponent)) { @@ -173,24 +179,30 @@ private static boolean checkTransformerSide(String id, TwoSides side, double err LOGGER.warn("{} {}: {} side {}: error {}", ValidationType.TWTS, ValidationUtils.VALIDATION_ERROR, id, side, error); return false; } - // if error is negative, i.e if voltage is lower than target, and an increase is possible, - // check that voltage is inside the downward deadband, taken equal to the possible increase + double threshold = config.getThreshold(); + // Rule 1 (voltage is lower than target): + // if error (voltageDeviation) is negative, i.e. if voltage is lower than target, and an increase is possible, + // check that voltage is inside the downward deadband, taken equal to the possible increase: |deviation| <= downDeadband + threshold + // Rule 3 if maxIncrease is NaN, check in the corresponding side is not applied, if (error < 0 && !Double.isNaN(maxIncrease)) { - double downDeadband = maxIncrease; + double requiredIncrease = -error; + double downDeadband = maxIncrease; // available Increase - if (error + downDeadband < -config.getThreshold()) { + if (requiredIncrease > downDeadband + threshold) { LOGGER.warn("{} {}: {} side {}: error {} upIncrement {} downIncrement {}", ValidationType.TWTS, ValidationUtils.VALIDATION_ERROR, id, side, error, upIncrement, downIncrement); validated = false; } } - - // if error is positive, i.e if voltage is higher than target, and a voltage decrease is possible, - // check that voltage is inside the upward deadband, taken equal to the possible decrease + // Rule 2 (voltage is higher than target): + // if error (voltageDeviation) is positive, i.e. if voltage is higher than target, and a voltage decrease is possible, + // check that voltage is inside the upward deadband, taken equal to the possible decrease: deviation < upDeadband + threshold + // Rule 3 if maxDecrease is NaN, check in the corresponding side is not applied if (error > 0 && !Double.isNaN(maxDecrease)) { - double upDeadband = -maxDecrease; + double requiredIncrease = error; + double upDeadband = -maxDecrease; // available Decrease - if (error - upDeadband > config.getThreshold()) { + if (requiredIncrease > upDeadband + threshold) { LOGGER.warn("{} {}: {} side {}: error {} upIncrement {} downIncrement {}", ValidationType.TWTS, ValidationUtils.VALIDATION_ERROR, id, side, error, upIncrement, downIncrement); validated = false; diff --git a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/ValidationUtils.java b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/ValidationUtils.java index 696fa65115b..d23ac1de253 100644 --- a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/ValidationUtils.java +++ b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/ValidationUtils.java @@ -8,6 +8,8 @@ package com.powsybl.loadflow.validation; import com.powsybl.commons.config.ConfigurationException; +import com.powsybl.iidm.network.Bus; +import com.powsybl.iidm.network.Terminal; import com.powsybl.loadflow.validation.io.ValidationWriter; import com.powsybl.loadflow.validation.io.ValidationWriterFactory; @@ -18,6 +20,7 @@ /** * * @author Massimo Ferraro {@literal } + * @author Samir Romdhani {@literal } */ public final class ValidationUtils { @@ -55,6 +58,17 @@ public static boolean areNaN(ValidationConfig config, float... values) { return areNaN; } + public static boolean areNaN(double... values) { + boolean areMissing = false; + for (double value : values) { + if (Double.isNaN(value)) { + areMissing = true; + break; + } + } + return areMissing; + } + public static boolean areNaN(ValidationConfig config, double... values) { Objects.requireNonNull(config); if (config.areOkMissingValues()) { @@ -89,4 +103,34 @@ public static boolean isMainComponent(ValidationConfig config, boolean mainCompo return !config.isCheckMainComponentOnly() || mainComponent; } + public record TerminalState(double v, boolean connected, boolean mainComponent) { } + + public static TerminalState getTerminalState(Terminal terminal) { + Objects.requireNonNull(terminal); + Bus bus = terminal.getBusView().getBus(); + Bus connectableBus = terminal.getBusView().getConnectableBus(); + boolean connected = bus != null; + boolean connectableMainComponent = connectableBus != null && connectableBus.isInMainConnectedComponent(); + boolean mainComponent = connected ? bus.isInMainConnectedComponent() : connectableMainComponent; + double v = connected ? bus.getV() : Double.NaN; + return new TerminalState(v, connected, mainComponent); + } + + public static boolean isUndefinedOrZero(double value, double threshold) { + return Double.isNaN(value) || Math.abs(value) <= threshold; + } + + public static boolean isOutsideTolerance(double actual, double expected, double threshold) { + return Math.abs(actual - expected) > threshold; + } + + public static boolean isOutsideOrAtTolerance(double actual, double expected, double threshold) { + return Math.abs(actual - expected) >= threshold; + } + + public static boolean isConnectedAndMainComponent(boolean connected, boolean mainComponent, ValidationConfig config) { + Objects.requireNonNull(config); + return connected && isMainComponent(config, mainComponent); + } + } diff --git a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/BusesValidationTest.java b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/BusesValidationTest.java index 55980afb392..4457bc295f4 100644 --- a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/BusesValidationTest.java +++ b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/BusesValidationTest.java @@ -9,6 +9,9 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.io.IOException; import java.util.stream.Stream; @@ -16,6 +19,7 @@ import com.powsybl.iidm.network.*; import org.apache.commons.io.output.NullWriter; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -26,6 +30,7 @@ /** * * @author Massimo Ferraro {@literal } + * @author Samir Romdhani {@literal } */ class BusesValidationTest extends AbstractValidationTest { @@ -55,6 +60,7 @@ class BusesValidationTest extends AbstractValidationTest { private BoundaryLine boundaryLine; @BeforeEach + @Override void setUp() throws IOException { super.setUp(); @@ -183,11 +189,11 @@ void checkBuses() { @Test void checkNetworkBuses() throws IOException { - Network.BusView networkBusView = Mockito.mock(Network.BusView.class); - Mockito.when(networkBusView.getBusStream()).thenAnswer(dummy -> Stream.of(bus)); - Network network = Mockito.mock(Network.class); - Mockito.when(network.getId()).thenReturn("network"); - Mockito.when(network.getBusView()).thenReturn(networkBusView); + Network.BusView networkBusView = mock(Network.BusView.class); + when(networkBusView.getBusStream()).thenAnswer(dummy -> Stream.of(bus)); + Network network = mock(Network.class); + when(network.getId()).thenReturn("network"); + when(network.getBusView()).thenReturn(networkBusView); assertTrue(BusesValidation.INSTANCE.checkBuses(network, looseConfig, data)); assertFalse(BusesValidation.INSTANCE.checkBuses(network, strictConfig, data)); @@ -199,7 +205,7 @@ void checkNetworkBuses() throws IOException { assertTrue(ValidationType.BUSES.check(network, looseConfig, validationWriter)); // Consider paired boundaryLines - Mockito.when(boundaryLine.isPaired()).thenReturn(true); + when(boundaryLine.isPaired()).thenReturn(true); assertTrue(BusesValidation.INSTANCE.checkBuses(network, looseConfig, data)); assertFalse(BusesValidation.INSTANCE.checkBuses(network, strictConfig, data)); @@ -210,4 +216,57 @@ void checkNetworkBuses() throws IOException { validationWriter = ValidationUtils.createValidationWriter(network.getId(), looseConfig, NullWriter.INSTANCE, ValidationType.BUSES); assertTrue(ValidationType.BUSES.check(network, looseConfig, validationWriter)); } + + // Rule: |incomingP + loadP| <= threshold and |incomingQ + loadQ| <= threshold" + @DisplayName("P and Q balanced") + @Test + void checkBusesShouldSucceedWhenPAndQBalanced() { + // Given threshold (0.01) + Bus busForBalance = mockBusForBalance(100.0, 50.0, -100.0, -50.0); + // When + boolean result = BusesValidation.INSTANCE.checkBuses(busForBalance, strictConfig, NullWriter.INSTANCE); + // Then + assertTrue(result); + } + + @DisplayName("P and Q unbalanced") + @Test + void checkBusesShouldSucceedWhenPAndQUnbalanced() { + // Given threshold (0.01) + Bus busForBalance = mockBusForBalance(100.0, 50.0, -100.0, -49.8); + // When + boolean result = BusesValidation.INSTANCE.checkBuses(busForBalance, strictConfig, NullWriter.INSTANCE); + // Then + assertFalse(result); + } + + private Bus mockBusForBalance(double loadP, double loadQ, double genP, double genQ) { + Terminal loadTerminal = mock(Terminal.class); + when(loadTerminal.getP()).thenReturn(loadP); + when(loadTerminal.getQ()).thenReturn(loadQ); + Load load = mock(Load.class); + when(load.getTerminal()).thenReturn(loadTerminal); + Terminal genTerminal = mock(Terminal.class); + when(genTerminal.getP()).thenReturn(genP); + when(genTerminal.getQ()).thenReturn(genQ); + Generator generator = mock(Generator.class); + when(generator.getTerminal()).thenReturn(genTerminal); + + Bus busForBalance = mock(Bus.class); + when(busForBalance.getId()).thenReturn("bus-test"); + when(busForBalance.isInMainConnectedComponent()).thenReturn(true); + when(busForBalance.getLoadStream()).thenAnswer(i -> Stream.of(load)); + when(busForBalance.getGeneratorStream()).thenAnswer(i -> Stream.of(generator)); + // other contributors = 0 + when(busForBalance.getBatteryStream()).thenAnswer(i -> Stream.empty()); + when(busForBalance.getShuntCompensatorStream()).thenAnswer(i -> Stream.empty()); + when(busForBalance.getStaticVarCompensatorStream()).thenAnswer(i -> Stream.empty()); + when(busForBalance.getVscConverterStationStream()).thenAnswer(i -> Stream.empty()); + when(busForBalance.getLineStream()).thenAnswer(i -> Stream.empty()); + when(busForBalance.getBoundaryLineStream(any())).thenAnswer(i -> Stream.empty()); + when(busForBalance.getTwoWindingsTransformerStream()).thenAnswer(i -> Stream.empty()); + when(busForBalance.getThreeWindingsTransformerStream()).thenAnswer(i -> Stream.empty()); + return busForBalance; + } + } diff --git a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/FlowsValidationTest.java b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/FlowsValidationTest.java index 6a024a40376..34f1d223dc0 100644 --- a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/FlowsValidationTest.java +++ b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/FlowsValidationTest.java @@ -14,7 +14,11 @@ import com.powsybl.loadflow.validation.io.ValidationWriter; import org.apache.commons.io.output.NullWriter; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import java.io.IOException; @@ -22,11 +26,13 @@ import java.util.Optional; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * @author Geoffroy Jamgotchian {@literal } + * @author Samir Romdhani {@literal } */ class FlowsValidationTest extends AbstractValidationTest { @@ -64,6 +70,7 @@ class FlowsValidationTest extends AbstractValidationTest { private ValidationConfig strictConfigSpecificCompatibility; @BeforeEach + @Override void setUp() throws IOException { super.setUp(); @@ -284,4 +291,88 @@ void checkNetworkFlows() throws IOException { ValidationWriter validationWriter = ValidationUtils.createValidationWriter(network.getId(), looseConfig, NullWriter.INSTANCE, ValidationType.FLOWS); assertTrue(ValidationType.FLOWS.check(network, looseConfig, validationWriter)); } + + @DisplayName("Rule 1: checks disconnected terminal 1 : P and Q must be undefined or ~0") + @Test + void checkFlowsShouldSucceedRuleWhenDisconnectedTerminal1PQShouldBeUndefinedOrZero() { + // Given + // threshold: 0.01 + BusView busViewTerminal1 = mock(BusView.class); + when(busViewTerminal1.getBus()).thenReturn(null); // disconnect flow: line, tieLine, TWT + when(terminal1.getBusView()).thenReturn(busViewTerminal1); + // When + boolean validationLineResult = FlowsValidation.INSTANCE.checkFlows(line1, strictConfig, NullWriter.INSTANCE); + boolean validationTieLineResult = FlowsValidation.INSTANCE.checkFlows(tieLine1, strictConfig, NullWriter.INSTANCE); + boolean validationTransformerResult = FlowsValidation.INSTANCE.checkFlows(transformer1, strictConfig, NullWriter.INSTANCE); + // Then + assertFalse(validationLineResult); + assertFalse(validationTieLineResult); + assertFalse(validationTransformerResult); + // p is outside tolerance + assertTrue(Math.abs(terminal1.getP()) > 0.01); + // q is outside tolerance + assertTrue(Math.abs(terminal1.getQ()) > 0.01); + } + + @DisplayName("Rule 1: checks disconnected terminal 2 : P and Q must be undefined or ~0") + @Test + void checkFlowsShouldSucceedRuleWhenDisconnectedTerminal2PQShouldBeUndefinedOrZero() { + // Given + // threshold: 0.01 + BusView busViewTerminal2 = mock(BusView.class); + when(busViewTerminal2.getBus()).thenReturn(null); // disconnect flow: line, tieLine, TWT + when(terminal2.getBusView()).thenReturn(busViewTerminal2); + // When + boolean validationLineResult = FlowsValidation.INSTANCE.checkFlows(line1, strictConfig, NullWriter.INSTANCE); + boolean validationTieLineResult = FlowsValidation.INSTANCE.checkFlows(tieLine1, strictConfig, NullWriter.INSTANCE); + boolean validationTransformerResult = FlowsValidation.INSTANCE.checkFlows(transformer1, strictConfig, NullWriter.INSTANCE); + // Then + assertFalse(validationLineResult); + assertFalse(validationTieLineResult); + assertFalse(validationTransformerResult); + // p is outside tolerance + assertTrue(Math.abs(terminal1.getP()) > 0.01); + // q is outside tolerance + assertTrue(Math.abs(terminal1.getQ()) > 0.01); + } + + @DisplayName("Rule 2: checks connected terminal : P and Q should follows Pcalc and Qcalc (consistent)") + @ParameterizedTest(name = "{0}") + @MethodSource("connectedTerminalCases") + void checkFlowsShouldSucceedRuleWhenConnectedTerminalAndPQConsistent(String caseName, double p1, double q1, double p2, double q2, boolean expectedValid) { + // Given + when(terminal1.getP()).thenReturn(p1); // P, terminal1 + when(terminal1.getQ()).thenReturn(q1); // Q, terminal1 + when(terminal2.getP()).thenReturn(p2); // P, terminal2 + when(terminal2.getQ()).thenReturn(q2); // Q, terminal2 + // When + boolean validationLineResult = FlowsValidation.INSTANCE.checkFlows(line1, strictConfig, NullWriter.INSTANCE); + boolean validationTieLineResult = FlowsValidation.INSTANCE.checkFlows(tieLine1, strictConfig, NullWriter.INSTANCE); + boolean validationTransformerResult = FlowsValidation.INSTANCE.checkFlows(transformer1, strictConfig, NullWriter.INSTANCE); + // Then + assertEquals(expectedValid, validationLineResult); + assertEquals(expectedValid, validationTieLineResult); + assertEquals(expectedValid, validationTransformerResult); + } + + private static Stream connectedTerminalCases() { + // threshold = 0.01 + // connected terminal 1 + // branch ComputedP1: 39.50497415111174 + // branch ComputedQ1: -3.729703101282256 + double computedP1 = 39.50497415111174; + double computedQ1 = -3.729703101282256; + // connected terminal 2 + // branch ComputedP2: -39.50385098344379 + // branch ComputedQ2: 3.7415805993708426 + double computedP2 = -39.50385098344379; + double computedQ2 = 3.7415805993708426; + return Stream.of( + Arguments.of("Terminal 1/2: |P - Pcalc| <= ε and |Q - Qcalc| <= ε -> valid", computedP1, computedQ1, computedP2, computedQ2, true), + Arguments.of("Terminal 1: |P - Pcalc| > ε and |Q - Qcalc| <= ε -> invalid", computedP1 + 0.02, computedQ1, computedP2, computedQ2, false), + Arguments.of("Terminal 1: |P - Pcalc| <= ε and |Q - Qcalc| > ε -> invalid", computedP1, computedQ1 + 0.02, computedP2, computedQ2, false), + Arguments.of("Terminal 2: |P - Pcalc| > ε and |Q - Qcalc| <= ε -> invalid", computedP1, computedQ1, computedP2 + 0.02, computedQ2, false), + Arguments.of("Terminal 1: |P - Pcalc| <= ε and |Q - Qcalc| > ε -> invalid", computedP1, computedQ1, computedP2, computedQ2 + 0.02, false) + ); + } } diff --git a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/GeneratorsValidationTest.java b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/GeneratorsValidationTest.java index 633aa827332..f6317e22342 100644 --- a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/GeneratorsValidationTest.java +++ b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/GeneratorsValidationTest.java @@ -7,15 +7,22 @@ */ package com.powsybl.loadflow.validation; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.io.IOException; import java.util.stream.Stream; import org.apache.commons.io.output.NullWriter; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import com.powsybl.iidm.network.Bus; @@ -29,6 +36,7 @@ /** * * @author Massimo Ferraro {@literal } + * @author Samir Romdhani {@literal } */ class GeneratorsValidationTest extends AbstractValidationTest { @@ -259,4 +267,126 @@ void checkNetworkGenerators() throws IOException { assertTrue(GeneratorsValidation.INSTANCE.checkGenerators(network, looseConfig, NullWriter.INSTANCE)); } + @DisplayName("Rule 1: A validation error should be detected if there is both a voltage and a target but no p or q") + @Test + void checkGeneratorsShouldSucceedRuleWhenPAndQMissingButTargetsExist() { + // Given + when(genTerminal.getP()).thenReturn(Double.NaN); + when(genTerminal.getQ()).thenReturn(Double.NaN); + when(generator.getTargetP()).thenReturn(10.0); + when(generator.getTargetQ()).thenReturn(10.0); + // When + boolean result = GeneratorsValidation.INSTANCE.checkGenerators(generator, strictConfig, NullWriter.INSTANCE); + // Then + assertFalse(result); + } + + @DisplayName("Rule 2: If reactive limits are inverted (`maxQ < minQ`) and noRequirementIfReactiveBoundInversion = true, generator validation pass") + @ParameterizedTest(name = "noRequirementIfReactiveBoundInversion flag={0} => valid={1}") + @CsvSource({"true, true", "false, false"}) + void checkGeneratorsShouldSucceedRuleWhenReactiveBoundsInvertedAndFlagEnabled(boolean noRequirementIfReactiveBoundInversion, boolean expectedValid) { + // Given + strictConfig.setNoRequirementIfReactiveBoundInversion(noRequirementIfReactiveBoundInversion); + // maxQ < minQ + ReactiveLimits invertedLimits = mock(ReactiveLimits.class); + when(invertedLimits.getMinQ(anyDouble())).thenReturn(0.0); + when(invertedLimits.getMaxQ(anyDouble())).thenReturn(-10.0); + when(generator.getReactiveLimits()).thenReturn(invertedLimits); + when(genTerminal.getQ()).thenReturn(5.0); // bypassed by rule 1 + // When + boolean result = GeneratorsValidation.INSTANCE.checkGenerators(generator, strictConfig, NullWriter.INSTANCE); + // Then + assertEquals(expectedValid, result); + } + + @DisplayName("Rule 3: Active setpoint outside bounds, if `targetP` is outside `[minP, maxP]` and noRequirementIfSetpointOutsidePowerBounds = true, generator validation pass") + @ParameterizedTest(name = "noRequirementIfReactiveBoundInversion flag={0} => valid={1}") + @CsvSource({"true, true", "false, false"}) + void checkGeneratorsShouldSucceedRuleWhenTargetPOutsideBoundsAndFlagEnabled(boolean noRequirementIfReactiveBoundInversion, boolean expectedValid) { + // Given + strictConfig.setNoRequirementIfSetpointOutsidePowerBounds(noRequirementIfReactiveBoundInversion); + when(generator.getMinP()).thenReturn(20.0); + when(generator.getMaxP()).thenReturn(30.0); + when(generator.getTargetP()).thenReturn(40.0); // outside [minP=20.0, maxP=30.0] + // When + boolean result = GeneratorsValidation.INSTANCE.checkGenerators(generator, strictConfig, NullWriter.INSTANCE); + // Then + assertEquals(expectedValid, result); + } + + @DisplayName("Rule 4: Active power p matches expected setpoint = TargetP") + @Test + void checkGeneratorsShouldSucceedRuleWhenActivePowerNotMatchExpectedP() { + // Given + when(generator.getTargetP()).thenReturn(20.0); + when(genTerminal.getP()).thenReturn(-22.0); + // When + boolean result = GeneratorsValidation.INSTANCE.checkGenerators(generator, strictConfig, NullWriter.INSTANCE); + // Then + assertFalse(result); + } + + @DisplayName("Rule 5: If voltage regulator is disabled, reactive power Q matches targetQ") + @Test + void checkGeneratorsShouldSucceedRuleWhenVoltageRegulatorDisabledAndQNotMatchTargetQ() { + // Given + when(generator.isVoltageRegulatorOn()).thenReturn(false); + // keep p consistent + when(generator.getTargetP()).thenReturn(20.0); + when(genTerminal.getP()).thenReturn(-20.0); + + when(generator.getTargetQ()).thenReturn(10.0); + when(genTerminal.getQ()).thenReturn(-12.0); // expected q = -targetQ = -10 + // When + boolean result = GeneratorsValidation.INSTANCE.checkGenerators(generator, strictConfig, NullWriter.INSTANCE); + // Then + assertFalse(result); + } + + @DisplayName("Rule 6: If voltage regulator is enabled, reactive power q follow V/targetV logic") + @ParameterizedTest(name = "{0}") + @MethodSource("cases") + void checkGeneratorsShouldSucceedRuleWhenVoltageRegulationEnabled(String caseName, double v, double q, boolean expectedValid) { + // Given + strictConfig.setNoRequirementIfReactiveBoundInversion(false); + strictConfig.setNoRequirementIfSetpointOutsidePowerBounds(false); + Bus genBus = generator.getTerminal().getBusView().getBus(); + when(genBus.getV()).thenReturn(v); + when(generator.isVoltageRegulatorOn()).thenReturn(true); + // keep p consistent + when(generator.getTargetP()).thenReturn(20.0); + when(genTerminal.getP()).thenReturn(-20.0); + + // minQ and maxQ [-10, 0] + ReactiveLimits reactiveLimits = mock(ReactiveLimits.class); + when(reactiveLimits.getMinQ(anyDouble())).thenReturn(-10.0); + when(reactiveLimits.getMaxQ(anyDouble())).thenReturn(0.0); + when(generator.getReactiveLimits()).thenReturn(reactiveLimits); + // q + when(genTerminal.getQ()).thenReturn(q); + // When + boolean result = GeneratorsValidation.INSTANCE.checkGenerators(generator, strictConfig, NullWriter.INSTANCE); + // Then + assertEquals(expectedValid, result, caseName); + } + + private static Stream cases() { + return Stream.of( + // TargetV 380 + // V > targetV + threshold -> qGen ~= minQ (-10) -> q ~= +10 + Arguments.of("V > TargetV -> qGen ~ minQ -> valid", 400.0, 10.0, true), + Arguments.of("V > TargetV -> qGen not ~ minQ -> invalid", 400.0, 5.0, false), + + // TargetV 380 + // V < targetV - threshold -> qGen ~= maxQ (0) -> q ~= 0 + Arguments.of("V < TargetV -> qGen ~ maxQ -> valid", 360.0, 0.0, true), + Arguments.of("V < TargetV -> qGen not ~ maxQ -> invalid", 360.0, 5.0, false), + + // TargetV 380 + // |V-targetV| <= threshold -> qGen in [minQ, maxQ] = [-10, 0] + Arguments.of("V ~ TargetV -> qGen within bounds -> valid", 380.0, 5.0, true), + Arguments.of("V ~ TargetV -> qGen out of bounds -> invalid", 380.0, 11.0, false) + ); + } + } diff --git a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ShuntCompensatorsValidationTest.java b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ShuntCompensatorsValidationTest.java index 71b927f7416..8f0c2093917 100644 --- a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ShuntCompensatorsValidationTest.java +++ b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ShuntCompensatorsValidationTest.java @@ -7,30 +7,36 @@ */ package com.powsybl.loadflow.validation; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.IOException; import java.util.stream.Stream; import com.powsybl.iidm.network.*; import org.apache.commons.io.output.NullWriter; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; import com.powsybl.iidm.network.Terminal.BusView; import com.powsybl.loadflow.validation.io.ValidationWriter; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; /** * * @author Massimo Ferraro {@literal } + * @author Samir Romdhani {@literal } */ class ShuntCompensatorsValidationTest extends AbstractValidationTest { private double q = 170.50537; private double p = Float.NaN; - private int currentSectionCount = 1; + private final int currentSectionCount = 1; private final int maximumSectionCount = 1; private final double bPerSection = -0.0010387811; private final double v = 405.14175; @@ -134,4 +140,59 @@ void checkNetworkShunts() throws IOException { ValidationWriter validationWriter = ValidationUtils.createValidationWriter(network.getId(), strictConfig, NullWriter.INSTANCE, ValidationType.SHUNTS); assertTrue(ValidationType.SHUNTS.check(network, strictConfig, validationWriter)); } + + @DisplayName("Rule 1: |p| < e") + @ParameterizedTest(name = "connected p={0} => valid={1}") + @MethodSource("connectedShuntPCase") + void checkShuntShouldSucceedRulePMustBeZero(double p, boolean expectedValid) { + when(shuntTerminal.getP()).thenReturn(p); + boolean result = ShuntCompensatorsValidation.INSTANCE.checkShunts(shunt, strictConfig, NullWriter.INSTANCE); + assertEquals(expectedValid, result); + } + + private static Stream connectedShuntPCase() { + // threshold = 0.01 + return Stream.of( + Arguments.of(Double.NaN, true), + Arguments.of(0.0, true), + Arguments.of(0.011, false), // |p| > threshold + Arguments.of(-0.011, false) // |p| > threshold + ); + } + + @DisplayName("Rule 2: q must match expectedQ: | q + expectedQ | <= ε") + @ParameterizedTest(name = "epsilon={0} => valid={1}") + @CsvSource({"0.000, true", "0.009, true", "-0.009, true", "0.010, true", "0.011, false", "-0.011, false"}) + void checkShuntsShouldSucceedRuleQMatchExpectedQ(double epsilon, boolean expectedValid) { + // Given + // connected and mainComponent = true, threshold = 0.01 + when(shuntTerminal.getP()).thenReturn(Double.NaN); // Rule1 OK + double expectedQ = -bPerSection * currentSectionCount * v * v; + when(shuntTerminal.getQ()).thenReturn(expectedQ + epsilon); + boolean result = ShuntCompensatorsValidation.INSTANCE.checkShunts(shunt, strictConfig, NullWriter.INSTANCE); + assertEquals(expectedValid, result); + } + + @DisplayName("Rule 3: if the shunt is disconnected, q should be undefined or 0") + @ParameterizedTest(name = "disconnected q={0} => valid={1}") + @MethodSource("disconnectedShuntCase") + void checkShuntsShouldSucceedRuleWhenDisconnectedShuntQMustBeUndefinedOrZero(double qValue, boolean expectedValid) { + when(shuntBusView.getBus()).thenReturn(null); // disconnect shunt + // assert that shunt is disconnected + assertFalse(shunt.getTerminal().isConnected()); + when(shuntTerminal.getQ()).thenReturn(qValue); + boolean valid = ShuntCompensatorsValidation.INSTANCE.checkShunts(shunt, strictConfig, NullWriter.INSTANCE); + assertEquals(expectedValid, valid); + } + + private static Stream disconnectedShuntCase() { + // threshold = 0.01 + return Stream.of( + Arguments.of(Double.NaN, true), + Arguments.of(0.0, true), + Arguments.of(0.001, true), // |q| <= threshold + Arguments.of(0.011, false), // |q| > threshold + Arguments.of(-0.011, false) // |q| > threshold + ); + } } diff --git a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/StaticVarCompensatorsValidationTest.java b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/StaticVarCompensatorsValidationTest.java index 622eda11898..12a51b4a5f1 100644 --- a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/StaticVarCompensatorsValidationTest.java +++ b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/StaticVarCompensatorsValidationTest.java @@ -7,16 +7,17 @@ */ package com.powsybl.loadflow.validation; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.io.IOException; import java.util.stream.Stream; import org.apache.commons.io.output.NullWriter; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import com.powsybl.iidm.network.Bus; import com.powsybl.iidm.network.Network; @@ -30,6 +31,7 @@ /** * * @author Massimo Ferraro {@literal } + * @author Samir Romdhani {@literal } */ class StaticVarCompensatorsValidationTest extends AbstractValidationTest { @@ -54,32 +56,32 @@ class StaticVarCompensatorsValidationTest extends AbstractValidationTest { void setUp() throws IOException { super.setUp(); - Bus svcBus = Mockito.mock(Bus.class); - Mockito.when(svcBus.getV()).thenReturn(v); - Mockito.when(svcBus.isInMainConnectedComponent()).thenReturn(mainComponent); - - svcBusView = Mockito.mock(BusView.class); - Mockito.when(svcBusView.getBus()).thenReturn(svcBus); - Mockito.when(svcBusView.getConnectableBus()).thenReturn(svcBus); - - VoltageLevel voltageLevel = Mockito.mock(VoltageLevel.class); - Mockito.when(voltageLevel.getNominalV()).thenReturn(nominalV); - - svcTerminal = Mockito.mock(Terminal.class); - Mockito.when(svcTerminal.getP()).thenReturn(p); - Mockito.when(svcTerminal.getQ()).thenReturn(q); - Mockito.when(svcTerminal.getBusView()).thenReturn(svcBusView); - Mockito.when(svcTerminal.getVoltageLevel()).thenReturn(voltageLevel); - - svc = Mockito.mock(StaticVarCompensator.class); - Mockito.when(svc.getId()).thenReturn("svc"); - Mockito.when(svc.getTerminal()).thenReturn(svcTerminal); - Mockito.when(svc.getReactivePowerSetpoint()).thenReturn(reactivePowerSetpoint); - Mockito.when(svc.getVoltageSetpoint()).thenReturn(voltageSetpoint); - Mockito.when(svc.getRegulationMode()).thenReturn(regulationMode); - Mockito.when(svc.isRegulating()).thenReturn(regulating); - Mockito.when(svc.getBmin()).thenReturn(bMin); - Mockito.when(svc.getBmax()).thenReturn(bMax); + Bus svcBus = mock(Bus.class); + when(svcBus.getV()).thenReturn(v); + when(svcBus.isInMainConnectedComponent()).thenReturn(mainComponent); + + svcBusView = mock(BusView.class); + when(svcBusView.getBus()).thenReturn(svcBus); + when(svcBusView.getConnectableBus()).thenReturn(svcBus); + + VoltageLevel voltageLevel = mock(VoltageLevel.class); + when(voltageLevel.getNominalV()).thenReturn(nominalV); + + svcTerminal = mock(Terminal.class); + when(svcTerminal.getP()).thenReturn(p); + when(svcTerminal.getQ()).thenReturn(q); + when(svcTerminal.getBusView()).thenReturn(svcBusView); + when(svcTerminal.getVoltageLevel()).thenReturn(voltageLevel); + + svc = mock(StaticVarCompensator.class); + when(svc.getId()).thenReturn("svc"); + when(svc.getTerminal()).thenReturn(svcTerminal); + when(svc.getReactivePowerSetpoint()).thenReturn(reactivePowerSetpoint); + when(svc.getVoltageSetpoint()).thenReturn(voltageSetpoint); + when(svc.getRegulationMode()).thenReturn(regulationMode); + when(svc.isRegulating()).thenReturn(regulating); + when(svc.getBmin()).thenReturn(bMin); + when(svc.getBmax()).thenReturn(bMax); } @Test @@ -168,19 +170,19 @@ void checkSvcsValues() { void checkSvcs() { // active power should be equal to 0 assertTrue(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); - Mockito.when(svcTerminal.getP()).thenReturn(-39.8); + when(svcTerminal.getP()).thenReturn(-39.8); assertFalse(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); // the unit is disconnected - Mockito.when(svcBusView.getBus()).thenReturn(null); + when(svcBusView.getBus()).thenReturn(null); assertTrue(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); } @Test void checkNetworkSvcs() throws IOException { - Network network = Mockito.mock(Network.class); - Mockito.when(network.getId()).thenReturn("network"); - Mockito.when(network.getStaticVarCompensatorStream()).thenAnswer(dummy -> Stream.of(svc)); + Network network = mock(Network.class); + when(network.getId()).thenReturn("network"); + when(network.getStaticVarCompensatorStream()).thenAnswer(dummy -> Stream.of(svc)); assertTrue(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(network, looseConfig, data)); @@ -189,4 +191,177 @@ void checkNetworkSvcs() throws IOException { ValidationWriter validationWriter = ValidationUtils.createValidationWriter(network.getId(), looseConfig, NullWriter.INSTANCE, ValidationType.SVCS); assertTrue(ValidationType.SVCS.check(network, looseConfig, validationWriter)); } + + @DisplayName("Rule 1: active power should be equal to 0") + @Test + void checkSVCActivePowerShouldBeZeroWithinThreshold() { + // Given (p = 0) + when(svcTerminal.getP()).thenReturn(0.0); + assertTrue(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + // Given (p = 0.01 ~ threshold) + when(svcTerminal.getP()).thenReturn(0.01); + assertTrue(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + // Given (p > 0.01) + when(svcTerminal.getP()).thenReturn(0.02); + assertFalse(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + } + + @DisplayName("Rule 2: reactivePowerSetpoint** must be 0 if p or q is missing (NaN") + @Test + void checkSVCReactivePowerSetpointWhenPOrQMissing() { + // non-zero setpoint with missing p/q => KO + when(svcTerminal.getP()).thenReturn(Double.NaN); + when(svcTerminal.getQ()).thenReturn(Double.NaN); + when(svc.getReactivePowerSetpoint()).thenReturn(5.0); + assertFalse(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + // zero setpoint with missing p/q => OK + when(svc.getReactivePowerSetpoint()).thenReturn(0.0); + assertTrue(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + } + + @DisplayName("Rule 3: regulationMode = REACTIVE_POWER, OK only if okMissingValues=true") + @Test + void checkSVCReactivePowerModeMissingInputs() { + // Given regulation enabled, and regulationMode is REACTIVE_POWER + when(svc.getRegulationMode()).thenReturn(RegulationMode.REACTIVE_POWER); + when(svc.isRegulating()).thenReturn(true); + // Given Rule1, Rule2 (OK) + when(svcTerminal.getP()).thenReturn(0.0); + when(svc.getReactivePowerSetpoint()).thenReturn(Double.NaN); + // When + strictConfig.setOkMissingValues(false); + // Then + assertFalse(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + // When + strictConfig.setOkMissingValues(true); + // Then + assertTrue(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + } + + @DisplayName("Rule 3: regulationMode = REACTIVE_POWER, Q must match reactivePowerSetpoint") + @Test + void checkSVCReactivePowerModeQMustMatchSetpoint() { + // Given regulation enabled, and regulationMode is REACTIVE_POWER + when(svc.getRegulationMode()).thenReturn(RegulationMode.REACTIVE_POWER); + when(svc.isRegulating()).thenReturn(true); + + when(svcTerminal.getP()).thenReturn(0.0); + when(svc.getReactivePowerSetpoint()).thenReturn(3.0); + // When + when(svcTerminal.getQ()).thenReturn(3.01); + // Then + assertTrue(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + // When + when(svcTerminal.getQ()).thenReturn(3.5); // diff > (threshold = 0.01) + // Then + assertFalse(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + } + + @DisplayName("Rule 4: regulationMode = VOLTAGE, OK only if okMissingValues=true") + @Test + void checkSVCVoltageModeMissingInputs() { + // Given regulation enabled, and regulationMode is VOLTAGE + when(svc.getRegulationMode()).thenReturn(RegulationMode.VOLTAGE); + when(svc.isRegulating()).thenReturn(true); + // Given Rule1, Rule 2 (OK) + when(svcTerminal.getQ()).thenReturn(0.0); + when(svc.getVoltageSetpoint()).thenReturn(Double.NaN); + // When + strictConfig.setOkMissingValues(false); + // Then + assertFalse(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + // When + strictConfig.setOkMissingValues(true); + // Then + assertTrue(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + } + + @DisplayName("Rule 4: regulationMode = VOLTAGE, V controlled < V setPoint, then Q must match maxQ.") + @Test + void checkSVCVoltageModeVLowerThanSetpoint() { + // Given regulation enabled, and regulationMode is VOLTAGE + when(svc.getRegulationMode()).thenReturn(RegulationMode.VOLTAGE); + when(svc.isRegulating()).thenReturn(true); + when(svcTerminal.getP()).thenReturn(0.0); + // Given V controlled < V setpoint + Terminal regulatingTerminal = regulatingTerminalWithVoltage(360.0); // V controlled = 360.0 + when(svc.getRegulatingTerminal()).thenReturn(regulatingTerminal); + when(svc.getVoltageSetpoint()).thenReturn(380.0); // V setpoint 380.0 + double expectedQmax = -bMin * v * v; + assertEquals(1444.0, expectedQmax); + // Given q matching Qmax + when(svcTerminal.getQ()).thenReturn(expectedQmax); + assertTrue(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + // Given q not matching Qmax + when(svcTerminal.getQ()).thenReturn(1300.0); + assertFalse(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + } + + @DisplayName("Rule 4: regulationMode = VOLTAGE, V regulation > V setPoint, then Q must match minQ") + @Test + void checkSVCVoltageModeVHigherThanSetpoint() { + // Given regulation enabled, and regulationMode is VOLTAGE + when(svc.getRegulationMode()).thenReturn(RegulationMode.VOLTAGE); + when(svc.isRegulating()).thenReturn(true); + when(svcTerminal.getP()).thenReturn(0.0); + // Given V controlled > V setpoint + Terminal regulatingTerminal = regulatingTerminalWithVoltage(400.0); // V controlled = 400.0 + when(svc.getRegulatingTerminal()).thenReturn(regulatingTerminal); + when(svc.getVoltageSetpoint()).thenReturn(380.0); // V setpoint 380.0 + double expectedQmin = -bMax * v * v; + assertEquals(-14440.0, expectedQmin); + // Given q matching Qmin + when(svcTerminal.getQ()).thenReturn(expectedQmin); + assertTrue(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + // Given q not matching Qmin + when(svcTerminal.getQ()).thenReturn(-13000.0); + assertFalse(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + } + + @DisplayName("Rule 4: regulationMode = VOLTAGE, V regulation ~ V setPoint, then Q must be within [minQ, maxQ]") + @Test + void checkSVCVoltageModeVAtSetpoint() { + // Given regulation enabled, and regulationMode is VOLTAGE + when(svc.getRegulationMode()).thenReturn(RegulationMode.VOLTAGE); + when(svc.isRegulating()).thenReturn(true); + when(svcTerminal.getP()).thenReturn(0.0); + // Given V controlled ~ V setpoint + Terminal regulatingTerminal = regulatingTerminalWithVoltage(380.0); // V controlled = 380.0.0 + when(svc.getRegulatingTerminal()).thenReturn(regulatingTerminal); + when(svc.getVoltageSetpoint()).thenReturn(380.0); // V setpoint 380.0 + double expectedQmax = -bMin * v * v; + assertEquals(1444.0, expectedQmax); + double expectedQmin = -bMax * v * v; + assertEquals(-14440.0, expectedQmin); + // Given q inside bounds [-14440.0, 1444.0] + when(svcTerminal.getQ()).thenReturn(0.0); // inside bounds [-14440.0, 1444.0] + assertTrue(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + // Given q outside bounds [-14440.0, 1444.0] + when(svcTerminal.getQ()).thenReturn(2000.0); + assertFalse(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + } + + @DisplayName("Rule 5: regulationMode = VOLTAGE, if regulating is false then reactive power (Q) should be equal to 0 ") + @Test + void checkSVCWhenNoRegulatingQShouldBeZero() { + when(svc.getRegulationMode()).thenReturn(RegulationMode.VOLTAGE); + when(svc.isRegulating()).thenReturn(false); + when(svcTerminal.getP()).thenReturn(0.0); + when(svcTerminal.getQ()).thenReturn(0.009); // ~ threshold => invalid result + assertTrue(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + when(svcTerminal.getQ()).thenReturn(0.02); // > threshold => invalid result + assertFalse(StaticVarCompensatorsValidation.INSTANCE.checkSVCs(svc, strictConfig, NullWriter.INSTANCE)); + } + + private Terminal regulatingTerminalWithVoltage(double vControlled) { + Bus controlledBus = mock(Bus.class); + when(controlledBus.getV()).thenReturn(vControlled); + + BusView controlledBusView = mock(BusView.class); + when(controlledBusView.getBus()).thenReturn(controlledBus); + + Terminal regulatingTerminal = mock(Terminal.class); + when(regulatingTerminal.getBusView()).thenReturn(controlledBusView); + return regulatingTerminal; + } } diff --git a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/TransformersValidationTest.java b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/TransformersValidationTest.java index ce3e67e7c68..139fba5d349 100644 --- a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/TransformersValidationTest.java +++ b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/TransformersValidationTest.java @@ -15,6 +15,7 @@ import org.apache.commons.io.output.NullWriter; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -111,8 +112,9 @@ void checkTwtsValues() { targetV, regulatedSide, Float.NaN, connected, mainComponent, strictConfig, NullWriter.INSTANCE)); } + @DisplayName("Rule 1 (voltage is higher than target): if voltageDeviation (error) > 0 -> fail when requiredDecrease > availableDecrease + threshold") @Test - void checkTwts() { + void checkTransformerShouldSucceedWhenVoltageTooHigh() { assertTrue(TransformersValidation.INSTANCE.checkTransformer(transformer, strictConfig, NullWriter.INSTANCE)); Mockito.when(bus.getV()).thenReturn(highV); assertFalse(TransformersValidation.INSTANCE.checkTransformer(transformer, strictConfig, NullWriter.INSTANCE)); @@ -131,4 +133,11 @@ void checkNetworkTwts() throws IOException { assertTrue(ValidationType.TWTS.check(network, strictConfig, validationWriter)); } + @DisplayName("Rule 1 (voltage is lower than target): if voltageDeviation (error) < 0 -> fail when requiredIncrease > availableIncrease + threshold") + @Test + void checkTransformerShouldSucceedWhenVoltageTooLow() { + Mockito.when(bus.getV()).thenReturn(lowV); + assertFalse(TransformersValidation.INSTANCE.checkTransformer(transformer, strictConfig, NullWriter.INSTANCE)); + } + } diff --git a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ValidationUtilsTest.java b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ValidationUtilsTest.java index eddca833eff..a7d45991572 100644 --- a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ValidationUtilsTest.java +++ b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ValidationUtilsTest.java @@ -7,11 +7,10 @@ */ package com.powsybl.loadflow.validation; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + /** * * @author Massimo Ferraro {@literal } @@ -62,7 +61,7 @@ void boundedWithin() { } @Test - void isMainComponent() { + void isMainComponentShouldSucceed() { assertTrue(ValidationUtils.isMainComponent(looseConfig, true)); assertFalse(ValidationUtils.isMainComponent(looseConfig, false)); @@ -70,4 +69,41 @@ void isMainComponent() { assertTrue(ValidationUtils.isMainComponent(looseConfig, true)); assertTrue(ValidationUtils.isMainComponent(looseConfig, false)); } + + @Test + void isUndefinedOrZeroShouldSucceed() { + assertTrue(ValidationUtils.isUndefinedOrZero(Double.NaN, 0.01)); + assertTrue(ValidationUtils.isUndefinedOrZero(0.0, 0.01)); + assertTrue(ValidationUtils.isUndefinedOrZero(0.01, 0.02)); + assertFalse(ValidationUtils.isUndefinedOrZero(0.02, 0.01)); + } + + @Test + void isOutsideToleranceShouldSucceed() { + assertFalse(ValidationUtils.isOutsideTolerance(10.0, 10.001, 0.01)); + assertFalse(ValidationUtils.isOutsideTolerance(10, 11, 1)); + assertTrue(ValidationUtils.isOutsideTolerance(10.0, 10.02, 0.01)); + } + + @Test + void isOutsideOrAtToleranceShouldSucceed() { + assertFalse(ValidationUtils.isOutsideOrAtTolerance(10.0, 10.001, 0.01)); + assertTrue(ValidationUtils.isOutsideOrAtTolerance(10, 11, 1)); + assertTrue(ValidationUtils.isOutsideOrAtTolerance(10.0, 10.02, 0.01)); + } + + @Test + void isConnectedAndValidatedShouldSucceed() { + // Given (config parameter CheckMainComponentOnly true) + // config parameter CheckMainComponentOnly true + // When Then + assertTrue(ValidationUtils.isConnectedAndMainComponent(true, true, looseConfig)); + assertFalse(ValidationUtils.isConnectedAndMainComponent(true, false, looseConfig)); + assertFalse(ValidationUtils.isConnectedAndMainComponent(false, true, looseConfig)); + //Given (config parameter CheckMainComponentOnly false) + looseConfig.setCheckMainComponentOnly(false); + // When Then + assertTrue(ValidationUtils.isConnectedAndMainComponent(true, false, looseConfig)); + assertFalse(ValidationUtils.isConnectedAndMainComponent(false, false, looseConfig)); + } }