Repository navigation
Match OpenLoadFlow on hvdc limits, VSC curves, SVC standby b0 and remote voltage control - #222
Merged
Merged
Conversation
OpenLoadFlow saturates an angle-droop ("AC emulation") hvdc line at the
limit of its hvdcOperatorActivePowerRange extension, one value per
direction (opr_from_cs1_to_cs2 / opr_from_cs2_to_cs1), and only falls
back on the line's max_p when the extension is missing.
init_from_pypowsybl passed max_p in both directions, so on a line whose
operator range is narrower than max_p the hvdc active-power check of
get_physical_violations compared the flow with the wrong limit: a line
OLF saturates read as well within its limit, and the contingency
showed no physical violation at all.
_hvdc_pmax_per_direction now builds pmax_1to2 / pmax_2to1 from the
extension when present. gpusim2grid reads these limits off the LSGrid,
so it gets them too.
Tested in test_hvdc_pypowsybl.py: the limits with and without the
extension, and a HIGH_P violation against the operator range.
Assisted-by: Claude Code (claude-opus-5-5)
Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
OpenLoadFlow's AcHvdcAcEmulationLimits outer loop caps the active power the sending converter of an angle-droop hvdc line takes from the AC grid at the line's limit in that direction (the operator active power range, max_p otherwise). bake_outer_loops did not bake that loop: the baked network kept the droop, and the loop-free solve -- OLF baked as well as lightsim2grid -- let the line transmit beyond its limit, the flows of the AC lines in parallel off by as much. The new step _bake_hvdc_ac_emulation_limits (bake_outer_loops' bake_hvdc_ac_emulation_limits, on by default) finds the lines whose sending converter realized its limit (within _HVDC_P_LIMIT_TOL_MW: OLF imposes the limit as a setpoint, so a saturated line reproduces it to float rounding) and bakes them as OLF solved them: droop off, target_p at the limit, converters_mode with the sending side as rectifier. A line in its linear regime keeps its droop. Not covered: a line frozen at its limit cannot come back below it on a grid that asks it for less, and nothing reports that release yet. Tested in test_olf_bake.py: saturation in both directions, the loop-free re-solve reproducing the with-loops converter flows, a line in its linear regime left alone. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
With bake_saturated_voltage_control, the bake also froze a generator (or a voltage-mode SVC) whose target OLF held, as soon as its reactive output sat within the relative tolerance of _q_limit_tol (a fraction of its Q range). That tolerance exists for a unit OLF switched and that settled a hair inside its limit, whose voltage is free; on a held unit it also froze one OLF still regulated with some headroom left. A contingency that relieves such a unit makes OLF move its reactive output while the frozen copy cannot follow, and on a stiff bus the voltage deviation that would report the release (LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q) is far too small to show, although the reactive power moved is not. A held unit is now frozen only at its limit to the new _Q_SATURATED_HELD_TOL_MVAR; one with more headroom keeps regulating, as OLF does. Units OLF did not hold, and members switched out of a shared group, are frozen as before. test_svc_near_its_limit_follows_the_generator_rule now expects a held SVC with headroom left to stay regulating under the flag; test_olf_held_unit_with_headroom_not_frozen and test_saturated_held_svc_frozen_on_request check both sides of the tolerance. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
pypowsybl leaves min_q / max_q NaN for a station whose reactive_limits_kind is CURVE; its limits are only exposed through min_q_at_target_p / max_q_at_target_p, which are not default columns. init_from_pypowsybl read min_q / max_q alone, so such a station was unlimited: a contingency asking it for more than its curve allows was neither modelled nor reported by the reactive-limit physical check, while OpenLoadFlow switches it to PQ at that limit. The stations are now fetched with all their attributes and read like the generators: the curve at the target P, the fixed box otherwise, min and max swapped back when malformed curve data inverts them. Tested in test_hvdc_pypowsybl.py: a station with a capability curve gets the curve's limits. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
A generator can hold the voltage of a bus other than its own. Holding a remote target can take its own bus far from anything realistic while its reactive output stays inside its limits, so the reactive-limit check has nothing to say. OpenLoadFlow's ReactiveLimits loop, in its default "robust" remote voltage control mode (voltageRemoteControlRobustMode), switches such a controller bus to PQ at its target reactive power as soon as its voltage leaves [minRealisticVoltage * 1.02, maxRealisticVoltage / 1.02] pu of its nominal voltage (the margin checked against OpenLoadFlow: it switches between the two sides of the bound). An outer-loop-free solve keeps it regulating; on a real grid snapshot a contingency left a generator holding its remote target from well above that bound, with a large voltage gap around it and no physical violation reported. It is now a physical check: - LSGrid.set_remote_voltage_control_vm_range(min_vm_pu, max_vm_pu) stores the range (NaN by default: no check, one NaN side checks the other). Never read by a powerflow; copied with the grid, not part of get_state or the binary format, like set_keep_vinit_at_group_controlled_buses. - RemoteVoltageControlCheck.hpp: the generator controllers of the voltage-control plan the solve used whose own bus is not the bus their group regulates (held ones skipped) are checked on their own bus, and reported as LOW_VOLTAGE_REMOTE_CONTROL (13) / HIGH_VOLTAGE_REMOTE_CONTROL (14) on the GENERATOR, value and limit in kV, by LSGrid.get_physical_violations and every batch's compute_physical_violations (a generator contingency, or a masked own or regulated bus, skips it). Category PHYSICAL. - init_from_pypowsybl(remote_voltage_control_vm_range="olf") sets OpenLoadFlow's default range (None: no check, or a pair). Tested in test_remote_voltage_control.py, against OpenLoadFlow itself on IEEE 14 with a generator regulating a remote bus: reported exactly where OpenLoadFlow switches, on both bounds, and the same in a contingency analysis. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
OpenLoadFlow discards from voltage control a generator whose reactive range is below 1 MVAr, and injects its target_q instead. The bake freezes such a unit, except when its regulated bus held its target in the reference solve. But "held" is a property of the bus: when the unit shares its regulated bus with a controller OLF keeps, that other unit holds the bus, and the small one read as held and stayed regulating. lightsim2grid then counted its reactive range in the bus' capability, larger than OLF's, and missed the reactive-limit switch OLF makes after a contingency (on a real grid snapshot, a bus OLF switches to PQ while lightsim2grid saw it well within its capability). A unit with too small a range is now frozen at its realized q even when its bus is held, as soon as another regulating generator with a large enough range (and a plausible target) regulates the same bus. A small unit alone on its bus keeps the held exemption. Tested in test_olf_bake.py: a sub-1 MVAr unit added on the bus of an IEEE 14 generator is frozen at its target_q, the other one keeps regulating, and the loop-free re-solve reproduces the reference. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
OpenLoadFlow models an SVC carrying a standbyAutomaton extension -- in standby or not -- as the automaton's b0, a fixed susceptance, plus the SVC itself, and holds the SVC's own susceptance in [b_min, b_max]: its ReactiveLimits loop compares what the SVC part produces with those limits. The total output, the one pypowsybl reports and lightsim2grid models, can therefore only range over [b_min + b0, b_max + b0]. lightsim2grid and the bake used [b_min, b_max] for that total: on a real grid snapshot a contingency asked an SVC with a capacitive b0 for an output OLF's loop found beyond its limit by the b0 share and switched to PQ, while lightsim2grid, holding the same voltage, saw it well within its range and reported nothing (the remaining gap was that b0 share, to the digit). _svc_standby_b0 reads the automaton's b0 (0 without the extension); init_from_pypowsybl shifts the SVC's b_min / b_max by it, so the reactive-limit physical check (and gpusim2grid, which reads the limits off the LSGrid) compares the total output with OLF's range, and the bake's SVC saturation step does the same before freezing an SVC at its limit. Tested in test_svc_standby.py on the four substations network: reported exactly when OLF switches (an inductive b0 pushes the SVC part past b_max, a capacitive one leaves it room), and the bake freezes an SVC OLF switched at its shifted limit, the loop-free re-solve reproducing the reference. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
OpenLoadFlow shares the distributed slack from the raw set-points: p = clamp(target_p + lambda * weight, [min_p, max_p]), one common lambda. A unit it capped at max_p therefore had target_p + lambda * weight above max_p, possibly by a lot when the reference mismatch was large. After a contingency it shares the new total from the raw set-points again: the capped unit only leaves max_p once lambda has dropped by enough to use up that overshoot. The pre-pass (consider_only_main_component(True), the batches' redistribute_slack) let a flagged "can participate in the slack" unit move as soon as the contingency's own imbalance was of the other sign. On a real grid snapshot whose reference distribution had a large positive mismatch, a contingency then moved a unit OLF kept capped by a few MW, and every flow with it. - SlackParticipation carries, next to the "can participate" weight, each unit's overshoot beyond the limit it sits at (MW, 0 by default): LSGrid.set_gen_can_participate_slack_overshoot / set_storage_can_participate_slack_overshoot, GenInfo / StorageInfo can_participate_slack_overshoot_mw; binary format 15 -> 16, fixture regenerated (LS2G_HAS_CAN_PARTICIPATE_SLACK_OVERSHOOT for gpusim2grid). - slack_redistribution::distribute, when a unit carries an overshoot, solves OLF's rule as it is: one common shift with p_k = clamp(v_k + shift * w_k), v_k the injection plus the overshoot on the side of its limit, by bisection (the total is monotone in the shift). Without any overshoot the round-based algorithm runs unchanged. - bake_outer_loops(..., return_details=True) returns can_participate_slack_overshoot: lambda is read off the units OLF neither excluded nor capped, and each capped unit's overshoot is target_p + lambda * weight beyond its limit; init_from_pypowsybl(can_participate_slack_overshoot=...) sets it. Tested in test_can_participate_slack.py: an overshoot is used up first (the capped unit gives part of an islanded load, or none when the overshoot is larger than the shift), the batch matches the single solve, the value survives copy, pickle and the binary format, and the bake returns it for init to set. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
…sks for less LSGrid.set_hvdc_ac_emulation_frozen flags the lines bake_outer_loops froze at their limit (droop off, parameters kept); the physical check reports HVDC_AC_EMULATION_RELEASE when the droop flow falls below that limit. init_from_pypowsybl(hvdc_ac_emulation_frozen=...), binary format 17. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
…ring key OpenLoadFlow gives each controller bus of a remote voltage control group a share keyed on ALL its connected generators (q_percent, or reactive range), regulating or not; lightsim2grid only summed the controllers. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
…ulates OpenLoadFlow switches a whole bus PV -> PQ; a unit at its limit while another unit of its bus keeps headroom was only clamped when the bus' reactive power was split among its units. Neither freezing rule of the bake freezes it now. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
Both sides added entries at the top of the 1.1.1 changelog section. Keep both and sort the section by kind (BREAKING, FIXED, ADDED, IMPROVED), as dev_1.1.1 already does. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
distribute_with_overshoot only reported "all saturated" when every participant ran out of room. A unit only flagged "can participate" with room left kept that false, every unit of the Newton solve's slack was marked saturated, and the caller took them all out of it: the next solve had an empty distributed slack. Apply the rule `distribute` already applies: when every in_slack unit hit its bound, clear the mask and set all_saturated. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_01GbSAUSwx459CCq4pn9oTkD Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
The overshoot was a magnitude, and distribute_with_overshoot guessed its side from where the unit sits. A charging storage unit a positive mismatch capped at 0 MW sits at 0, which the sign rule reads as the LOWER bound of an injecting unit: the overshoot was put below 0, a later negative mismatch could not move the unit at all, and a positive one moved it above 0 once a phantom overshoot was used up. The overshoot is now signed: > 0 beyond the upper limit, < 0 beyond the lower one. The bake returns it so, the setter accepts any finite value, and at 0 MW the sign tells which side of 0 the unit is on. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_01GbSAUSwx459CCq4pn9oTkD Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
The swap of inverted min_q / max_q writes into the arrays `Series.to_numpy(float)` returns, which pandas copy-on-write (the default from pandas 3) hands out read-only: init_from_pypowsybl raised "assignment destination is read-only" on every network with an HVDC line, swapped limits or not (an empty boolean mask still writes). Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_01GbSAUSwx459CCq4pn9oTkD Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
`can_participate_slack_overshoot` was only read inside the branch that applies `can_participate_slack`: given alone, or with an explicit slack, it was dropped silently and every capped unit left its limit at once. Raise, as `can_participate_slack` itself does in the same situation. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_01GbSAUSwx459CCq4pn9oTkD Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
The other bake flags (can_be_pv, the "can participate in the slack" weight and overshoot) are compared; this one was not, so two grids differing only by which hvdc lines are frozen compared equal and a regression in its binary round trip or its propagation went unseen. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_01GbSAUSwx459CCq4pn9oTkD Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
The VSC converter read duplicated the generators' one (the curve at the target P when the flat box is NaN, the sort of an inverted pair), and the two copies had already drifted apart: one wrote into a read-only array under pandas copy-on-write. Both now go through _aux_reactive_limits_at_target_p; each keeps its own bound for NaN. No behaviour change: the limits init_from_pypowsybl reads are bit-identical before and after on ieee14, ieee118, the four-substations network and on CURVE / swapped-curve / HVDC variants. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_01GbSAUSwx459CCq4pn9oTkD Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
The bake rebuilt the main-component filter by hand (connected and synchronous component 0 on both stations) next to the module's own _keep_only_main_comp: two definitions that would drift apart the day the rule changes. Filter the stations through it instead. No behaviour change: the lines baked, their target_p and converters mode are identical before and after on the hvdc test networks (saturated, linear, operator-range, a station disconnected), with and without keep_only_main_comp. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_01GbSAUSwx459CCq4pn9oTkD Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
sharing_weight recomputed, for every controller, the share of the bus of each of its peers, each by a pass over all the members of the group: cubic in the size of a group, at every rebuild of the plan. What it sums does not depend on the controller beyond its own membership, so the per-bus shares, whether the buses of the active members are all keyed, and the sums inside each bus are computed once per group; a held controller adds its own bus and terms on top, as its peer list did. The sums are accumulated in the same order: the weights are bit-identical before and after (checked on 1335 controllers over mixed keyed / unkeyed buses, held members and passive units), and a group of 2000 controllers on 400 buses builds in about a millisecond instead of several seconds. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_01GbSAUSwx459CCq4pn9oTkD Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
init_from_pypowsybl(remote_voltage_control_vm_range="olf") used a hard-coded minRealisticVoltage / maxRealisticVoltage of 0.8 / 1.2, but OpenLoadFlow's defaults differ between versions: the one pypowsybl 1.16.1 ships uses 0.5 / 2.0. There, lightsim2grid reported remote controllers OpenLoadFlow never switches, and test_reported_where_olf_switches failed in CI. "olf" now reads both defaults off the installed OpenLoadFlow (get_provider_parameters), with the margin of its loop; the constants are only the fallback when pypowsybl does not expose them. The two tests that check the reporting itself, not the default, pass an explicit range. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_01GbSAUSwx459CCq4pn9oTkD Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
Review fixes for #222: slack overshoot, VSC limits, OLF voltage range, sharing-key cost
OpenLoadFlow reads the standby automaton's b0 in LfStaticVarCompensatorImpl.setupVoltageControl, i.e. only for an SVC regulating in VOLTAGE mode. init_from_pypowsybl shifted [b_min, b_max] by b0 for every SVC carrying the extension, so a REACTIVE_POWER or OFF SVC got a reactive envelope OpenLoadFlow never uses. The bake already filtered on voltage mode; the loader now does the same, and also keeps the shift for an SVC the bake froze out of voltage control (can_be_pv), whose release brings it back. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_013nEtWC1QUbwyMjfDJ8JGnA Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
OpenLoadFlow switches a whole controller bus, not one of its units: an SVC it clamped at its limit while splitting a bus' reactive power with a generator that still has room keeps regulating. The generator rules already exclude that case (_bus_still_regulating); the SVC rules did not, so the bake froze such an SVC to REACTIVE_POWER -- through the "switched out of a shared group" rule by default, and through the saturated rule with bake_saturated_voltage_control. Both now skip an SVC whose own bus has another controller inside its range. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_013nEtWC1QUbwyMjfDJ8JGnA Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
The flag says an outer loop froze the line at its active power limit, and the release check re-derives the direction it is frozen in from the current converters_mode. Nothing cleared it when a caller (the grid2op backend, through change_p_dcline) later moved or reversed the set-point, so a line no longer at that limit kept being checked -- against the limit of whatever direction the new set-point pointed to, which reported a spurious HVDC_AC_EMULATION_RELEASE. change_p now clears the flag when the set-point or the direction actually changes; writing the same set-point again keeps it. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_013nEtWC1QUbwyMjfDJ8JGnA Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
distribute_with_overshoot returned early with not_distributed_mw = 0 when the participants' weights summed to zero or less, while distribute, in the same case, reports the whole mismatch as not distributed. Nothing moved, so a caller reading the report was told everything had been placed. Both paths now agree. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_013nEtWC1QUbwyMjfDJ8JGnA Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
A unit OpenLoadFlow capped beyond its active limit carries that overshoot into the redistribution pre-pass, but nothing ever reduced or cleared it: redistribute_active_power applied the full reference overshoot again on every call, and a unit that rejoined the slack and was later saturated brought the old value back. Two redistributions in a row therefore did not land where one redistribution of their sum does. distribute (both paths) now reports, per unit, how far beyond its bound the common shift leaves it; redistribute_active_power stores that on every unit out of the slack afterwards, and a unit joining (or removed from) the slack drops its overshoot. The batch pre-pass, which starts each row from the base case, is unchanged. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_013nEtWC1QUbwyMjfDJ8JGnA Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
The range of the LOW_/HIGH_VOLTAGE_REMOTE_CONTROL check was copied with the grid but left out of get_state, and init_from_pypowsybl turns it on by default. A grid pickled to worker processes, deep-copied (pybind falls back on the pickle state) or saved with save_binary therefore came back with the check silently off. The two bounds are appended to LSGrid::StateRes and restored through the setter, which also validates what a file holds. BINARY_FORMAT_VERSION 17 -> 18; the reference fixture is regenerated as case14_sandbox_format18.lsb. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_013nEtWC1QUbwyMjfDJ8JGnA Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
…share OpenLoadFlow builds the reactive keys of a shared voltage control from every LfGenerator of each controller bus (Control.createReactiveKeys): generators, but also batteries, VSC converter stations and SVCs, none of which can carry a key -- so one of them on a controller bus makes the whole group fall back on the reactive ranges. _collect_passive_gens only looked at generators, so a battery next to a keyed generator left lightsim2grid sharing by keys where OpenLoadFlow shares by ranges. The passive units of a controller bus now include the connected storage units, VSC stations (LCC stations are loads in OpenLoadFlow) and SVCs that control nothing; an SVC's susceptance range is converted to MVAr with the grid's sn_mva, which build_solver_side now takes. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_013nEtWC1QUbwyMjfDJ8JGnA Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
LSGrid::set_gen_can_participate_slack_overshoot said the values are >= 0, but they are signed -- negative for a unit capped below its lower limit -- and distribute_with_overshoot relies on that sign to tell, for a unit at 0 MW, which side of 0 it was capped from. The header comment and the GenInfo / StorageInfo attribute doc now say so; the python docstring also names the right init_from_pypowsybl argument (can_participate_slack_overshoot). Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_013nEtWC1QUbwyMjfDJ8JGnA Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
HVDC_AC_EMULATION_RELEASE and LOW_/HIGH_VOLTAGE_REMOTE_CONTROL are reported by LSGrid.get_physical_violations and by every batch's compute_physical_violations, but most of the documentation of those entry points still described the earlier set of checks: the LSGrid docstring listed only HIGH_P for an hvdc line, the batch and ContingencyAnalysis docs counted "five checks" and "five shapes", and the LimitViolation value / limit / side docs did not say what they hold for the release. Each place now lists both checks, with the meaning of side, value and limit, the tolerance each is compared with, and that the release check works in DC while the remote one is AC only. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_013nEtWC1QUbwyMjfDJ8JGnA Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
The previous commit had redistribute_active_power give a unit it saturated a fresh overshoot (how far the common shift would have taken it past its bound). That holds the unit at its limit on the next mismatch of the other sign, where OpenLoadFlow's distribution step lets a unit it capped move at once -- test_main_component_slack's test_redistribute_active_power_standalone checks exactly that and failed in CI. A redistribution now only uses up an overshoot a unit already carried (from the reference solve), never makes one and never flips its sign. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_013nEtWC1QUbwyMjfDJ8JGnA Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
add_pickle handed the whole LSGrid::StateRes (a tuple of the nested states of every container) to pybind11 in one go. That instantiates a single enormous tuple_caster, which - overflows MSVC's 64K type-record limit (C1067 in pybind11/detail/descr.h) on binding_lsgrid.cpp, so the Windows editable install failed and the tests then could not import lightsim2grid; - needs several GB of compiler memory, which OOM-killed cc1plus of gcc 8 even on the large resource class. The state is now converted recursively one tuple element at a time (C++14, no fold expressions), which gives the very same python tuple, so existing pickles stay readable. Peak compiler memory on binding_lsgrid.cpp drops from about 3.1 GB to about 1.1 GB. Assisted-by: Claude Code (claude-sonnet-5-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ToXXBvRV1cdc9sM3PE4AZF Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com> (cherry picked from commit 7b2dcca)
Fix the review findings on #222 (slack overshoot, SVC b0, hvdc frozen flag, reactive keys)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR brings lightsim2grid's outer-loop-free solve (and the physical-limit checks that stand in for OpenLoadFlow's outer loops) closer to OpenLoadFlow on hvdc lines, VSC stations, SVCs and remote voltage control. Each case below was a contingency where OLF switched or capped an element and lightsim2grid either modelled it differently or reported nothing.
HVDC lines and VSC converter stations
init_from_pypowsyblnow readshvdcOperatorActivePowerRangeper direction as the line's limit, like OLF does, and only falls back onmax_pwhen that range is missing.bake_outer_loops(bake_hvdc_ac_emulation_limits=True)(on by default) freezes an angle-droop line that OLF saturated at its limit to a fixed setpoint.LSGrid.set_hvdc_ac_emulation_frozenreportsHVDC_AC_EMULATION_RELEASEwhen the droop asks for less than the limit the line is frozen at.Generators, SVCs and the bake
LSGrid.set_remote_voltage_control_vm_rangeadds a physical check (LOW_/HIGH_VOLTAGE_REMOTE_CONTROL) for a generator that holds a remote bus from an unrealistic voltage on its own bus. This is OLF's robust remote voltage control.init_from_pypowsybl(remote_voltage_control_vm_range="olf")sets OLF's range.b0, both ininit_from_pypowsybland in the bake.set_gen_can_participate_slack_overshoot, filled by the bake).Breaking
BINARY_FORMAT_VERSIONgoes from 15 to 17: the slack overshoot and the hvdc AC-emulation frozen flag are now serialized. The test fixture is regenerated ascase14_sandbox_format17.lsb.Merge with
dev_1.1.1origin/dev_1.1.1is merged into the branch. The only conflict was inCHANGELOG.rst, where both sides had added entries at the top of the 1.1.1 section. Both sets are kept and the section is sorted by kind.Testing
New or extended tests:
test_hvdc_pypowsybl,test_olf_bake,test_remote_voltage_control,test_svc_standby,test_can_participate_slack,test_voltage_control_pypowsyblandtest_binary_serialization.After the merge,
LSGrid.cpp,help_fun_msg.cppandbinding_lsgrid.cpppass a C++14 syntax check. The Python tests were not re-run on the merged tree; CI covers that.🤖 Generated with Claude Code