pydynamicestimator.tests.test_dist_load
ZIP-aware LOAD disturbance handler — comprehensive tests.
Exercises Dae.dist_load across every load-model variant, the two line_dyn branches, sign conventions, and error handling.
Strategy: each test programmatically builds a one-scenario testcase under tmp_path by copying the frozen 3_bus_loadstep/ fixture and rewriting the StaticZIP line (or substituting StaticLoadImpedance / StaticLoadPower) plus the disturbance line. This avoids a fixture-folder explosion while keeping the test inputs reproducible — the test body is the source of truth.
Attributes
Functions
|
Stage a scenario folder under tmp_path. |
|
Return a config that runs the staged scenario on the sim side. |
|
|
|
|
|
Pure-Z load + 30 MW step. Linear KCL — no PV nose, IDA converges. |
|
Pure-P with the heavy default 100 MW baseline + 30 MW step under |
|
Pure-P with a small (1 MW) baseline and step under DAE |
|
Pure-I with a 2 MW step under DAE (line_dyn=False / idas). |
Pure-I with a 30 MW step under dynamic lines (line_dyn=True / |
|
Mixed Z/I/P shares with Z-dominant (0.8/0.1/0.1). The Z anchor |
|
StaticLoadImpedance should be treated as z=1 (and produce results |
|
StaticLoadPower must be inferred as p=1 and produce numerics |
|
|
LOAD disturbance at a bus without any load device (here: bus 1, |
|
Two LOAD events at the same bus should stack (post-second total |
|
A negative p_delta = load reduction should RAISE the bus voltage. |
|
Positive q_delta = inductive consumption, should drop voltage |
|
The line_dyn=True branch of dist_load must also be ZIP-aware |
q_share(branch, k) returns the P-side share when Q-side is NaN |
|
q_share(branch, k) returns the Q-side value when it's set (not NaN). |
|
Different entries can independently opt in or out of Q-side shares. |
|
|
A bus declared with only P-side shares (no _share_q fields) must |
|
A pure Q-only LOAD step (p_delta=0, q_delta>0) on a bus with |
|
The 'heating with controls' scenario from the design doc: |
|
If Q-side shares don't sum to 1.0, StaticZIP.finit logs a warning. |
The sum-to-1 warning fires for the P-side too, mirroring the Q |
|
Verify the parser handles a sim_param.txt mixing old-style and |
|
StaticLoadImpedance has no q_share() method — the dist_load |
Module Contents
- pydynamicestimator.tests.test_dist_load.FIXTURE_ROOT
- pydynamicestimator.tests.test_dist_load.TEMPLATE
- pydynamicestimator.tests.test_dist_load.BUS2_IDX = 1
- pydynamicestimator.tests.test_dist_load._stage(tmp_path: pathlib.Path, *, load_line: str | None = None, bus_init_line: str | None = None, sim_dist: str | None = None) pathlib.Path[source]
Stage a scenario folder under tmp_path.
Copies the 3_bus_loadstep template; optionally rewrites the load line for bus 2, the BusInit line for bus 2, and the entire sim_dist.txt content.
The default 3-bus fixture has a 100 MW + 10 MVAr load at bus 2. For pure-P or pure-I share variants, this baseline is past the static voltage-stability limit (a known property of constant-power loads near the grid’s PV nose); use bus_init_line to dial the baseline back into the stable region for those tests.
- Parameters:
tmp_path (pathlib.Path)
load_line (str | None)
bus_init_line (str | None)
sim_dist (str | None)
- Return type:
pathlib.Path
- pydynamicestimator.tests.test_dist_load._LIGHT_BASELINE = 'BusInit, bus = "2",\tp = 20,\tq = 5,\ttype = "PQ"'
- pydynamicestimator.tests.test_dist_load._base_cfg(tmp_path: pathlib.Path, **overrides)[source]
Return a config that runs the staged scenario on the sim side.
- Parameters:
tmp_path (pathlib.Path)
- pydynamicestimator.tests.test_dist_load._bus_voltage_magnitude(sim, bus_idx: int, k: int) float[source]
- Parameters:
bus_idx (int)
k (int)
- Return type:
float
- pydynamicestimator.tests.test_dist_load._pre_post_voltages(sim, bus_idx: int = BUS2_IDX) tuple[float, float][source]
Return (|V| at t ≈ 0.5s, |V| at t = T_end).
- Parameters:
bus_idx (int)
- Return type:
tuple[float, float]
- pydynamicestimator.tests.test_dist_load.test_pure_z_load_step_converges(tmp_path)[source]
Pure-Z load + 30 MW step. Linear KCL — no PV nose, IDA converges. The 3-bus grid is tightly coupled, so the steady-state voltage drop is modest (~0.01 pu); the key invariants are (a) the sim runs to completion without IDA_CONV_FAIL and (b) voltage sags (not rises).
- pydynamicestimator.tests.test_dist_load.test_pure_p_load_large_step_dae_raises(tmp_path)[source]
Pure-P with the heavy default 100 MW baseline + 30 MW step under the DAE formulation (line_dyn=False / idas). IDA must fail. This confirms (a) the user’s “P” choice is honoured by the handler (not silently swallowed) and (b) the well-known DAE-side numerical fragility of constant-P loads on a small grid is surfaced as an error rather than masked.
- pydynamicestimator.tests.test_dist_load.test_pure_p_small_step_dae_converges(tmp_path)[source]
Pure-P with a small (1 MW) baseline and step under DAE (line_dyn=False / idas) — exercises the P-share branch of dist_load where IDA’s algebraic-IC computation can still bridge the disturbance cleanly.
- pydynamicestimator.tests.test_dist_load.test_pure_i_small_step_dae_converges(tmp_path)[source]
Pure-I with a 2 MW step under DAE (line_dyn=False / idas). Larger pure-I steps trigger IDA Newton failure at the disturbance boundary (the atan2/cos/sin nonlinearity in gcall_i makes consistent-IC computation across a step harder than for a Z increment), so we test the small-step regime here.
- pydynamicestimator.tests.test_dist_load.test_pure_i_large_step_line_dyn_true_converges(tmp_path)[source]
Pure-I with a 30 MW step under dynamic lines (line_dyn=True / cvodes). The ODE formulation has no algebraic-IC computation, so even large pure-I steps integrate cleanly. Establishes that the handler’s I-share math is correct — the DAE-side failures elsewhere in this file are an IDA convergence property, not a dist_load math error.
- pydynamicestimator.tests.test_dist_load.test_mixed_z_dominant_zip_load_step_converges(tmp_path)[source]
Mixed Z/I/P shares with Z-dominant (0.8/0.1/0.1). The Z anchor keeps the baseline stable while still exercising the I and P branches of dist_load. Verifies that the share-apportioned increment correctly sags the bus voltage.
StaticLoadImpedance should be treated as z=1 (and produce results indistinguishable from StaticZIP, z_share=1).
StaticLoadPower must be inferred as p=1 and produce numerics indistinguishable from an explicit StaticZIP(p_share=1) of the same rated power. Run both on a small-step regime where the P-share branch converges cleanly, then compare voltage trajectories.
- pydynamicestimator.tests.test_dist_load.test_no_load_at_bus_raises(tmp_path)[source]
LOAD disturbance at a bus without any load device (here: bus 1, the slack generator) must raise a clear ValueError.
- pydynamicestimator.tests.test_dist_load.test_stacking_two_load_events(tmp_path)[source]
Two LOAD events at the same bus should stack (post-second total demand = P₀ + 2·Δp).
- pydynamicestimator.tests.test_dist_load.test_negative_p_delta_raises_voltage(tmp_path)[source]
A negative p_delta = load reduction should RAISE the bus voltage.
- pydynamicestimator.tests.test_dist_load.test_q_delta_sign_convention_inductive(tmp_path)[source]
Positive q_delta = inductive consumption, should drop voltage magnitude (consistent with gcall_z’s b convention).
The line_dyn=True branch of dist_load must also be ZIP-aware (it modifies dae.fnode via the ω_b / B_sum capacitor coupling instead of dae.g). Use Z-only + modest step + short horizon.
q_share(branch, k) returns the P-side share when Q-side is NaN (the default / ‘not declared’ case).
q_share(branch, k) returns the Q-side value when it’s set (not NaN).
Different entries can independently opt in or out of Q-side shares.
A bus declared with only P-side shares (no _share_q fields) must produce numerically identical dist_load behaviour to an explicit redeclaration where the Q-side equals the P-side. This is the backward-compatibility property — the fallback path can’t drift from the historical single-share calibration.
A pure Q-only LOAD step (p_delta=0, q_delta>0) on a bus with z_share_q = 1.0 (vs the implicit z_share_q = 0.5) should produce a different voltage response. Confirms the Q-side share is actually wired into dist_load’s Q-axis contributions.
Uses a modest step (q_delta=3) at the default 100 MW + 10 MVAr baseline: a larger step here sits at the static voltage-stability nose, where the integration is on a knife-edge (and previously only succeeded by luck of the solver’s floating-point path). The routing signal at this step is small (~3e-6) but ~10 orders above the pre-disturbance match (~1e-15), so it is an unambiguous, robust check that Q routing changes the response.
- pydynamicestimator.tests.test_dist_load.test_heating_load_z_for_p_p_for_q(tmp_path)[source]
The ‘heating with controls’ scenario from the design doc: z_share=1 / p_share=0 (active power purely resistive) plus z_share_q=0 / p_share_q=1 (reactive power purely constant-power). End-to-end sanity: init succeeds, small LOAD step simulates cleanly, voltage moves the right direction.
- pydynamicestimator.tests.test_dist_load.test_q_side_sum_to_1_warning(tmp_path, caplog)[source]
If Q-side shares don’t sum to 1.0, StaticZIP.finit logs a warning. We only need init to fire — no disturbance involved — so empty out sim_dist and use a light pure-Z baseline that’s robust to the deliberate Q-side misconfiguration.
- pydynamicestimator.tests.test_dist_load.test_p_side_sum_to_1_warning_unchanged_behaviour(tmp_path, caplog)[source]
The sum-to-1 warning fires for the P-side too, mirroring the Q check. Sanity that the new code path didn’t drop the P check.
- pydynamicestimator.tests.test_dist_load.test_mixed_grammar_old_and_new_in_one_file(tmp_path)[source]
Verify the parser handles a sim_param.txt mixing old-style and new-style StaticZIP lines side by side — entries with no *_share_q fields fall back via NaN, entries with them use them. Use Z-dominant shares + a light baseline so the P-share portion doesn’t tip bus 2 into the constant-P-instability region.
StaticLoadImpedance has no q_share() method — the dist_load handler must fall back to identical P/Q shares (z=1 across both sides) and produce results equivalent to a StaticZIP(z=1) with no _share_q fields. Confirms the hasattr() guard works.