BMOPFTools.jl

A Julia library for parsing, validating, analysing and reporting on BMOPF-format distribution network datasets — the JSON data model developed by the IEEE Task Force on Benchmarking Multiconductor OPF for Distribution Systems for up-to-four-wire optimal power flow (OPF) benchmarks (ref. 1).

The term optimal power flow is used throughout, but the Task Force's scope extends well beyond generation cost minimisation. The unifying requirement across all targeted problem classes — CVR, Dynamic Operating Envelopes, state estimation, maximum load delivery — is a faithful, conductor-level representation of an unbalanced distribution network subject to a selectable set of bounds. Voltage limits, current ratings and power constraints are therefore optional in the data model; different formulations activate different subsets. See Optimal power flow for the full motivation.

The library serves three use cases:

  • Dataset producers converting utility-derived OpenDSS models into clean, spec-conformant BMOPF JSON benchmark cases,
  • dataset consumers who want to understand exactly what a case contains — its modeling conventions, hidden assumptions, data-quality issues and OPF-readiness — before building optimization models on it, and
  • optimization practitioners who, given a solved BMOPF case and an OPF result dict, want to flag bound violations, near-active constraints, constraint residuals, and solution-quality issues without access to solver internals.

For research packages, the embedded IVR-EN engine also exposes staged model construction, typed replaceable device ownership, semantic JuMP object and parameter registries, and an explicit DiffOpt-compatible nonlinear encoding. See Parameterized and differentiable extensions.

Design

The network data model is a plain Dict{String,Any} mirroring the BMOPF JSON structure exactly. There are deliberately no wrapper types: data flows to and from JSON (and out to PowerModelsDistribution via to_pmd) without conversion layers, and the only structs in the library are the outputsFinding, SummaryReport, and SolutionReport — which need stable shape for rendering and programmatic use.

Ingestion follows from this. from_dss is a semantic projection onto that canonical model, not a byte-faithful transcode: it infers phase and neutral identity, fingerprints voltage regulators, and records every transformation as provenance, so the result is analysable and reproducible rather than merely a copy of the source deck. The goal is reproducible compatibility with deliberate surgery, not losslessness — see the design philosophy for the rationale and its precedents in CIM, PowerModelsDistribution, FAIR, and tidy-data.

Every diagnostic is a Finding with a stable dot-separated code (E./W./I. for error/warning/info) — see the finding-code reference for the complete catalogue. Match on codes, never on message text.

Installation

BMOPFTools requires Julia ≥ 1.10. New to Julia? The Installation & first steps page covers installing Julia itself (via juliaup), what the julia> prompt is and where these commands go, and how to set up for the tutorials — start there. The package is not yet in the General registry, so install it from its Git URL at the julia> prompt:

using Pkg
Pkg.add(url = "https://github.com/frederikgeth/BMOPFTools.jl")

Parsing, validation, analysis, reporting, OpenDSS ingestion (from_dss, via the PowerIO.jl dependency), and the to_pmd exporter work out of the box. One capability pulls in extra tooling, activated only when you load it:

  • OPF / power flow (solve_opf, solve_pf, solve_feasibility_opf) lives in a package extension that is loaded once JuMP and a solver such as Ipopt are present in the environment:

    Pkg.add(["JuMP", "Ipopt"])
Tracking a moving target

The package is under rapid development, with breaking changes landing directly on main. Pin a specific revision when you need reproducibility: Pkg.add(url = "https://github.com/frederikgeth/BMOPFTools.jl", rev = "<commit-sha>").

Quickstart

Analysing an existing BMOPF JSON case:

using BMOPFTools

net    = parse_bmopf("case.json")
report = analyze(net)

render(report, stdout)              # terminal report
render(report, "case_report.md")    # Markdown report

errors(report)                      # findings with ERROR severity
report.results[:provenance]["convention"]
report.results[:benchmark]["suggestions"]

Profiling an OPF result dict (requires solve_opf or any compatible solver):

using BMOPFTools, JuMP, Ipopt

net    = parse_bmopf("case.json")
result = solve_opf(net; optimizer=Ipopt.Optimizer)

report = profile_solution(net, result)
render_solution(report, stdout)              # Markdown to terminal
render_solution(report, "solution.md")      # Markdown file

errors(report)     # bound violations and infeasibility findings
warnings(report)   # near-active bounds and residual warnings

Converting from OpenDSS (parsed in-process by PowerIO.jl):

using BMOPFTools

net = from_dss("Master.dss")
write_bmopf(net, "case.json")
analyze(net) |> r -> render(r, "case_report.md")

The pipeline

OpenDSS .dss ──(from_dss / PowerIO.jl)──► BMOPF Dict{String,Any} ◄── parse_bmopf ◄── BMOPF JSON
                                                 │               └──── write_bmopf
                                                 │ analyze
                                                 ▼
                                          SummaryReport ──► render
                                                 │ fix_case
                                                 ▼
                                          net′ (repaired)
                                                 │ add_generators / add_ibrs  (optional DER)
                                                 ▼
                                          net′ + DERs
                                                 │ augment_case
                                                 ▼
                                    net″ (benchmark-ready) ──► write_bmopf
                                                 │ solve_opf / solve_pf / to_pmd
                                                 ▼
                                         result Dict{String,Any}
                                                 │ profile_solution
                                                 ▼
                                        SolutionReport ──► render_solution

analyze runs fifteen passes (see Analysis & reports) and the report renders in nine sections, including a one-line modeling convention statement (wires per voltage level, grounding style, normalisations) so the case's assumptions are explicit rather than implied.

Where to go next

  • End-to-end tutorialstart here: the full load → analyze → fix → place DERs → augment → solve pipeline on one real feeder, with every code block executed at build time.
  • Positioning & ecosystem — where BMOPFTools fits among distribution optimization and modelling tools, and the benchmarking gap it targets.
  • Data model conventions — units, terminal names, transformer subtypes, grounding semantics.
  • Conversion guide — every deliberate decision in to_pmd, with the impedance-base formulas.
  • Analysis & reports — what each pass computes and how to read the report.
  • Finding-code reference — complete catalogue of finding codes, with triggers and rationale.
  • Methodology notes — the physics and linear algebra behind the provenance checks, with literature references.
  • Optimal power flow — running solve_opf, solver options, and the OPF extension model.
  • OPF result dictionary — structure of the result dict returned by solve_opf, including the "initialisation" diagnostics block.
  • Case augmentation — the fix_case → add_generators → augment_case pipeline that turns a faithful import into a meaningful OPF benchmark, why the sequence and the diverse DER strategies matter, and the manifest audit trail.
  • API reference.