API reference
All exported names, grouped by capability.
Module
PowerOptLab — Module
PowerOptLabA research laboratory for four-wire distribution-network decisions when the network state or model is uncertain. It connects model evidence and forensics, informative interventions, and verification of operating decisions. The complete workflow is a research direction; current APIs provide separate foundations and state their local, scenario, or prototype limits explicitly.
PowerOptLab builds on the BMOPFTools reference current–voltage OPF engine, using its public extension seams (model_hook! / solution_hook! and the staged build_opf_model / enforce_kcl! / generation_cost / extract_result API) rather than forking the engine. Experimental devices and formulations reuse the engine's neutral-explicit physics, per-unit handling, and result extraction. Stable foundational work can later be proposed for the BMOPF spec.
Contributions are organised by what layer of the engine they extend:
Component models — new network elements (src/components/)
Stamped into a solve through model_hook! / solution_hook!.
- Storage / EV (
StorageDevice,EVDevice) — battery/EV inverter ports stamped as current injections with an inter-temporal state-of-charge state (an energy/power "PE" model with fixed efficiency). - IVQ battery (
IVQBattery) — the current–voltage counterpart: cells of aBatteryChemistrymodelled in the voltage–current–charge space (v = OCV(soc) − i·R), so voltage and current limits bind individually and a current-dependent cell efficiency emerges from the physics (a Rint proxy, not a full round-trip energy efficiency). Reuses theAdvancedInverterfor the AC↔DC converter. - Advanced inverter (
AdvancedInverter) — an experimental internal-AC-node IBR with an output filter, internal-EMF/DC-modulation bounds, grid-forming operation, converter losses, and double-frequency ripple limits.
Problem specifications — new formulations over the staged API (src/problems/)
A different objective/variable/constraint structure on the same physics.
- Multi-period OPF (
solve_multiperiod_opf) — several network snapshots co-optimised in one model with storage/EV state linking each step to the next. - State estimation (
solve_state_estimation) — weighted least-squares estimation of the network state from noisy measurements (an inverse problem on the same physics). - Constrained NLLS state estimation (
solve_sparse_state_estimator) — a compiled neutral-explicit residual/constraint model with tangent-space observability, selected local covariance, and sparse or dense reference solves. - Parameter estimation (
solve_parameter_estimation) — calibration of uncertain line lengths and transformer tap ratios from smart-meter data across multiple time steps (the shared-parameter dual of state estimation). - Inverse Carson reconstruction (
solve_inverse_carson) — screens discrete overhead construction candidates against diagonal sequence data, retaining ambiguity and local identifiability diagnostics while reconstructing a full primitive, neutral-explicit line model. - Dynamic operating envelopes (
solve_operating_envelope) — per-connection-point active-power import/export capacity with parameterized fairness, forecast/model scenarios, explicit corner-security semantics, and prescribed IBR Q-V controls retained from the network model. - Bilevel PV/tap POC (
solve_bilevel_pv_tap) — differentiated aggregate-export and local-controller lower levels with native Volt-var/ Volt-watt controls, compared with a centralized single-level tap/export solve. - Network-scale inverter-control studies (
solve_controlled_inverter_fleet) — selected dataset IBRs are replaced by phase-aware advanced inverters in one simultaneous snapshot, with explicit ownership provenance and table-ready hardware-stress results.
Bespoke algorithms — new solution methods (src/algorithms/)
Question-driven custom solve loops and alternative solution methods.
- HELM (
solve_pf_helm) — the Holomorphic Embedding Load-flow Method: a non-iterative power flow that expands each voltage as a power series in a load-scaling parameter and evaluates it by Padé analytic continuation. Physical mismatch, Padé spread, and coefficient-tail diagnostics distinguish convergence from finite-order numerical divergence without treating the latter as a non-existence certificate.
Everything is SI at the interface; per-unit conditioning inside the solve is handled via the engine's opf_bases(ctx) accessor.
Shared contracts
PowerOptLab.SolveOutcome — Type
SolveOutcomeNormalized MathOptInterface solve status used by PowerOptLab entry points. has_primal records whether the optimizer returned a candidate point, feasible requires a fully feasible primal status, and optimal additionally requires OPTIMAL or LOCALLY_SOLVED termination. Only optimal outcomes are published as final numerical results by the research optimization wrappers.
acceptable identifies MOI's relaxed ALMOST_OPTIMAL / ALMOST_LOCALLY_SOLVED outcomes. It is exposed for diagnostics but is not silently promoted to an optimal result.
PowerOptLab.SolveStatus — Type
Normalized, user-facing status returned by solve_status.
PowerOptLab.AbstractSolveResult — Type
Common supertype for structured solver results. Use solve_status and solve_diagnostics instead of interpreting result-specific strings.
PowerOptLab.solve_status — Function
Return the normalized SolveStatus for a structured solve result.
PowerOptLab.solve_diagnostics — Function
Return result-specific numerical diagnostics as a named tuple.
PowerOptLab.TimeGrid — Type
TimeGrid(durations_h)
TimeGrid(periods, duration_h=1.0)Validated period durations in hours. durations_h[t] weights both the period's rate objective and its state transition, so nonuniform horizons do not silently assume one-hour snapshots.
PowerOptLab.MultiContext — Type
A JuMP model and the BMOPFTools contexts built into that shared model.
PowerOptLab.build_multi_context — Function
build_multi_context(nets; hook_factory, kwargs...) -> MultiContextBuild every network snapshot into one JuMP model through BMOPFTools' staged API. hook_factory(t) returns the model_hook! for snapshot t. The builder centralizes the shared optimizer, solver options, time index, and common unit settings; callers add linking constraints/objectives and then enforce KCL. Pass an existing model when shared variables/operators must be created first.
Extension interfaces
PowerOptLab.AbstractDevice — Type
Common supertype for network devices contributed by PowerOptLab.
PowerOptLab.device_id — Function
Return the stable identifier used to key a device's handles and results.
PowerOptLab.validate_device — Function
Validate a device against a horizon before adding variables or constraints.
PowerOptLab.stamp_device! — Function
Stamp one device into one BMOPFTools model context and return its handles.
PowerOptLab.link_device! — Function
Link one device's per-period handles across a TimeGrid.
PowerOptLab.extract_device — Function
Extract one device's published numerical result from its model handles.
PowerOptLab.AbstractMeasurement — Type
Common supertype for scalar telemetry consumed by estimation problems.
PowerOptLab.measurement_kind — Function
Return the symbolic quantity represented by a scalar measurement.
PowerOptLab.measurement_value — Function
Return a scalar measurement's SI value.
PowerOptLab.measurement_sigma — Function
Return a scalar measurement's positive SI standard deviation.
PowerOptLab.measurement_prediction — Function
measurement_prediction(kind, dvr, dvi;
ir=nothing, ii=nothing, magnitude=nothing, magnitude_epsilon=0)Evaluate the shared scalar measurement model from a complex voltage drop dvr + im*dvi and, for power/current quantities, a complex current ir + im*ii.
Supported node kinds are :vr, :vi, :vmag, :pinj, and :qinj; supported branch kinds are :ire, :iim, :imag, :pflow, and :qflow. Injection and flow powers use the same V * conj(I) convention. Pass an auxiliary nonnegative magnitude when a JuMP formulation represents |V| explicitly; otherwise the magnitude is evaluated directly. A positive magnitude_epsilon supplies the smooth magnitude used by compiled state estimation.
The function is generic over numbers, automatic-differentiation values, and JuMP expressions so every estimation formulation uses the same physical measurement equations.
Devices
PowerOptLab.StorageDevice — Type
StorageDevice(; id, bus, kwargs...)A grid-connected battery (or generic energy-storage) inverter with an inter-temporal state of charge. Powers are SI watts, energies SI watt-hours.
Required
id::String— unique device id.bus::String— connection bus.p_charge_max,p_discharge_max— charge / discharge power limits (W ≥ 0).energy_max— usable energy capacity (Wh).energy_init— energy at the start of the horizon (Wh).
Optional
phase_terminals=["1"],neutral="n"— the inverter's phase conductor(s) and the return terminal (nothingif referenced directly to ground).energy_min=0.0— lower energy bound (Wh).q_min=0.0,q_max=0.0— reactive-power box (var); default is unity power factor.eff_charge=1.0,eff_discharge=1.0— one-way efficiencies (0,1].cyclic=true— require the terminal state of charge to equalenergy_init.energy_final=nothing— if set, pin the terminal energy to this value (Wh), overridingcyclic.
PowerOptLab.EVDevice — Type
EVDevice(; id, bus, available, departure_energy, kwargs...)An electric-vehicle charger: a storage device that is only controllable while plugged in and must reach a target energy by departure. Set p_discharge_max > 0 for bidirectional (V2G) charging; the default 0.0 gives unidirectional (V1G).
Required
id,bus— asStorageDevice.p_charge_max— charge power limit (W).energy_max,energy_init— battery capacity and plug-in energy (Wh).available::Vector{Bool}— per period, whether the vehicle is plugged in. While unavailable the charger is idle and the state of charge is held.departure_energy— energy (Wh) required bydeparture_period.
Optional
p_discharge_max=0.0— V2G discharge limit (W);0.0⇒ charge-only.departure_period=nothing— period index by whose enddeparture_energymust be met;nothing⇒ the end of the horizon.phase_terminals,neutral,energy_min,q_min,q_max,eff_charge,eff_discharge— asStorageDevice.
Multi-period OPF
PowerOptLab.solve_multiperiod_opf — Function
solve_multiperiod_opf(nets, devices; kwargs...) -> MultiperiodResultCo-optimise a sequence of network snapshots nets (one BMOPFTools net dict per period, in chronological order) with a set of storage/EV devices whose state of charge couples the periods. The snapshots share one JuMP model and one objective (the sum of each snapshot's generation cost); the devices arbitrage across time subject to their power, energy, efficiency, and terminal/departure constraints.
Per-period economics come from the snapshots themselves — e.g. a time-varying slack import price set via each net's voltage_source cost, or differing loads.
Arguments
nets::Vector—Tnetwork dicts (parse_bmopfoutput), one per period.devices::Vector—AbstractDeviceinstances implementing the validation/stamp/link/extract lifecycle. Built-in storage and EV devices require their bus/terminals to exist in every snapshot.
Keywords
dt_h=1.0— uniform period duration in hours (compatibility shorthand).time_grid=nothing— passTimeGrid([Δt₁, Δt₂, ...])for nonuniform durations. When supplied it takes precedence overdt_h.per_unit=true,s_base=1e6— engine unit handling (results are SI regardless).optimizer=Ipopt.Optimizer,verbose=false,solver_options=()— solver control.
Returns
A MultiperiodResult with the per-period solutions and each device's SI charge/discharge/SOC trajectory.
PowerOptLab.MultiperiodResult — Type
MultiperiodResultResult of solve_multiperiod_opf.
Fields
termination_status::String— JuMP status of the single shared solve.objective::Float64— optimal objective value.snapshots::Vector{Dict{String,Any}}— the per-period BMOPFTools result dict (SI), one per input net, in order.dispatch::Dict{String,NamedTuple}— per device id, SI trajectories over the horizon:p_charge,p_discharge,p_net(discharge positive),q(each lengthT), andsoc(lengthT+1, energy in Wh at each step boundary,soc[1]= initial).solve::SolveStatus— exact normalized status and publication decision.
Legacy WLS state estimation
PowerOptLab.Measurement — Type
Measurement(; kind, bus, value, sigma, terminal="1", reference=missing)A single scalar measurement for solve_state_estimation. value and sigma are SI (volts for :vmag, watts for :pinj, vars for :qinj); sigma is the measurement standard deviation (WLS weight 1/sigma²), and must be finite and strictly positive.
kind::Symbol—:vr,:vi, or:vmag(a rectangular component or the magnitude of the voltage across(bus, terminal)→reference),:pinj(active power injected into the network at that terminal pair), or:qinj(reactive power injection).bus::String,terminal::String="1"— the measured phase conductor.reference— the return terminal the quantity is referenced to.missing(the default) inherits the solve'sneutral; aStringnames an explicit return terminal on the same bus;nothingreferences terminal-to-ground. Voltage and power measurements at a bus therefore share one reference — a smart-meter reading is phase-to-neutral for both.
Construction validates kind, finiteness, sigma > 0, and non-empty identifiers; it does not check that the identifiers exist in a particular net.
PowerOptLab.solve_state_estimation — Function
solve_state_estimation(net, measurements; kwargs...) -> StateEstimationResultEstimate the network state of net — a physics-only BMOPFTools net (buses, lines, transformers, shunts, and a voltage source; no loads, generators, IBRs, or operational limits) — that best fits measurements in a weighted-least-squares sense.
The network is treated as a contract and validated up front (see allow_operational). Every non-source phase terminal must be either a measured injection (a :pinj+:qinj pair) or declared in zero_injection; an un-declared, un-measured bus is an error, never a silent zero injection.
Keywords
neutral="n"— default return terminal for injection measurements, free injections, and voltage references. Passnothingif phase terminals are referenced directly to ground.zero_injection=String[]— buses (or(bus, terminal)pairs) known to carry no injection. A bare bus id expands over its phase terminals.allow_operational=false— whentrue, downgrade the network-contract check to a warning instead of an error (estimate against loads/limits deliberately).check_observability=true— compute a local identifiability diagnostic.per_unit=true,s_base=1e6— engine unit handling; measurements stay SI.optimizer=Ipopt.Optimizer,verbose=false,solver_options=().
Observability
The returned observability NamedTuple reports a LOCAL numerical check: the Jacobian of the measurement + zero-injection equations with respect to the rectangular node voltages is formed at the returned point (reusing ybus_passive), and observable = rank == n_states. It reports redundancy (surplus equations), the smallest singular value, and the condition number. This detects local rank deficiency / critically-weak measurement sets; it is not a global uniqueness proof, and solver convergence alone never establishes one.
Returns
A StateEstimationResult; its bus voltages are NaN unless primal_status == "FEASIBLE_POINT".
PowerOptLab.StateEstimationResult — Type
StateEstimationResultResult of solve_state_estimation.
Fields
termination_status::String,primal_status::String— the solver's termination status and primal-point status. Trust the estimate only whenprimal_status == "FEASIBLE_POINT".objective::Float64— the optimal weighted-residual sum∑ (z−h)²/σ²(NaNif no feasible point was found).bus::Dict{String,Any}— the estimated SI bus voltages (vr,vi,vm,vaper terminal).NaNthroughout whenprimal_statusis notFEASIBLE_POINT— an unconverged solver iterate is not an estimate.residuals::Vector{NamedTuple}— per input measurement, in order:(kind, bus, terminal, reference, measured, estimated, residual, standardized)withresidual = measured − estimatedandstandardized = residual/σ.standardizedis the σ-normalised RAW residual, not the classical leverage-adjusted normalised residual (rᴺ = r / √(Sᵢᵢ)) used for bad-data identification; it is a scale-free residual, not a χ²/rᴺ test statistic.observability::NamedTuple— a LOCAL numerical identifiability diagnostic(observable, n_states, rank, redundancy, min_singular, cond)from the measurement Jacobian at the returned point (seesolve_state_estimation).
Constrained NLLS state estimation
PowerOptLab.TerminalConnection — Type
One oriented device branch: current is positive from positive to negative.
PowerOptLab.ExactInjectionSpecification — Type
ExactInjectionSpecificationMarker hierarchy for information which is genuinely exact. Meter readings and forecasts are intentionally absent: they belong in the stochastic residual model, not in this hierarchy.
PowerOptLab.ConstantPowerDevice — Type
Exact constant-power branches; positive power denotes consumption.
PowerOptLab.ConstantCurrentDevice — Type
Exact constant-current branches; positive current flows into the device.
PowerOptLab.ZIPDevice — Type
Exact ZIP branches: conj(S/V) + I + YV, all in SI branch quantities.
PowerOptLab.ExactDeviceEquation — Type
ExactDeviceEquation(model)Wrap an exact device model so its terminal KCL equations enter c(x)=0. Use only for a device law known exactly; uncertain load/generation information belongs in measurements or priors.
PowerOptLab.BranchMeasurement — Type
A scalar telemetry reading on one end/conductor of a named BMOPF line.
PowerOptLab.SEStructure — Type
SEStructure{Ti}Immutable compiled structure for the voltage-only, four-wire state-estimation formulation. It imports BMOPFTools' passive, conductor-to-earth Ybus exactly once. free_state_map maps a free conductor to its position in the rectangular state [real(V_free); imag(V_free)]; ideal-source conductors are held in the parameter vector instead. Closed ideal switches are already represented by the node aliases in ybus_passive.
The evaluator supports terminal voltage components/magnitudes and terminal active/reactive injection measurements, exact zero-injection equations, and connection-aware exact constant-power, constant-current, and ZIP devices. Branch telemetry and sparse linear algebra belong to subsequent phases.
PowerOptLab.SEParameters — Type
SEParametersMutable numerical data paired with an SEStructure. Updating the measurement values, standard deviations, or fixed source phasors does not alter terminal ordering or symbolic sparsity. Standard deviations are the diagonal whitening factors for this first implementation.
PowerOptLab.SEEvaluation — Type
Values of the compiled residual/constraint model at one voltage state.
PowerOptLab.compile_state_estimator — Function
compile_state_estimator(net, measurements=Measurement[];
neutral="n", zero_injection=String[], exact_devices=[]) -> SEStructureCompile the immutable voltage-state evaluator. The network is represented by BMOPFTools' passive I = YV relation in SI units. Every source terminal with a specified phasor is eliminated from the state; ungrounded neutrals and floating conductors remain explicit states, so gauge/reference deficiencies are visible to the later rank diagnostics rather than silently grounded.
PowerOptLab.evaluate_state_estimator — Function
evaluate_state_estimator(structure, parameters, x) -> SEEvaluationEvaluate SI phasors, whitened stochastic residuals (h(x)-z)/σ, and exact zero-injection residuals. The constraint vector is never whitened or otherwise softened.
PowerOptLab.residual_jacobian — Function
Analytic Jacobian of the whitened stochastic residual vector.
PowerOptLab.constraint_jacobian — Function
Analytic Jacobian of exact zero-injection constraints.
PowerOptLab.ConstrainedStateEstimationResult — Type
ConstrainedStateEstimationResultResult from the dense composite-step reference solver. status distinguishes a numerically converged but underobserved estimate from failure to establish the exact equations. history records the scaled trust-region radius, merit value, measurement objective, and exact-constraint norm at accepted iterates.
PowerOptLab.solve_compiled_state_estimator — Function
solve_compiled_state_estimator(structure, parameters, x0; kwargs...)
-> ConstrainedStateEstimationResultDense reference implementation of the plan's equality-constrained Gauss–Newton method. It uses a scaled Byrd–Omojokun composite step: a normal least-squares step for exact-equation violation, followed by a null-space tangential measurement step. An exact-penalty merit function globalises both quantities; rejected nonlinear constraint steps receive one trust-region-limited second-order correction before the radius contracts.
This intentionally transparent solver is for small-system verification. It uses dense SVD rank/null-space diagnostics; the compiled evaluator preserves the sparsity used by solve_sparse_state_estimator, the sparse Hachtel-system implementation for larger networks.
PowerOptLab.SparseConstrainedStateEstimationResult — Type
SparseConstrainedStateEstimationResultResult from solve_sparse_state_estimator. constraint_multipliers are recovered directly from the final Hachtel system, in the same row order as evaluation.constraints; they are useful for ranking suspicious exact zero-injection or device equations.
PowerOptLab.solve_sparse_state_estimator — Function
solve_sparse_state_estimator(structure, parameters, x0; kwargs...)
-> SparseConstrainedStateEstimationResultSparse augmented-system implementation for larger compiled networks. Each iteration solves the scaled Hachtel system using SuiteSparse QR, retaining the residual and constraint operators rather than explicitly forming H' * H or a dense null-space basis. The dense SVD helper is used only for the small final rank diagnostic/status; it is not part of the linear step.
The trust-region acceptance uses the same exact-penalty merit function as the dense reference solver. damping is a small scaled primal regulariser used only to select a stable representative in rank-deficient linear systems.
PowerOptLab.ContinuationStateEstimationResult — Type
Result and per-stage diagnostics from constant-power continuation.
PowerOptLab.solve_with_continuation — Function
solve_with_continuation(structure, parameters, x0; alphas=0:0.25:1, kwargs...)Advance exact constant-power/ZIP constraints from the regularised internal model (α=0) to their physical, unregularised equations (α=1). Each stage warm starts the dense constrained solver. A returned :power_flow_initialization_failed status means a stage could not establish feasibility; no final estimate is claimed unless the last stage is at α=1.
PowerOptLab.SEObservability — Type
Local tangent-space observability diagnostic for a compiled estimator.
PowerOptLab.observability_diagnostics — Function
observability_diagnostics(structure, parameters, x) -> SEObservabilityReport local identifiability on the feasible tangent space, using H*Z where C*Z = 0. This is deliberately not the rank of the unconstrained measurement Jacobian. The current dense diagnostic is intended for small/reference cases; the sparse solver uses it only after convergence, not in its linear step.
PowerOptLab.unobservable_directions — Function
unobservable_directions(structure, parameters, x; count=nothing)Generate requested local unobservable state directions on demand. No basis is stored in SEStructure; callers that only need ranks should use observability_diagnostics instead.
PowerOptLab.selected_state_covariance — Function
selected_state_covariance(structure, parameters, x, indices)Return only the requested covariance block of the rectangular voltage state. The measurement residual Jacobian is already whitened, so this is the local Gauss–Newton covariance under the diagonal covariance model. It throws for a rank-deficient tangent space rather than returning a misleading finite matrix.
PowerOptLab.derived_covariance — Function
derived_covariance(structure, parameters, x, jacobian)Return the local covariance of selected derived quantities. jacobian has one row per requested quantity and one column per state variable.
PowerOptLab.StatePrior — Type
A stochastic prior on selected rectangular voltage-state entries.
PowerOptLab.set_state_prior! — Function
Replace the stochastic state prior without recompiling network structure.
PowerOptLab.TimeSeriesStateEstimationResult — Type
Outcome of sequential estimation over one unchanged compiled topology.
PowerOptLab.solve_time_series_state_estimator — Function
solve_time_series_state_estimator(structure, parameters, x0;
previous_state_sigma=nothing, solver=:sparse, kwargs...)Solve successive snapshots without rebuilding terminal maps, Ybus structure, or measurement/device incidence. parameters contains one mutable SEParameters per snapshot; callers update its numerical measurements, covariances, sources, and device values in place before calling. Each snapshot warm-starts from the preceding result. When previous_state_sigma is supplied, that preceding state becomes a whitened stochastic prior for the next snapshot, not an exact equation.
solver is :sparse (default) or :dense. The result preserves every per-snapshot result and stops at the first non-converged snapshot, reporting :time_series_stalled without claiming later estimates.
Parameter estimation
PowerOptLab.CalibLine — Type
CalibLine(; id, bus_from, bus_to, r_per_length, x_per_length, ...)An uncertain line whose length is a free parameter estimated by solve_parameter_estimation. The series impedance is (r_per_length + j·x_per_length)·length [Ω]; only length is estimated (the nominal per-length impedance is treated as known, following the construction-code model Z = Zⁿᵒᵐ·ℓ of Vanin et al.).
The uncertain lines must be omitted from the physics nets — they are the unknowns, stamped by this function. Single-phase: it connects (bus_from, terminal) to (bus_to, terminal); add one CalibLine per phase for a multi-phase line.
Keywords
id::String— label for reporting.bus_from::String,bus_to::String,terminal::String="1"— the endpoints.r_per_length::Float64,x_per_length::Float64— per-unit-length series R/X [Ω].length_init::Float64=1.0— starting guess handed to the solver.length_min::Float64=0.1,length_max::Float64=10.0— bounds on the estimate.
PowerOptLab.CalibTap — Type
CalibTap(; id, tap_min=0.9, tap_max=1.1)An uncertain transformer/regulator tap estimated by solve_parameter_estimation. Unlike CalibLine, the transformer stays in the physics nets: this just names the transformer id and the tap bounds. The function sets tap_min/tap_max on that transformer so the engine creates its native free-tap variable, then couples the tap equal across all snapshots and reports the estimate.
The reported estimate is the tap multiplier τ on the nominal ratio N₀ = v_nom_from / v_nom_to (so τ = 1 is nominal); the effective turns ratio is N = N₀·τ.
Keywords
id::String— transformer id present in every snapshot net.tap_min::Float64=0.9,tap_max::Float64=1.1— bounds on the multiplierτ.
PowerOptLab.solve_parameter_estimation — Function
solve_parameter_estimation(nets, measurements; lines, taps, kwargs...)
-> ParameterEstimationResultCalibrate uncertain line lengths and transformer tap ratios from smart-meter time series. The uncertain elements' parameters are shared, time-invariant unknowns; each snapshot supplies noisy (P, Q, |V|) meter readings that are fit in a weighted-least-squares (or robust weighted-least-absolute-value) sense across the whole horizon.
The meters' loads are not baked in as exact injections — each measured user gets a free injection current fit to its noisy P/Q readings (as in state estimation), and the phase-to-neutral voltage magnitude is fit to |V|. Buses with no injection measurement are zero-injection.
Arguments
nets::AbstractVector—Tper-snapshot physics nets (parse_bmopfoutput): the known source, the known lines, and the transformers named bytaps(kept, so the engine frees their taps). The uncertainlinesare omitted — they are the unknowns. No load objects are needed; injections come from the measurements.measurements::AbstractVector— parallel tonets;measurements[t]is aVector{Measurement}of that snapshot's meter readings.:vmag(SI volts),:pinj,:qinj(SI W/var, injection into the network — negative for a load) are all treated as noisy, weighted by theirsigma.
Keywords
lines::AbstractVector=CalibLine[],taps::AbstractVector=CalibTap[]— the uncertain elements to estimate (at least one required).neutral="n"— return terminal for the phase-to-neutral projections; passnothingif phase terminals are referenced directly to ground.objective=:wls—:wls(weighted least squares, smooth) or:wlav(weighted least absolute value, less sensitive to isolated large residuals; the choice in Vanin et al.). WLAV does not automatically identify or remove bad data.per_unit=true,s_base=1e6— engine unit handling; measurements stay SI.optimizer=Ipopt.Optimizer,verbose=false,solver_options=().
Returns
A ParameterEstimationResult with the estimated lengths and tap multipliers, the RMS voltage residual, and the per-snapshot fitted state.
PowerOptLab.ParameterEstimationResult — Type
ParameterEstimationResultResult of solve_parameter_estimation.
Fields
termination_status::String,objective::Float64— solver status and the optimal weighted-residual value.line_length::Dict{String,Float64}— estimated length perCalibLineid.tap::Dict{String,Float64}— estimated tap multiplierτperCalibTapid.residual_rms::Float64— root-mean-square voltage-measurement residual [V], a quick goodness-of-fit / noise-floor check.snapshots::Vector{Dict{String,Any}}— per-snapshotextract_result(SI), the fitted network state at the estimated parameters.
Inverse Carson reconstruction
PowerOptLab.SequenceLineObservation — Type
SequenceLineObservation(; z0, z1, frequency, ...)Diagonal zero- and positive-sequence line data for solve_inverse_carson. z0 and z1 are complex impedances. Optional b0 and b1 are real shunt susceptances. Values are converted to SI per metre internally.
Keywords
z_units=:ohm_per_km—:ohm_per_mor:ohm_per_km.b_units=:micro_siemens_per_km—:siemens_per_m,:siemens_per_km, or:micro_siemens_per_km.sigma— standard deviations in the same input units, ordered(R0, X0, R1, X1[, B0, B1]). If omitted, a descriptive 1% tolerance is used with an absolute floor in the declared input units; candidate scores then have no formal statistical interpretation.covariance— full positive-definite covariance matrix in the same ordered input units. It is mutually exclusive withsigma; correlations affect the Mahalanobis objective whilestandardized_residualremains marginal.frequency[Hz] andearth_resistivity=100.0[Ω·m].
The observation is assumed to come from a three-phase matrix, with any circuit neutral Kron-reduced before the symmetrical-component transform.
PowerOptLab.OverheadCarsonCandidate — Type
OverheadCarsonCandidate(; id, geometry, r_ac_ref, gmr, radius, lower, upper, ...)One discrete overhead construction considered by solve_inverse_carson. Conductor arrays are ordered (a,b,c[,n]) and use SI units: r_ac_ref [Ω/m], gmr, radius, and cap_radius [m]. Separate phase and neutral conductors are supported.
The continuous parameter order is returned in each fit and depends on geometry:
:horizontal_3:(half_span, height, temperature):triangle_3:(half_span, height, temperature);angleis fixed:horizontal_4:(inner_offset, outer_gap, height, temperature):neutral_under_4:(half_span, phase_height, neutral_drop, temperature)
Geometry values are metres and temperature is °C. lower, upper, and initial follow that order. alpha_20 may be a scalar or one value per conductor; resistance is corrected from temperature_ref using IEC's linear temperature relation.
PowerOptLab.solve_inverse_carson — Function
solve_inverse_carson(observation, candidates; kwargs...) -> InverseCarsonResultFit every discrete overhead construction candidate to diagonal sequence data. Each candidate is solved independently as a bound-constrained smooth NLP with deterministic multistart. Fits are returned in increasing weighted-residual order; ambiguity is retained rather than collapsed to a single winner.
Keywords
starts=16— deterministic starts per candidate.acceptance_sigma=3.0— a candidate is compatible only when every standardized residual is within this threshold.rank_tolerance=1e-6— relative threshold for the singular values of the standardized prediction Jacobian.confidence_level=0.95— level for the local linearized parameter intervals.optimizer=Ipopt.Optimizer,verbose=false;solver_optionsaccepts an iterable of name-value pairs or a named tuple. Ipopt receives safe defaultshessian_approximation="limited-memory"andbound_relax_factor=0.0before user options are applied. Other optimizers receive no Ipopt-specific options.
The solver uses modified Carson only. Returned Z_primitive and C_primitive retain an explicit circuit neutral; Kron reduction is used solely to compare against the sequence observation.
PowerOptLab.profile_inverse_carson — Function
profile_inverse_carson(fit, candidate, observation; kwargs...)Compute connected one-parameter profile-likelihood confidence intervals around an inverse-Carson fit. Each parameter is fixed successively while all remaining parameters are reoptimized. An endpoint status of :threshold means the chi-square threshold was crossed, :bound means the candidate bound was reached, and :failed conservatively returns the bound because the profile solve failed.
The default threshold is the one-degree-of-freedom chi-square quantile implied by fit.confidence_level. points=12 traces each side before bisection; bisection_steps=16 refines the first crossing. A failed continuation solve is retried from deterministic alternative starts before the endpoint is labelled :failed. These are local connected profiles, not a guarantee that disconnected feasible regions do not exist.
PowerOptLab.InverseCarsonFit — Type
One fitted candidate returned by solve_inverse_carson.
PowerOptLab.InverseCarsonResult — Type
Result of solve_inverse_carson, retaining all candidate fits.
PowerOptLab.InverseCarsonProfileInterval — Type
Connected profile-likelihood interval for one inverse-Carson parameter.
PowerOptLab.materialize_inverse_carson — Function
materialize_inverse_carson(fit, candidate) -> DictCreate BMOPF-ready wire_data and line_geometry blocks for a successful inverse fit. The returned dictionary is not inserted into a network and no linecode is compiled automatically.
Dynamic operating envelopes
PowerOptLab.ConnectionPoint — Type
ConnectionPoint(; id, bus, export_max=0.0, import_max=0.0, kwargs...)An active connection point whose positive operating-envelope capacity is calculated by solve_operating_envelope.
export_max/import_maxare the connection's active-power nameplate limits (W, both non-negative). Select which one is used withdirectionon the solve.ibr_id=nothingretains the lightweight legacy port: aggregate unity-PF power is stamped directly atbususingphase_terminalsandneutral.ibr_id="pv1"binds the envelope to an existing BMOPFToolsibr. This is the recommended representation for PV and batteries: the IBR keeps its prescribed Volt-VAr/Volt-Watt or fixed-power-factor control law, apparent/current limits, phase topology, and DC coupling while the DOE controls active power only.requestedis an optional requested/forecast capacity (W), used byFairnessPolicy(normalization=:request).normalizationis an optional custom fairness reference (W), used byFairnessPolicy(normalization=:custom).
PowerOptLab.FairnessPolicy — Type
FairnessPolicy(; kind=:equal, normalization=:none, weights=Dict(),
alpha=1.0, epsilon=1e-6)Parameterized policy for allocating active-power envelope capacity.
normalization defines the reference in xᵢ = capacityᵢ/referenceᵢ:
:none— absolute watts (the reference is 1 W);:capacity— the connection's export/import nameplate for this direction;:request—ConnectionPoint.requested;:custom—ConnectionPoint.normalization.
Supported kind values:
:equal— require equal normalized allocations and maximize their level;:max_total— maximize the weighted sum of normalized allocations;:proportional— weighted proportional fairness,Σwᵢ log(xᵢ+ε);:alpha— weighted alpha fairness (alpha=0is weighted sum andalpha=1is proportional fairness);:max_min— maximize the minimum normalized allocation, then maximize total allocation while retaining that locally optimal minimum;:equal_curtailment— require equal normalized curtailment from nameplate and minimize it.
weights is keyed by connection-point id and defaults to one. All weights must be finite and strictly positive. epsilon regularizes logarithmic/negative-power utilities at zero; it does not create physical capacity.
PowerOptLab.solve_operating_envelope — Function
solve_operating_envelope(nets, connection_points; kwargs...)
-> OperatingEnvelopeResultCalculate active-power operating-envelope capacity for each interval.
nets accepts three shapes:
- one network
Dict— one interval, one forecast scenario; Vector{Dict}— several intervals, one scenario each (backward compatible);Vector{Vector{Dict}}— several intervals, each containing one or more forecast/model scenarios. One capacity allocation is shared by every scenario in an interval.
Important keywords:
direction=:exportor:import; returned capacities are positive magnitudes;fairness=:equal, the legacy symbols:sum/:proportional, or aFairnessPolicy;security=:bound_pointenforces only simultaneous full utilisation;security=:cornersembeds all2^Nzero/full-utilisation corners for every scenario. It is deliberately capped bymax_exact_corners=10and is reported as local AC feasibility at tested points, not a global robust certificate;volt_var_watt_eps=2e-3controls the engine's smooth approximation of mandatory IBR Volt-VAr/Volt-Watt curve corners.
Loads retain their known P/Q from each network snapshot. Connection-bound IBRs retain their prescribed Q-V law. Other network devices, including BMOPFTools STATCOM IBRs, remain available to the OPF; comparing otherwise identical nets with and without a STATCOM therefore quantifies its impact on active-power DOEs. Network voltage, phase-to-neutral, negative-sequence (vneg_max), branch thermal, neutral-current, and device limits declared by BMOPFTools remain in force.
PowerOptLab.compare_operating_envelope_policies — Function
compare_operating_envelope_policies(nets, connection_points, policies; kwargs...)Solve the same DOE study under several fairness policies. policies is a dictionary or vector of label => fairness pairs. The returned dictionary maps each label to an OperatingEnvelopeResult, whose fairness_metrics make the capacity/fairness trade-off directly comparable.
PowerOptLab.verify_operating_envelope — Function
verify_operating_envelope(nets, connection_points, capacities; kwargs...)
-> OperatingEnvelopeVerificationCheck an already-issued capacity trajectory without optimising it. The active power capacities are fixed and the normal network physics, including prescribed Q-V IBR controls and any STATCOM model present in the network, is solved at every requested scenario and utilisation point. utilizations is :bound_point, :corners, or explicit vectors in [0, 1]^N.
PowerOptLab.OperatingEnvelopeResult — Type
OperatingEnvelopeResultResult of solve_operating_envelope. Capacities are positive SI watts for the selected direction.
snapshots[t] is the first scenario at the all-active upper corner. When an interval has no feasible primal solution it contains only status metadata and all capacities for that interval are NaN; infeasible solver iterates are never published as envelopes.
total_export is retained as a backward-compatible alias of total_capacity. For a new import study use total_capacity and inspect direction.
fairness_metrics reports allocation and curtailment metrics for the published capacity at each interval. schedule records the issue/validity metadata and whether a non-optimised fallback was published.
PowerOptLab.OperatingEnvelopeVerification — Type
OperatingEnvelopeVerificationResult of verify_operating_envelope. Each interval reports whether a fixed, already-issued allocation was locally feasible at every requested utilisation point and forecast/model scenario. This is a verification result, not a new allocation.
Bilevel distribution-network proof of concept
PowerOptLab.BilevelPVResult — Type
BilevelPVResultResult of solve_bilevel_pv_tap. tap is the user-facing tap multiplier, exported_power_W is the lower-level aggregate PV export, and voltages_V contains monitored voltage magnitudes keyed by (bus, phase). converged and termination_reason describe the upper-level search; a lower-level LOCALLY_SOLVED status alone is not an upper-level convergence certificate.
The lower-level consumers are represented by one BMOPFTools native IBR each. Their Volt-var and Volt-watt curves are therefore part of the differentiated network model. lower_level=:aggregate uses a shared aggregate-export objective; lower_level=:local_controller instead ties each PV's active output to its own Volt-watt response, with Volt-var priority enforced through a smooth apparent-power cap.
PowerOptLab.BilevelPVResponse — Type
BilevelPVResponseFixed-tap lower-level response used for sensitivity validation and experiment scripts. The response contains the solved PV export, monitored voltages, and the local DiffOpt derivative of each monitored voltage with respect to the utility tap multiplier.
PowerOptLab.SingleLevelPVResult — Type
Result of the centralized single-level comparison solve.
PowerOptLab.solve_bilevel_pv_tap — Function
solve_bilevel_pv_tap(net; transformer_id, pv_ids, monitored_buses,
tap_initial=1.0, tap_bounds=(0.95, 1.05), ...)Solve the POC hierarchy. With lower_level=:aggregate, the lower level maximises aggregate PV active export with native BMOPFTools Volt-var/Volt-watt control curves active. With lower_level=:local_controller, each PV follows its own Volt-var/Volt-watt response and the lower-level objective is constant; the extra equations are differentiated as part of the network equilibrium. The upper level minimises a smooth fourth-power voltage-stress metric plus tap movement, using DiffOpt's implicit KKT derivative and a safeguarded one-dimensional derivative search. voltage_measurement selects the monitored phase-to-neutral, phase-to-ground, or cyclic phase-to-phase quantity.
This is intentionally a local, smooth-region experiment: it is not a global solution method for a nonconvex bilevel problem. Each upper-level trial builds and solves a fresh lower-level model, which avoids making the reported response path depend on a rejected warm start. At a Volt-watt kink or active-set transition, inspect differentiability_report and validate the final point by perturbing the tap and resolving the lower level.
PowerOptLab.solve_bilevel_pv_response — Function
solve_bilevel_pv_response(net; transformer_id, pv_ids, monitored_buses,
tap=1.0, tap_bounds=(0.95, 1.05), lower_level=:aggregate, ...)Solve one fixed-tap lower-level response and return its local voltage sensitivity with respect to the tap. This is the public diagnostic counterpart to the differentiated response used by solve_bilevel_pv_tap: it is useful for comparing DiffOpt derivatives against finite differences and for checking sensitivity quality near Volt-watt transitions. voltage_measurement selects phase-to-neutral (:pn), phase-to-ground (:pg), or cyclic phase-to-phase (:pp) monitoring; outputs are keyed by (bus, phase).
PowerOptLab.solve_single_level_pv_tap — Function
solve_single_level_pv_tap(net; transformer_id, pv_ids, monitored_buses, ...)Solve the centralized comparison: utility tap and all PV operating points are chosen in one BMOPFTools/JuMP OPF. The objective is the same voltage stress and tap movement used by the bilevel upper level. An optional normalized PV export reward can be enabled explicitly with export_weight. This is a benchmark for the coordination gap, not the consumer hierarchy.
PowerOptLab.bilevel_demo_network — Function
Return a small reproducible single-phase feeder for the bilevel tutorial.
Advanced inverter
PowerOptLab.AdvancedInverter — Type
AdvancedInverter(; id, bus, s_max, kwargs...)An experimental inverter with an internal AC node behind an output filter. All parameters are SI. Features are opt-in: with only id, bus, s_max (and a zero filter) it is a plain grid-following converter at the POC.
Required
id::String,bus::String— identifier and point-of-connection bus.s_max::Float64— converter apparent-power rating (VA), applied on the converter side (internal-node voltage × current).
Connection
phase_terminals=["1"],neutral="n"— phase conductor(s) and return terminal (nothing⇒ referenced to ground). The three-phase topologies and grid-forming require three phases.
Topology (three-phase switching-polytope DC-utilisation model)
topology=:SINGLE_PHASE— one of:SINGLE_PHASE(internal-EMF model),:THREE_LEG,:FOUR_LEG,:SPLIT_DC. The three-phase topologies apply the time-sampled switching-polytope voltage feasibility on the internal-node voltage and requirev_dcandc_dc. The 4-wire topologies additionally need their neutral current bounded::FOUR_LEGrequiresIn_max(the 4th-leg rating),:SPLIT_DCrequiresIn_maxori_cap_max(its neutral flows through the capacitors, so a bank rating bounds it on its own).v_dc— DC-link voltage (V).c_dc— DC-link capacitance (F; per half for split).c_dc_upper,c_dc_lower— optional split-link half-bank capacitances (F). Each defaults toc_dc; supplying either exposes unequal series capacitance, unequal neutral-current sharing, and a mean midpoint offset.m_max=1.0— utilisation factor applied to the ideal two-level switching hull (dimensionless,0 < m_max ≤ 1).In_max— neutral current limit (A). For:FOUR_LEGthis is the 4th leg's device (thermal) rating. For:SPLIT_DCit is the half-bank capacitor ripple rating net of the 2ω allocation: the same capacitors carry both the fundamental neutral current and the 2ω bus current, which sit at different frequencies and so combine in RMS. For a symmetric link,In_max = 2·√(I_half_rated² − I_2ω,rms² − I_sw,rms²)withI_2ω,rms = |S̃|/(√2·v_dc). Passing the raw bank rating double-counts the capacitors and overstates neutral capability. Preferi_cap_max, which makes that allocation endogenous instead of asking you to pre-compute it.i_cap_max— capacitor RMS ripple-current rating (A), three-phase topologies only: per half-bank for:SPLIT_DC, the whole DC-link bank otherwise. Supplying it stamps the simultaneous thermal allocation on both split half-banks. In the symmetric, equal-weight case this is(|I_n|/2)² + I_2ω,rms² + i_sw² ≤ i_cap_max²; unequal banks use their capacitance-proportional neutral shares. The neutral term is present only for:SPLIT_DC; the 4-leg's neutral flows through its 4th leg, not the caps. Thus the split between capacitor-rating and neutral-current limits is decided by the solve at the operating point rather than fixed as a nameplate. Composes withIn_max— whichever binds, binds. For:SPLIT_DCit can also be supplied instead ofIn_max, since it bounds|I_n|on its own. Rating convention: because electrolytic ESR falls with frequency, the equal-weight sum above is only conservative ifi_cap_maxis referred to the lowest frequency in it — the 50/60 Hz neutral term. Refer the datasheet rating to that frequency using the capacitor manufacturer's own multiplier; passing a raw 100/120 Hz rating understates 50/60 Hz heating.i_cap_upper_max,i_cap_lower_max— optional per-half split-link thermal- equivalent current ratings (A). They compose with the commoni_cap_max.cap_thermal_weights=(1,1,1)— squared-current weights for the fundamental neutral, 2ω, and switching components, respectively. Use ESR ratios referred to the rating frequency; the default reproduces the original RMS sum.esr_dc— monolithic-bank reference ESR (Ω).esr_dc_upperandesr_dc_lowerare the split half-bank values. Together withcap_thermal_weights, these produce capacitor dissipation and add it top_dc; zero defaults preserve the lossless-link model.q_mid_balance_max— optional split-link charge-transfer authority (C). The actuator can shift mean midpoint voltage by2q/(C_upper+C_lower).v_mid_mean_max— optional magnitude limit on mean midpoint offset (V).i_sw— optional constant switching-frequency RMS allowance (A) reserved out of a supplied common or half-bank capacitor rating (use when the electrolytics, not a parallel film cap, carry thef_swcomponent).pwm_strategy=:NONE— optional carrier-level DC-link ripple model.:SPWMuses zero common-mode injection;:CENTEREDuses centered carrier PWM (the carrier-based SVM equivalent). The convenience solver closes the resulting capacitor-current reserve by conservative outer iteration; direct device stamping requirespwm_strategy=:NONEand an explicitly calibratedpwm_current_factor(plus any independenti_swresidual).f_sw— switching frequency (Hz), required whenpwm_strategy != :NONE.pwm_fundamental_samplesandpwm_carrier_samplesset the two numerical quadrature grids used by the post-solve switching-state audit.pwm_dc_source_r=nothing,pwm_dc_source_l=0— optional series R–L impedance of the upstream DC source as seen from the link at carrier frequencies. With no branch the source is open to switching ripple and the capacitors carry all of it.pwm_dc_harmonicscontrols the finite-branch Fourier truncation.pwm_current_factor=0— direct-stamping approximationI_sw = pwm_current_factor*sqrt(sum(I_leg,rms^2)). The convenience solver updates this factor from the carrier audit; set it explicitly only when composing a larger model around an externally calibrated operating region.pwm_ac_ripple=false— pass the ideal pole-voltage carrier harmonics through the conductor-domain reduced-L or LCL network. This reports converter/grid/ shunt and neutral switching currents and closes them into any suppliedi_max,i_grid_max, andIn_maxratings.pwm_ac_harmonicstruncates the carrier Fourier series and must not exceed half the carrier sample count.pwm_ac_converter_reserve,pwm_ac_grid_reserve, andpwm_ac_neutral_reserveare expert direct-stamping inputs (A RMS). The convenience solver calibrates them automatically; leave them zero otherwise.dv2_max— optional cap on the 2ω bus-ripple amplitude (V).dv_mid_max—:SPLIT_DConly: optional cap on the RMS fundamental midpoint-to-ideal-midpoint voltage ripple (V).i_zero_max,i_positive_max,i_negative_max— optional RMS symmetrical- component current limits (A). These are useful for grid-code limits and for separating zero-sequence neutral stress from negative-sequence 2ω stress.n_samples=36— time-sampling grid for an outer approximation of continuous- time voltage feasibility. Increase it for boundary-sensitive studies.f=50.0— fundamental frequency (Hz).
Output filter
r_filter=0.0,x_filter=0.0— series filter impedance per phase (Ω).r_filter_neutral=0.0,x_filter_neutral=0.0— neutral-conductor series impedance (Ω). Its common voltage drop couples all phase-to-neutral KVL equations; it requires a physicalneutralterminal.r_filter_matrix=nothing,x_filter_matrix=nothing— optional primitive conductor-domain series-impedance matrices (Ω), ordered asphase_terminalsfollowed byneutralwhen present. They capture unequal conductors and mutual coupling. Supplying either matrix supersedes the scalar impedances; mixing the two parameterisations is rejected. Matrices must be finite and symmetric, and the resistance matrix must be positive semidefinite.r_filter_grid,x_filter_gridand their_neutral/_matrixvariants — optional grid-side series arm. Supplying any grid-side arm orc_filter_midactivates an explicit LCL midpoint; the original filter becomes the converter-side arm.c_filter_mid=0.0— per-phase midpoint filter capacitance (F).r_filter_damping=0.0is its optional series damping resistance (Ω).i_grid_max=nothing— optional grid-side arm current limit (A), distinct from converter-sidei_maxwhen the midpoint capacitor carries current.b_filter_shunt=0.0— additional grid-side (POC) shunt susceptance (S), kept separate from the LCL midpoint capacitor for backward compatibility.
Internal EMF box / single-phase modulation
v_int_min,v_int_max— per-phase EMF magnitude box (V; applies to all topologies).modulation_max— SINGLEPHASE only: legacy scalar DC-link convention `|Vint| ≤ modulationmax·vdc/√3`. This is not a bridge-specific full-/half-bridge switching model.
Grid-forming
grid_forming=false— balanced positive-sequence internal EMF; the magnitudev_gfm ∈ [v_int_min, v_int_max]is a decision variable (composes with a topology).
Converter losses
p_loss_fixed=0.0(W),a_loss=0.0(W/A),c_loss=0.0(W/A²).The
a_lossterm needs the current MAGNITUDE, whose exact norm is non-differentiable at zero current. It is stamped as the shifted smooth normsqrt(|I|² + ε²) − ε, which underestimates the exact norm by at mostε. The modelled loss is therefore biased LOW by at mosta_loss·εper conducting leg — a closed-form, one-sided budget, not a tuning knob.εis1e-6of the leg's own current rating, so the relative bias is identical for every device in a heterogeneous fleet and identical in SI and per-unit. Witha_loss == 0no magnitude term is stamped at all. Reported currents are recomputed exactly after the solve and carry none of this bias.
Ratings are stamped as squared per-unit inequalities (p² + q² ≤ (s_max/s_base)² and similar). Ipopt relaxes bounds before solving by bound_relax_factor · max(1, |bound|) (default 1e-8; Eqn 35 of Wächter & Biegler 2006). The max(1, ·) floor is the problem: once the per-unit squared bound (s_max/s_base)² falls below 1 — which it always does — the relaxation stops scaling with it and becomes a fixed 1e-8 ABSOLUTE slackening of a bound that keeps shrinking as 1/s_base². The admissible physical violation is therefore
δ|S| ≈ bound_relax_factor · s_base² / (2·s_max),quadratic in the base. A 20 kVA rating is honoured to ~0.25 VA at s_base=1e6, but exceeded by ~2.4 kVA (12 %) at s_base=1e8.
This is Ipopt's documented behaviour meeting a modelling choice made here, not a defect in either: a minimal max p s.t. p² ≤ (s_max/s_base)² in plain JuMP reproduces the numbers exactly, and bound_relax_factor=0 removes them. The durable fix belongs in this layer — normalizing the constraint by the rating, (p/s_pu)² + (q/s_pu)² ≤ 1, restores a bound of exactly 1 and was measured to hold the rating to <0.1 VA at every base. Until that lands: keep s_base within a couple of decades of the device ratings, or pass bound_relax_factor=0 (as solve_inverse_carson already does, for the same underlying reason). Independent of the square-root smoothing above.
Double-frequency ripple / current
p_ripple_max— SINGLE_PHASE only: bound on the 2ω power-ripple amplitude (VA); it does not by itself calculate capacitor voltage or RMS current.i_max=nothing— optional per-phase-conductor current limit (A). Strongly recommended for every three-phase topology: the aggregates_maxcircle does not bound phase-redistribution currents whose per-phase powers cancel.
PowerOptLab.solve_advanced_inverter — Function
solve_advanced_inverter(net, inverter; objective=:max_export, kwargs...)
-> InverterResultSolve the experimental circuit-aware inverter model. When inverter.pwm_strategy != :NONE, the solver alternates between the smooth NLP and a carrier-level switching audit. A current-norm coefficient is updated until its capacitor allocation covers the modeled carrier ripple; the residual manual i_sw allowance then composes in quadrature. This is a conservative sequential closure, not a mixed-integer switched-converter optimisation.
With inverter.pwm_ac_ripple=true, the same outer loop also updates per-path RMS reserves until the carrier-harmonic converter, grid, and neutral currents are covered by any supplied i_max, i_grid_max, and In_max ratings.
PWM closure keywords are pwm_tolerance=1e-3 (relative current tolerance), pwm_max_iterations=16, and pwm_reserve_factor=1.01. A cold start reaches its fixed point in a handful of solves, so the cap sits well clear of it: a closure reported at the iteration limit is a genuine non-convergence, not a budget that ran out. Inspect result.pwm_reserve_margin, result.pwm_modulation_margin, and result.pwm_iterations for every publishable PWM-enabled solve.
Objective
:max_export— maximise active power delivered to the grid at the POC.:min_loss— minimise converter plus capacitor loss subject top_set.
Other keywords
p_set=nothing— required active-power target (W) for:min_loss.q_set=nothing— optional reactive-power constraint at the POC (var).per_unit=false,s_base=1e6,optimizer=Ipopt.Optimizer,verbose=false,solver_options=(). Results are returned in SI regardless of formulation.
PowerOptLab.InverterResult — Type
InverterResultResult of solve_advanced_inverter. Powers SI (W / var / VA), voltages V, currents A.
Fields
termination_status::String,topology::Symbolp_poc,q_poc— active/reactive power injected at the POC (grid side).p_conv,q_conv— converter-side power (at the internal node).p_loss,p_cap_loss,p_dc— semiconductor loss, capacitor ESR loss, and DC-link power (p_dc = p_conv + p_loss + p_cap_loss).p_filter_loss— total real loss in both series arms and midpoint damping (W).v_int_mag::Vector{Float64}— internal EMF magnitude per phase.v_filter_mag— LCL midpoint phase-to-neutral voltage magnitudes (V); equals the POC voltage in the reduced single-series-arm model.i_mag,i_grid_mag,i_filter_shunt_mag— converter-side, grid-side, and midpoint shunt-branch RMS current magnitudes per phase (A).i_neutral— neutral current magnitude|I_n|(0 for 3-wire / single-phase).i_zero,i_positive,i_negative— RMS symmetrical-component current magnitudes (A; zero for:SINGLE_PHASE). For a four-wire connection,i_neutral = 3i_zeroup to numerical tolerance.ripple— 2ω power-ripple magnitude|Σ V_int·I|(VA).dv2— 2ω bus-ripple amplitude (V; three-phase topologies, else 0).dv_mid— split-link midpoint fundamental-ripple RMS magnitude (V;:SPLIT_DC, else 0).v_mid_mean,q_mid_balance— signed mean midpoint shift (V) used by the switching hull and balancing charge transfer (C; split link only).i_cap— total capacitor RMS ripple current (A; three-phase topologies, else 0): the 2ω component, neutral share, andi_sw. For an asymmetric split link it is the larger half-bank RMS current.i_cap_thermal— maximum thermally equivalent current after applyingcap_thermal_weights; this is the quantity constrained by current ratings.i_cap_upper,i_cap_lower,i_cap_thermal_upper,i_cap_thermal_lower— split half-bank physical and thermal-equivalent currents (A; zero for monolithic links).i_cap_switching,i_dc_bridge_switching_rms, andi_dc_source_switching_rms— carrier-model RMS currents (A) in the DC-link capacitor, ideal bridge input, and optional upstream source branch.i_cap_switching_reservedis the conservative capacitor value allocated by the NLP's calibrated current-norm term.pwm_reserve_marginis allocated modeled ripple minus the carrier prediction; the user's residuali_swallowance is added separately in quadrature.p_dc_source_switching_loss— dissipation (W) inpwm_dc_source_r; this is an upstream-network diagnostic and is deliberately not included inp_dc.dv_switching_rms,dv_switching_pp— carrier-model total DC-bus switching voltage ripple (V RMS and maximum local peak-to-peak). These are distinct from the low-frequencydv2amplitude.dv_switching_upper_rms,dv_switching_lower_rms,dv_switching_upper_pp, anddv_switching_lower_ppresolve that total ripple across the two half-banks of a split link (zero for monolithic links).pwm_dc_network_margin— minimum normalized magnitude of the parallel source-capacitor admittance. Values near zero flag a poorly damped carrier- harmonic parallel resonance; it is one for the default open-source model.pwm_modulation_margin— minimum carrier-reference headroom (V); a negative value orNaNmeans the selected PWM strategy cannot realize the solved fundamental reference.pwm_iterationsreports outer reserve solves.i_ac_switching_rms,i_grid_switching_rms, andi_filter_shunt_switching_rms— carrier-harmonic RMS currents (A) on the converter arm, grid arm, and LCL midpoint branch. The corresponding phase peak-to-peak values arei_ac_switching_ppandi_grid_switching_pp.i_neutral_switching_rms,i_neutral_switching_pp— converter-side neutral switching ripple.i_ac_switching_reserved,i_grid_switching_reserved, andi_neutral_switching_reservedare the values allocated by the smooth current constraints.i_ac_total_rms,i_grid_total_rms, andi_neutral_total_rmscombine orthogonal fundamental and switching RMS values.switching_margin— minimum rail headroom (V) on a dense, independent post-solve time grid. Positive values pass that audit; negative values reveal a between-sample violation accepted by the optimisation grid. This is a useful numerical audit, not a formal continuous-time certificate (0 for:SINGLE_PHASE).filter_resonance_hz— undamped scalar LCL resonance estimate (Hz), orNaNwhen the filter is reduced, matrix-valued, or lacks two positive inductive arms.bus::Dict{String,Any}— the BMOPFToolsresult["bus"](POC voltages, …).
Phase-aware inverter controls
PowerOptLab.AbstractInverterControlLaw — Type
Supertype for local inverter controllers composed with a physical device.
PowerOptLab.AbstractPositiveSequencePolicy — Type
Supertype for policies that form a positive-sequence power request.
PowerOptLab.AbstractUnbalancePolicy — Type
Supertype for policies that form a negative-sequence current request.
PowerOptLab.AbstractLimiterPolicy — Type
Supertype for algebraic controller-output limiters.
PowerOptLab.AbstractCurrentTarget — Type
Supertype for the physical current location commanded by a controller.
PowerOptLab.ConverterCurrentTarget — Type
Command the phase-leg current at the converter terminals.
PowerOptLab.GridCurrentTarget — Type
Command the grid-side current after the output filter.
PowerOptLab.PiecewiseLinearLaw — Type
PiecewiseLinearLaw(breakpoints, values; smoothing_epsilon)A continuous, flat-clamped piecewise-linear scalar law. smoothing_epsilon is an absolute width in the same units as breakpoints; it is used only by the smooth JuMP representation. evaluate_exact retains exact corners.
PowerOptLab.WorstPhaseVoltVarWatt — Type
WorstPhaseVoltVarWatt(; volt_watt=nothing, volt_var=nothing,
conflict_policy=:dominant, extrema_epsilon=0.05,
conflict_epsilon=0.01,
volt_watt_basis=:available)Direction-aware positive-sequence policy for three-phase local voltage control. Volt-watt observes the largest phase-voltage magnitude. Volt-var observes both the smallest and largest phase: low-voltage injection and high-voltage absorption are retained. When both occur, conflict_policy=:net adds the two opposing requests, :dominant continuously blends toward the branch with the larger normalized response, :low_voltage prioritizes injection, and :high_voltage prioritizes absorption.
Volt-watt ordinates are fractions in [0, 1]; Volt-var ordinates are fractions of InverterControlRequest.q_scale. Both curves must be non-increasing. extrema_epsilon is an SI voltage width for smooth phase extrema; conflict_epsilon is the dimensionless transition width in normalized branch severity and is part of both the firmware and JuMP laws. volt_watt_basis is :available or :rated.
Both curve inputs are phase magnitudes and therefore move with the local zero-sequence voltage — a component a three-leg bridge cannot control, and one whose measured value depends on the sensing reference documented in SequenceController.
PowerOptLab.AverageVoltageVoltVarWatt — Type
AverageVoltageVoltVarWatt(; volt_watt=nothing, volt_var=nothing,
extrema_epsilon=0.05,
volt_watt_basis=:available)Legacy balanced-current comparator. Both PWL laws observe the arithmetic mean of the three local phase-voltage magnitudes. Like WorstPhaseVoltVarWatt, that input moves with the local zero-sequence voltage and with the sensing reference documented in SequenceController.
PowerOptLab.PositiveSequenceVoltVarWatt — Type
PositiveSequenceVoltVarWatt(; volt_watt=nothing, volt_var=nothing,
worst_phase_watt_guard=true,
guard_epsilon=0.05,
volt_watt_basis=:available)Balanced-current sequence comparator. Volt-var observes |U1|. Volt-watt observes the largest phase magnitude when worst_phase_watt_guard=true, so an overvoltage phase cannot be hidden by the positive-sequence magnitude.
PowerOptLab.NoUnbalanceControl — Type
Balanced-current baseline: request no negative-sequence current.
PowerOptLab.NegativeSequenceAdmittanceDroop — Type
NegativeSequenceAdmittanceDroop(gain; impedance_angle=0,
ripple_blend=0, voltage_floor=1)Request negative-sequence current from local voltage only. gain maps eta = |U2| / sqrt(|U1|^2 + voltage_floor^2) to an admittance magnitude in A/V. impedance_angle fixes the configured negative-sequence actuation angle, and ripple_blend blends the voltage-oriented request with the analytical double-frequency-ripple-cancelling target.
PowerOptLab.CommonScaleLimiter — Type
CommonScaleLimiter(; current_epsilon=nothing, power_epsilon=nothing,
current_epsilon_fraction=2.5e-5,
power_epsilon_fraction=5e-8,
pq_priority=:proportional,
priority_headroom_fraction=1e-3)Apply a common scalar to positive- and negative-sequence current commands so the reconstructed currents at the configured current target respect its declared per-leg limit. The physical plant independently retains every converter- and grid-side current constraint. The positive-sequence P,Q request is first scaled to the converter apparent- power rating. The exact evaluator uses hard maxima. The smooth model represents every magnitude by an implicit nonnegative square root and uses the stated SI epsilons only in smooth maximum selectors. pq_priority is :proportional, :watt, or :var for the positive-sequence apparent-power allocation. By default, smoothing widths are rating-relative fractions, so heterogeneous fleets receive the same dimensionless regularization. Supplying current_epsilon or power_epsilon explicitly opts into an absolute SI width for that quantity. priority_headroom_fraction reserves a small declared fraction of s_max before a watt- or var-priority component reaches the axis of the capability circle. It is a control/protection margin, not a square-root regularization.
PowerOptLab.SequenceController — Type
SequenceController(positive, unbalance, limiter;
current_target=ConverterCurrentTarget(),
power_voltage_floor=1)Closed-form three-leg controller combining a positive-sequence Volt-var/Watt policy, a negative-sequence policy, and an algebraic feasibility limiter. current_target selects converter-leg or post-filter grid current. power_voltage_floor is the strictly positive SI voltage floor in the regularized P,Q-to-I1 conversion; it is not a magnitude-smoothing epsilon.
The curves observe the local POC voltage referred to the plant's declared neutral terminal. When the composed AdvancedInverter has neutral=nothing — the three-wire case, and the only one accepted for a native THREE_LEG fleet record — that reference is the network's ground, not a neutral conductor, so the measured phasors carry the local zero-sequence displacement. |U_1| and |U_2| are invariant to that choice, so PositiveSequenceVoltVarWatt Volt-var and NegativeSequenceAdmittanceDroop are unaffected; the phase-magnitude policies are not. See the sensing-reference discussion in the phase-aware control design.
The controller equalities themselves use only these voltage phasors, but the final plant-aware capability backoff in stamp_smooth_control! additionally reads converter-terminal internal voltage and both filter-arm currents. It is a protection surrogate, not part of the voltage-curve law.
PowerOptLab.InverterControlMeasurement — Type
Local three-phase RMS voltage phasors in the declared a-b-c terminal order.
PowerOptLab.InverterControlRequest — Type
Per-operating-point active availability, rated active power, and reactive-power base, in SI.
PowerOptLab.InverterControlRatings — Type
Fundamental apparent-power and per-phase conductor-current ratings, in SI.
PowerOptLab.InverterControlResult — Type
Numeric controller record shared by exact evaluation and smooth-model extraction. sequence_power, total_power, and ripple_power are command-space diagnostics formed from the POC voltage measurement and commanded current at the configured current target. They are not converter-terminal powers when an output filter separates those locations. Complex powers use the injection sign convention.
PowerOptLab.ConverterTerminalResult — Type
Converter-terminal phasors and powers derived from the physical plant solution. All phasors are RMS and powers follow the injection sign convention. The authoritative total converter power is sum(phase_power). Sequence powers use S_k = 3U_k*conj(I_k), and ripple_power is the complex twice-fundamental coefficient sum(U_phase*I_phase).
PowerOptLab.ControlledDevice — Type
ControlledDevice(device, controller)Typed composition of a physical inverter plant and a local control law. The first supported plant is AdvancedInverter.
PowerOptLab.ControlledInverterResult — Type
Result of solve_controlled_inverter.
PowerOptLab.evaluate_exact — Function
evaluate_exact(controller, measurement, request, ratings)Evaluate the exact local controller without JuMP. This is the firmware oracle for regression tests and exact-versus-smooth residuals. It uses only local RMS phasors and fixed controller/rating data.
PowerOptLab.evaluate_smooth — Function
evaluate_smooth(controller, measurement, request, ratings)Numerically evaluate the same differentiable computation graph stamped by stamp_smooth_control!. This is intended for inexpensive SI-valued extraction and comparison with evaluate_exact; it does not solve an optimization problem.
PowerOptLab.stamp_smooth_control! — Function
stamp_smooth_control!(ctx, controller, request, inverter, handles)Constrain an already-stamped AdvancedInverter to the smooth, fixed-structure counterpart of evaluate_exact. The voltage-curve law uses local POC phasors referred to the plant's declared neutral (see SequenceController) and commands the configured converter- or grid-side phase currents.
The subsequent plant-aware capability backoff is deliberately wider than evaluate_exact: it also reads converter-terminal internal voltage and both filter-arm currents so that a per-leg, apparent-power, or dv2_max limit saturates the command instead of making the controller equality infeasible. solve_controlled_inverter applies the same backoff to the exact law at the solved point, which is why the two commands are comparable.
PowerOptLab.solve_controlled_inverter — Function
solve_controlled_inverter(net, controlled, request; kwargs...)
-> ControlledInverterResultSolve one network snapshot with a local phase-aware controller composed around an AdvancedInverter. The voltage-curve law uses only the inverter's own POC voltage phasors (see SequenceController for the sensing reference); the subsequent capability backoff also uses converter-terminal voltage and both filter-arm currents. The nonlinear controller formulation requires per_unit=true; controller configuration and returned results remain SI. This is a controlled power flow: the controller equalities determine the command, while selection_objective only selects remaining plant allocation freedom (:loss, the default, or :zero for an objective-invariance check).
exact_smooth_current_residual compares the exact law with the stamped smooth law at the smooth model's own solved operating point, using the same solved plant phasors for both. It therefore bounds the smoothing error of the controller algebra, not the distance between the exact-law and smooth-law network equilibria; an independent fixed-point oracle is still outstanding research work.
PowerOptLab.inverter_spec — Function
Return the physical AdvancedInverter owned by a control wrapper.
PowerOptLab.inverter_handles — Function
Return stamped advanced-inverter handles through a control wrapper.
Current–voltage (IVQ) battery
PowerOptLab.IVQBattery — Type
IVQBattery(; id, bus, chemistry, n_series, n_parallel, soc_init, inverter, kwargs...)A current–voltage battery: n_series × n_parallel cells of a BatteryChemistry, coupled to the grid through an AdvancedInverter at the DC port. The pack terminal voltage is n_series · v_cell and the pack current n_parallel · i_cell.
Required
id::String,bus::String— identifier and point-of-connection bus (must match theinverter'sbus).chemistry::BatteryChemistry— the cell model.n_series::Int,n_parallel::Int— pack configuration.soc_init::Float64— state of charge (0–1) at the operating point / horizon start.inverter::AdvancedInverter— the AC↔DC converter (reused as-is).
Optional (reserved for multi-period use; ignored in the single-snapshot solve)
cyclic::Bool=true— require terminal SoC to return tosoc_init.soc_final::Union{Float64,Nothing}=nothing— pin terminal SoC (overridescyclic).
The multi-period charge balance uses a forward update q[t+1] = q[t] − i[t]·Δt, which is exact for the piecewise-constant current of each period (there is one current variable per period, not per time node). A true trapezoidal rule needs currents at all T+1 nodes with consistent endpoint semantics and is deferred.
PowerOptLab.solve_ivq_battery — Function
solve_ivq_battery(net, battery; objective=:max_export, kwargs...) -> IVQBatteryResultStamp battery (and its AdvancedInverter) into net and solve at a single operating point (SoC fixed at battery.soc_init), demonstrating the coupled cell + converter feasible region. The network supplies the surrounding grid (a voltage source, lines); the battery exchanges power at its POC bus through the inverter.
Objective
:max_export— maximise active power delivered to the grid (battery discharges until a cell voltage/current limit or the converter rating binds).:max_charge— maximise power drawn from the grid into the battery.:min_loss— minimise converter loss subject to ap_set(W) POC delivery.
Keywords
p_set=nothing— required active-power target (W) for:min_loss.q_set=nothing— optional reactive-power constraint at the POC (var).per_unit=true,s_base=1e6,optimizer=Ipopt.Optimizer,verbose=false,solver_options=(). Results are returned in SI regardless ofper_unit. Per-unit conditions the voltage–current bilinear coupling and is markedly more robust for this nonconvex solve; passper_unit=falseonly to reproduce a raw SI solve.
PowerOptLab.IVQBatteryResult — Type
IVQBatteryResultResult of solve_ivq_battery. SI units throughout.
Fields
termination_status::Stringp_poc,q_poc— active/reactive power at the grid POC (from the inverter).p_conv— converter-side active power at the internal node (W).p_dc— DC-link power (W; = pack discharge power,p_dc = p_conv + p_loss).p_loss— converter loss (W).soc— state of charge at the operating point (0–1).v_cell,i_cell— cell terminal voltage (V) and signed current (A; > 0 discharge).v_pack,i_pack— pack terminal voltage (V) and current (A).bus::Dict{String,Any}— the BMOPFToolsresult["bus"].
PowerOptLab.solve_multiperiod_ivq — Function
solve_multiperiod_ivq(nets, batteries; kwargs...) -> MultiperiodIVQResultCo-optimise a chronological sequence of network snapshots nets with a set of IVQBattery devices whose state of charge couples the periods. Each battery arbitrages across time subject to its cell voltage/current limits, SoC window, and terminal/cyclic condition, exchanging power through its AdvancedInverter. The snapshots share one model and one objective (the total generation cost across the horizon); period economics come from the snapshots (e.g. a time-varying slack import price via each net's voltage_source cost).
Arguments
nets::Vector—Tnetwork dicts (parse_bmopfoutput), one per period.batteries::Vector{IVQBattery}— each battery'sbus/inverter must exist in every snapshot.soc_initsets the horizon start;cyclic/soc_finalthe terminal condition. The charge balance is a forward update, exact for the piecewise-constant per-period current, so it conserves charge exactly.
Keywords
dt_h=1.0— uniform period duration in hours (compatibility shorthand).time_grid=nothing— aTimeGridfor nonuniform durations; when supplied it takes precedence overdt_h.per_unit=true,s_base=1e6,optimizer=Ipopt.Optimizer,verbose=false,solver_options=(). Results are SI regardless ofper_unit. Per-unit conditions the coupled nonconvex solve and converges far more reliably across platforms/Ipopt builds;per_unit=falsereproduces a raw SI solve.
PowerOptLab.MultiperiodIVQResult — Type
MultiperiodIVQResultResult of solve_multiperiod_ivq. SI units throughout.
Fields
termination_status::String,objective::Float64.snapshots::Vector{Dict{String,Any}}— the per-period BMOPFTools result dict.dispatch::Dict{String,NamedTuple}— per battery id:i_cell,v_cell,i_pack,v_pack,p_poc,q_poc,p_dc(each lengthT) andsoc(lengthT+1, 0–1, withsoc[1]the initial state). SI throughout.
The coupled cell + inverter model is nonconvex, so solve_multiperiod_ivq finds a local optimum and may not converge for every configuration; a non-LOCALLY_SOLVED /OPTIMAL status returns NaN trajectories rather than an unconverged point.
PowerOptLab.BatteryChemistry — Type
BatteryChemistryOne battery cell in the voltage–current–charge (IVQ) variable space. The cell terminal voltage follows the Thévenin/Rint decomposition v(soc, i) = OCV(soc) − i · R(soc) with i > 0 on discharge, so charging (i < 0) raises the terminal voltage above OCV and discharging lowers it.
Construct one with thevenin_chemistry, linear_chemistry or tabulated_chemistry, or use a preloaded shape (illustrative_lfp, illustrative_nmc, illustrative_nca, illustrative_lead_acid, illustrative_leaf).
Fields
name::String— chemistry label.ocv::Function—soc ∈ [0,1] → open-circuit voltage (V); non-decreasing and smooth (C¹).linear/theveninOCV is affine;tabulatedOCV is a monotone cubic (PCHIP) so it is safe to embed as a function of the SoC variable.r_internal::Function—soc ∈ [0,1] → internal resistance (Ω) ≥ 0.ocv_affine::Union{Tuple{Float64,Float64},Nothing}—(intercept, slope)when OCV is affine (so the multi-period model embeds it as a plain expression rather than a registered operator), elsenothing.r_constant::Union{Float64,Nothing}— the resistance value whenRis constant, elsenothing.q_cell::Float64— cell capacity (Ah).v_cell_min,v_cell_max::Float64— terminal-voltage operating bounds (V).i_charge_max,i_discharge_max::Float64— current magnitude limits (A ≥ 0).soc_min,soc_max::Float64— usable SoC window; the device clamps to it so the optimiser cannot exploit extrapolation outside the fitted range.source::String— data provenance / reference for the OCV and R values.
PowerOptLab.thevenin_chemistry — Function
thevenin_chemistry(; name, v_nominal, r_internal, q_cell, kwargs...)A Thévenin cell: a fixed open-circuit voltage v_nominal behind a constant internal resistance r_internal (Ω). v(soc, i) = v_nominal − i·R. The paper's PE-model is the further special case r_internal = 0 (constant voltage, so power and current are proportional and the current limit reduces to a power limit). Useful as a floor-fidelity model and as a warm-start generator.
Keywords
name="Thevenin",v_nominal,r_internal,q_cell(Ah).v_cell_min = 0.8·v_nominal,v_cell_max = 1.2·v_nominal— voltage bounds (V).i_charge_max = q_cell,i_discharge_max = q_cell— current limits (A; default 1C).soc_min = 0.0,soc_max = 1.0.source="Thévenin / Rint model".
PowerOptLab.linear_chemistry — Function
linear_chemistry(; name, v_full, v_empty, r_internal, q_cell, kwargs...)A state-dependent voltage source: the open-circuit voltage falls linearly from v_full at soc = 1 to v_empty at soc = 0, behind a constant internal resistance. OCV(soc) = v_empty + (v_full − v_empty)·soc. A good first model whenever only the charged/discharged voltage endpoints and a resistance are known, and the natural discretisation of a real OCV curve.
Keywords
name="Linear",v_full,v_empty(V, withv_full ≥ v_empty),r_internal(Ω),q_cell(Ah).v_cell_min = 0.95·v_empty,v_cell_max = 1.05·v_full— voltage bounds (V).i_charge_max,i_discharge_max,soc_min,soc_max,source— asthevenin_chemistry.
PowerOptLab.tabulated_chemistry — Function
tabulated_chemistry(; name, soc_points, ocv_points, r_internal, q_cell, kwargs...)Open-circuit voltage (and optionally resistance) from data points — e.g. a published OCV curve or a slow-rate (≈ C/20) discharge test. OCV(soc) is a monotone cubic (PCHIP) interpolant of (soc_points, ocv_points), which must be strictly increasing in soc and non-decreasing in ocv (a physical OCV curve). Outside the fitted range it is held flat so the optimiser cannot extrapolate into non-physical voltage — note this makes the function only C⁰ at the two outer knots, so keep soc_min/soc_max strictly inside them (the default nudges them in). r_internal may be a scalar Ω or a matching vector r_points interpolated the same way.
Keywords
name,soc_points::Vector(strictly increasing),ocv_points::Vector(non-decreasing, V).r_internal— scalar Ω, or passr_points::Vector(Ω) for an R(soc) table.q_cell(Ah),v_cell_min = minimum(ocv_points),v_cell_max = maximum(ocv_points),i_charge_max,i_discharge_max,soc_min = minimum(soc_points),soc_max = maximum(soc_points),source.
PowerOptLab.illustrative_lfp — Function
illustrative_lfp(; q_cell=100.0, r_internal=0.006, kwargs...)Illustrative lithium iron phosphate (LFP) shape — a flat ~3.2–3.3 V plateau between sharp knees. The flat OCV is where a constant-voltage PE-model is least wrong in mid-SoC yet where the terminal-voltage and current limits still bite hardest at the knees — a good stress case for the IVQ model. Hand-drawn to a typical LFP voltage band (knee ≈ 2.5 V, nominal ≈ 3.2 V, charged ≈ 3.65 V); not a fit. q_cell/r_internal are representative round numbers, not a specific cell.
PowerOptLab.illustrative_nmc — Function
illustrative_nmc(; q_cell=5.0, r_internal=0.03, kwargs...)Illustrative NMC (nickel-manganese-cobalt) shape — a monotonic slope from ≈ 3.0 V to 4.2 V, the workhorse EV form. Hand-drawn to a typical NMC voltage band; not a fit. For a calibrated NMC811 set to fit against, see PyBaMM Chen2020 (LG INR21700-M50, from GITT/EIS) — this preset is not that set.
PowerOptLab.illustrative_nca — Function
illustrative_nca(; q_cell=3.2, r_internal=0.035, kwargs...)Illustrative NCA (nickel-cobalt-aluminium) shape — a sloped ≈ 2.5–4.2 V profile similar to NMC (Panasonic/Tesla cylindrical form). Hand-drawn; not a fit.
PowerOptLab.illustrative_lead_acid — Function
illustrative_lead_acid(; q_cell=100.0, r_internal=0.004, kwargs...)Illustrative lead-acid shape — a roughly linear OCV from ≈ 1.75 V (empty) to ≈ 2.15 V (full) per cell, via linear_chemistry. Consistent with the textbook flooded/AGM OCV–SoC relation (e.g. IEEE 1188); endpoints only, not a fit.
PowerOptLab.illustrative_leaf — Function
illustrative_leaf(; kwargs...)Illustrative shape in the voltage band of the 2013 Nissan-Leaf cell used by the source paper (Aaslid et al., 2020) — an LMO/NMC blend. The voltage bounds and capacity/current match the paper's Table 2 (Vmin/Vmax = 3.20/4.15 V, Qmax = 29 Ah, Ib,ch/Ib,dch = 30/90 A), but the OCV curve is a hand-drawn monotone line through that band — it does not reproduce the paper, which used an empirical current–SoC voltage surface f_v(soc, i). To actually reproduce the paper, fit that surface from the cell dataset (Wiggins, Allu & Wang, ORNL, 2020, https://doi.org/10.5281/zenodo.2580327) and supply an R(soc)/surface model.
HELM power flow
PowerOptLab.solve_pf_helm — Function
solve_pf_helm(net; config=_DEFAULT_CONFIG, switches=:alias,
ideal_xfmrs=:constrain, max_order=40, tol=1e-8)
-> Dict{String,Any}Solve the power flow with HELM (helm_series) and return the standard result dictionary (SI units, same "bus" shape as the OPF results, compatible with write_result/read_result).
Top-level keys:
"termination_status"—"HELM_CONVERGED"|"HELM_SERIES_DIVERGED"(the computed coefficient tail is growing) |"HELM_MAX_ORDER"(series order exhausted before the tolerance was met). Neither non-converged status is a proof of power-flow non-existence."feasible"—trueiff converged."solve_time"— wall-clock seconds."bus"—bus_id => terminal => {vr, vi, vm [V], va [rad]}. NaN-filled when HELM does not converge (the standard infeasible convention)."coupling"—id => conductor => {kind, ir, ii [A], im [A]}: the PHYSICAL current through each ideal coupling (closed-switch conductor withswitches = :constrain, ideal-transformer winding core)."helm"— diagnostics:order,residual(A), rawpade_spread,coefficient_tail_norms,coefficient_tail_ratios, and the heuristicsingularity_estimate.load_marginis retained as a compatibility alias.
PowerOptLab.helm_series — Function
helm_series(net; config=_DEFAULT_CONFIG, switches=:alias,
ideal_xfmrs=:constrain, max_order=40, tol=1e-8) -> HelmResultSolve the 4-wire multiphase power flow of net with the Holomorphic Embedding Load-flow Method on the augmented nodal admittance matrix.
Deterministic and non-iterative: no initial guess, one LU factorization for the germ and every series order. A converged result is the operational branch continuously connected to the no-load state. A non-converged result reports whether its finite coefficient tail grew (:series_diverged) or merely exhausted the requested order (:max_order_reached); neither is a proof that no power-flow solution exists.
Keywords:
switches,ideal_xfmrs— forwarded toybus_augmented;switches = :constrainadditionally yields every switch-conductor current.max_order— highest series order (default 40).tol— relative convergence tolerance on the s = 1 current mismatch, scaled by the largest load-current magnitude.
Requirements/limitations (v1): at least one WYE / SINGLE_PHASE voltage source; loads restricted to their constant-power + constant-impedance (ZIP Z/P) parts — constant-current fractions and non-integer exponential models raise an ArgumentError naming the offending loads.
PowerOptLab.HelmResult — Type
HelmResultResult of helm_series — the HELM power-flow solution and its diagnostics.
Fields:
V—Dict{(bus,terminal),ComplexF64}node-to-earth voltages ats = 1(every declared terminal, including source/fixed, aliased, and earth-referenced ones).w— unscaled bordered-row solution entries; the PHYSICAL current ofcouplings[j]iscouplings[j].scale * w[j].couplings— theIdealCouplings of the underlyingybus_augmentedmatrix, inworder.coeffs—(n_unknown + n_couplings) × (order+1)series coefficients (diagnostic; row order = non-fixed nodes then couplings).converged—trueiff the s = 1 nonlinear current mismatch is within tolerance.status—:converged|:series_diverged(the finite coefficient tail is growing) |:max_order_reached.residual— max |current mismatch| (A) over non-source nodes and constraint rows, evaluated at the returned solution.n_order— highest series order computed.pade_spread— absolute difference between the last two Wynn-epsilon Padé estimates for each row ofcoeffs(node rows, then coupling rows). Values have the units of their corresponding row.coefficient_tail_norms— max-norm of the final coefficient orders used by the status classifier.coefficient_tail_ratios— adjacent ratios of those tail norms. A tail with every ratio above one is classified as:series_diverged.singularity_estimate— heuristic Domb–Sykes estimate of the nearest coefficient-dominating singularity in the loading parameter. It is not a certified loading margin.NaNmeans the series is too short or featureless to extrapolate.
result.load_margin remains a compatibility alias for result.singularity_estimate; new code should use the latter name.