Repository navigation
Outer detection - #15
Conversation
…l ones Assisted-by: Claude Code (Fable 5.1) Signed-off-by: DONNOT Benjamin <benjamin.donnot@rte-france.com>
…rence and add storage unit as a voltage controler Assisted-by: Claude Code (Opus 5) Signed-off-by: DONNOT Benjamin <benjamin.donnot@rte-france.com>
…or were not regulating Assisted-by: Claude Code (Opus 5) Signed-off-by: DONNOT Benjamin <benjamin.donnot@rte-france.com>
Assisted-by: Claude Code (Opus 5) Signed-off-by: DONNOT Benjamin <benjamin.donnot@rte-france.com>
Assisted-by: Claude Code (Opus 5) Signed-off-by: DONNOT Benjamin <benjamin.donnot@rte-france.com>
Assisted-by: Claude Code (Opus 5) Signed-off-by: DONNOT Benjamin <benjamin.donnot@rte-france.com>
set_gen_v (InjectionSweepGPU, ScenarioSweepGPU, BatchPowerFlow) now keys a column on the bus its generator REGULATES (InjectionElements.gen_v_bus), not on the bus it stands on: a Vm-fixed bus is reseeded as before, while a bus held by a VoltageControl group gets that group's per-row v_set (GenVsetSlots, vc_vset_stride), which is what a remote regulator's set-point was silently dropped from. The Vm-fixed mask now excludes a slack bus that carries a ledger Vm unknown; conflicting_gen_v_rows and the gradient gate follow the same mapping and refuse a group holding an SVC / hvdc station asked for anything but its pinned set-point. Tied set-points share one derivative (get_gen_v_share parity). BatchTimings also carries the cuDSS factor statistics (LU nnz, memory estimates) read after every ANALYSIS, and benchmark_cudss_batch_raw runs the uniform-batch solver on an arbitrary CSR matrix to size a Jacobian no session builds yet. Assisted-by: Claude Code (Opus 5) Signed-off-by: DONNOT Benjamin <benjamin.donnot@rte-france.com>
…past their limits lightsim2grid 1.1.0's GenPCheck, as the third member of compute_physical_violations: the slack is solved inside the Jacobian by participation factors that know nothing about limits, so a participating machine's converged active power (its target plus its share of the imbalance) can leave [min_p_mw, max_p_mw] -- what OpenLoadFlow's DistributedSlack outer loop re-shares. Reported per machine (LOW_P / HIGH_P on the new GENERATOR / STORAGE element types, container id), never enforced. Generators and storage units are one check, as upstream: the share of a machine is a fraction of the raw participation of its whole bus, both families included, so the plan (GenPPlanData, built by lightsim2grid's own build_gen_p_plan through the bridge, or handed in as arrays; optional -- unset means nothing has a limit) carries every participant next to the reportable entries. The kernel reads no slack state: the live raw participation of a bus is summed on the device from the plan (upstream's w_norm(bus) * total_raw_w), and the share comes from the raw active residual of the slot's patched Ybus plus the droop hvdc flows leaving the bus. A generator's target is the row's own: the two sweep facades push gen_p through set_gen_p_targets; a storage unit's is the grid's, negated into the generator convention its limits and the reported value use. Pinned against ContingencyAnalysisCPP, InjectionSweepCPP and ScenarioSweepCPP, and against a single ac_pf's res_p_mw. Array mode is tested on one slack bus holding two machines: the tuple path has no distributed slack (uniform weights, trivial ledger), a limitation that predates this change. Assisted-by: Claude Code (Fable 5.1) Signed-off-by: DONNOT Benjamin <benjamin.donnot@rte-france.com>
…nal voltage The extremity-side current used the origin bus' kV base, so on a transformer (or any branch joining two voltage levels) i_ex / ka_ex and the CURRENT violations on side 2 were off by the voltage ratio. The GPU flow and limit-violation kernels now take a second base (d_base_current_ex_A), and the numpy (compute_branch_flows_cpu) and torch (compute_flows) helpers do the same. compute_branch_flows_cpu also handles a -1 (Kron-reduced / half-open) endpoint like the kernels: 0 A on that terminal, V=0 on that side, the other end's amp base. Before, numpy read V[-1] / bus_vn_kv[-1] and reported a wrong current on the live side. The new test checks both terminals against lightsim2grid's res_a1_ka / res_a2_ka. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: DONNOT Benjamin <benjamin.donnot@rte-france.com>
Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: DONNOT Benjamin <benjamin.donnot@rte-france.com>
The batched violation kernels (operational and physical) used to keep the first `capacity` records found per row, in scan order. They now keep, for each violation type, the `capacity` most severe ones, sorted most severe first, types one after the other in the row's output: - CURRENT, LOW_VOLTAGE, HIGH_VOLTAGE ranked by |value/limit - 1| - LOW_Q / HIGH_Q, HVDC HIGH_P, LOW_P / HIGH_P (generators and storage units ranked together) ranked by |value - limit| A shared device helper (TopKByType) does a sorted insertion into each type's own range of the row's slice, rejecting a less severe candidate in O(1) once the range is full, then packs the ranges to the front. Output buffers grow to n_types * capacity per row; the physical result structs expose it as `stride`. `truncated` now means some type exceeded capacity. Parity tests compare records with lightsim2grid as sets and check the ranking separately; a new test pins the kept records against an uncapped run. Also add AGENTS.md as a symlink to CLAUDE.md. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
lightsim2grid PR #216 makes its batch contingency analysis match OpenLoadFlow with outer loops on real RTE snapshots. Every batch-side change of it is a parity gap here: - redistribute_slack (ContingencyAnalysisGPU / ScenarioSweepGPU): what a row loses (a masked island, a disconnected generator) is shared on the remaining slack units before the solve, OLF's DistributedSlack way (verbatim port of upstream's distribute, slack_redistribution.hpp): bounded by [min_p, max_p], never crossing 0 MW, a saturated unit leaving that row's slack. Per-row weights and a per-chunk Sbus correction (SlotSlackRedistribution); ContingencyBatch gets a per-slot Sbus only when a correction exists, so the option off is bit-identical. The slack active-power check follows (moved set-points, no share for a saturated unit). BatchPowerFlow runs the pre-pass in the forward, with the exact piecewise-linear VJP, FD-checked. - A VoltageControl group every controller of which is stranded is recovered at any size (Q_first = 0, a reserved slot per group); a row stranding a regulated bus whose controllers stay live is skipped. - Automatic reference slack (reference_slack="auto"): the participant stranded by the fewest rows becomes the angle reference; move_reference reproduces lightsim2grid's own ledger for it and the base state is rebuilt (now possible on ContingencyAnalysisSession too). - PQ -> PV release check (LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q), built from lightsim2grid's own plan, with physical_violation_tol_vm_pu. - Fix: a hot ScenarioSweep run wiped the row masks (disconnected / stranded groups), which also gave a stranded group a gen_v gradient. The operational checks also take violation_rel_tol (default 1e-9, as lightsim2grid): a value is reported only beyond its limit by more than that relative margin, so a bus a regulator holds exactly at its vmax is not reported or not depending on the last bit of the solve. Kernel, numpy n-case, sessions, engines and facades; documented in CLAUDE.md and docs/api.rst. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
Port lightsim2grid's standby SVC check (SvcStandbyCheck.hpp) to the batch sessions: an idle SVC flagged with LSGrid.set_svc_standby whose regulated bus leaves its automaton's [low, high] thresholds, which OpenLoadFlow's MonitoringVoltageOuterLoop would switch to voltage control, is reported as LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY (11 / 12) on the new ViolationElementType.SVC (7), value and limit in kV. It is routed through the PQ -> PV release plan rather than a plan of its own: the test is the same (below a target at "min", above it at "max"), so an SVC is two entries of the new GenPvReleasePlanData.el_type SVC, its low and its high threshold, appended by the bridge from lightsim2grid's own build_svc_standby_plan (behind __has_include). The kernel gives them two groups of their own (N_GEN_PV_RELEASE_VIOLATION_GROUPS 2 -> 4), never masks them by a generator contingency nor moves them by a row's targets (NaN columns in _push_gen_pv_release_targets), and the Python decoding reads the element type off the type code. An empty el_type keeps every entry a generator, so array-mode callers are unchanged. Adds a CHANGELOG.rst, which the repository did not have. Assisted-by: Claude Code (Opus 5.5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
Port lightsim2grid's release check of the SVCs flagged as frozen at a reactive limit (LSGrid.set_svc_can_be_pv): a fixed-Q SVC at the absorbing (resp. producing) end of its range whose regulated bus is below (resp. above) its target is reported as LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q on the SVC. The release plan already carried SVC entries for the standby check; an SVC entry is now either kind: GenPvReleasePlanData.el_type says the element (5 GENERATOR / 7 SVC) and the new optional GenPvReleasePlanData.standby marks the standby entries. The bridge takes the frozen SVCs from lightsim2grid's own build_gen_pv_release_plan. As types 9 / 10 can now be on a generator or an SVC, every record carries its element type (GenPvReleaseViolationsResult.el_type), which the Python decoding reads instead of inferring it from the type. Also restores the standby SVC CHANGELOG entry: the previous commit picked up another CHANGELOG.rst created at the same time, without it. Assisted-by: Claude Code (Opus 5.5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
Port lightsim2grid's "can participate in the slack" flag to the redistribute_slack pre-pass: a unit left out of the slack only because it sat at an active limit (LSGrid.set_gen_can_participate_slack) takes a share of what a row loses within its [min_p, max_p] -- only away from the limit it sits at -- but never enters the solve's slack weights. SlackRedistributionData gets an optional in_slack (one per unit, empty = all in the slack): the bridge adds the flagged units with in_slack 0, in upstream's append_participants order; row_slack_weights skips them and a row never records one as saturated (including through set_external_slack_saturation). Assisted-by: Claude Code (Opus 5.5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
lightsim2grid's build_gen_pv_release_plan now pins a generator flagged can_be_pv at the nearer of its reactive limits instead of requiring its setpoint within the tolerance of one. The bridge calls it without the tolerance (extract_gen_pv_release_plan_from_lsgrid keeps its tol_mva argument for its callers, ignored), and the facades no longer rebuild a grid-built release plan when physical_violation_tol_mva changes. Assisted-by: Claude Code (Opus 5.5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
Port lightsim2grid's release check of the VSC converter stations flagged as frozen at a reactive limit (LSGrid.set_hvdc_can_be_pv): reported on the HVDC line, `side` the station's end. The release plan takes HVDC entries (el_type 4, gen_id the hvdc line id) and an optional GenPvReleasePlanData.side; the bridge copies lightsim2grid's entry side (behind LS2G_HAS_RELEASE_ENTRY_SIDE). The kernel runs the generators' test on them -- never masked by a generator contingency nor moved by a row's targets -- and writes each record's side (GenPvReleaseViolationsResult.side), which the Python decoding reads. Assisted-by: Claude Code (Opus 5.5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
Follows lightsim2grid: redistribute_slack on a scenario sweep also pre-shares the imbalance a row's injections create against the grid's set-points, which went to the unbounded Newton slack before. - SlackRedistributionData.base_p_mw: sn * sum Re(grid Sbus), filled by the bridge (NaN in array mode without it: no such term; optional last element of the tuple and of the binding's constructor). - injection_change_lost_mw: base_p_mw minus the row's total active injection, the generators it disconnects put back at their row set-point (term (a) counts them) -- upstream's element-wise sum on per-bus data. prepass_all takes the row totals; the contingency session keeps the old overload, without the term. - BatchPowerFlow's torch pre-pass adds the same term. - Tests against lightsim2grid's batch (element-wise and per-bus injections, with an island and a generator off in one row, BatchPowerFlow, array mode without base_p_mw). The gradient check's row 2 now raises a load: at a row losing exactly nothing, the units at 0 MW leave the slack as soon as the lost power turns negative, a jump the central difference straddled. Assisted-by: Claude Code (Opus 5.5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
Brings in the build CI (#16), the Modal GPU test job (#17) and the scenario-sweep / adjoint fixes of #14. Conflict resolutions: - scenario_sweep_session.cu: keep main's warm path (rebuild the driver when the live chunk capacity would split the active rows into too many chunks, or break keep_final_jacobian's single chunk) and its reset_rows lambda; drop main's per-row slack weights from the ScenarioSweepBatch constructor, since this branch hands them over after the build through set_slack_redistribution. - _batch_pf.py: take main's rule that _topology_mask only changes once the session accepted the topology; _row_masks (redistribute_slack's pre-pass, which runs before the op hands the topology over) now reads the queued topology and keys its cache on that tensor. - _flows.py: per-terminal ampere bases (this branch) with main's half-open handling; a -1 side falls back to the live end's vn_kv, as set_branch_data does. - scenario_sweep/gpu_facade.py: main's _push_gen_v helper, with this branch's gen_v_bus mapping. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_01Afi372v35Q3tbaDtypd9Fa Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
Main keeps each batch session's driver across runs (BatchDriverConfig). In contingency analysis, two things this branch adds change what that driver is built from, so either now forces a new driver: - the automatic reference slack rebuilds the base state (base_state_generation_ is part of the snapshot); - the redistribute_slack pre-pass lives in the source (toggling it or handing in new participants bumps contingencies_generation_). The injection sweep keeps main's hot path and passes the VoltageControl group map to build_gen_v_override. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
- Per-row targets of the physical checks: the sweep sessions clear their
dirty flags only on a run with the checks on, so set-points changed
while they were off reach the live driver once they are back on.
- v_base_dlpack(): base_state_ is a shared_ptr the capsule co-owns, so a
view taken before a base-state rebuild (automatic reference slack,
generator-contingency reserved set) no longer reads freed memory.
- ContingencyAnalysisSession refuses slack participants given with
another sn_mva than the branch data's, as ScenarioSweepSession does.
- choose_reference_bus_from_ledger: the automatic-reference candidate
rule, shared by both sessions instead of copied.
- ScenarioSweepGPU derives the gen_v skip mask again at compute() when
set_gen_v() came with another row count than the injections, so the
setters may come in any order.
- violation_tol / violation_rel_tol / violation_capacity live once in
LimitViolations{Engine,Facade}Mixin.
- Physical checks on a reused driver: the setup (plan uploads, output
allocation, n-case kernels) is redone only when its PhysicalSetupKey
changes, otherwise the -1 sentinels are re-seeded; turning the checks
off disarms the driver.
- Docs: api.rst lists the five physical checks; CLAUDE.md qualifies the
bit-for-bit claim for a resistive droop HVDC line.
Assisted-by: Claude Code (claude-opus-5-5)
Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
lightsim2grid master (1.2) adds LOW_VOLTAGE_REMOTE_CONTROL (13), HIGH_VOLTAGE_REMOTE_CONTROL (14) and HVDC_AC_EMULATION_RELEASE (15), all PHYSICAL. test_enum_parity_with_lightsim2grid walks every upstream member, so the GPU CI job (which builds lightsim2grid master) failed with a KeyError. Add the three codes to both mirrors (the Python IntEnum and the C++ limit_violation_types.hpp) and to the PHYSICAL set of violation_category(). No gpusim2grid kernel writes them yet; this only keeps the codes in sync. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
test_context_init_is_negligible_after_warmup compared one wall-clock reading against 5 ms on a shared Modal T4 and failed now and then (9.8 ms on main after PR #14, 23 ms on this PR), unrelated to the code under test. A missed warmup costs tens of ms (cuSPARSE/cuDSS dlopen + PTX JIT, context creation), so 50 ms still catches the regression the test is there for. Assisted-by: Claude Code (claude-opus-5-5) Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
| ``get_violations()``. A row that was never simulated or did not converge has | ||
| an **empty** entry (no sentinel), exactly as lightsim2grid reports it. | ||
|
|
||
| Today the category holds two checks: |
There was a problem hiding this comment.
There was a problem hiding this comment.
Fixed in 96d2ebd: api.rst now says the category holds five checks and lists them all: bus reactive capability, PQ → PV release, standby SVC switch-on, HVDC droop P saturation, and the slack machines' active power (LOW_P / HIGH_P against min_p / max_p, generators and storage units together). It also documents physical_violation_tol_vm_pu.
Generated by Claude Code
| if (phys_.compute_physical_violations) | ||
| solver_->upload_gen_p_no_share(rd_ns_gen, slack_rd_.n_gen, rd_ns_sto, slack_rd_.n_sto); | ||
| rd_gp_last_ = rd_gp; | ||
| gen_p_targets_dirty_ = false; |
There was a problem hiding this comment.
Stale per-row targets after turning compute_physical_violations off and on. gen_p_targets_dirty_ / gen_pv_release_targets_dirty_ are cleared on every run. But when the checks are off, physical_checks::before_solve returns immediately (physical_checks_impl.cuh:61) and uploads nothing. The flag isn't part of ScenarioSweepDriverConfig, so turning it back on reuses the same driver, which still holds the old targets. sync_targets then sees has && !dirty && driver_has and skips the upload.
Repro: checks on, set_injections_from_elements(A), compute() → checks off, set_injections_from_elements(B) (same row count), compute() (hot run: the flags are cleared, nothing is uploaded) → checks on, compute(). LOW_P/HIGH_P and PQ→PV release records are evaluated against A's set-points, not B's. The same applies to the set_gen_v release targets.
Suggested fix: clear the two flags only when the checks are on (i.e. when before_solve actually synced), or pass gen_p_targets_dirty_ || !phys_.compute_physical_violations-style state through.
Generated by Claude Code
There was a problem hiding this comment.
Fixed in 96d2ebd. Both ScenarioSweepSession::run() and InjectionSweepSession::run() (the injection sweep also reuses its driver since the merge) now clear gen_p_targets_dirty_ / gen_pv_release_targets_dirty_ only on a run with compute_physical_violations on, so targets changed while the checks were off still reach the driver. In addition, turning the checks off now disarms the live driver (BatchPfDriver::disable_physical_checks), so a reused driver no longer runs the four kernels. Pinned by test_gen_p_violations.py::test_targets_changed_while_the_checks_were_off[InjectionSweepGPU|ScenarioSweepGPU], your repro: A, off, B, on.
Generated by Claude Code
| const int want_ref = _wanted_reference_bus(); | ||
| if (want_ref != ref_bus_) { | ||
| ref_bus_ = want_ref; | ||
| _build_base_state(); |
There was a problem hiding this comment.
v_base_dlpack() can point at freed memory, now by default. run() rebuilds the base state here whenever _wanted_reference_bus() differs from ref_bus_, and reference_slack='auto' is the new default. export_v_base_dlpack (dlpack_export.cu:117) makes the session the capsule's owner, but base_state_ is a unique_ptr that this line replaces. A tensor taken before compute() (torch.from_dlpack(inner.v_base_dlpack())) then reads freed GPU memory, although the Python docstring says the view lives as long as the solver.
Before this PR, this could only happen through ScenarioSweep generator contingencies (and CLAUDE.md documents it there); now it happens by default in contingency analysis with handle_disconnected_grid=True whenever a contingency strands the grid's reference.
Options: make the capsule own a shared_ptr to the base state (and turn base_state_ into one), or at least document it in ContingencyAnalysis*.v_base_dlpack as it already is for ScenarioSweep.
Generated by Claude Code
There was a problem hiding this comment.
Fixed in 96d2ebd with the first option. base_state_ is now a std::shared_ptr<AcPfNrState> in both ContingencyAnalysisSession and ScenarioSweepSession, and export_v_base_dlpack / export_v_base_dlpack_ss make the capsule co-own it (std::static_pointer_cast<void>(self->base_state_)). A view taken before a rebuild keeps the previous base voltages alive instead of reading freed memory. The v_base_dlpack docstrings and CLAUDE.md say so. Pinned by test_scenario_sweep_gen_contingency.py::test_v_base_view_outlives_a_base_state_rebuild.
Generated by Claude Code
| if (redistribute_slack_) { | ||
| _check_redistribute_slack(); | ||
| const int n_bus = base_state_->n_bus; | ||
| const double sn = slack_rd_.sn_mva; |
There was a problem hiding this comment.
slack_rd_.sn_mva is used without being checked. The pre-pass converts the per-unit h_Sbus_base_ to MW with slack_rd_.sn_mva. Neither set_slack_redistribution_data (L779) nor _check_redistribute_slack (L791) checks that it matches the base h_Sbus_base_ is expressed on. ScenarioSweepSession::_check_redistribute_slack does (the "given with another sn_mva" error).
Failure: in array mode, set_slack_redistribution_data(..., sn_mva=1.0) on a grid in per-unit on 100 MVA under-counts an island's lost power 100×, so units aren't clamped and the report's mismatch_mw is wrong, with no error.
Note: this session only learns its sn_mva_ from set_branch_data(), so the check may need that, or an explicit sn_mva at construction.
Generated by Claude Code
There was a problem hiding this comment.
Fixed in 96d2ebd. ContingencyAnalysisSession::_check_redistribute_slack now refuses participants whose sn_mva differs from the branch data's ("the slack participants were given with another sn_mva than the branch data's (set_branch_data)"), like the scenario sweep. As you noted, the session only learns sn_mva_ from set_branch_data(). But build_contingencies() already refuses to run without it, so by run() the branch data's sn_mva is always known, and the check runs there. Pinned by test_redistribute_slack.py::test_ca_refuses_participants_on_another_sn_mva.
Generated by Claude Code
| mask_cfg_ = build_mask_config(*base_state_, ledger_ptr); | ||
| } | ||
|
|
||
| int ContingencyAnalysisSession::_wanted_reference_bus() |
There was a problem hiding this comment.
Duplicated code (maintainability). This is a verbatim copy of ScenarioSweepSession::_wanted_reference_bus(): the same candidate rule (positive weight, has a P row), the same choose_reference_bus call and the same fallback. If the rule changes in one copy only (e.g. to exclude pre-pass-only units), contingency analysis and scenario sweep will quietly pick different references. A shared helper next to choose_reference_bus that takes the LedgerData would remove the duplication.
Generated by Claude Code
There was a problem hiding this comment.
Fixed in 96d2ebd. New choose_reference_bus_from_ledger(contingencies, Ybus_rm, ledger, forced, fallback) next to choose_reference_bus in contingency_analysis_helper.{hpp,cpp} holds the candidate rule (positive weight, has a P row), the call and the fallback. Both sessions' _wanted_reference_bus() now call it, so the two can no longer drift apart.
Generated by Claude Code
| vc_group_of_bus=self._inner.vc_group_of_bus, | ||
| vc_pinned_v_set=self._inner.vc_pinned_v_set) | ||
| if bad.any(): | ||
| self._inner.set_skipped_rows(bad) |
There was a problem hiding this comment.
Calling set_gen_v() / set_contingency_gens() before set_injections*() breaks the next compute(). The skip mask is pushed right away, from set_gen_v() and set_contingency_gens(). C++ set_skipped_rows throws when its row count differs from the injections the session currently holds. Nothing re-derives the mask when set_injections* later changes the row count.
Repro: the session holds 10-row injections.
set_gen_v(<20 rows, some conflicting>)storesself._gen_vand pushes it (C++set_gen_vaccepts any row count), then raisesrow count must match set_injections()here._push_gen_pv_release_targets()is never reached, so the release-check targets are stale too.- After
set_injections_from_elements(<20 rows>),compute()fails with "set_skipped_rows()'s row count no longer matches". That names an internal API the facade user never called, and the only way out is to callset_gen_vagain.
Suggested fix: derive and push the skip mask lazily at compute() time, or also call _update_skipped_rows() from set_injections* / set_topology.
Generated by Claude Code
There was a problem hiding this comment.
Fixed in 96d2ebd for set_gen_v(). The facade now records the injections' row count (_n_injection_rows, set by set_injections / set_injections_from_elements), and set_gen_v pushes gen_v and the PQ → PV release targets before deriving the skip mask. If the row counts differ, it clears the skipped rows and marks them stale; compute() re-derives them (_update_skipped_rows) before run(). New injections also mark the mask stale. So set_gen_v(<20 rows>) followed by set_injections_from_elements(<20 rows>) then compute() works, and the release targets are no longer left stale. Pinned by test_gen_v.py::TestConflictingSetpoints::test_gen_v_may_change_row_count_before_the_injections.
Not changed: set_contingency_gens() with a row count different from the current injections is still refused straight away by the C++ session, with an explicit row-count error naming set_contingency_gens (not an internal API). It fails at the call, not at a later compute().
Generated by Claude Code
| self._inner.compute_limit_violations = value | ||
|
|
||
| @property | ||
| def violation_rel_tol(self): |
There was a problem hiding this comment.
Duplicated code (maintainability). The violation_rel_tol property (docstring, getter, setter and the [0, 1) validation) is copy-pasted in both engines and both facades (8 def violation_rel_tol in src/gpusim2grid). The PR already moved the physical-check and slack-redistribution properties into shared mixins; a small limit-violations mixin would do the same here, and could also hold get_violations / violation_capacity.
Generated by Claude Code
There was a problem hiding this comment.
Fixed in 96d2ebd. _limit_violations.py now has LimitViolationsEngineMixin (violation_tol, violation_rel_tol with its [0, 1) validation, violation_capacity) and LimitViolationsFacadeMixin (violation_rel_tol forwarded to the engine). The two engines and ContingencyAnalysisGPU / ScenarioSweepGPU inherit them instead of carrying copies. def violation_rel_tol now appears only in the two mixins.
Generated by Claude Code
| "set_bus_q_capability() first (the *GPU facades do it from a lightsim2grid grid " | ||
| "through set_bus_q_capability_from_grid(); an EMPTY plan is accepted when no " | ||
| "machine regulates a voltage)."); | ||
| drv.set_bus_q_check(cfg.bus_q_plan, cfg.physical_violation_tol_mva, |
There was a problem hiding this comment.
Hot runs redo work that could be skipped (perf). On the persistent ScenarioSweep driver, every compute() with physical checks on re-uploads all four plans, reallocates every output buffer with assign(), re-runs the four single-thread n-case kernels, and waits on several cs.synchronize() calls, even when only the injections changed.
The plans and the n-case report only change when the plan or the base state changes. A generation counter could skip the upload and the n-case work, keeping just a fill() to reset the -1 sentinels. This matters for the BatchPowerFlow training loop.
Generated by Claude Code
There was a problem hiding this comment.
Fixed in 96d2ebd. physical_checks::before_solve now builds a PhysicalSetupKey: a plan generation (PhysicalChecksConfig::plan_generation, bumped by every set_*_plan), the tolerances, residual tol, sn_mva, capacity, the generator-mask pointer, n_gen and the base case's convergence. When the key matches the driver's, the plan uploads, the assign() reallocations and the four n-case kernels (with their syncs) are skipped; only BatchPfDriver::reset_physical_outputs() runs (a thrust::fill re-seeding the -1 sentinels and truncated flags), plus the per-row target sync when they changed. The base-state generation is in the driver snapshot, so a base-state change is a cold driver and redoes the setup. With only the injections changing, a BatchPowerFlow training step pays a fill.
Generated by Claude Code
| - **Structural supersets** (`ledger_extend.{hpp,cpp}`): the CA/SS sessions may solve a J whose pattern is a strict *superset* of lightsim2grid's — every extra entry is filled per slot by value, so one cuDSS analysis still serves the batch. Two extensions exist: `reserve_stranded_controller_slots` (one `(v_row, q_col)` zero per lone GEN controller, see `handle_disconnected_grid`) and `add_switchable_vm_buses` (one Vm column + Q equation per bus, with the full dS pattern derived from Ybus, **appended at the end** of the ledger so no existing index moves — which is why the VoltageControl custom rows are first frozen into explicit `LedgerData::vc_v_rows`/`vc_share_rows` by `materialize_vc_custom_rows`; an empty `vc_v_rows` still means lightsim2grid's own "last `n_controllers` rows" layout). `AcPfGPU`/`InjectionSweepGPU` ledgers are never extended. `drop_multislack_augmentation` must run *before* either extension. | ||
| - **Feature kernels** are small, conditionally-gated additions layered on the same driver: MultiSlack (`adjust_slack_mismatch`/`fill_slack_feature`/`update_slack_absorbed`, gated on `slack_col>=0`), HVDC droop (`hvdc_adjust_mismatch`/`hvdc_fill_feature`, additive J slopes — forces `d_J_values` to be zeroed before `fill_J` when active), and VoltageControl (`vc_adjust_mismatch`/`vc_vrow`/`vc_share`/`vc_apply_step`, a bordered formulation that adds custom rows). Shared inline helpers `nr_feature_{mismatch,zero_J,fill_J,update}` (`nr_iter_step.cuh`) are called from both the single-system step and the batched `driver.cuh` loop, so CA/InjectionSweep get the augmented system "for free" once the base session carries a `LedgerData*`. | ||
| - **Structural supersets** (`ledger_extend.{hpp,cpp}`): the CA/SS sessions may solve a J whose pattern is a strict *superset* of lightsim2grid's — every extra entry is filled per slot by value, so one cuDSS analysis still serves the batch. Two extensions exist: `reserve_stranded_controller_slots` (one `(v_row, q_col_first)` zero per VoltageControl group whose first controller is not an SVC, see `handle_disconnected_grid`) and `add_switchable_vm_buses` (one Vm column + Q equation per bus, with the full dS pattern derived from Ybus, **appended at the end** of the ledger so no existing index moves — which is why the VoltageControl custom rows are first frozen into explicit `LedgerData::vc_v_rows`/`vc_share_rows` by `materialize_vc_custom_rows`; an empty `vc_v_rows` still means lightsim2grid's own "last `n_controllers` rows" layout). `AcPfGPU`/`InjectionSweepGPU` ledgers are never extended. `drop_multislack_augmentation` must run *before* either extension. | ||
| - **Feature kernels** are small, conditionally-gated additions layered on the same driver: MultiSlack (`adjust_slack_mismatch`/`fill_slack_feature`/`update_slack_absorbed`, gated on `slack_col>=0`), HVDC droop (`hvdc_adjust_mismatch`/`hvdc_fill_feature`, additive J slopes — forces `d_J_values` to be zeroed before `fill_J` when active; the receiving-side slope is the *exact* derivative of the flow, `k·(1−lf1)(1−lf2)·(1−2·r·line_in)` — NR converges without the last factor, the adjoint does not: it was a ~1e-4 relative error on real-grid P gradients, see `test_hvdc_droop_resistive_loss_slope_is_exact`), and VoltageControl (`vc_adjust_mismatch`/`vc_vrow`/`vc_share`/`vc_apply_step`, a bordered formulation that adds custom rows). Shared inline helpers `nr_feature_{mismatch,zero_J,fill_J,update}` (`nr_iter_step.cuh`) are called from both the single-system step and the batched `driver.cuh` loop, so CA/InjectionSweep get the augmented system "for free" once the base session carries a `LedgerData*`. |
There was a problem hiding this comment.
The docs now contradict each other. The exact receiving-side slope here is intentional and well justified: the adjoint needs it, and test_hvdc_droop_resistive_loss_slope_is_exact pins it. But the intro to this section (L140) still says results "match lightsim2grid bit-for-bit (to ~1e-15) on both the single solve and the batched … paths". As this note says, lightsim2grid's Jacobian lacks the (1−2·r·line_in) factor. So for a droop HVDC line with r != 0, the NR path differs: the converged state still matches to NR tolerance, but a fixed nb_iter / partially converged batch (e.g. direct_iter0_only) no longer matches to ~1e-15.
Suggest qualifying the L140 claim (e.g. "… except the Jacobian slope of a resistive droop HVDC line, see below, which changes the iterates but not the converged state").
Generated by Claude Code
There was a problem hiding this comment.
Fixed in 96d2ebd. The L140 claim now reads "… match lightsim2grid bit-for-bit (to ~1e-15) … except on a grid with a droop HVDC line of non-zero resistance, whose receiving-side Jacobian slope gpusim2grid takes exactly (see Feature kernels below): that changes the NR iterates, not the converged state, so it still matches to the NR tolerance but a fixed nb_iter / partially converged batch (e.g. direct_iter0_only) no longer matches to ~1e-15."
Generated by Claude Code
No description provided.