API reference
Which function do I need?
A quick map from intent to entry point — each row links to the guide that explains it in context. The end-to-end tutorial runs the whole sequence on one feeder.
Module
BMOPFTools — Module
BMOPFToolsA Julia library for parsing, validating, analysing, and reporting on BMOPF-format distribution network datasets.
The network data model is a plain Dict{String,Any} that mirrors the BMOPF JSON schema exactly. No custom wrapper types are used for network data, so data flows naturally to and from JSON without conversion.
Public API
net = parse_bmopf(path) # load from BMOPF JSON file
net = from_dss("Master.dss") # parse OpenDSS (via PowerIO.jl)
report = analyze(net) # run all analyses
render(report, stdout) # terminal output
render(report, "report.md") # markdown file
to_dss(net, "out/Master.dss") # write OpenDSS (via PowerIO.jl)See also: write_bmopf, to_pmd, to_dss.
Types
BMOPFTools.Finding — Type
FindingA single diagnostic finding produced by any analysis or validation pass.
Fields
severity—ERROR,WARNING, orINFOcode— stable dot-separated identifier, e.g."E.VOLT.LINE_CROSSING". Use these for programmatic filtering; do not match onmessage.section— which report section produced this, e.g.:voltage_levelscomponent_type—:bus,:line,:load,:transformer,:network, etc.component_id— the dict key of the affected component, ornothingfor network-level findingsmessage— human-readable descriptiondetail— optional machine-readable metadata (actual vs expected values, timestamps, etc.) for renderer and downstream use
BMOPFTools.SummaryReport — Type
SummaryReportAssembled output of analyze. Holds all section result dicts and the complete finding log. Passed to render for output.
results maps section name → analysis output dict. Renderers walk this structure; the keys are stable across versions.
BMOPFTools.SolutionReport — Type
SolutionReportOutput of profile_solution. Holds the network name, result metadata, per-check summary dicts, and the complete finding log for the solution profile. Pass to render_solution for Markdown output.
BMOPFTools.Severity — Type
SeveritySeverity level for a Finding.
ERROR— will directly compromise OPF correctness or prevent executionWARNING— degrades result quality or indicates suspicious dataINFO— informational; worth knowing but not actionable
BMOPFTools.ERROR — Constant
Finding severity: will compromise OPF correctness or prevent execution.
BMOPFTools.WARNING — Constant
Finding severity: degrades result quality or indicates suspicious data.
BMOPFTools.INFO — Constant
Finding severity: informational — worth knowing, not necessarily actionable.
BMOPFTools.OpfModelKey — Type
OpfModelKey(kind, family, index=nothing)Stable semantic identifier for an object in a staged OPF model. kind is typically :variable, :expression, or :constraint; family identifies the physical/model quantity (for example :vr, :tap, or a downstream package's own symbol); and index identifies the component/terminal/phase.
Keys are intentionally independent of JuMP and extensible by downstream packages. Use register_opf_object! and opf_object rather than depending on the internal layout of ctx.vars.
BMOPFTools.OpfParameterScope — Type
OpfParameterScope(kind, id=nothing)Research-facing scope attached to an OpfParameterBinding. kind is one of :global, :snapshot, or :scenario. id is deliberately untyped so an extension can use its own stable time/scenario identifier without BMOPFTools prescribing a multi-period data model.
BMOPFTools.OpfParameterBinding — Type
OpfParameterBindingImmutable metadata for a caller-owned JuMP parameter linked to one or more semantic OPF decision variables. The link convention is target = to_working_scale * parameter; consequently a reverse derivative with respect to the input parameter includes this scale by the ordinary chain rule. input_unit and working_unit are descriptive symbols, not an implicit unit-conversion system.
The vectors contain live JuMP constraint references and are defensively copied by opf_parameter_binding and opf_parameter_bindings.
BMOPFTools.OpfDifferentiabilityReport — Type
OpfDifferentiabilityReportConservative diagnostic snapshot for a staged OPF model. ready requires finalized construction, a successful solve, no discrete variables or unused providers, and no detected near-active, weakly-active, violated, or rejected-KKT conditions or blocking differentiability annotations. The three annotation vectors disclose extension-declared nonsmooth operators, parameter-dependent construction branches, and unsupported parameter locations. Active-set fields use caller-configurable numerical tolerances. This remains a diagnostic rather than a proof of LICQ, second-order sufficiency, or global solution-branch stability.
BMOPFTools.OpfKKTDiagnostic — Type
OpfKKTDiagnosticResult recorded by opf_checked_kkt_factorization. pivot_ratio is a global-scale-invariant LU pivot proxy, not a condition-number certificate. A :rejected status means differentiation was stopped before a regularized or zero sensitivity could be mistaken for a valid implicit derivative.
BMOPFTools.OpfDifferentiationError — Type
Error thrown when the checked KKT factorization rejects a derivative.
BMOPFTools.OpfBuildManifest — Type
OpfBuildManifestProvenance for a staged OPF construction. problem identifies the recipe, formulation the network formulation, per_unit and s_base record the working-unit choice, stages records completed construction stages in order, and component_owners records which package owns each stamped device family.
opf_build_manifest returns a defensive snapshot: mutating the returned vector or dictionary does not alter the context's internal construction record.
BMOPFTools.OpfDeviceBuilder — Type
OpfDeviceBuilder(owner, build!)A downstream device-formulation callback and its provenance owner. owner is a stable package/research-code symbol. BMOPFTools calls build!(ctx, ids), where ids is the deterministically ordered vector of component identifiers assigned to this builder. The callback must use public context, registry, and KCL helpers.
BMOPFTools.OpfBuildSpec — Type
OpfBuildSpec(; family_builders, component_builders, coefficient_providers)Typed ownership specification for staged device construction. A family_builders entry replaces the native formulation for the complete family. A component_builders[(family, id)] entry replaces one component while leaving unassigned components native. Mixed per-component ownership is currently supported for flat device collections such as :ibr; unsupported combinations are rejected before the device-physics stage mutates the model. In particular, :line, :transformer, and :dc_network family replacement is rejected until their branch-result or DC-KCL ledgers have public extension seams; per-component :line replacement is rejected for the same reason.
BMOPFTools.OpfCoefficientKey — Type
OpfCoefficientKey(category, family, component, field, index=nothing)Stable identifier for a non-structural coefficient consumed while a device builder stamps equations. category is normally :load, :availability, :setpoint, :cost, :limit, :controller, or :physics; downstream packages may introduce additional semantic categories. Values returned for a key are in the model's working units.
BMOPFTools.OpfCoefficientProvider — Type
OpfCoefficientProvider(owner, provide)Typed coefficient callback with package provenance. A custom device builder calls opf_coefficient with a semantic key and native/default value; when registered, provide(ctx, key, default) returns the number, JuMP parameter, or scalar expression to stamp instead. Providers must return values in model working units.
BMOPFTools.ScientificContractResult — Type
ScientificContractResultStructured result of an executable scientific-contract check.
status is one of :passed, :failed, :inapplicable, or :indeterminate:
:passed— every implemented obligation held in the declared domain;:failed— an implemented obligation was violated with recorded evidence;:inapplicable— a domain precondition did not hold, so no preservation conclusion was drawn;:indeterminate— the supplied models or mapping lacked evidence needed to decide the implemented obligation.
passed applies only to checked_dimensions; it says nothing about the dimensions listed in unassessed_dimensions.
Finding accessors
BMOPFTools.errors — Function
errors(findings::Vector{Finding}) -> Vector{Finding}
errors(report::SummaryReport) -> Vector{Finding}The subset of findings with ERROR severity. See also warnings, infos.
BMOPFTools.warnings — Function
warnings(findings::Vector{Finding}) -> Vector{Finding}
warnings(report::SummaryReport) -> Vector{Finding}The subset of findings with WARNING severity. See also errors, infos.
BMOPFTools.infos — Function
infos(findings::Vector{Finding}) -> Vector{Finding}
infos(report::SummaryReport) -> Vector{Finding}The subset of findings with INFO severity. See also errors, warnings.
Scientific contracts
BMOPFTools.check_parallel_member_limit_preservation — Function
check_parallel_member_limit_preservation(source, target;
member_ids, aggregate_id, atol=1e-9, rtol=1e-8)
-> ScientificContractResultCheck the scalar, fixed-linear, series-only portion of scientific contract parallel_member_limit_preservation (PSK-000001). source must retain the identified parallel members and their current ratings; target must contain the declared aggregate. The explicit member_ids/aggregate_id mapping is required because a reduced target cannot reveal discarded member identity.
The check first verifies the aggregate terminal admittance. If that relation is preserved, it compares the exact scalar voltage-drop regions induced by source member limits and the target aggregate limit:
\[|ΔV| ≤ minₗ(Iₗmax / |Yₗ|), |ΔV| ≤ Ieqmax / |Σₗ Yₗ|.\]
It returns :failed with W.CONTRACT.PARALLEL_MEMBER_LIMIT_LOSS when the target is an inner restriction or outer relaxation, and includes a concrete voltage-drop/current witness. A :passed result establishes only terminal behavior and scalar member-current-limit preservation; member identity, outages, switching, provenance, measurements, and protection quantities remain explicitly unassessed. Multiconductor, shunted, singular, or state-dependent cases return :inapplicable rather than being guessed.
BMOPFTools.check_neutral_ground_reference_preservation — Function
check_neutral_ground_reference_preservation(source, target;
bus_mapping, atol=1e-9, rtol=1e-8) -> ScientificContractResultCheck the representation-level portion of scientific contract neutral_ground_reference_preservation (PSK-000002). bus_mapping must be a one-to-one mapping for at least two source buses with explicit neutral terminals. The initial executable domain checks whether the target retains:
- an identifiable neutral terminal at every mapped bus;
- the pairwise neutral-continuity relation among mapped buses; and
- each mapped bus's declared perfect-ground, scalar finite-grounding-shunt, and voltage-source-reference relations.
The check deliberately distinguishes an intentional perfectly grounded neutral from conflating every neutral with the mathematical reference. A pass applies only to the declared representation relations. It does not establish equal terminal equations, explicit-earth behavior, fault current, touch voltage, protection operation, or grounding-asset identity. Coupled multiconductor grounding shunts are refused as :inapplicable rather than reduced to a scalar guess.
BMOPFTools.check_claimed_solution_validity — Function
check_claimed_solution_validity(net, result) -> ScientificContractResultCheck the initial executable portion of scientific contract claimed_solution_validity (PSK-000003). The result must report a claimed-feasible termination status and provide vr, vi, and vm for every declared bus terminal. The contract then reuses profile_solution to independently check:
- finiteness of the complete result tree; and
- declared bus voltage-magnitude, sequence-voltage, and angle-difference limits recomputed from the primal bus voltages.
An accepted solver termination status is an applicability precondition, not evidence that these checks passed. A claimed-feasible result with non-finite values or a declared bus-limit violation returns :failed with E.CONTRACT.CLAIMED_FEASIBLE_SOLUTION_INVALID and retains the underlying E.SOL.* evidence.
A pass is deliberately narrow. It does not establish network-equation residuals, thermal or device limits, load-model residuals, power balance, objective optimality, global optimality, or solver derivative quality. Those dimensions remain explicitly unassessed and require their own validators or a broader future contract.
BMOPFTools.check_load_voltage_base_consistency — Function
check_load_voltage_base_consistency(net; load_ids=nothing,
ratio_min=0.8, ratio_max=1.25) -> ScientificContractResultCheck the initial executable portion of scientific contract load_voltage_base_consistency (PSK-000004). The contract applies to voltage-dependent WYE and DELTA loads whose buses have a nominal phase-to-neutral voltage reachable from a declared voltage source through the existing BMOPFTools voltage-level propagation.
For each selected load, the expected nominal anchor is the propagated phase-to-neutral bus base for WYE and that base multiplied by sqrt(3) for DELTA. Every scalar or per-subload v_nom must lie within the declared ratio band. This uses the same connection-coordinate conversion and default plausibility band as W.LOAD.VNOM_MISMATCH in domain_rules_check.
A pass establishes only consistency among the declared source-propagated bus base, load connection, and load-model nominal anchor. It does not validate the source or transformer declarations used to infer the base, the load law or coefficients, operating-point voltage, network equations, equipment limits, or unit provenance.
BMOPFTools.check_transformer_tap_domain_preservation — Function
check_transformer_tap_domain_preservation(source, target;
source_subtype, source_id, target_subtype=source_subtype,
target_id=source_id, atol=1e-9, rtol=1e-8)
-> ScientificContractResultCheck the initial executable portion of scientific contract transformer_tap_domain_preservation (PSK-000005). The source must contain an adjustable two-winding isolating transformer with a finite positive tap_min < tap_max interval. The target transformer must have the same subtype and identical non-tap declarations under the explicit mapping.
The check compares the complete continuous tap intervals. Omitting target bounds is interpreted according to the BMOPFTools data model as a fixed singleton at tap (default 1.0). A narrower target interval is an inner restriction; a wider interval is an outer extension; shifted or disjoint intervals are different decision domains. Any mismatch returns :failed with E.CONTRACT.TRANSFORMER_TAP_DOMAIN_LOSS and an interval witness.
A pass establishes only preservation of the mapped base-factor declaration, tap-start admissibility, and continuous decision domain. It does not establish pointwise terminal equations, tap-dependent losses, discrete positions, automatic controls, network feasible-set equality, objective equality, an optimal tap, or solver guarantees.
BMOPFTools.check_transformer_winding_convention_preservation — Function
check_transformer_winding_convention_preservation(source, target;
source_subtype, source_id, target_subtype=source_subtype,
target_id=source_id, bus_mapping, terminal_mapping=Dict(),
atol=1e-9, rtol=1e-8) -> ScientificContractResultCheck the initial executable portion of scientific contract transformer_winding_convention_preservation (PSK-000006). The check covers fixed-tap single_phase, wye_delta, and delta_wye transformer records with the same subtype, an explicit one-to-one source-bus to target-bus mapping, and stable terminal labels or an explicit global terminal-label mapping.
It compares mapped winding-side identity, ordered terminal-to-coil incidence, positive winding reference-voltage declarations, and the resulting fixed effective coil ratio. A bare bus_from/bus_to swap therefore fails: a transformer side is a typed winding role, not an arbitrary branch arrow.
A pass does not establish equality of leakage, excitation, grounding, limits, the complete terminal factor, tap decision domains, controls, network feasible sets, objectives, or solver results. Adjustable taps are explicitly :inapplicable here and belong to transformer_tap_domain_preservation.
BMOPFTools.check_decision_preservation_manifest — Function
check_decision_preservation_manifest(manifest) -> ScientificContractResultCheck declaration-level evidence completeness for a transformation manifest that claims exact decision equivalence (PSK-000007). The manifest must name its source and target models and give each required dimension—admissible domain, terminal behavior, observations, constraints, decision variables, objective, and recovery—one of four dispositions:
verified, with one or more nonemptyevidence_ids; ornot_required, with a nonemptyjustification; ornot_preserved; orunassessed.
An exact decision-equivalence claim passes this completeness gate only when every dimension is closed by verified evidence or a justified not_required disposition. Missing evidence produces E.CONTRACT.DECISION_MANIFEST_EVIDENCE_GAP; an explicit not_preserved or unassessed dimension produces E.CONTRACT.DECISION_MANIFEST_UNRESOLVED_OBLIGATION. A manifest that claims a different exactness object is :inapplicable, so a correctly scoped terminal- only certificate is not mislabeled as a failure.
A pass establishes only that the declaration is complete and internally non-contradictory at this schema boundary. BMOPFTools does not resolve or authenticate the cited evidence, prove the maps correct, compare feasible sets or objectives, or establish solver or optimization equivalence.
BMOPFTools.check_kron_boundary_recovery — Function
check_kron_boundary_recovery(source, target;
source_line_id, target_line_id, bus_mapping,
phase_terminals=["a", "b", "c"], neutral_terminal="n",
terminal_mapping=Dict(), recovery_map, atol=1e-9, rtol=1e-8)
-> ScientificContractResultCheck the initial executable portion of the Kron boundary/recovery contract (PSK-000008). The supported case is a single four-conductor source line and three-conductor target line with aligned phase terminal coordinates, no line shunts, and the source neutral perfectly grounded at both source buses. The target series impedance must equal the Schur complement obtained by eliminating the source neutral row and column. An explicit recovery-map declaration for the eliminated terminal is also required.
This is a boundary relation check, not a claim that Kron reduction preserves internal equipment, protection, state, limits, decisions, objectives, or solver results. Floating or finite-grounded neutrals return :failed with E.CONTRACT.KRON_GROUNDING_PRECONDITION; missing declarations return :indeterminate, and unsupported wire or shunt shapes return :inapplicable.
BMOPFTools.check_positive_sequence_collapse — Function
check_positive_sequence_collapse(source, target;
source_line_id, target_line_id, declarations,
phase_terminals=["a", "b", "c"], terminal_mapping=Dict(),
atol=1e-9, rtol=1e-8)
-> ScientificContractResultCheck the guarded positive-sequence specialization (PSK-000009). The supported case is a three-conductor source factor with circulant series and shunt matrices and a scalar target whose impedance is the positive-sequence eigenvalue. declarations must explicitly close balanced boundary data, sequence-compatible grounding, two-terminal factor closure, phase-symmetric decisions, and positive-sequence observations.
BMOPFTools.check_state_dependent_equivalent — Function
check_state_dependent_equivalent(source, target;
source_model_id, target_model_id)
-> ScientificContractResultCheck the declaration boundary for a fixed versus state-dependent equivalent (PSK-000010). Both records must provide a state_dependent object with a non-singleton numeric domain, parameter identity, base state, and update-rule provenance. A target that freezes the source at one base point fails rather than being silently promoted to a reusable equivalent.
BMOPFTools.check_reference_singularity — Function
check_reference_singularity(source, target;
source_model_id, target_model_id, island_mapping=Dict())
-> ScientificContractResultCompare declared reference/rank evidence for mapped connected islands (PSK-000011). Each model must provide reference_analysis.islands, where each island has an id, has_voltage_reference, dimension, and rank.
BMOPFTools.check_terminal_permutation_invariance — Function
check_terminal_permutation_invariance(source, target;
source_line_id, target_line_id, permutation)
-> ScientificContractResultCheck that a target series primitive is exactly the source primitive under an explicit bijective terminal permutation (PSK-000012). The permutation is a one-based vector whose target coordinate i takes source coordinate permutation[i]; both endpoint terminal maps must transform accordingly.
BMOPFTools.check_solved_network_feasibility — Function
check_solved_network_feasibility(result; tolerances=Dict()) -> ScientificContractResultValidate an independently computed residual witness for a claimed solved network (PSK-000013). The result must contain finite residual norms for equations, KCL, power balance, and recovery, plus a nonnegative device-limit violation count. Solver termination is recorded separately and is never used as a substitute for these measurements.
BMOPFTools.check_unit_base_serialization_invariance — Function
check_unit_base_serialization_invariance(source, target;
source_model_id, target_model_id) -> ScientificContractResultCheck explicit unit/base metadata and a canonical semantic hash across a serialization round trip (PSK-000014). The contract compares the declared unit system, base map, and semantic payload hash; it does not infer units from numeric magnitudes or claim that metadata equality proves network equivalence.
BMOPFTools.contract_result_to_dict — Function
contract_result_to_dict(result::ScientificContractResult) -> Dict{String,Any}Convert a scientific-contract result to a JSON-compatible dictionary. Complex quantities in built-in checks are recorded as explicit real/imaginary fields, so the returned structure can be serialized directly with JSON3.
BMOPFTools.execute_contract — Function
execute_contract(contract_id, source, target; parameters, inputs=[])Run a contract through the stable, JSON-ready BMOPFTools execution interface. The response preserves the scientific-contract status (passed, failed, inapplicable, or indeterminate), package identity, explicit request parameters, optional input hashes, and the complete structured contract result.
The curated interface supports parallel_member_limit_preservation, with member_ids and aggregate_id, and neutral_ground_reference_preservation, with an explicit bus_mapping. Optional atol and rtol entries are passed to the underlying domain API. Unsupported contracts raise ArgumentError instead of dynamically invoking arbitrary Julia functions.
BMOPFTools.execute_case_parse — Function
execute_case_parse(net; inputs=[])Summarize a BMOPF dict that has already passed through parse_bmopf using the stable, JSON-ready execution envelope. The result reports document identity, component counts, time-series detection, and parse-time migration or terminal-normalization evidence.
This operation does not run JSON Schema validation, domain checks, analyze, or a solver. A completed response establishes only that JSON decoding and BMOPFTools' supported ingest migrations/normalizations ran. Use execute_analysis when structured validation Findings are needed.
BMOPFTools.execute_analysis — Function
execute_analysis(net; t_index=1, inputs=[])Run BMOPFTools' standard case analysis through the stable, JSON-ready execution interface. A completed operation has status completed even when the report contains ERROR or WARNING Findings: Finding severity describes the case, while the envelope status describes whether the requested operation ran.
t_index selects a snapshot for time-series inputs and is ignored for snapshot networks. Optional input records bind the response to caller-supplied file hashes in the same way as execute_contract.
BMOPFTools.execute_solution_verification — Function
execute_solution_verification(net, result; t_index=1, inputs=[])Profile a solver result against its BMOPF case through the stable, JSON-ready execution interface. The operation recomputes the package's solution checks and returns the complete structured SolutionReport.
A response has status completed whenever profiling ran, even if the solver reported an infeasible status or the report contains ERROR Findings. Solver termination, Finding severity, and execution status are deliberately separate.
BMOPFTools.explain_finding — Function
explain_finding(code) -> Dict{String,Any}Return the checked offline catalogue entry for one stable BMOPFTools Finding code. The explanation records severity, namespace, catalogue section, canonical meaning, documentation provenance, and any existing executable-contract/PSK links. It does not infer a cause, recommend an automatic repair, or inspect a case-specific Finding instance.
Codes authored by external conversion pipelines (for example EMIT.* or READ.*) are intentionally outside this registry and raise UnknownFindingCode rather than receiving a guessed explanation.
BMOPFTools.execute_finding_explanation — Function
execute_finding_explanation(code) -> Dict{String,Any}Look up one stable BMOPFTools Finding code through the versioned JSON execution interface. This is a deterministic catalogue lookup over explain_finding, not case analysis, scientific retrieval, diagnosis, or automatic remediation.
BMOPFTools.execution_error_response — Function
execution_error_response(message; code="invalid_request",
operation="check_contract",
contract_id=nothing, inputs=[])Construct the same stable response envelope for a transport or request error. This status is distinct from the four scientific-contract statuses and does not represent scientific evidence.
IO
BMOPFTools.parse_bmopf — Function
parse_bmopf(path::AbstractString; terminal_aliases) -> Dict{String,Any}
parse_bmopf(io::IO; terminal_aliases) -> Dict{String,Any}
parse_bmopf(json::AbstractString; from_string=true, terminal_aliases) -> Dict{String,Any}Parse a BMOPF JSON file (or IO stream, or raw JSON string) into a plain Dict{String,Any} normalized for analysis and calculation. Migration and terminal normalization can change the representation. Explicit transformer no_load_shunt records become winding-connected shunts; their source values and transformer ownership remain in _meta["explicit_transformer_core_shunts"]. Use PowerIO modules when source-preserving exchange is required.
The OPF/PF dictionary build boundary reuses these normalization operations on a private copy. Combined and split Yd/Dy leakage for the same r or x component, or explicit and legacy excitation on one transformer, are rejected rather than silently selecting a representation.
The returned dict is mutable — analysis functions treat it as read-only but callers may modify it freely.
Enums from PowerModelsDistribution (e.g. WYE, DELTA) are stored as plain strings in BMOPF JSON and remain strings after parsing. The to_pmd conversion layer handles the string → Enum translation.
Terminal normalization
The data model requires terminal identifiers to be strings. Non-string entries (e.g. integer terminals [1,2,3,4]) are coerced at ingest:
- if every numeric token is covered by
terminal_aliases(default1→"1", 2→"2", 3→"3", 4→"n"), the alias map is applied; - otherwise — the profiling guard — every token becomes its verbatim decimal string and the neutral-identification heuristic decides semantics downstream.
Coercion is recorded under _meta["terminal_coercions"] and surfaced as a W.SPEC.TERMINAL_TYPES conformance finding. Pass a custom terminal_aliases::Dict to override the convention.
Raises
ArgumentErrorif the file cannot be found or is not valid JSON.- Does not validate against the BMOPF JSON Schema here; call
schema_checkandspec_conformance_checkfor that.
BMOPFTools.write_bmopf — Function
write_bmopf(net::Dict{String,Any}, dest; meta=nothing, indent=2)Serialise a BMOPF network dict to JSON.
dest::IO— writes to the IO streamdest::AbstractString— writes to a file at that path
A meta block is always written. Fields are assembled in this priority order (highest wins): the meta keyword argument → net["meta"] → auto-generated defaults ($schema, case_study_generator, created). Caller-supplied values are never overwritten by auto-generation.
The input net is never mutated. Tool-private state is not serialised verbatim: the "_meta" key is persisted under meta.provenance, and non-spec bus fields (neutral_terminal, plus the longitude/latitude attached by sideload_coordinates!) are stripped so the output satisfies the schema's additionalProperties: false on bus objects. Dropping the coordinates is lossy by design: they are not recoverable on read.
Output is newline-terminated in both modes, so files round-trip through diff and shell pipelines without a "\ No newline at end of file" marker.
Keyword arguments
meta: aDictof fields to include or override in the writtenmetablock. All fields are optional; common ones aretitle,description,license,authors,data_sources, andversion. Seedocs/src/conventions.mdfor the full field reference.indent: number of spaces for pretty-printing (default2). Passindent=nothingfor compact single-line output — one line of JSON plus the trailing newline.
Example
write_bmopf(net, "output.json";
meta = Dict(
"title" => "LV network 1, Feeder 1",
"license" => "https://creativecommons.org/licenses/by/4.0/",
"authors" => [Dict("name" => "Frederik Geth", "orcid" => "0000-0001-9534-2265")],
"data_sources" => [Dict("name" => "ENWL dataset", "format" => "OpenDSS",
"url" => "https://www.enwl.co.uk/…")]
))BMOPFTools.write_result — Function
write_result(result::Dict{String,Any}, dest; indent=2)Serialise an OPF result dict (as returned by solve_opf) to JSON.
dest::IO— writes to the IO streamdest::AbstractString— writes to a file at that path
Pass indent=nothing for compact (single-line) output. The result structure is documented in OPF result dictionary. Read it back with read_result.
BMOPFTools.read_result — Function
read_result(src; from_string=false) -> Dict{String,Any}Read an OPF result JSON (written by write_result) back into a Dict{String,Any}.
src::IO— reads from the IO streamsrc::AbstractString— a file path, or the JSON text itself whenfrom_string=true
BMOPFTools.migrate — Function
migrate(net::Dict{String,Any}) -> Dict{String,Any}Forward-migrate a BMOPF network dict to the current spec version.
If the dict already targets the current spec — or carries no meta.$schema at all (hand-written fixtures) — it is returned unchanged apart from the unconditional field-level migrations. A meta.$schema URI that this build does not recognise raises an ArgumentError: the file was most likely written against a newer BMOPF spec than this BMOPFTools build understands, and silently proceeding could corrupt results. Once older spec versions exist, each intermediate migration step is applied in sequence and recorded under net["_meta"]["migration_notes"] so the transformation is auditable.
Called automatically by parse_bmopf; can also be called directly on an already-parsed dict.
BMOPFTools.is_timeseries — Function
is_timeseries(net::Dict{String,Any}) -> BoolReturn true if the network dict contains a root-level "time_series" collection with at least one entry, and at least one component references it via a "time_series" sub-dict.
This follows PMD's convention: a network with a "time_series" key but no component references is treated as a snapshot network.
BMOPFTools.get_snapshot — Function
get_snapshot(net::Dict{String,Any}, t_index::Int) -> Dict{String,Any}Materialise a snapshot of a time-series network at time step t_index (1-based).
Returns a deep copy of net with all "time_series" parameter references resolved to concrete scalar/vector values and all component-level "time_series" sub-dicts removed. The root-level "time_series" key is also removed from the result.
For a network without time series, returns a deep copy unchanged.
References are resolved on every collection in TS_COMPONENT_COLLECTIONS, and on transformers via their subtype keys. control_profile is the one component type deliberately not resolved — its scalable values are nested inside control-law sub-objects, which the resolver cannot reach; see TS_COMPONENT_COLLECTIONS. A time-series reference on a control profile is therefore left untouched rather than materialised.
PMD convention: time series values are scaling factors applied multiplicatively to the static parameter value:
resolved_value = static_value * scale_factor[t_index]Raises
BoundsErrorift_indexis out of range for any referenced series.
BMOPFTools.sideload_coordinates! — Function
sideload_coordinates!(net, csv_path) -> (n_matched, n_skipped)Read a OpenDSS-style bus coordinate CSV (bus_id,x,y, no header) and add "longitude" and "latitude" fields to matching bus objects in net.
Returns the number of buses matched and the number of CSV rows skipped because the bus ID was not present in net (open-switch stub buses and synthetic slack buses are the usual cause).
The CSV x column is treated as longitude and y as latitude — the convention used by OpenDSS Buscoords files. No coordinate-system transformation is performed; values are stored as-is.
Admittance export
BMOPFTools.line_yprim — Function
line_yprim(line, linecodes) -> (nodes, Y)Return one line's nominal-Π primitive admittance block in the same convention as ybus_passive: I_into = Y * V_to_ground, with positive terminal currents flowing from the bus into the line. nodes lists the from-side terminals followed by the to-side terminals; Y is SI siemens and symmetric under transpose (not conjugate transpose).
This public element-level seam is useful for branch-current and branch-power measurements without reconstructing a branch model from a network Ybus. It uses exactly the linecode, terminal-map truncation, and shunt-stamping rules used by ybus_passive and the OPF branch formulation.
BMOPFTools.transformer_yprim — Function
transformer_yprim(xfmr, subtype) -> (nodes, Y)Return the nodal admittance block for a single transformer data dict.
nodes—Vector{Tuple{String,String}}of(bus_id, terminal_name)pairs, in the same order as the rows/columns ofY.Y—Matrix{ComplexF64}, SI siemens. Symmetric:Y ≈ transpose(Y).
subtype must be one of "single_phase", "center_tap", "wye_delta", "delta_wye".
The ordinary subtypes use the effective ratio N_eff = (v_nom_from/v_nom_to) · tap (schema convention; tap defaults to 1.0), so a fixed off-nominal tap is reflected in the exported admittance.
The regulator subtypes "single_phase_autotransformer" and "open_delta_regulator" use the fixed-tap effective ratio n_eff (BMOPFTools._autotransformer_ratio) in place of the nameplate turns ratio.
Raises an ArgumentError for unknown subtypes. Returns ([], zeros(0,0)) when the transformer is degenerate (zero v_nom_to, missing terminal maps).
BMOPFTools.export_yprim — Function
export_yprim(net) -> DictBuild the Yprim block for every transformer in the network and return a nested Dict:
Dict(
subtype => Dict(
id => Dict("nodes" => [...], "Y_real" => [...], "Y_imag" => [...])
)
)Each "nodes" entry is a Vector of [bus_id, terminal_name] pairs. "Y_real" and "Y_imag" are row-major dense matrices (Vector of Vector).
BMOPFTools.write_yprim — Function
write_yprim(net, path)Write the Yprim export for all transformers in net to a JSON file at path.
BMOPFTools.ybus_passive — Function
ybus_passive(net; config=_DEFAULT_CONFIG) -> YbusResultAssemble the passive system nodal admittance matrix from the network's lines, shunts, capacitors, and transformers. Loads, generators, and IBRs (the nonlinear current-injecting elements) are NOT included — this is the linear, voltage-invariant network. Voltage sources contribute no admittance (they are ideal in the BMOPF model) and set the boundary, not the matrix.
SI siemens, full multiphase, referenced to earth. Closed switches, negligible-Z lines, and 1:1 zero-leakage transformers are node-aliased (not stamped). See the module header for the sign/symmetry convention.
A zero-leakage transformer with a non-unity ratio has no finite admittance form; here it is stamped via transformer_yprim as the (singular, shunt-only) block with that function's warning. ybus_augmented models it exactly instead, as an ideal-coupling constraint row.
BMOPFTools.YbusResult — Type
YbusResultSystem nodal admittance matrix and its node ordering.
Fields:
Y—SparseMatrixCSC{ComplexF64,Int}, SI siemens,I = Y·VwithVnode-to-earth. Symmetric for a reciprocal network (Y ≈ transpose(Y)).nodes—Vector{Tuple{String,String}}of(bus_id, terminal_name), in row/column order ofY.index—Dict{Tuple{String,String},Int}from any(bus, terminal)to its row (0= earth reference; aliased terminals share a row).
BMOPFTools.ybus_linearized — Function
ybus_linearized(net; config=_DEFAULT_CONFIG, fold=:constant_z, v0=nothing)
-> LinearizedYbusAssemble the passive Ybus with the load elements folded in.
fold = :constant_z(default) — fold only the voltage-invariant constant-Z part of each load intoY; the constant-I / constant-P parts are returned as thei_comp(V)closure (the OpenDSS SolutionMode split).fold = :all— fold the WHOLE load as its equivalent admittance at the operating pointv0(required);i_comp ≡ 0. This is OpenDSS's converged system Y and the load-as-admittance seed for state estimation.
v0, if given, is a Dict{Tuple{String,String},<:Number} of node-to-earth complex voltages (e.g. from solve_pf, a solved OPF, or OpenDSS); it sets the folding point for :all and yields i0 = i_comp(v0).
BMOPFTools.LinearizedYbus — Type
LinearizedYbusThe passive Ybus with loads folded in (see the module header for the model).
Fields:
Y—SparseMatrixCSC{ComplexF64,Int}, passive + folded load admittances.nodes—Vector{Tuple{String,String}}, row/column order ofY.index—Dict{Tuple{String,String},Int}from(bus, terminal)to row (0= earth reference; aliased terminals share a row).i_comp—V -> i: maps a node-ordered voltage vector to the compensation current vector (the constant-I / constant-P load injection). Forfold = :allthis is the zero function.i0—i_comp(v0)if an operating pointv0was supplied, elsenothing.fold—:constant_zor:all.
Solving Y·V = i_comp(V) by fixed-point iteration is the Z-bus power flow; the first iterate (flat or v0) is the standard linear power-flow approximation.
BMOPFTools.ybus_augmented — Function
ybus_augmented(net; config=_DEFAULT_CONFIG,
switches=:alias, ideal_xfmrs=:constrain) -> AugYbusResultAssemble the augmented (bordered, MNA-style) system nodal admittance matrix: the passive Ybus of ybus_passive extended with one constraint row and auxiliary current unknown per ideal coupling, K·[V; w] = [I_inj; 0].
Keywords:
switches = :alias | :constrain—:alias(default) fuses closed-switch terminals into shared nodes exactly likeybus_passive;:constrainkeeps them distinct and ties them per conductor withV_from − V_to = 0rows, so each switch-conductor current is a solution unknown. Both modes yield identical node voltages.ideal_xfmrs = :constrain | :stamp—:constrain(default) models every zero-leakage, NON-unity-ratio two-winding transformer (single_phase,wye_delta,delta_wye) exactly, as ideal winding-core constraintsu_w1 − ratio·u_w2 = 0from_xfmr_winding_incidence;:stampfalls back toybus_passive's singular shunt-only block (with its warning). Unity-ratio zero-leakage transformers stay node-aliased in either mode (an exact identity — strictly better than a constraint).
The constraint coefficients must be real (they are, for all supported subtypes — ratios are v_nom quotients times a real tap); this keeps K = transpose(K). A degenerate coil ratio (zero/non-finite, e.g. tap = 0) raises an ArgumentError.
Non-source shunt-only networks aside, K is nonsingular whenever every node group has a path to a voltage reference; islands and floating ideal deltas surface as singular factorizations in the consuming solver, not here.
BMOPFTools.AugYbusResult — Type
AugYbusResultAugmented system nodal admittance matrix (see ybus_augmented).
Fields:
K—SparseMatrixCSC{ComplexF64,Int},(n+m)×(n+m)withnnodes andmcouplings,K = transpose(K). The leadingn×nblock is the passive Ybus (SI siemens, node-to-earth voltages); row/columnn+jis couplingj's scaled constraint.nodes—Vector{Tuple{String,String}}, node order of the leading block (same rules asYbusResult).index—Dict{Tuple{String,String},Int}from any(bus, terminal)to its node row (0= earth reference; aliased terminals share).couplings—Vector{IdealCoupling}, in bordered-row order.
BMOPFTools.IdealCoupling — Type
IdealCouplingOne ideal coupling (per conductor / winding core) in an AugYbusResult: a linear constraint Σ coeffs[k]·V[nodes[k]] = 0 with an auxiliary current unknown occupying one bordered row/column of K.
Fields:
kind—:switch(closed switch conductor) or:ideal_xfmr(zero-leakage winding core).id— element id;"subtype/id"for transformers.conductor— conductor / winding-core index within the element.nodes— system node indices (earth-referenced entries already dropped; aliased terminals resolved to their representative).coeffs— constraint coefficients: ±1 for switches, the winding incidenceC_w1 − ratio·C_w2for ideal transformers.scale— conditioning scale applied to the stamped row/column; the PHYSICAL coupling current isscale · w, wherewis the bordered-row solution entry. Oriented so the coupling drawsscale·w·coeffs[k]out of nodenodes[k].
Line constants
BMOPFTools.overhead_line_constants — Function
overhead_line_constants(r_ac, gmr, radius, x, y;
cap_radius=radius, frequency, earth_model="modified_carson",
earth_resistivity=100.0) -> (; Z, C)Compute the full primitive series-impedance matrix Z [Ω/m] and Maxwell capacitance matrix C [F/m] of an overhead conductor arrangement. Input vectors are ordered by conductor and use SI units: r_ac [Ω/m], gmr, radius, x, y, and cap_radius [m]. frequency is required [Hz]; earth_resistivity is in Ω·m.
Unlike compile_linecode, this is a pure numerical API: it does not read or mutate a BMOPF dictionary, apply wire-data defaults, stamp provenance, or Kron-reduce any circuit conductor. It uses the same Carson/Deri and Maxwell kernels as the compiler. compile_linecode remains the preferred entry point for ordinary network data.
The implementation is generic over Real element types so numerical clients can propagate alternative scalar types. Modified Carson is smooth on the interior of the physical domain (gmr > 0, positive separation, y > 0). The full_carson model retains its documented near-ground height clamp and is not smooth at that clamp.
BMOPFTools.compile_linecode — Function
compile_linecode(net, geometry_id; id=geometry_id, force=false) -> StringCompute the per-metre series-impedance and shunt-susceptance matrices of net["line_geometry"][geometry_id] and stamp them as net["linecode"][id], returning id.
The compiled linecode covers the full primitive system of the geometry's circuit conductors, in the order they are listed — one matrix row per conductor, matching a line's terminal_map_* of the geometry's terminal labels. There is no Kron elimination of circuit conductors; grounding belongs on bus perfectly_grounded_terminals. Cable (cn/ts) shield subconductors — sub-terminal structure held at earth potential — are the one exception: they are built internally and reduced into the cable's equivalent conductors (Kersting treatment), recorded under derivation.shields_reduced.
Earth model (modified_carson default | full_carson | deri), earth resistivity [Ω·m], frequency [Hz] (required — no ambient default, no rescaling; both 50 and 60 Hz are computed exactly from ω), and conductor temperature [°C] are read from the geometry object. Note OpenDSS's default earth model is deri — set it on the geometry when cross-validating against OpenDSS. Physically impossible inputs (GMR > radius, overlapping conductor circles, non-nesting cable layers) error; inputs that strain the analytical models' validity domain (Carson truncation parameter, buried conductors under full_carson, frequency above a wire's critical skin frequency, implausible earth resistivity) warn here and surface as W.DOM.* findings in analyze.
Shunt susceptance: overhead conductors get the Maxwell potential-coefficient capacitance (perfect-earth images, electrostatic radius); cables their coaxial phase-to-shield capacitance (diagonal). Bare conductors at or below ground level contribute zero shunt (warned).
Provenance is stamped on the linecode: source="geometry", a line_geometry back-reference, and a derivation block (method, ρ, f, temperature, applied cross-defaults, tool version). The provenance analyzer re-derives and cross-checks linecodes that carry these fields.
An existing linecode id is only overwritten with force=true.
BMOPFTools.compile_linecodes! — Function
compile_linecodes!(net; force=false) -> Vector{String}Compile every line_geometry in net into a same-id linecode (see compile_linecode). Geometries whose linecode already exists are skipped with a warning unless force=true. Returns the compiled ids.
Conversion
BMOPFTools.to_pmd — Function
to_pmd(net::Dict{String,Any}; frequency=50.0, sbase=1e6) -> Dict{String,Any}Convert a BMOPF network dict to a PowerModelsDistribution ENGINEERING model dictionary, suitable for passing directly to PMD's solve_mc_* functions or transform_data_model.
The output includes the "data_model" => ENGINEERING marker that PMD expects.
Keyword arguments
frequency: system frequency [Hz] written tosettings.base_frequency(default 50.0 — set 60.0 for North American feeders; BMOPF itself carries no frequency field).sbase: power base [W] written tosettings.sbase_default(default 1e6).
Key reverse mappings
- BMOPF
terminal_namesstring arrays → PMDterminalsint arrays - BMOPF
terminal_map_*string arrays → PMDf_connections/t_connectionsint arrays - BMOPF voltage values in V → PMD in p.u. (requires settings voltage base)
- BMOPF power values in W/var → PMD scaled by
power_scale_factor - BMOPF
v_anglein radians → PMDvain degrees - BMOPF
open_switchbool → PMDstate(0=OPEN, 1=CLOSED) - BMOPF pattern keys (Rseries11, G1_1, ...) → PMD matrices
Notes
- A minimal
settingsdict is always included in the output. - If the BMOPF net contains
_pmdsub-dicts (carried through from a PMD ENGINEERING source), those fields are merged back into the PMD component.
BMOPFTools.from_dss — Function
from_dss(path::AbstractString; name=nothing) -> Dict{String,Any}Parse an OpenDSS Master file directly to a BMOPF network dict using PowerIO.jl.
This is the recommended path for reading OpenDSS networks: the Rust parser (bound in-process by PowerIO.jl) parses a typed module and explicitly emits the BMOPF 0.1.0 profile with diagnosed extension relocations. Conversion diagnostics remain separate from schema validation and numerical validation against OpenDSS. Source metadata is read through a generation-2 PowerIO IR snapshot for the conversion audit.
OpenDSS identifiers are case-insensitive but case-preserving, whereas BMOPF keys are matched exactly. To reconcile the two, every identifier and every reference to one (bus names, linecodes, component ids) is case-folded to lower case on ingest, so references resolve regardless of the casing each OpenDSS statement happened to use.
OpenDSS numeric terminal names ("1", "2", "3", "4") are remapped to the task-force convention ("a", "b", "c", "n") and neutral_terminal is set to "n" on every affected bus.
Arguments
path: path to the OpenDSS Master.dss file (or any .dss entry point)name: optional network name string set onnet["name"]after parsing. Defaults to the relative path of the Master file from the working directory.frequency: optional system frequency [Hz] override. By default the base frequency PowerIO parsed from the DSS circuit (Set DefaultBaseFreq, which itself defaults to 60 Hz in OpenDSS) is captured intonet["meta"]["frequency"]. Pass this to override it — e.g. when the source file relied on a base frequency the deck never stated, or you know the intended value. The chosen value and its source ("powerio"or"override") are recorded onnet["_meta"]["frequency_source"]. The frequency is never used to rescale impedances (there is no OpenDSS-style base-frequency scaling in BMOPF); it is metadata that makes the case self-contained and feeds the cross-object consistency checks (W.DOM.FREQUENCY_MISMATCH).findings: optionalVector{Finding}to append the conversion's findings to (the same pattern the analysis passes follow). The findings are recorded on the network regardless; this is for a caller that wants them in hand at ingest.
Conversion findings
PowerIO reports every piece of information that cannot be represented in BMOPF JSON (e.g. shunt admittance, load shape time series, RegControl OLTC taps). Two views of the same list are recorded on the returned dict:
_meta["powerio_warnings"]— every diagnostic verbatim, as itsCODE: messageline, untruncated and ungrouped._meta["powerio_diagnostics"]— the same diagnostics folded into one record per(code, severity, component type)class, whichpowerio_findingsreads back asFindings andanalyzereports alongside every other finding.
Errors
PowerIO.PowerIOErrorwith codeBUILD.DIST.ELECTRICAL_INCOMPLETEif a geometry-defined line lacks calculated conductor impedances or a resolved terminal map. No substituted-impedance network is returned.ArgumentErrorif the DSS file does not exist.ErrorExceptionif PowerIO produces no output (parse failure or schema error).
Example
net = from_dss(joinpath(pkgdir(BMOPFTools), "test", "data", "pf_comparison", "pf_1ph_line.dss"))
report = analyze(net)
render(report, stdout)BMOPFTools.powerio_source_behavior_contract — Function
powerio_source_behavior_contract(net; plan_auxiliary_constraints = false)Return the explicit, non-mutating contract for source-side voltage-behavior metadata retained by from_dss. OpenDSS vminpu/vmaxpu values describe a load law's voltage-behavior domain; they are not silently promoted to BMOPF bus bounds. The returned records expose enough topology and nominal-voltage context for a caller or domain plugin to construct an auxiliary diagnostic problem deliberately.
When plan_auxiliary_constraints=true, the same records are returned as candidate terminal-voltage-ratio constraints. This is a plan only: no JuMP or BMOPF model is modified and no constraint is active in the original model.
BMOPFTools.to_dss — Function
to_dss(net::Dict{String,Any}; name=nothing) -> (dss_text, warnings)Convert a BMOPF network dict to OpenDSS text using PowerIO.jl, returning the generated DSS string and a vector of fidelity-loss warnings.
The dict is serialised to BMOPF JSON (via write_bmopf) and handed to PowerIO's DSS writer. BMOPF terminal labels ("a", "b", "c", "n") are accepted by the writer and re-normalised to OpenDSS numeric nodes (.1, .2, .3, .0); merged polyphase sources are re-expanded as needed by the writer.
This is the inverse of from_dss. It produces valid OpenDSS but does not (yet) guarantee that a from_dss → to_dss round trip reproduces the original file byte-for-byte or that the regenerated network solves identically in OpenDSS — inspect warnings for anything the writer could not represent.
Arguments
net: a BMOPF network dict (as returned byfrom_dssorparse_bmopf).name: optional network name to write instead ofnet["name"]. The input dict is not mutated.findings: optionalVector{Finding}to append the writer's findings to, carrying powerio's own diagnostic code and severity. The returned warnings are the same list as theCODE: messagelines it renders as.
Conversion warnings
PowerIO reports every piece of information that its BMOPF→DSS writer cannot represent or had to assume. These are returned as the second element so callers can inspect fidelity losses without losing the converted text.
Errors
ErrorExceptionif PowerIO produces no DSS output (writer failure).
Example
net = from_dss("test/data/pf_comparison/pf_3ph_line.dss")
dss_text, warnings = to_dss(net)
isempty(warnings) || @warn "DSS writer fidelity losses" warningsto_dss(net::Dict{String,Any}, path::AbstractString; name=nothing) -> warningsConvert a BMOPF network dict to OpenDSS and write the result to path, returning the vector of fidelity-loss warnings (see the two-argument to_dss for details).
Example
net = from_dss(joinpath(pkgdir(BMOPFTools), "test", "data", "pf_comparison", "pf_1ph_line.dss"))
warnings = to_dss(net, "roundtrip/Master.dss")BMOPFTools.powerio_findings — Function
powerio_findings(net::Dict{String,Any}; section=:provenance) -> Vector{Finding}
powerio_findings(records::AbstractVector; section=:provenance) -> Vector{Finding}The PowerIO conversion diagnostics of a network as Findings: one per diagnostic class, carrying powerio's own code verbatim, its message, and its severity mapped onto this package's three levels.
For a network, the records read come from _meta["powerio_diagnostics"], which from_dss writes at ingest and which survives a write_bmopf / parse_bmopf round trip under meta.provenance. A network that never crossed the PowerIO boundary yields nothing. analyze calls this, so the findings are already in the SummaryReport; call it directly to read them off a network without running the analysis passes.
A powerio diagnostic that carries no severity of its own — the CODE: message lines a handle retains, as opposed to the records the conversion entry points return — is taken as a WARNING.
BMOPFTools.project_solution — Function
project_solution(net, result; t_index=1) -> Dict{String,Any}Return a deep copy of net with every controllable device pinned to the setpoint it takes in result (as returned by solve_opf or solve_pf), so the projected net is a fully determined snapshot.
Pinned devices:
- Generators and IBRs — per-phase active/reactive dispatch fixed to the solved
pg/qg(p_min == p_max == pg,q_min == q_max == qg). An IBR under acontrol_profile(Volt-var / power-factor) has the profile reference removed and explicit fixed bounds written, freezing the smart-inverter control at its solved operating point. - Free transformer taps — a tap that was an OPF decision variable (reported as
tap/tap_ratioinresult) is written back onto the transformer. Ordinary transformertap_min/tap_maxbounds are removed to fix that ratio; regulatortap_ratio_min/tap_ratio_maxbounds are pinned per solved arm. Fixed-tap transformers are left untouched. Foropen_delta_regulatorthe tap is a two-element vector; only regulators that were free (non-missing) are updated.
Loads, capacitors and voltage sources are not decision variables and are copied unchanged. The input net is never mutated.
The projected net is a legal input to solve_pf (every generator satisfies p_min == p_max, q_min == q_max) and, via to_dss, to OpenDSS.
result["_meta"]["projection"] on the returned net records what was pinned (generators, ibrs, free_taps) and which IBR control profiles were frozen, so downstream diagnostics can attribute any oracle mismatch.
Errors
ArgumentErrorifresultis infeasible (result["feasible"] == false), since an infeasible result carriesNaNsetpoints and cannot be projected.
Example
net = from_dss("test/data/pf_comparison/pf_pv_4leg.dss")
result = solve_opf(net)
snap = project_solution(net, result)
solve_pf(snap) # re-solve as a determined power flow
to_dss(snap, "snap.dss") # export the pinned snapshot to OpenDSSBMOPFTools.dispatch_as_loads — Function
dispatch_as_loads(net) -> Dict{String,Any}Return a deep copy of net with every pinned generator and IBR replaced by an equivalent constant-power negative load (p_nom = -p_min, q_nom = -q_min), carrying the device's terminal map and the load configuration implied by its connection (WYE for generator-WYE / IBR-FOUR_LEG, DELTA for generator-DELTA / IBR-THREE_LEG, SINGLE_PHASE for IBR-SINGLE_PHASE). Generators/IBRs sitting on a voltage_source bus are dropped, not converted — the slack source is the reference and unbounded injector in a power flow, so its dispatch must not be re-imposed as a load.
This is the bridge that makes an OPF snapshot exportable to OpenDSS today: to_dss (via PowerIO) cannot yet emit a BMOPF ibr/generator, but it exports loads faithfully, and a fixed PQ injection is exactly a negative constant- power load. Intended to be applied to the output of project_solution (so every generator/IBR already has p_min == p_max); a device without fixed bounds is skipped and recorded under _meta.
The negative-load form is equivalent to the pinned generator/IBR form under a determined power flow, so solve_pf(dispatch_as_loads(project_solution(net, r))) matches the original OPF voltages — the load form is what gets exported and solved in OpenDSS.
_meta["dispatch_as_loads"] records the ids converted, the ids dropped as slack, and any skipped (unpinned) devices.
Direct generator/ibr OpenDSS mappings can replace this bridge once PowerIO supports them.
Top-level analysis and rendering
BMOPFTools.analyze — Function
analyze(net::Dict{String,Any}; t_index::Int=1) -> SummaryReportRun all analysis and validation passes on a BMOPF network dict and return a SummaryReport.
For snapshot networks, t_index is ignored. For networks with a "time_series" key, the snapshot at t_index is materialised first.
analyze(path::AbstractString; kwargs...) -> SummaryReportParse a BMOPF JSON file and run analyze.
BMOPFTools.render — Function
render(report::SummaryReport, dest; kwargs...)Render a SummaryReport to dest.
dest::IO— writes terminal-formatted text (ANSI colour if tty)dest::AbstractString— writes to file; format inferred from extension (.json→ structured JSON viarender_json,.md→ Markdown, anything else → plain text)
Keyword arguments
color::Bool— force-enable/disable ANSI colour for IO dest (default: auto)verbose::Bool— include INFO-level findings (default:true)
BMOPFTools.render_terminal — Function
render_terminal(report, io; color, verbose)Write a human-readable text report to io.
BMOPFTools.render_markdown — Function
render_markdown(report, io; verbose)Write a Markdown-formatted report to io.
BMOPFTools.render_json — Function
render_json(report, io)Write a machine-readable JSON serialization of a SummaryReport to io.
Unlike render_markdown (human-facing prose) this emits the structured analysis verbatim: every section result dict under results, plus the complete finding log as an array of records. The results keys are stable across versions, so downstream consumers (e.g. dataset feature-tagging) can read the numbers directly instead of parsing Markdown.
Shape:
{
"network_name": "...",
"generated_at": "yyyy-mm-ddTHH:MM:SS",
"summary": {"errors": N, "warnings": N, "info": N},
"results": { "<section>": { ... } , ... },
"findings": [
{"severity": "INFO"|"WARNING"|"ERROR", "code": "...", "section": "...",
"component_type": "...", "component_id": "..."|null, "message": "...",
"detail": {...}|null},
...
]
}BMOPFTools.render_ascii_tree — Function
render_ascii_tree(net, io; max_buses, max_depth, fold_chains, legend_limit,
split_by_level)Write an ASCII tree of the network graph to io.
Node labels – the source bus is tagged SRC; loads appear as L1,L2,… and generators as G1,G2,… (numbered per section, mapped in the legend printed after each section). Transformer and switch edges are annotated.
Large-network handling – when a section's bus count exceeds max_buses:
fold_chains=true(default) collapses unbranched corridors with no attached loads or generators into⋮ (N buses)stubs.- Subtrees deeper than
max_depthare replaced by a[+N buses, M loads]line.
Multi-voltage split – when split_by_level=true (default) and the spanning tree crosses ≥ 2 voltage level boundaries, the file is split into a root section (MV backbone) and one LV section per distribution transformer.
Solution profiling
BMOPFTools.profile_solution — Function
profile_solution(net, result; t_index::Int=1) -> SolutionReportProfile an OPF result dict against the BMOPF network that produced it. Checks bound satisfaction (voltage, thermal, generator dispatch), constraint residuals, power balance, and produces informational summaries (loss fraction, neutral shift).
net may be a snapshot or time-series network dict; t_index selects the snapshot when time-series data is present. result is the dict returned by solve_opf or any compatible solver.
Returns a SolutionReport which can be rendered to Markdown with render_solution.
BMOPFTools.render_solution — Function
render_solution(report::SolutionReport, dest; verbose::Bool=true)Render a SolutionReport to dest.
dest::IO— writes Markdown textdest::AbstractString— writes to file (.mdextension recommended)
BMOPFTools.solution_check — Function
solution_check(net, result, findings) -> Dict{String,Any}Profile an OPF result dict against the network that produced it. Appends Finding objects to findings and returns a summary dict.
net must be a snapshot (non-time-series) BMOPF network dict. result is the dict returned by solve_opf (or any compatible solver).
BMOPFTools.voltage_zone_summary — Function
voltage_zone_summary(net, result) -> Dict{String,Any}Aggregate the solved bus voltages into a per-galvanic-zone band summary for at-a-glance inspection. Galvanic zones (the connected components left after cutting transformer windings) are the natural grouping because every bus in a zone shares one voltage base, so the per-unit magnitudes are directly comparable; volts across a transformer boundary are not.
Each zone is reduced over its phase terminals (the neutral is excluded from the magnitude band but tracked separately as a neutral shift):
v_base— phase-to-neutral voltage base (V). Taken from the zone's voltage source if it contains one, else the midpoint of the buses'v_min/v_max, else the median solved phase magnitude.vm_min_pu/vm_max_pu— min/max phase magnitude in the zone, per unit.vm_min_bus/vm_max_bus— the buses carrying those extremes.max_imbalance_pct— worst per-bus phase-magnitude spread (max−min)/v_base.max_imbalance_bus— bus carrying that worst imbalance.max_neutral_shift_v/max_neutral_shift_bus— worst neutral terminal voltage in the zone.status—"ok","active"(within 1 % of a bound), or"violation", derived from the buses' ownv_min/v_maxusing the same threshold as the bound checks.bus_rows— per-bus drill-down records (bus,vm_min_v/vm_max_v,vm_min_pu/vm_max_pu,imbalance_pct,neutral_shift_v,deviation_pu,status), sorted worst-deviation-from-1.0-pu first. Rendered only in verbose output.
Returns Dict("zones" => Vector{Dict}, "n_zones" => Int), zones sorted by label. Returns empty zones when there is no bus result.
Staged OPF extension interface
BMOPFTools.OpfScaling — Function
OpfScaling(mode=:custom; kwargs...)Construct the coordinate-scaling specification accepted by the staged OPF builders. This is the public scaling entry point; concrete implementation policy types are deliberately private so their representation can evolve.
Supported forms are:
OpfScaling(:si)for raw SI coordinates;OpfScaling(:classic; power_base=1e6)for the historical system-base per-unit convention; andOpfScaling(; voltage_bases, power_base=...)orOpfScaling(; voltage_bases, power_bases=...)for explicit voltage bases with respectively a system-wide or galvanic-zone power base.
Current, impedance, and admittance bases are always derived from voltage and power bases. Supplying both power_base and power_bases is rejected.
BMOPFTools.OpfDiagnosticSchema — Type
OpfDiagnosticSchemaImmutable, versioned description of the numerical coordinate system and semantic structure of one staged OPF model. The contained dictionaries and semantic-block records are defensive copies. BMOPFTools reports what it built; diagnostic interpretation remains the responsibility of downstream tools.
BMOPFTools.OpfRegularization — Type
OpfRegularizationExplicit downstream declaration that a registered semantic objective term is a regularization. weight and units describe its reported coefficient; term_key identifies the objective contribution, targets identify the regularized model quantities, and purpose records why the mathematical problem was changed. BMOPFTools records this declaration but does not infer it from arbitrary JuMP expressions or verify that the term is included in the current model objective.
BMOPFTools.OpfDifferentiabilityAnnotation — Type
OpfDifferentiabilityAnnotationExplicit audit declaration for a differentiability hazard that cannot be reliably inferred from a completed JuMP graph. kind is one of :nonsmooth_operator, :dynamic_branch, or :unsupported_parameter_location; key optionally identifies the affected semantic object or coefficient. A blocking annotation makes opf_differentiability_report return ready=false. Nonblocking annotations remain visible as qualifications and in research provenance.
BMOPFTools.opf_bus_voltage_key — Function
Return the native real/imaginary bus-terminal voltage key.
BMOPFTools.opf_ground_current_key — Function
Return the native real/imaginary perfect-ground current key.
BMOPFTools.opf_line_current_key — Function
Return the native line-current key for a conductor and branch side.
BMOPFTools.opf_switch_current_key — Function
Return the native switch-current key for a conductor.
BMOPFTools.opf_load_current_key — Function
Return the native load-current key for a phase/conductor position.
BMOPFTools.opf_generator_current_key — Function
Return the native generator-current key for a phase/conductor position.
BMOPFTools.opf_voltage_source_current_key — Function
Return the native voltage-source current key for a conductor position.
BMOPFTools.opf_transformer_current_key — Function
Return the native two-winding transformer-current key.
BMOPFTools.opf_transformer_tap_key — Function
Return the native scalar transformer-tap key.
Return the native per-regulator transformer-tap key.
BMOPFTools.opf_nwinding_current_key — Function
Return the native n-winding transformer-current key.
BMOPFTools.opf_ibr_current_key — Function
Return the native inverter/IBR current key for a phase position.
BMOPFTools.opf_ibr_power_key — Function
Return the native active/reactive power auxiliary key for one IBR phase.
BMOPFTools.opf_ibr_voltage_magnitude_key — Function
Return the native monitored-voltage-magnitude auxiliary key for one IBR phase.
reference may be :pg, :pn, or :pp for the per-phase monitored magnitude, or the corresponding :_averaged form for the actual shared controller input when phase magnitudes are averaged. The averaged key refers to a registered expression rather than a per-phase voltage variable.
BMOPFTools.opf_dc_voltage_key — Function
Return the native signed DC bus-terminal voltage key.
BMOPFTools.opf_dc_ground_current_key — Function
Return the native perfect-ground DC-current key.
BMOPFTools.opf_dc_branch_current_key — Function
Return the native DC branch-current key for a conductor.
BMOPFTools.opf_converter_dc_current_key — Function
Return the native AC/DC converter DC-port current key.
BMOPFTools.opf_dc_load_current_key — Function
Return the native constant-power DC-load current key.
BMOPFTools.opf_dc_source_current_key — Function
Return the native DC-source current key.
BMOPFTools.opf_dc_source_power_key — Function
Return the native dispatched DC-source power key.
BMOPFTools.opf_model — Function
opf_model(ctx)
opf_network(ctx)
opf_bases(ctx)
opf_coordinate_bases(ctx, location; domain=:ac)
opf_diagnostic_schema(ctx; voltage_bases, power_bases)
opf_lifecycle(ctx)Stable accessors for a context returned by build_opf_model. They provide the live JuMP model, prepared working network, raw per-unit metadata, public coordinate bases, versioned diagnostic evidence, and construction lifecycle state. Prefer the latter two interfaces for cross-package tooling.
BMOPFTools.opf_network — Function
Return the prepared working network owned by a staged OPF context.
BMOPFTools.opf_bases — Function
Return per-unit base metadata, or nothing for an SI staged model.
For zone-local policies, s_base is a compatibility/reference value rather than a universal local power base. Use opf_coordinate_bases for bus-local hook literals and residual interpretation.
BMOPFTools.piecewise_linear_value — Function
piecewise_linear_value(input, breakpoints, values;
epsilon=nothing, encoding=:softplus)Evaluate the continuous piecewise-linear function through corresponding breakpoints and values, clamped flat outside the breakpoint interval. Breakpoints must be finite and strictly increasing and both vectors must have the same length of at least two.
With the default epsilon=nothing, evaluation is exact and retains the PWL kinks. Passing a finite epsilon > 0 replaces every hinge with a smooth ReLU surrogate. encoding=:softplus uses epsilon * log1pexp(z / epsilon). encoding=:swish uses z * logistic(z / epsilon) and is intended to match softplus=:swish in opf_piecewise_linear_expression. input, breakpoints, and epsilon must use the same units; values determine the output units.
This numeric method is suitable as an exact control-law oracle outside an optimisation model and as a reference for quantifying smoothing error.
BMOPFTools.opf_piecewise_linear_expression — Function
opf_piecewise_linear_expression(ctx, input, breakpoints, values;
epsilon) -> expressionBuild a smooth JuMP expression for the continuous PWL function through breakpoints and values, clamped flat outside the breakpoint interval. The method is implemented by the OPF extension and requires JuMP plus a compatible optimizer to be loaded.
input may be a JuMP variable or scalar expression. All curve data are fixed, finite real numbers; breakpoints must be strictly increasing. The finite, positive epsilon is the absolute softplus width in the same working units as input and breakpoints. values determine the expression's output units.
Operator registration is cached by epsilon in ctx, so any number of curves in one staged OPF context can share the same analytic smooth-ReLU operator. The expression follows the context's softplus build mode: the numerically stable registered softplus operator by default, the native log1p(exp(⋅)) expression selected with softplus=:builtin, or the native logistic primitive selected with softplus=:swish. The Swish mode is intended for solvers that expose a native logistic operator, such as Gurobi. Curve construction does not add constraints or modify the staged OPF lifecycle.
Use opf_bases to convert physical breakpoints and values to model working units before calling this function. Use piecewise_linear_value for exact numeric evaluation or for the corresponding smooth numeric oracle.
BMOPFTools.opf_coordinate_bases — Function
opf_coordinate_bases(ctx, location; domain=:ac)Return the physical coordinate bases at one AC or DC bus. AC records contain voltage, current, power, impedance, and admittance; DC records contain voltage, current, power, and impedance. SI-coordinate models return unity bases.
BMOPFTools.opf_diagnostic_schema — Function
opf_diagnostic_schema(ctx; voltage_bases=nothing, power_bases=nothing)Return a defensive, versioned description of a staged model's scaling, semantic blocks, initialization provenance, and qualified coordinate interfaces. Optional proposed AC bases affect only the transformer-interface audit; this function never modifies the model. Native semantic blocks are materialised lazily after KCL finalisation; inspect capabilities for semantic_blocks_available and semantic_blocks_registered before relying on their completeness. Bind the returned schema once when several fields are needed; each call rebuilds its defensive evidence dictionaries.
BMOPFTools.opf_neutral_labels — Function
Return the declared neutral terminal labels for one staged OPF context.
BMOPFTools.opf_lifecycle — Function
Return the staged OPF construction lifecycle (:building or :kcl_finalized).
BMOPFTools.opf_build_manifest — Function
Return the construction provenance for a staged OPF context.
BMOPFTools.opf_build_spec — Function
Return a defensive copy of the typed build specification owned by a context.
BMOPFTools.opf_stage_completed — Function
Return whether construction stage stage has completed for this context.
BMOPFTools.initialize_opf_model — Function
initialize_opf_model(net; kwargs...) -> ctxPrepare one OPF snapshot, create its native variables, and return a context without adding start values, operational limits, device equations, an objective, or KCL. This is the low-level entry point for composing the public construction stages. build_opf_model remains the standard all-stage recipe.
BMOPFTools.set_opf_start_values! — Function
Apply the standard IVR-EN voltage start-value stage.
BMOPFTools.add_opf_operational_limits! — Function
Add the standard OPF voltage and bus operational-limit stage.
BMOPFTools.add_opf_device_constraints! — Function
Add build-spec-selected native/custom device physics and terminal currents.
BMOPFTools.set_opf_objective! — Function
Set the standard generation-cost objective on a staged OPF model.
BMOPFTools.register_opf_result_extractor! — Function
register_opf_result_extractor!(ctx, owner, extract!; replace=false)Register a deterministic post-solve callback extract!(ctx, result). It runs before a caller-supplied solution_hook! and before native per-unit results are unwrapped. The callback must place physical-unit downstream outputs in result. Duplicate owners are rejected unless replace=true.
BMOPFTools.register_opf_object! — Function
register_opf_object!(ctx, key::OpfModelKey, object; replace=false)
opf_object(ctx, key::OpfModelKey)
opf_object_keys(ctx; kind=nothing)Register and retrieve stable semantic references to JuMP variables, expressions, constraints, or downstream objects. Duplicate keys are rejected unless replace=true. Native JuMP variables are registered automatically using their existing variable-family symbol and raw index.
BMOPFTools.opf_object — Function
Retrieve a JuMP/downstream object registered under an OpfModelKey.
BMOPFTools.opf_object_keys — Function
List registered OpfModelKeys, optionally filtered by kind.
BMOPFTools.register_opf_objective_term! — Function
register_opf_objective_term!(ctx, key::OpfModelKey, term; replace=false)Register a scalar objective contribution under a stable semantic key. key must have kind == :objective; term may be a real constant or a scalar JuMP variable/expression belonging to the context model. Registration identifies a contribution but does not change the model's objective.
BMOPFTools.opf_primal — Function
opf_primal(ctx, key::OpfModelKey; result=1)Return the solved value of a registered variable, parameter, expression, or objective term. Constraint keys are deliberately rejected; use opf_constraint_value for the function value of a constraint.
BMOPFTools.opf_constraint_value — Function
opf_constraint_value(ctx, key::OpfModelKey; result=1)Return the solved function value on the left-hand side of a registered constraint. This is not a residual: interpret it together with the constraint's JuMP set, or use opf_constraint_slack for scalar inequalities.
BMOPFTools.opf_constraint_slack — Function
opf_constraint_slack(ctx, key::OpfModelKey; result=1)Return signed slack for a registered scalar LessThan, GreaterThan, or Interval constraint. Positive values are feasible, zero is active, and negative values indicate violation. Returns nothing for equality and non-scalar/conic sets.
BMOPFTools.opf_dual — Function
opf_dual(ctx, key::OpfModelKey; result=1)Return the solver dual of a registered constraint, preserving JuMP/MOI's sign convention and scalar or vector shape. No economic sign reinterpretation or unit conversion is applied.
BMOPFTools.opf_objective_value — Function
Return the solved value of the model's current objective.
BMOPFTools.register_opf_regularization! — Function
register_opf_regularization!(ctx, name; method, weight, term_key,
targets=[], purpose, units=:dimensionless, owner=:downstream,
metadata=Dict(), replace=false)Declare a downstream regularization without modifying the JuMP model. The objective term_key and every target must already be registered semantic objects. Duplicate names are rejected unless replace=true.
BMOPFTools.opf_regularizations — Function
Return defensive copies of all explicit regularization declarations.
BMOPFTools.register_opf_differentiability_annotation! — Function
register_opf_differentiability_annotation!(ctx, name; kind, description,
owner=:downstream, key=nothing, blocking=true, metadata=Dict(),
replace=false)Declare a differentiability hazard without changing the JuMP model. Use this for extension-defined nonsmooth operators, Julia control flow that depends on a parameter value, or formulation-specific locations that cannot safely accept a parameter. Duplicate names are rejected unless replace=true.
BMOPFTools.opf_differentiability_annotations — Function
Return defensive copies of all explicit differentiability annotations.
BMOPFTools.opf_research_hashes — Function
opf_research_hashes(ctx) -> Dict{String,Any}Return versioned SHA-256 fingerprints for the prepared working network, JuMP model structure, current parameter state, regularization declarations, and differentiability annotations. These are construction/reproduction fingerprints, not certificates of algebraic equivalence between independently formulated models.
BMOPFTools.bind_opf_parameter! — Function
bind_opf_parameter!(ctx, key, parameter, targets; kwargs...)Bind a caller-created JuMP Parameter to one or more registered native or downstream decision variables. key must have kind == :parameter; every target must be a registered OpfModelKey(:variable, ...) in the same model. The generated equality is target = to_working_scale * parameter.
Bindings may only be added before KCL finalization. role=:structural is rejected because topology, terminal maps, and dimensions require rebuilding the model. Supported roles are :decision and :coefficient.
BMOPFTools.opf_parameter — Function
Return the live JuMP parameter identified by its canonical key or alias.
BMOPFTools.opf_parameter_binding — Function
Return a defensive metadata copy for one canonical parameter key or alias.
BMOPFTools.opf_parameter_bindings — Function
Return defensive metadata copies for all registered parameter bindings.
BMOPFTools.opf_coefficient — Function
opf_coefficient(ctx, key::OpfCoefficientKey, default)Resolve a coefficient for a custom device builder. Returns default when no provider is registered; otherwise calls the typed provider with (ctx, key, default). The returned scalar is in model working units.
BMOPFTools.opf_coefficient_provider — Function
Return the registered coefficient provider, or nothing when absent.
BMOPFTools.opf_coefficient_providers — Function
Return a defensive copy of the coefficient-provider registry.
BMOPFTools.opf_coefficient_usage — Function
Return provider-consumption counts keyed by OpfCoefficientKey.
BMOPFTools.opf_differentiability_report — Function
opf_differentiability_report(ctx; active_tolerance=1e-7,
transition_tolerance=1e-5, dual_tolerance=1e-7)
-> OpfDifferentiabilityReportReturn a conservative readiness and qualification report for downstream implicit differentiation. Scalar inequalities are classified as active, near-active, weakly active, or violated using normalized primal slack and dual magnitude. Explicit extension annotations classify graph-invisible nonsmooth operators, dynamic branches, and unsupported parameter locations. This function computes diagnostics only; JVP/VJP operations remain the responsibility of DiffOpt or another downstream package.
BMOPFTools.opf_checked_kkt_factorization — Function
opf_checked_kkt_factorization(ctx; pivot_tolerance=1e-10)Return a factorization callback suitable for DiffOpt's nonlinear KKT factorization attribute. It records an OpfKKTDiagnostic on ctx and throws OpfDifferentiationError when LU fails or its pivot-ratio proxy is at or below pivot_tolerance. This API has no DiffOpt dependency; the downstream package remains responsible for installing and invoking the callback.
BMOPFTools.opf_kkt_diagnostic — Function
Return the most recent checked KKT diagnostic, or nothing.
BMOPFTools.opf_research_provenance — Function
opf_research_provenance(ctx; kwargs...) -> Dict{String,Any}Return a defensive, JSON-compatible experiment snapshot for a staged OPF context. The record includes software and solver versions, formulation and construction choices, statuses, objective and residual summaries, parameter and coefficient maps, initialization/smoothing metadata, active-set diagnostics, regularizations, differentiability annotations, reproducibility hashes, and the latest checked-KKT result. Active-set tolerance keywords are forwarded to opf_differentiability_report.
BMOPFTools.extension_state! — Function
extension_state!(ctx, owner[, init])Return the state namespace owned by owner, creating it from the zero-argument function init when absent (Dict{Symbol,Any} by default). owner should normally be the downstream package module or a package-specific singleton type; namespaces prevent independent extensions from colliding in one OPF context.
BMOPFTools.add_terminal_injection! — Function
add_terminal_injection!(ctx, bus, terminal, cr, ci) -> ctxAdd a real/imaginary current expression injected into a bus terminal to the staged model's KCL accumulator. This is the supported low-level seam for custom devices. It must be called before enforce_kcl!; unknown buses or terminals and post-finalization mutation are rejected.
Currents use the model's working units (per-unit by default). A WYE device must also add the negative return current at its neutral terminal.
Objective building blocks
Composable, individually weighted objective terms — losses, sequence-component unbalance, and the magnitude primitive behind them. See Choosing an objective for what each one does to the answer and when not to use it.
BMOPFTools.OpfObjectiveTerm — Type
OpfObjectiveTerm(name, expr; weight=1.0, units=:dimensionless, purpose="")One named, weighted contribution to a composed OPF objective.
expr must be in the PHYSICAL unit named by units — watts, volts, V², or currency/hour — not in the model's working (per-unit) coordinates. That is what makes a composed objective mean the same thing in per_unit=true and per_unit=false: the term constructors (opf_loss_term, opf_sequence_term, opf_generation_cost_term) convert for you via opf_physical_scale, and a hand-built term should do the same.
weight is then in objective-units per physical-unit, so a weight tuned once stays correct across unit modes and under a future nondimensionalisation.
units is recorded rather than checked: it reaches the regularization declaration and opf_research_hashes, so what was combined — and in what units — is auditable after the fact.
valid_sense records whether the term's expression means what it says under either optimisation direction (:any) or only when pushed DOWN (:min). An epigraph reduction such as norm=:max is :min: its variable is bounded from below by the targets and from above by nothing, so it equals the maximum only while it is being minimised with a non-negative weight. Maximising it, or giving it a negative weight, is unbounded rather than wrong-by-a-little. set_opf_objective! rejects those combinations instead of handing the solver an unbounded problem.
BMOPFTools.opf_loss_term — Function
opf_loss_term(ctx; blocks, weight=1.0, name=:losses)
opf_sequence_term(ctx, buses; component=:negative, norm=:squared, weight=1.0)
opf_generation_cost_term(ctx; weight=1.0)Ready-made OpfObjectiveTerms, in physical units (W, V²/V, currency/hour respectively), for set_opf_objective!.
Implemented in the BMOPFOpfExt extension.
BMOPFTools.opf_sequence_term — Function
opf_loss_term(ctx; blocks, weight=1.0, name=:losses)
opf_sequence_term(ctx, buses; component=:negative, norm=:squared, weight=1.0)
opf_generation_cost_term(ctx; weight=1.0)Ready-made OpfObjectiveTerms, in physical units (W, V²/V, currency/hour respectively), for set_opf_objective!.
Implemented in the BMOPFOpfExt extension.
BMOPFTools.opf_generation_cost_term — Function
opf_loss_term(ctx; blocks, weight=1.0, name=:losses)
opf_sequence_term(ctx, buses; component=:negative, norm=:squared, weight=1.0)
opf_generation_cost_term(ctx; weight=1.0)Ready-made OpfObjectiveTerms, in physical units (W, V²/V, currency/hour respectively), for set_opf_objective!.
Implemented in the BMOPFOpfExt extension.
BMOPFTools.opf_total_loss — Function
opf_element_loss(ctx, block, id) -> JuMP expression
opf_total_loss(ctx; blocks=("line","transformer","switch")) -> JuMP expressionActive-power loss of one two-port element, or of the whole network, as a JuMP expression in the model's WORKING units (per-unit when per_unit=true).
Built from the same per-device terminal-injection ledger the post-solve result uses, so these expressions and result["losses"]["p_loss"] are the same quantity: an objective built on one cannot silently disagree with the other.
S_loss = Σ V · conj(I_into_element) is BILINEAR in voltage and current, hence an exact smooth quadratic. A loss objective needs no smoothing and no magnitude — smooth_norm has no business here. (Semiconductor CONDUCTION loss is a different quantity: it is linear in |I| and does need the norm, but that is a device model rather than a network loss.)
Minimising loss is not minimising generation_cost. With heterogeneous generation prices the two disagree: least-loss dispatch will source from an expensive nearby unit rather than transport cheap distant power. Combine them deliberately with explicit weights.
Implemented in the BMOPFOpfExt extension (requires JuMP and a compatible optimizer loaded).
BMOPFTools.opf_element_loss — Function
opf_element_loss(ctx, block, id) -> JuMP expression
opf_total_loss(ctx; blocks=("line","transformer","switch")) -> JuMP expressionActive-power loss of one two-port element, or of the whole network, as a JuMP expression in the model's WORKING units (per-unit when per_unit=true).
Built from the same per-device terminal-injection ledger the post-solve result uses, so these expressions and result["losses"]["p_loss"] are the same quantity: an objective built on one cannot silently disagree with the other.
S_loss = Σ V · conj(I_into_element) is BILINEAR in voltage and current, hence an exact smooth quadratic. A loss objective needs no smoothing and no magnitude — smooth_norm has no business here. (Semiconductor CONDUCTION loss is a different quantity: it is linear in |I| and does need the norm, but that is a device model rather than a network loss.)
Minimising loss is not minimising generation_cost. With heterogeneous generation prices the two disagree: least-loss dispatch will source from an expensive nearby unit rather than transport cheap distant power. Combine them deliberately with explicit weights.
Implemented in the BMOPFOpfExt extension (requires JuMP and a compatible optimizer loaded).
BMOPFTools.opf_sequence_voltage — Function
opf_sequence_voltage(ctx, bus; component=:negative) -> (re, im)Symmetrical-component voltage at bus as a pair of affine JuMP expressions in the model's working units. component is :zero, :positive, or :negative.
These are the same expressions the bus sequence BOUNDS are built from (vneg_max, vzero_max, vpos_min/vpos_max), so a penalty and a limit on the same quantity cannot drift apart.
The reference is phase-to-neutral where the bus neutral floats and phase-to-ground otherwise; using phase-to-ground on a floating-neutral bus would fold the neutral displacement into the zero-sequence component. Requires a three-phase bus and throws otherwise, rather than reporting the unbalance of an imagined one.
The Fortescue transform is linear in the rectangular voltage variables, so both returned expressions are affine. re^2 + im^2 is therefore an exact smooth quadratic needing no smoothing; only a MAGNITUDE penalty needs smooth_norm.
Implemented in the BMOPFOpfExt extension (requires JuMP and a compatible optimizer loaded).
BMOPFTools.opf_current_term — Function
opf_current_term(ctx, elements; quantity=:neutral, component=:zero,
norm=:squared, weight=1.0)An OpfObjectiveTerm penalising branch currents over elements, each (block, id) or (block, id, side). quantity is :neutral or :sequence.
Converted to amps before reduction, so elements at different voltage levels are weighted on one physical footing and the weight is unit-mode independent.
Implemented in the BMOPFOpfExt extension.
BMOPFTools.opf_branch_currents — Function
opf_branch_currents(ctx, block, id; side=:from) -> Vector{(terminal, re, im)}
opf_neutral_current(ctx, block, id; side=:from) -> (re, im)
opf_sequence_current(ctx, block, id; side=:from, component=:zero) -> (re, im)Conductor currents at one end of a two-port element, in the model's working units. The sign is the current flowing INTO the element (out of the bus) — the conductor current, not the ledger's "into bus" convention.
opf_neutral_current is the current in the neutral terminal: the quantity that physically heats a neutral conductor, and usually what a 4-wire unbalance study wants to reduce. For a 4-wire element with no parallel earth path it equals −3·I₀, so it and a zero-sequence penalty are the same objective up to a factor of three — but this one is in amps of real conductor current. It throws on a three-wire element rather than returning a zero that would silently make the penalty vanish.
opf_sequence_current uses the same Fortescue convention as opf_sequence_voltage and requires exactly three phase terminals.
Implemented in the BMOPFOpfExt extension.
BMOPFTools.opf_neutral_current — Function
opf_branch_currents(ctx, block, id; side=:from) -> Vector{(terminal, re, im)}
opf_neutral_current(ctx, block, id; side=:from) -> (re, im)
opf_sequence_current(ctx, block, id; side=:from, component=:zero) -> (re, im)Conductor currents at one end of a two-port element, in the model's working units. The sign is the current flowing INTO the element (out of the bus) — the conductor current, not the ledger's "into bus" convention.
opf_neutral_current is the current in the neutral terminal: the quantity that physically heats a neutral conductor, and usually what a 4-wire unbalance study wants to reduce. For a 4-wire element with no parallel earth path it equals −3·I₀, so it and a zero-sequence penalty are the same objective up to a factor of three — but this one is in amps of real conductor current. It throws on a three-wire element rather than returning a zero that would silently make the penalty vanish.
opf_sequence_current uses the same Fortescue convention as opf_sequence_voltage and requires exactly three phase terminals.
Implemented in the BMOPFOpfExt extension.
BMOPFTools.opf_sequence_current — Function
opf_branch_currents(ctx, block, id; side=:from) -> Vector{(terminal, re, im)}
opf_neutral_current(ctx, block, id; side=:from) -> (re, im)
opf_sequence_current(ctx, block, id; side=:from, component=:zero) -> (re, im)Conductor currents at one end of a two-port element, in the model's working units. The sign is the current flowing INTO the element (out of the bus) — the conductor current, not the ledger's "into bus" convention.
opf_neutral_current is the current in the neutral terminal: the quantity that physically heats a neutral conductor, and usually what a 4-wire unbalance study wants to reduce. For a 4-wire element with no parallel earth path it equals −3·I₀, so it and a zero-sequence penalty are the same objective up to a factor of three — but this one is in amps of real conductor current. It throws on a three-wire element rather than returning a zero that would silently make the penalty vanish.
opf_sequence_current uses the same Fortescue convention as opf_sequence_voltage and requires exactly three phase terminals.
Implemented in the BMOPFOpfExt extension.
BMOPFTools.opf_reduce_norm — Function
opf_reduce_norm(ctx, pairs; norm=:squared, scale=1.0, eps_rel=1e-3, name="")Reduce complex quantities pairs (a vector of (re, im) expression pairs) to one scalar, by :squared (L2), :magnitude (group-lasso, via smooth_norm) or :max (L∞ through a convex-quadratic epigraph, minimisation only).
The choice changes the answer, not just the arithmetic: :squared improves every target a little, :magnitude drives a few to zero, :max protects the worst one. :squared and :max return the SQUARE of the input unit; :magnitude returns the input unit.
Implemented in the BMOPFOpfExt extension.
BMOPFTools.opf_control_effort_term — Function
opf_control_effort_term(ctx, devices; reference=nothing, norm=:magnitude,
weight=1.0)An OpfObjectiveTerm penalising how far dispatchable devices move from a reference operating point, as injected-current deviation in amps. devices is a collection of (block, id) with block "ibr" or "generator".
Current rather than P/Q deliberately: injected current is LINEAR in the decision variables, so the penalty is a norm of affine expressions.
With the default norm=:magnitude each device contributes ONE grouped norm over all its phases, making the penalty a group-lasso over devices: it drives whole devices to their reference rather than nudging every device slightly. That is the difference between "re-dispatch these two units" and "re-dispatch all forty by 3% each", which :squared cannot express.
Implemented in the BMOPFOpfExt extension.
BMOPFTools.opf_vuf_term — Function
opf_vuf_term(ctx, buses; weight=1.0, percent=true)An OpfObjectiveTerm penalising the SQUARED voltage unbalance factor, Σ (|V2|/|V1|)^2, in percent-squared by default (EN 50160 §3.5 and IEC 61000-2-2 state their limit as 2%, i.e. 4.0 here).
Squared deliberately. VUF looks like the one objective that must contain a square root, and building it from smooth_norm is actively wrong: the shift subtracts eps from numerator and denominator alike, and an eps sized to condition a norm heading to zero is comparable to the numerator itself — a true |V2| of 0.6 V against eps = 0.26 V mis-states the ratio by over 40%. The squared ratio needs no eps, is exact and smooth, and is strictly monotone in VUF so it orders solutions identically. Take sqrt post-solve.
The voltage base cancels in the ratio, so the term is identical in per-unit and SI by construction.
A ratio is well posed only while its denominator is bounded away from zero, and |V1| is decision-dependent. Every listed bus must declare vpos_min, which bus.jl enforces as an actual constraint; this throws otherwise rather than handing the solver a term it can drive to 0/0.
Prefer opf_sequence_term unless you specifically need the ratio: with |V1| near nominal the two order solutions almost identically, and the plain magnitude is exact, convex, cheaper, and has no denominator to guard.
Implemented in the BMOPFOpfExt extension.
BMOPFTools.opf_report_sequence_voltage — Function
opf_report_sequence_voltage(ctx, bus; component=:negative) -> Float64
opf_report_vuf(ctx, bus; percent=true) -> Float64
opf_report_current(ctx, block, id; side=:from, quantity=:neutral) -> Float64Post-solve reporting for the objective building blocks: solved magnitudes in PHYSICAL units (volts, percent, amps) in either unit mode. Call after JuMP.optimize!.
opf_report_vuf is the counterpart to opf_vuf_term, which minimises the SQUARED ratio — this returns the unsquared, directly comparable percentage. Reading a squared-VUF objective value as if it were a VUF is the mistake these helpers exist to prevent.
Implemented in the BMOPFOpfExt extension.
BMOPFTools.opf_report_vuf — Function
opf_report_sequence_voltage(ctx, bus; component=:negative) -> Float64
opf_report_vuf(ctx, bus; percent=true) -> Float64
opf_report_current(ctx, block, id; side=:from, quantity=:neutral) -> Float64Post-solve reporting for the objective building blocks: solved magnitudes in PHYSICAL units (volts, percent, amps) in either unit mode. Call after JuMP.optimize!.
opf_report_vuf is the counterpart to opf_vuf_term, which minimises the SQUARED ratio — this returns the unsquared, directly comparable percentage. Reading a squared-VUF objective value as if it were a VUF is the mistake these helpers exist to prevent.
Implemented in the BMOPFOpfExt extension.
BMOPFTools.opf_report_current — Function
opf_report_sequence_voltage(ctx, bus; component=:negative) -> Float64
opf_report_vuf(ctx, bus; percent=true) -> Float64
opf_report_current(ctx, block, id; side=:from, quantity=:neutral) -> Float64Post-solve reporting for the objective building blocks: solved magnitudes in PHYSICAL units (volts, percent, amps) in either unit mode. Call after JuMP.optimize!.
opf_report_vuf is the counterpart to opf_vuf_term, which minimises the SQUARED ratio — this returns the unsquared, directly comparable percentage. Reading a squared-VUF objective value as if it were a VUF is the mistake these helpers exist to prevent.
Implemented in the BMOPFOpfExt extension.
BMOPFTools.smooth_norm — Function
smooth_norm(ctx, x, y; scale, eps_rel=1e-3, annotate=true, name="")Smooth 2-norm sqrt(x^2 + y^2 + eps^2) - eps of a complex quantity (x, y), as a JuMP expression on ctx's model, with eps = eps_rel * scale.
The building block for any objective or constraint that needs the MAGNITUDE of a phasor where the exact norm's gradient would be singular. It is C-infinity everywhere including the origin (the exact norm's AD gradient there is 0/0, which Ipopt rejects outright), and it underestimates the exact norm by at most eps — a closed-form one-sided error budget rather than a tuning knob.
scale is the quantity's characteristic magnitude in the model's WORKING units (a conductor rating for a current, a nominal voltage for a voltage), so the relative smoothing is the same for every device in a heterogeneous fleet and the same in SI and per-unit. See ctx.bases for the conversion.
eps_rel's safe range depends on how the norm is used, and guidance correct in one regime is wrong in the other:
- The norm is being minimised (it is the objective, or a penalty term). The solve's endgame happens inside the smoothed region, so
epscontrols conditioning: keep it large, around the1e-3default. Shrinking it to solver-tolerance scale is measurably unreliable. - The norm is a coefficient in a term whose optimum is away from zero (a current-linear conduction loss, say). The solver never enters the smoothed region,
epscosts nothing, and it can be as small as the accuracy target wants.
Implemented in the BMOPFOpfExt extension (requires JuMP and a compatible optimizer loaded). See also register_opf_differentiability_annotation!, which this records by default so the approximation is never silent.
BMOPFTools.opf_physical_scale — Function
opf_physical_scale(ctx, unit; bus=nothing) -> Float64Physical value of one working unit of unit — the factor converting an expression in the model's working coordinates into SI. Returns 1.0 throughout when the model was built in SI, so a term written against it needs no branch.
unit is :W, :var, :VA, :V, :V2, :A, :A2, or :dimensionless. Voltage and current bases are PER BUS, so bus is required for those and omitting it throws — a system-wide voltage scale would be wrong on any network with more than one voltage level.
Implemented in the BMOPFOpfExt extension.
Configuration
BMOPFTools.load_config — Function
load_config(path=nothing) -> Dict{String,Any}Load a BMOPFTools threshold configuration.
With no argument, returns a fresh copy of the shipped defaults. With a path to a user TOML file, the file is parsed and deep-merged over the defaults, so the file need only specify the keys it wants to change.
The returned dict is sectioned exactly like config/default.toml (["domain_rules"], ["thermal"], ["provenance"]["grounding"], …) and is suitable to pass to any entry point's config= keyword:
cfg = load_config("my_lv_feeder.toml")
report = analyze(net; config=cfg)Advanced OPF semantic extension seam
The semantic-block registry is intentionally an advanced, qualified API rather than an exported convenience layer. Downstream builders that need to publish custom coordinate or residual semantics may call these names explicitly:
BMOPFTools.OpfSemanticBlock — Type
OpfSemanticBlock(id, kind, members, components, quantity, physical_unit;
set_contract=:none, model_to_canonical=I,
reference_physical_scale=nothing,
reference_scale_source=nothing, owner=:BMOPFTools,
metadata=Dict())Authoritative declaration that registered OPF objects form one semantic coordinate or residual block. members are ordered model coordinates; components name the corresponding canonical outputs (for example [:real, :imag] or [:active, :reactive]). model_to_canonical maps the ordered model components into that canonical component basis before physical unit scaling is applied.
reference_physical_scale is an optional positive device or operating scale for nondimensionalisation experiments. It is not the model-to-SI coordinate conversion and its provenance must be stated in reference_scale_source. The declaration is metadata only: registering a block never changes the model.
BMOPFTools.register_opf_semantic_block! — Function
register_opf_semantic_block!(ctx, block; replace=false)Register a non-mutating semantic coordinate/residual block whose members are already present in the public OPF object registry. A model object may belong to at most one registered block of its kind. Duplicate ids and overlapping members are rejected unless replacing the same block id explicitly. Register custom blocks after enforce_kcl! when possible: post-KCL registration materializes native blocks and reports conflicts at the registration call. If an overlapping custom block is registered before KCL, native materialization can fail later when a schema or provenance report is requested; the context must then be rebuilt because there is no unregister operation.
BMOPFTools.opf_semantic_blocks — Function
opf_semantic_blocks(ctx; kind=nothing)Return defensive copies of registered semantic blocks in deterministic id order. kind may be :variable, :constraint, or nothing.
Native semantic blocks are registered lazily on the first schema/provenance request after KCL finalisation, or when register_opf_semantic_block! is called after KCL finalisation. Ordinary solves therefore do not pay for diagnostic metadata they never inspect, while post-KCL custom registration incurs the same materialisation cost and surfaces overlaps at that call. Before KCL finalisation the schema reports semantic_blocks_available=false; a complete native registry is only claimed once the KCL rows and late auxiliary bounds exist.
Network simplification
BMOPFTools.simplify_network — Function
simplify_network(net;
open_switches = true,
closed_switches = true,
dangling_lines = true,
series_lines = true,
series_merge_policy = :allow_approximate,
allow_drop_bus_constraints = false) -> net′Apply selected topology simplifications in order:
collapse_closed_switches— merge bus pairs joined by zero-impedance closed switchesremove_open_switches— delete open switch elementsremove_dangling_lines— remove stub lines and their leaf busesmerge_series_lines— fuse consecutive same-linecode lines at pass-through buses
Each operation can be disabled individually via keyword arguments. Returns a deep copy of net with all selected operations applied; outcomes accumulate in net′["_simplification_log"].
BMOPFTools.merge_series_lines — Function
merge_series_lines(net; series_merge_policy=:allow_approximate,
allow_drop_bus_constraints=false) -> net′Return a deep-copied network with consecutive same-linecode lines fused at pass-through buses (buses with exactly two line connections and no other elements). Iterates to convergence so chains of three or more lines are fully collapsed in one call.
A pass-through bus is blocked — and a log entry emitted — when:
- the intermediate bus carries any non-line element (load, shunt, etc.): code
NON_LINE_ON_BUS(info) - a switch is the blocking element: code
SWITCH_IN_CHAIN(warning; likely user error — a switch placeholder left in the middle of a cable run) - the intermediate bus has grounded terminals (
perfectly_grounded_terminals): codeGROUNDED_BUS(warning) — the ground fixes terminal voltages and would be lost if the bus were deleted - nonzero π shunts in
:exactmode, or unsupported inline shunt overrides:PI_SHUNT_PRESENT(warning) - intermediate bus bounds, segment apparent-power or angle limits:
INTERMEDIATE_CONSTRAINT(warning); recovery is not implemented - adjacent linecodes differ: code
LINECODE_MISMATCH(info) - terminal maps at the shared bus are incompatible: code
TERMINAL_MISMATCH(warning)
The default series_merge_policy=:allow_approximate also combines same-linecode π sections by summing lengths, retaining the construction parameters. Each inexact merge emits SERIES_MERGE_APPROXIMATE with the original line IDs, removed bus, redistributed-shunt flag, and dropped constraint values. It does not claim small error or preservation of segment current limits. :exact refuses these merges; :off disables series merging. Set allow_drop_bus_constraints=true in approximate mode to permit removal of intermediate bus bounds separately. Segment apparent-power and angle limits remain blockers in every enabled mode.
Successful merges record _merged_from on the surviving line and emit LINES_MERGED (info). The merged series-only corridor's i_max is the element-wise minimum of the two segments' effective limits (each segment's line-level override if present, else its linecode rating), and constraints requiring intermediate voltage recovery block the merge.
This is a one-way, lossy transformation: the intermediate bus and the absorbed line's per-segment impedance are removed, and the reduction is recorded only in the package-level _simplification_log/_merged_from, not in the versioned data-model schema. Keep the original case as the exchanged artifact and treat the simplified network as a solve-time compile target — see the simplification tutorial and object identity.
All outcomes are appended to net′["_simplification_log"].
BMOPFTools.remove_dangling_lines — Function
remove_dangling_lines(net) -> net′Return a deep-copied network with all stub lines removed. A stub is a line whose far-end bus has exactly one line connection and no active elements (loads, generators, shunts, voltage sources, transformers, switches). The far-end bus is removed along with the line. Iterates to convergence so dangling chains are fully pruned.
Successful removals emit LINE_REMOVED (info). A stub that carries non-zero shunt admittance to earth (inline G_*/B_*, or via its linecode) is a shunt-to-earth, not electrically "nothing": pruning it drops an injection from the surviving bus's balance and can move the feasible set (negligible for LV overhead, material for cable charging). Such removals additionally emit SHUNT_DROPPED (warning); the line is still pruned. All outcomes appended to net′["_simplification_log"].
BMOPFTools.remove_open_switches — Function
remove_open_switches(net) -> net′Return a deep-copied network with all open switch elements (open_switch=true) deleted. The buses they connected are retained. Emits SWITCH_REMOVED (info) for each deleted switch and ISOLATED_BUS (warning) for any bus that has no remaining connections after the removal.
All outcomes appended to net′["_simplification_log"].
BMOPFTools.collapse_closed_switches — Function
collapse_closed_switches(net) -> net′Return a deep-copied network with closed switches (open_switch=false) removed by merging the two buses they connect. The bus_from bus always survives; the bus_to bus is absorbed and every element that referenced it is redirected. Terminal names and grounding sets are unioned; voltage bounds are tightened to their intersection. Iterates to convergence.
Collapse is blocked — and a warning logged — when:
- both buses carry a voltage source: code
MERGE_CONFLICT_SOURCE - the switch's terminal-map arities differ: code
MERGE_CONFLICT_TERMINALS - the switch is a self-loop: code
MERGE_CONFLICT_TERMINALS
Successful collapses emit SWITCH_COLLAPSED (info).
A closed switch carrying an i_max is flow-limited in the OPF like a line, but collapsing fuses its two buses into a single node — the cut its rating constrained no longer exists, so the limit cannot be projected onto a surviving branch and is dropped. This emits SWITCH_LIMIT_DROPPED (warning) and the feasible set may change; the collapse still proceeds. Keep closed_switches = false to retain a rated switch as an explicit zero-impedance branch.
All outcomes appended to net′["_simplification_log"].
Analysis passes
BMOPFTools.inventory_analysis — Function
inventory_analysis(net, findings) -> Dict{String,Any}Count components and compute summary statistics for each component type. Returns a dict keyed by component type name; each value is a sub-dict with at least a "total" count and type-specific breakdowns.
BMOPFTools.voltage_level_analysis — Function
voltage_level_analysis(net, findings) -> Dict{String,Any}Propagate nominal voltage through the network graph via BFS from each voltage source, assigning a voltage level to every bus. Transformers are level-change edges; all other edges (lines, switches) must connect buses at the same level.
Returns identified voltage levels, bus assignments, and any consistency violations.
BMOPFTools.connectivity_analysis — Function
connectivity_analysis(net, findings) -> Dict{String,Any}Build an undirected graph of the network and compute:
- Number of connected components
- Radial vs meshed topology (cycle detection)
- Degree statistics
- Tree depth and longest path from voltage source
- Open-switch isolated sections
BMOPFTools.diversity_analysis — Function
diversity_analysis(net, findings) -> Dict{String,Any}Assess parameter diversity across component categories. Flags datasets with high symmetry (low coefficient of variation, duplicate tuples) which may indicate copy-paste or templated parameterisation.
BMOPFTools.operational_analysis — Function
operational_analysis(net, findings) -> Dict{String,Any}Compute operational loading statistics:
- Total load and generation
- Load by voltage level and configuration
- Transformer utilisation at nominal setpoints
- Line thermal headroom
- Generation/load ratio
BMOPFTools.load_model_analysis — Function
load_model_analysis(net, findings) -> Dict{String,Any}Summarise load voltage-dependence and emit OPF-formulation flags:
I.LOAD.EXP_ZIP_EQUIVALENT— exponential loads with only integer exponents {0,1,2}; these can be represented as ZIP, keeping the OPF quadratic.W.LOAD.NL_NO_VMIN— voltage-dependent loads on buses without any lower voltage magnitude bound; the OPF squared-voltage variable then relies on a default floor rather than an engineering bound.
BMOPFTools.provenance_analysis — Function
provenance_analysis(net, findings) -> Dict{String,Any}Detect signatures of how the dataset was produced, and make implicit modeling assumptions explicit:
Linecode impedance classification — per linecode, classify the phase block of the series impedance matrix:
decoupled— diagonal matrix: positive-sequence-only data; the three phases are mathematically independent.exactly_balanced— equal self and equal mutual entries: constructed from sequence parameters (r1,x1,r0,x0) or under a transposition assumption. The implied Z1/Z0 are recovered and reported.near_balanced— balanced within 1%: possibly physical (twisted / bundled symmetric cable construction).distinct— consistent with first-principles geometry (Carson).
Also checks reciprocity (Z symmetric) and passivity (R block PSD).
The π-model topology of every line/linecode is classified separately (
series/symmetric_pi/asymmetric_pi/gamma) and the case study is audited for model consistency: a network-wide model is reported as uniform, a mix of topologies is flagged for review, and asymmetric-π, Γ-sections and modelled shunt conductance are called out individually.Wires per voltage level — 3-wire vs 4-wire classification. A 3-wire LV level is flagged as likely Kron-reduced (LV is physically 4-wire); 3-wire MV is normal. A 4-wire network whose neutrals are all perfectly grounded is exactly Kron-reducible (redundant variables).
Neutral grounding — builds the neutral-conductor continuity graph and verifies every neutral section reaches a grounding (perfect grounding, grounding shunt, or a source that references the neutral). Floating neutral sections leave the zero-sequence path undefined.
The summary string under "convention" states the inferred modeling convention explicitly; renderers print it in the report.
BMOPFTools.infeasibility_preflight — Function
infeasibility_preflight(net, findings) -> Dict{String,Any}Pre-flight checks that predict infeasibility risk before running the OPF. Does not run power flow — all checks are static data inspection.
Covers:
- Generation adequacy (local capacity vs load)
- Voltage bound tightness
- Constraint conflict detection (vmin > vmax, pmin > pmax)
- Topological single-point-of-failure risk
Validation passes
BMOPFTools.schema_check — Function
schema_check(net, findings) -> Dict{String,Any}Two-layer schema validation:
Layer 1 — JSONSchema.jl validation against the bundled JSON Schema for the spec version declared in meta.$schema. Catches type errors, missing required fields, enum violations (configuration must be "WYE", "DELTA", or "SINGLE_PHASE"), and nonnegative_number range constraints. Errors are reported as ERROR findings; additional (unknown) properties as INFO.
Layer 2 — hand-rolled unknown-field catalogue. Always runs; produces the unknown_fields_by_type result dict used by the dataset-assessment report section regardless of whether layer 1 ran.
Layer 1 is skipped (with an INFO finding) when the spec version in meta.$schema is not recognised — the hand-rolled catalogue still runs as a fallback.
BMOPFTools.completeness_check — Function
completeness_check(net, findings) -> Dict{String,Any}Two-layer completeness check:
- Hard: are all schema-required fields present?
- Soft: which optional fields are absent across the dataset?
BMOPFTools.domain_rules_check — Function
domain_rules_check(net, findings) -> Dict{String,Any}Check numerical plausibility of field values. All thresholds are configurable via the thresholds keyword (see _DEFAULT_THRESHOLDS).
BMOPFTools.redundancy_check — Function
redundancy_check(net, findings) -> Dict{String,Any}Identify redundant or trivial elements that do not contribute to the OPF problem and may indicate data quality issues.
BMOPFTools.integrity_check — Function
integrity_check(net, findings) -> Dict{String,Any}Structural integrity checks:
- Referential integrity: every
bus/bus_from/bus_toexists; every line'slinecodeexists; every terminal-map entry is a terminal of the referenced bus (this also enforces that nodal elements never connect directly to ground). - Dimension consistency: line terminal-map arity vs linecode matrix size;
i_maxvector length vs conductor count; load setpoint length vs configuration; sourcev_magnitude/v_anglelengths vs terminal map. - Padded matrices: all-zero row/column pairs in linecode impedances.
- Galvanic islands: transformer windings are galvanic separations; each island needs a voltage reference (source, perfect grounding, or a grounding shunt) or its voltages are defined only up to a shift.
- Wye without neutral: wye-configured loads/generators at buses with no identifiable neutral imply an undeclared ground return.
- Low-impedance lines: lines far below the network's typical series impedance — model them as (lossless) switches instead.
- Generator cost symmetry: identical cost vectors create dispatch degeneracy (any split between them is optimal).
BMOPFTools.spec_conformance_check — Function
spec_conformance_check(net, findings) -> Dict{String,Any}Check conformance with the TF data model beyond what the JSON Schema can express:
- exactly one voltage source (spec Eq. 17);
- load/generator configuration strings and configuration ↔ terminal-map arity consistency (SINGLE_PHASE = 2, WYE = 4, DELTA = 3 terminals);
- transformer terminal-map arities per subtype;
- linecode impedance matrices stored with all n² entries (spec §4.1.1 row-first format) rather than upper-triangular shorthand.
BMOPFTools.benchmark_readiness_check — Function
benchmark_readiness_check(net, findings) -> Dict{String,Any}Assess whether the case defines a non-trivial, well-posed OPF benchmark per the TF spec (generation-cost objective, Eq. 135), and list the augmentation steps needed to make it one. Also flags degeneracy risks that could cause primal non-uniqueness or flat objective directions.
Case preparation
See Case augmentation for the full reference and worked examples. The main entry points are fix_case / FixRecipe (structural repairs) and augment_case / AugmentationRecipe / default_recipe (standards-grounded gap-filling), together with the shared TransformationManifest / TransformEntry / manifest_to_dict / render_manifest audit trail.