Network-scale control studies
solve_controlled_inverter_fleet is the single-snapshot bridge between the phase-aware controller/advanced-inverter model and prepared BMOPFTools network datasets. It is intended for comparative studies of local control laws, per-leg current requirements, and DC-link stress across many independent LV network snapshots.
Scientific scope
The solver answers a deliberately narrow question: given one network state, one physical inverter design per selected IBR, one locally measured voltage-phasor control law, and one available-power request, what simultaneous network and inverter operating point satisfies all algebraic equations and physical limits?
It is a controlled power flow, not model-predictive control and not a local optimization. Every controller is a fixed smooth computation graph. Ipopt is used to solve the coupled network/device equations at scale; it does not choose the control policy. Independent time, penetration, and uncertainty snapshots belong in an outer study loop so failures and limit activation remain visible.
The initial fleet implementation supports selected dataset IBR records with THREE_LEG or FOUR_LEG AC port descriptions, each exposing three ordered phase conductors. Their PowerOptLab replacements are three-leg AdvancedInverters. Native DC-network-coupled IBRs are rejected because replacing their AC formulation would otherwise remove native DC balance physics.
Selecting a native FOUR_LEG record therefore means deliberately studying a three-leg replacement at the same four-wire connection; it is not a claim that the hardware topologies are equivalent. A native THREE_LEG record is accepted only when the replacement has neutral=nothing and has neither an LCL midpoint shunt nor a POC shunt. These conditions preserve the native line-to-line port: the replacement has zero summed phase current and introduces no phase-to-ground or neutral current path.
Replacement, not parallel stamping
The dataset continues to identify an inverter by network["ibr"][id]. A ControlledInverterFleetSpec maps that same identifier to a ControlledDevice and an InverterControlRequest:
spec = ControlledInverterFleetSpec(
Dict("pv_17" => ControlledDevice(advanced_inverter, controller)),
Dict("pv_17" => InverterControlRequest(
p_available=6.4e3, p_rated=8e3, q_scale=8e3)))
result = solve_controlled_inverter_fleet(network, spec)PowerOptLab assigns each selected (:ibr, id) to a per-component OpfDeviceBuilder. BMOPFTools therefore omits the native IBR equations and KCL injection for that identifier while retaining native physics for every unselected IBR. The advanced plant adds the only live terminal injection.
BMOPFTools declares its semantic native IBR current variables before ownership is resolved. PowerOptLab fixes those unused placeholders to zero. Published results then follow one unambiguous ownership rule:
result.devices[id]contains each replaced, controlled advanced inverter;result.network["ibr"]contains only unselected native IBRs; andresult.build_manifest.component_owners[(:ibr, id)]records:PowerOptLabor:BMOPFTools.
This contract is stronger than inferring replacement from a coincident bus or from a near-zero duplicate injection. A selected native record with tight native power bounds cannot constrain the replacement plant. The number of placeholders fixed is derived from BMOPFTools' resolved dataset neutral labels, not assumed to equal the replacement's three phases. This also closes every unused variable when a non-conventional neutral spelling makes BMOPFTools declare an additional semantic IBR-current pair.
One upstream qualification remains: if BMOPFTools does not recognise that neutral spelling, its native :network_cost construction also treats the terminal as a fourth phase. A costed record must consequently provide four cost entries or BMOPFTools rejects it while indexing the objective. This affects native and replaced IBRs alike; PowerOptLab closes the extra unused current pair, but does not reinterpret native cost data. Prefer BMOPFTools-recognised neutral metadata/spelling for production datasets.
Input contract
For each selected identifier, construction validates before model mutation:
- the device and request dictionaries have identical nonempty key sets;
- the dictionary key equals the wrapped advanced-inverter identifier;
- the identifier exists in the network's native
ibrcollection; - native and replacement bus identifiers agree;
- phase order agrees exactly, and a
FOUR_LEGrecord places the matching neutral after the three phases; and - a
THREE_LEGrecord is paired only with a neutral-free, shunt-free replacement; and - the native IBR has no DC-network coupling.
Phase order is a physical convention, not presentation metadata: it determines the symmetrical-component transform and the reconstructed phase currents. Silently sorting phase labels would change the control law.
The controller formulation is currently supported only with per_unit=true. Configuration and extracted results nevertheless remain in SI units. The advanced inverter must use pwm_strategy=:NONE; calibrated PWM reserve terms may be supplied explicitly, but the separate PWM outer iteration is not part of the fleet solve.
Objective semantics
Local controller equalities determine their current commands. The objective only selects freedom left elsewhere in the algebraic model:
selection_objective=:loss(default) minimizes selected inverter semiconductor and capacitor losses;:zerois an objective-invariance diagnostic; and:network_costapplies BMOPFTools' native generation-cost expression to dispatchable unselected network devices.
There is intentionally no silent weighted sum of economic cost and electrical loss: the quantities have no universal commensurate scale. For comparative control-law studies, pin exogenous loads, generation, and unselected IBR setpoints in each prepared snapshot, then use :loss. If dispatch of native devices is part of the experiment, state and use the economic objective explicitly. Results from different objective semantics are not interchangeable.
Result tables and aggregation
ControlledInverterFleetResult retains both the device-local results and the ordinary BMOPFTools network result, plus a defensive copy of the study specification used for provenance. solve_status(result) is the authoritative publication gate. Do not drop infeasible or non-publishable snapshots; retain their scenario identifiers and classify the failure mechanism.
The bus snapshot is canonicalized once per fleet result and shared by result.network["bus"] and every result.devices[id].bus. Treat extracted results as immutable: mutating that shared dictionary changes every view of the same snapshot.
Two dependency-free extraction helpers produce table-ready named tuples:
device_rows = controlled_inverter_rows(result)
phase_rows = controlled_inverter_phase_rows(result)device_rows has one row per inverter and scalar SI columns for POC and converter power, losses, voltage extrema and sequences (including U₀), VUF, requested power, limiter scales, maximum converter/grid current, sequence current, negative-sequence current angle, peak-phase/positive-sequence current ratio, 2ω ripple, DC-link ripple voltage, capacitor current, and the exact-versus-smooth current residual. phase_rows has one row per inverter and phase, retaining rectangular and magnitude voltage, converter-current, and grid-current values. Both helpers sort identifiers lexically, independent of dictionary insertion order.
The conventional voltage_unbalance_factor is $|U_2|/|U_1|$. It is distinct from regularized_voltage_unbalance, the controller's finite-denominator quantity. Keeping both prevents the controller's numerical regularization from being mistaken for a reported power-quality metric. It is also distinct from the NEMA/IEEE maximum-deviation approximations that share the name "voltage unbalance"; state which definition is reported (see Pillay and Manyage, and Girigoudar and Roald on unbalance metrics inside optimization models, in IBR references), and note that neither this quantity nor its regularized companion is measured over a standardised power-quality window.
solve_diagnostics(result) supplies lightweight fleet aggregates, including total controlled POC P/Q, total selected-inverter losses, the largest converter/grid phase current, and the largest exact-versus-smooth current residual. Study scripts should add scenario keys—time, feeder, penetration, random seed, and hardware/control configuration—before writing rows.
Verification obligations
The unit tests exercise discriminative rather than merely feasible cases:
- selected native records have near-zero native ratings while their replacement plants deliver kilowatts, detecting accidental parallel native stamping;
- selected and unselected IBRs coexist, with ownership and result channels checked explicitly;
- average-voltage and worst-phase Volt-watt laws share one unbalanced network state and produce materially different requests;
- output ordering and aggregate identities are deterministic; and
- each solution satisfies three-leg zero-sum current, per-leg current limits, converter-terminal complex-power reconstruction, filter active-power balance, and DC-link power balance.
These are software and model-consistency checks, not validation of a hardware controller. The study methodology still requires a stratified subset of balanced, unbalanced, current-limited, and ripple-limited cases against an independent averaged model and a switched EMT model. OpenDSS is suitable as a network-voltage oracle for cases whose inverter injection can be frozen, but it cannot independently validate the advanced converter, DC capacitor, or the simultaneous smooth controller equations. Use it to isolate network stamping; use averaged/EMT models for converter and control-law validation.
Study workflow
For a publishable comparison, freeze a factorial design before inspecting results: feeder/scenario, PV penetration and phase allocation, irradiance, source-voltage unbalance, load realization, hardware rating, current target, positive-sequence policy, negative-sequence policy, and limiter settings. Use the same snapshot and hardware across controller variants.
Report at least:
- voltage compliance and phase extrema;
- conventional VUF and negative-sequence current;
- energy curtailed relative to the declared available-power trajectory;
- empirical quantiles and worst cases of converter- and grid-side per-leg current;
- frequency and identity of binding physical limits;
- 2ω ripple power, DC voltage ripple, capacitor RMS/thermal current, and capacitor stored energy $C_{dc}V_{dc}^2/2$; and
- solver status, exact-versus-smooth residual, and sensitivity to smoothing widths on a representative subset.
Current and capacitor sizing conclusions require outer parameter sweeps. A feasible point at one installed rating does not identify the minimum rating, and a smooth-NLP failure is not by itself a physical infeasibility certificate.
Matched batch experiments
InverterControlStudyCase and run_inverter_control_study implement the outer single-snapshot experiment loop without coupling unrelated operating points into one NLP. A case combines:
scenario_id: the feeder, placement, load/PV, weather, and uncertainty realization that must remain common across variants;variant_id: the controller or hardware configuration being compared;- one prepared BMOPF network and
ControlledInverterFleetSpec; - a positive interval duration in hours and positive scenario weight; and
- copied dataset metadata such as feeder, penetration, seed, and timestamp.
cases = [
InverterControlStudyCase(
scenario_id="feeder_01/time_0042/seed_7",
variant_id="average_voltage",
network=network,
fleet=average_fleet,
duration_h=0.5,
weight=1.0,
metadata=Dict("penetration" => 0.75, "feeder" => "feeder_01")),
InverterControlStudyCase(
scenario_id="feeder_01/time_0042/seed_7",
variant_id="worst_phase",
network=network,
fleet=worst_phase_fleet,
duration_h=0.5,
weight=1.0,
metadata=Dict("penetration" => 0.75, "feeder" => "feeder_01")),
]
study = run_inverter_control_study(
cases; solver_options=("max_iter" => 500, "tol" => 1e-8))The returned study.settings records the common per-unit base, optimizer type, objective semantics, verbosity, and solver attributes. Retain this with exported tables; it is part of the computational provenance, not an incidental runtime detail.
Cases are solved serially in deterministic (scenario_id, variant_id) order. Serial execution is the reference reproducibility path. Large campaigns should partition the flat case list across worker processes and combine table rows; there is no mutable cross-snapshot state in this layer.
By default (continue_on_error=true, equivalently :validation), a case-level ArgumentError from dataset/model validation is retained with error_class=:validation, its exception type/message, and the study continues. Set continue_on_error=false to rethrow every exception. Programming, configuration, and unexpected solver exceptions are otherwise rethrown: they must not become apparent physical-failure statistics. For an unattended campaign, the explicit continue_on_error=:all mode retains nonfatal unexpected exceptions as error_class=:unexpected; these remain software/data failures and are counted separately from validation failures and physical solve status. Fatal process-level exceptions are always rethrown. A solve that returns a non-publishable status is retained as a fleet result with masked numerical data, distinct from a thrown error.
Extraction is split by purpose:
inverter_control_study_case_rowscontains every case, including errors and non-publishable solves;inverter_control_study_device_rowsandinverter_control_study_phase_rowsprefix fleet rows with the matched case keys, duration, weight, and metadata;inverter_control_study_summary_rowsalways groups by control/hardware variant, optionally adds metadata cohorts, reports failure fractions over all cases, and computes energy/tail metrics only from publishable cases; andinverter_control_paired_rowsreports device-levelvariant - baselinedifferences for every scenario. Missing, errored, non-publishable, or device-mismatched pairs remain as rows withpublishable_pair=falseand NaN differences. Duration, weight, metadata, device identifiers, physical hardware, requests, and the shared network must agree across a pair;matched_case_definitionexposes that check and prevents unmatched exposure or confounding from entering a paired estimate. A separately materialized network may be accepted only after external equivalence validation. Thebaseline_*andvariant_*columns retain each arm's own achieved value whenever that arm published, even when the pair is refused, withbaseline_published/variant_publishednaming which arms those are, so a dropped pair can be characterized rather than only counted. Onlydelta_*is gated onpublishable_pair; do not difference the two arms of an unmatched row by hand, because the refusal is the finding; andinverter_control_paired_summary_rowsreports candidate, definition-matched, publishable, and dropped pair counts plus exposure-weighted mean and p05/p50/p95 deltas. Negative, positive, and numerically indistinguishable fractions use a declared magnitude-relative tolerance with a one-unit floor in each metric's reported unit. Per-metric finite-pair counts expose unavailable quantities.dropped_pair_known_baseline_countstates how many dropped pairs have an inspectable baseline; comparing those baselines with the retained ones is what turns an attrition count into a bound on selection bias. Unmatched rows retain the baseline metadata, so they are attributed to the baseline cohort when grouped.
Summary energy is the weighted sum of power times duration_h and is reported in kWh. Current, VUF, capacitor-current, and ripple-voltage p50/p95/p99 values are duration-and-weight inverse empirical CDFs over finite, publishable inverter-device points, not unweighted quantiles or quantiles of scenario maxima. Every metric has its own *_finite_points count; optional ratings can therefore never produce a confident-looking utilization tail from an undisclosed subset. The summary reports both ordinary and duration-times-weight exposure-adjusted publication fractions. Do not interpret metrics from different publication fractions as a fair control-law comparison; examine retained failure rows and paired results first.
A publication fraction on its own does not say why cases failed, and the two reasons are scientifically opposite. termination_status_counts gives the per-cohort histogram of case termination statuses (with "ERROR" for thrown cases), and iteration_limit_case_count, locally_infeasible_case_count, and numerical_error_case_count are named conveniences drawn from it. A LOCALLY_INFEASIBLE case is a candidate physical statement about the controller and hardware under test. An ITERATION_LIMIT or NUMERICAL_ERROR case is a statement about the smooth NLP's conditioning, and it is not portable: the same source, Julia version, and pinned dependencies can converge on one platform's linear-solver build and not on another's. Report the histogram beside any failure fraction, and never attribute unconverged cases to a control law without first re-solving them from a different start.
Fleet tails include both absolute current and rating-normalized utilization. The total converter/grid current companions combine declared carrier reserves with fundamental current in RMS quadrature; capacitor utilization uses the thermally weighted equivalent current. This prevents a heterogeneous fleet's largest nameplate from being mistaken for its most highly stressed device.
Case construction defensively copies metadata once. Extracted case, device, and phase rows then share that copied dictionary to avoid one metadata allocation per inverter or phase. Treat row metadata as immutable during analysis.
The library accepts already-prepared cases. Dataset ingestion, scenario generation, random customer placement, persistence formats, plots, and cluster scheduling remain in scripts/studies/inverter_controls/ so the core API does not encode one dataset's schema.
Outer hardware grids and sizing diagnostics
Hardware is varied outside the nonlinear network problem. First call validate_inverter_control_campaign(cases) to enforce a complete controller matrix and identical snapshot, hardware, request, duration, weight, and metadata across variants. By default it requires variants to share the same network object; this is a cheap drift guard, while a dataset adapter may opt out only after independently proving network equivalence.
This keeps every solve interpretable as one realizable nameplate design and avoids treating a current or capacitance rating as a freely optimized operating variable. InverterHardwareSweepPoint scales installed ratings relative to each inverter's base data, so heterogeneous devices retain their relative sizes:
hardware = [
InverterHardwareSweepPoint(
id="reference"),
InverterHardwareSweepPoint(
id="more_silicon_and_capacitance",
converter_current_scale=1.20,
grid_current_scale=1.20,
dc_capacitance_scale=1.50,
capacitor_current_scale=1.20),
]
sweep_cases = expand_inverter_hardware_cases(cases, hardware)
sweep = run_inverter_control_study(sweep_cases)
requirements = inverter_control_hardware_requirement_rows(
sweep; allowed_dc_ripple_fraction=0.02, current_margin=1.10)
summaries = inverter_control_study_summary_rows(
sweep; group_by=["penetration", "hardware_point_id"])The expansion appends the hardware identifier to scenario_id but leaves variant_id as the control-law identifier. Paired rows therefore compare laws only at the same feeder snapshot and hardware point. The expansion records the base scenario, hardware identifier, and every effective scale in metadata (1.0 for an unchanged axis) and rejects reserved-key collisions. The sweep-point struct still uses nothing to mean "do not require or alter this optional rating." The network is still shared by reference; each fleet and advanced-inverter specification is new.
Converter-leg current, grid-side current, DC capacitance, and capacitor thermal current are distinct axes. A rating is changed only when its scale is explicitly supplied, and the all-nothing default is a true identity point. Asking to scale an absent i_max, i_grid_max, c_dc, or i_cap_max throws instead of silently introducing a physical constraint. Apparent-power rating and DC voltage remain fixed in this sweep; study them as separately declared hardware factors if required.
inverter_control_hardware_requirement_rows reports achieved converter/grid RMS current, capacitor thermally equivalent current, and the closed-form monolithic-link requirement
\[I_{2\omega,rms} = \frac{|\widetilde S|}{\sqrt{2}V_{dc}},\qquad C_{2\omega,req} = \frac{|\widetilde S|}{2\omega V_{dc}\Delta V_{allow}},\qquad E_{2\omega,req}=\frac12 C_{2\omega,req}V_{dc}^2,\qquad \Delta V_{allow}=\epsilon_{dc}V_{dc}.\]
The explicit 2ω current separates the low-frequency ripple mechanism from the total thermally equivalent capacitor current, which may also contain carrier and neutral-current contributions. Required and installed stored energy make capacitance alternatives comparable at their declared DC voltage. The rows also contain absolute ratings, explicitly named *_requirement_utilization ratios, and nullable compliance flags. These ratios include the declared current margin; the similarly named fleet operating utilizations do not. A current margin of at least one may be applied explicitly. Failed/non-publishable solves retain the known installed hardware but make no performance claim: requirements and utilizations are NaN, and compliance is missing.
Every scaled absolute rating is checked after multiplication. A scale that overflows to infinity or underflows to zero is rejected before model construction rather than becoming an invalid nameplate silently.
The allowed ripple is a zero-to-peak sinusoidal amplitude: a fraction of 0.02 means ±2% and therefore 4% peak-to-peak. Rows expose both the achieved dv2 and the model's enforced dv2_max, plus binding flags for converter current, grid current, capacitor thermal current, DC ripple, and converter apparent power. Controller-command curtailment and power/current allocation activity are reported separately. A missing binding flag means that no corresponding installed limit was declared or the solve was not publishable. A false flag only rules out that named limiter: it is not evidence that the operating point is unconstrained. In particular, the table does not exhaustively classify sampled modulation rails, internal-voltage bounds, sequence/neutral limits, or every optional plant constraint.
The independent dc_capacitance_scale changes ripple headroom only. It does not change esr_dc, capacitor-current rating, cost, tolerance, or lifetime, so it must not be interpreted as a realizable parallel-bank or capacitor-loss axis. A physical bank study must construct coupled plant alternatives—for an ideal parallel multiplication by n, typically C → nC, current rating → nI, and ESR → ESR/n—and state its component assumptions explicitly.
There are two scientifically different uses:
- Run every law at one common, demonstrably non-binding oversized hardware point. The achieved-current and closed-form capacitance columns are useful first-pass requirements.
- When a rating changes dispatch, limiter activation, switching feasibility, or network voltage, run the explicit hardware grid and judge each point using publication status plus the declared voltage, unbalance, curtailment, loss, and ripple service criteria.
Do not infer a minimum rating from the current achieved at a binding rating; that is circular because the controller may curtail to satisfy the installed limit. Do not apply bisection unless the chosen service predicate has been shown monotone for that controller and scenario. Retain the full grid when active regimes change.
Hardware points are counterfactual alternatives, not additional time samples. Study summaries automatically add "hardware_point_id" to their grouping when all cases carry that metadata; summing energy across hardware points would multiply the represented exposure. The 2ω formula assumes the modeled monolithic three-leg link supplies the ripple. It does not size split half-banks, hold-up energy, transients, tolerance/ageing, thermal lifetime, or a frequency-dependent DC source, and it does not replace averaged-model and EMT validation.
API
PowerOptLab.ControlledInverterFleetSpec — Type
ControlledInverterFleetSpec(devices, requests)Configuration for replacing selected native BMOPFTools IBRs with ControlledDevices in one network snapshot. devices and requests are dictionaries keyed by the native dataset IBR identifier. The two key sets must be identical and each key must equal the wrapped device's device_id.
Unselected IBRs retain their native BMOPFTools formulation. Selected IBRs are owned by PowerOptLab through BMOPFTools' per-component OpfDeviceBuilder contract, so the advanced plant replaces rather than supplements the native device.
PowerOptLab.solve_controlled_inverter_fleet — Function
solve_controlled_inverter_fleet(net, spec; kwargs...)
-> ControlledInverterFleetResultSolve one network snapshot containing any number of locally controlled advanced inverters. Every selected native network["ibr"][id] is replaced through BMOPFTools' typed per-component ownership seam. Unselected IBRs and all other network devices keep their native formulations.
The controllers are fixed algebraic computation graphs based only on their local POC voltage phasors. This is therefore a simultaneous controlled power flow, not an optimization-based controller. selection_objective=:loss minimizes residual converter allocation freedom; :zero is provided for objective-invariance checks. :network_cost uses BMOPFTools' native generation cost to select dispatchable unselected devices; it should only be used when that economic objective is part of the intended snapshot. The smooth nonlinear formulation currently requires per_unit=true and pwm_strategy=:NONE.
result.devices[id] is authoritative for replaced IBRs. To prevent accidental comparison with unused native placeholder currents, selected identifiers are removed from result.network["ibr"]; that dictionary contains only unselected native IBR results. result.build_manifest records ownership explicitly.
PowerOptLab.controlled_inverter_rows — Function
controlled_inverter_rows(result)Return deterministic, one-row-per-device scalar records suitable for DataFrame construction or CSV/Arrow output. All electrical quantities are SI. The ordinary VUF column is |U₂|/|U₁|; the controller's regularized ratio is reported separately as regularized_voltage_unbalance.
PowerOptLab.controlled_inverter_phase_rows — Function
controlled_inverter_phase_rows(result)Return deterministic long-form records with one row for each controlled device and phase. Complex voltage and current components, plus magnitudes, are retained so phase-current ratings and current-location effects can be studied without parsing nested solver results.
PowerOptLab.InverterControlStudyCase — Type
InverterControlStudyCase(; scenario_id, variant_id, network, fleet,
duration_h=1, weight=1, metadata=Dict())One independently solved point in a matched inverter-control experiment. scenario_id identifies the common feeder/load/PV realization reused across control or hardware variant_ids. duration_h converts powers to energy; weight is a positive scenario multiplicity/probability weight. Arbitrary dataset keys belong in metadata and are copied defensively.
The network is deliberately retained by reference: large matched studies may reuse one prepared snapshot across variants. Treat it as immutable while a study is running.
PowerOptLab.InverterControlStudyCaseResult — Type
Outcome of one InverterControlStudyCase.
PowerOptLab.InverterControlStudyResult — Type
Result of run_inverter_control_study.
PowerOptLab.run_inverter_control_study — Function
run_inverter_control_study(cases; kwargs...)
-> InverterControlStudyResultSolve independent matched cases in deterministic (scenario_id, variant_id) order. Each case invokes solve_controlled_inverter_fleet; snapshots are not coupled into one NLP. continue_on_error=true (the default, equivalent to :validation) records case-specific ArgumentErrors from dataset and model validation and continues. continue_on_error=false aborts on every exception. The explicit unattended-campaign mode continue_on_error=:all also records non-validation exceptions as error_class=:unexpected; fatal process-level exceptions are always rethrown. Use :all deliberately: unexpected errors are software/data failures, not physical infeasibility, and must be reported separately.
This implementation is deliberately serial and reproducible. Large studies should partition the case vector across processes outside this function and concatenate the resulting rows. All fleet-solver keyword arguments are shared across cases.
PowerOptLab.inverter_control_study_case_rows — Function
Return one deterministic status/provenance row per study case.
PowerOptLab.inverter_control_study_device_rows — Function
inverter_control_study_device_rows(result)Return one row per extracted controlled inverter, prefixed by case identity, duration, weight, and metadata. Cases that throw before producing a fleet result remain visible in inverter_control_study_case_rows, but have no device row. Non-publishable fleet solves retain device rows with masked numerical data.
PowerOptLab.inverter_control_study_phase_rows — Function
Return one case-prefixed long-form row per controlled inverter phase.
PowerOptLab.inverter_control_study_summary_rows — Function
inverter_control_study_summary_rows(result; group_by=String[])Return scalar cohort summaries as dictionaries. Every cohort is always grouped by variant_id; group_by may add standard case fields or metadata keys such as "penetration" and "feeder". If every case carries "hardware_point_id" metadata, that counterfactual axis is added automatically; partial hardware tagging is rejected. Failure fractions use all cases. Electrical and energy metrics use publishable device rows only.
Energy totals weight power by duration_h*weight. Current/VUF/capacitor tails are duration-and-weight empirical inverse-CDF quantiles at 50, 95, and 99 %. Every tail metric reports its own *_finite_points denominator; this is essential for utilization metrics whose optional rating may be absent.
termination_status_counts is the authoritative per-cohort histogram of case termination statuses, with "ERROR" covering thrown cases. The named iteration_limit_case_count, locally_infeasible_case_count, and numerical_error_case_count are conveniences drawn from it. Report them with any failure fraction: a non-publishable case may be a physical infeasibility or merely a solver that did not converge, and the second is toolchain-dependent rather than a property of the controller under test.
PowerOptLab.inverter_control_paired_rows — Function
inverter_control_paired_rows(result, baseline_variant;
require_shared_network=true)Return matched device-level differences variant - baseline for every scenario and non-baseline variant in the study. Missing, errored, or non-publishable pairs are retained with publishable_pair=false and NaN differences.
The baseline_* and variant_* columns report each arm's own achieved value whenever that arm published, whether or not the pair itself is publishable, and baseline_published/variant_published state which arms those are without inferring it from a NaN. A dropped pair whose baseline solved is therefore characterizable, so the attrition counts can be turned into a selection-bias argument instead of only a caveat. Only delta_* is gated on publishable_pair, because only the difference is a matched estimate; never difference the two arms of an unmatched row by hand.
A pair is matched only when exposure metadata, physical inverter specifications, requests, device identifiers, and (by default) the network object agree. This prevents both attrition and hardware/network confounding. Set require_shared_network=false only after an external adapter has proved separately materialized networks equivalent.
PowerOptLab.inverter_control_paired_summary_rows — Function
inverter_control_paired_summary_rows(result, baseline_variant;
group_by=String[], require_shared_network=true,
delta_relative_tolerance=1e-6)Summarize matched device-level variant - baseline rows without hiding attrition. Each cohort reports all candidate, definition-matched, publishable, and dropped device pairs. Delta means, p05/p50/p95 quantiles, and direction fractions use duration_h*weight exposure over finite publishable pairs only. A delta is indistinguishable when its magnitude is at most delta_relative_tolerance*max(abs(baseline), abs(variant), 1) in the metric's reported unit; the unit floor gives zero-valued comparisons a numerical band. Negative, positive, and indistinguishable fractions are all reported so ties and solver-scale noise are not conflated with one-sided effects. Per-metric finite-pair counts expose structurally unavailable quantities.
group_by accepts the same standard case fields as inverter_control_study_summary_rows, or baseline metadata keys. Unmatched pairs are assigned to the baseline case's cohort because their row retains baseline exposure/provenance.
PowerOptLab.InverterHardwareSweepPoint — Type
InverterHardwareSweepPoint(; id, converter_current_scale=nothing,
grid_current_scale=nothing, dc_capacitance_scale=nothing,
capacitor_current_scale=nothing)One counterfactual hardware point for an outer inverter-control study. Scales are applied to each device's installed rating, preserving heterogeneous fleet nameplates. nothing leaves the corresponding rating unchanged; a numerical scale requires that the base rating is explicitly present. Consequently a point constructed with only id is an identity transformation, including for legal plants whose optional ratings are absent.
Apparent-power rating, DC voltage, ripple allowance, controller parameters, and all other plant data remain fixed. This isolates the four named hardware axes.
PowerOptLab.resize_controlled_inverter_fleet — Function
resize_controlled_inverter_fleet(spec, point)Return a new fleet with every advanced inverter scaled by point. Controllers and operating-point requests are retained; the input fleet is not mutated. Optional grid-side and capacitor-current scales require explicit base ratings, preventing an absent physical constraint from being silently introduced.
PowerOptLab.validate_inverter_control_campaign — Function
validate_inverter_control_campaign(cases; require_shared_network=true)Validate the fixed-hardware matched-design contract used for control-law comparisons. Every scenario must contain the same variant set. Within a scenario, variants must have identical duration, weight, metadata, controlled device identifiers, physical AdvancedInverter specifications, and requests. Only the controller may differ.
By default, variants must also share the exact network object, an inexpensive and strong guard against accidental snapshot drift. Set require_shared_network=false only when equivalent networks were deliberately materialized separately and validated by the dataset adapter.
PowerOptLab.expand_inverter_hardware_cases — Function
expand_inverter_hardware_cases(cases, points)Expand matched inverter-control cases over a counterfactual hardware grid. Each hardware point is appended to scenario_id, while variant_id remains the controller identifier. Consequently inverter_control_paired_rows continues to compare controllers at identical hardware.
The original scenario identifier and effective hardware scales are added to metadata (1.0 for an unchanged axis). Expanded cases retain the original network by reference. Hardware points are counterfactual alternatives: aggregate summaries must group by "hardware_point_id" and must never sum their energy as if they were successive time intervals.
PowerOptLab.inverter_control_hardware_requirement_rows — Function
inverter_control_hardware_requirement_rows(study;
allowed_dc_ripple_fraction, current_margin=1,
binding_tolerance=1e-4)Return one first-pass hardware-requirement row per extracted inverter. For a publishable point, converter/grid total RMS currents and the capacitor's thermally weighted equivalent current are multiplied by current_margin. Converter and grid requirements combine the fundamental phase currents with any declared manual carrier-current reserves in RMS quadrature. The monolithic three-leg DC-link quantities are
\[I_{2\omega,rms}=|\widetilde S|/(\sqrt{2}V_{dc}),\qquad C_{2\omega,req}=|\widetilde S|/(2\omega V_{dc}\Delta V_{allow}),\qquad E_{2\omega,req}=\tfrac12 C_{2\omega,req}V_{dc}^2.\]
allowed_dc_ripple_fraction defines ΔV_allow = allowed_dc_ripple_fraction*Vdc and must lie in (0, 1). It is the zero-to-peak sinusoidal amplitude, so 0.02 means ±2% or 4% peak-to-peak. Non-publishable numerical requirements are NaN and compliance fields are missing.
Binding diagnostics cover the installed converter/grid/capacitor/ripple ratings and the converter apparent-power circle. Separate controller flags identify command curtailment and activation of its power/current allocation scales. A false value for one rating says only that particular rating is not active; it never proves that the operating point is free of other limits. Sampled modulation rails, internal-voltage bounds, sequence/neutral limits, and other optional plant constraints are not exhaustively classified here.
This diagnostic is a sizing result only when evaluated at a common hardware point that does not alter dispatch or activate a limiter. Otherwise use it to seed the explicit outer hardware grid. It assumes the modeled three-leg link supplies the 2ω ripple; it is not a split-link, hold-up, lifetime, or EMT sizing calculation.