gridoxide 0.1.0

AC power flow and state estimation solver with a sparse Newton-Raphson core
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
[workspace]
members = ["crates/gridoxide-core", "crates/gridoxide-sparse", "crates/gridoxide-mcp"]

# Shared by every crate in the workspace, so they release in lockstep.
[workspace.package]
version = "0.1.0"
edition = "2024"
repository = "https://github.com/m-mirz/gridoxide"

[package]
name = "gridoxide"
version.workspace = true
edition.workspace = true
build = "build.rs"
# Integration tests are one binary, `tests/all.rs`; see the comment there.
autotests = false
# gridoxide's own code is Apache-2.0. Every build depends on gridoxide-sparse,
# whose `src/klu_native/` (always built, no feature gate) is a close Rust
# translation of vendored SuiteSparse C — BSD-3-Clause for the AMD-derived
# pieces, LGPL-2.1-or-later for the BTF/KLU-derived pieces — so every build
# of gridoxide bundles all three. See
# crates/gridoxide-sparse/src/klu_native/PROVENANCE.md and
# docs/src/reference/provenance.md. (`--features klu` additionally links the
# vendored C itself, per crates/gridoxide-sparse/vendor/suitesparse/PROVENANCE.md
# — no licence beyond what's listed here.)
license = "Apache-2.0 AND BSD-3-Clause AND LGPL-2.1-or-later"
description = "AC power flow and state estimation solver with a sparse Newton-Raphson core"
repository.workspace = true
documentation = "https://m-mirz.github.io/gridoxide/"
readme = "README.md"
keywords = ["power-flow", "state-estimation", "newton-raphson", "sparse", "energy"]
categories = ["science", "mathematics", "simulation"]
# Keeps the published .crate to the source needed to *build* gridoxide. The
# excluded fixtures are only read by `cargo test`, which a crates.io consumer
# never runs, and two of them (tests/data/CGMES-Test-Configurations,
# tests/data/benchmark-grids) are git submodules that aren't checked out in the
# publish workflow at all — see .github/workflows/publish.yml.
exclude = [
    "tests/data/",
    # The VS Code extension that bundles gridmcp (crates/gridoxide-mcp).
    "editors/",
    "docs/book/",
    "scripts/bench/.case-cache/",
]

# "rlib" keeps the crate usable as a normal Rust dependency (examples/,
# tests/) exactly as before; "cdylib" is the loadable-by-Python extension
# module artifact maturin builds when the `python` feature is enabled — see
# that feature's doc comment below for why the two must never be built
# together in one `cargo` invocation.
# "staticlib" is for the `capi` feature: a C++ project linking gridoxide
# usually wants `libgridoxide.a` so the Rust runtime comes along in one
# artifact rather than as a second file to install. It costs nothing when the
# feature is off — cargo simply produces an archive nothing reads.
# docs.rs builds with these and nothing else, which is the set that makes every
# intra-doc link resolve. It is not `all-features`, and cannot be: `klu`,
# `pardiso`, `opf-highs`, `opf-ipopt` and `smallsignal-arpack` each link a system
# library docs.rs does not have.
#
# `python` and `capi` appear together here even though they must never be *built*
# together — `pyo3`'s `extension-module` leaves libpython unlinked, which is
# right for something Python dlopens and wrong for a library a C++ binary links.
# Documenting does not link, so the pair is fine for rustdoc and only for
# rustdoc.
#
# Fewer features is not an error, only a thinner crate: a link from an
# always-compiled file into a feature-gated module is unresolved without that
# feature, which is what `cargo doc` with no features reports and what it should.
[package.metadata.docs.rs]
features = ["cgmes", "ucte", "iidm", "opf", "rao", "dynamics", "capi", "python"]

[lib]
crate-type = ["cdylib", "rlib", "staticlib"]

[dependencies]
# The sparse LU backends and the vendored SuiteSparse they build on — see
# crates/gridoxide-sparse. Its modules are re-exported under their old names
# in src/lib.rs, so `gridoxide::sparse` and the rest resolve as before.
gridoxide-sparse = { path = "crates/gridoxide-sparse", version = "0.1.0" }
# The solvers — network model, power flow, state estimation, short circuit —
# re-exported the same way; see crates/gridoxide-core.
gridoxide-core = { path = "crates/gridoxide-core", version = "0.1.0" }
faer = "0.24.4"
num-complex = { version = "0.4.6", features = ["serde"] }
# Powers `crates/gridoxide-core/src/batch.rs`'s across-scenario parallelism. Not a new build cost:
# `faer` (and `gemm` beneath it) already pull rayon in, so this only promotes
# an existing transitive dependency to a direct one. Sharing rayon's pool
# rather than spawning raw OS threads is deliberate — it keeps any nested
# parallelism inside faer from oversubscribing the machine, and it means a
# batch never pays thread-creation cost per call.
rayon = "1.12"
# The IIDM importer's XML reader (`iidm` feature). Already present in the tree
# as a transitive dependency of `cimmodel`, so enabling `iidm` adds nothing to
# a build that already has `cgmes` — this promotes it to a direct dependency the
# same way `rayon` was promoted for `crates/gridoxide-core/src/batch.rs`.
quick-xml = { version = "0.37", optional = true }
pyo3 = { version = "0.29.0", features = ["extension-module"], optional = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# cimoxide (https://github.com/m-mirz/cimoxide) publishes its CGMES model and
# decoder as `cimoxide-model`, whose library is named `cimmodel`; `package =`
# makes the dependency's name match. It replaced 0.3's `cimoxide-decoder` and
# `cimoxide-structs` — see docs/src/reference/provenance.md.
cimmodel = { package = "cimoxide-model", version = "0.6.0", optional = true }

[build-dependencies]
bindgen = { version = "0.72", optional = true }
# The outward-facing counterpart of `bindgen`: generates `include/gridoxide.h`
# from the `#[no_mangle]` surface in `src/capi/`. See the `capi` feature.
cbindgen = { version = "0.29", optional = true }

[features]
default = []
# Builds the vendored SuiteSparse KLU solver
# (crates/gridoxide-sparse/vendor/suitesparse/) as a third, opt-in
# JacobianBackend — see docs/src/solvers/backends.md. Needs a C compiler and
# libclang (for bindgen) at build time. KLU and BTF (one of KLU's own
# dependencies) are LGPL-2.1-or-later — see that directory's PROVENANCE.md
# before enabling this in a distributed binary. The build itself happens in
# gridoxide-sparse; this forwards to it.
klu = ["gridoxide-sparse/klu", "gridoxide-core/klu"]
# Links a system-installed libklu.so instead of compiling the vendored
# source statically, for anyone who needs strict LGPL relinking compliance.
klu-dynamic = ["klu", "gridoxide-sparse/klu-dynamic"]
# Builds `src/python.rs`'s PyO3 bindings (the `gridoxide` Python extension
# module — see scripts/bench/README.md and pyproject.toml). Only ever build
# this via `maturin` (`maturin develop --features python`), never as part of
# a plain `cargo build`/`cargo test` invocation alongside examples or test
# binaries: `pyo3`'s `extension-module` feature (needed so the built
# `cdylib` doesn't try to link against libpython itself, since Python will
# dlopen it and provide the symbols at runtime) makes a normal standalone
# binary/test harness fail to link, since *those* do need libpython linked
# in directly. This is a well-known, expected PyO3 constraint, not a
# gridoxide-specific issue.
python = ["dep:pyo3", "matpower"]
# Builds a fifth, opt-in `JacobianBackend` (`Pardiso`) backed by Intel oneMKL's
# PARDISO sparse direct solver, linked dynamically against a locally-installed
# oneMKL (discovered via the `MKLROOT` env var — see
# docs/src/solvers/backends.md). Unlike `klu`, nothing is vendored or
# compiled from
# source: MKL is proprietary (Intel Simplified Software License, not OSS), so
# this only ever links a system install's `libmkl_rt.so` and generates FFI
# bindings via `bindgen` against that install's own `mkl_pardiso.h` — nothing
# MKL-derived is redistributed by this crate, so `Cargo.toml`'s `license`
# field doesn't need to change for this feature. Needs `libclang` (for
# `bindgen`) at build time, plus `MKLROOT` set (e.g. `source
# /opt/intel/oneapi/setvars.sh`). Not built or tested in CI — no CI runner
# has MKL installed — so this is a local/manual-verification-only backend.
pardiso = ["gridoxide-sparse/pardiso", "gridoxide-core/pardiso"]
# Builds CGMES (Common Grid Model Exchange Standard) RDF/XML dataset support
# (src/cgmes.rs), converting a CGMES EQ+SSH+TP+SV profile set into gridoxide's
# own Bus/Line/Transformer model. Depends on cimoxide's `cimoxide-model` crate
# from crates.io, renamed via `package =` above — see
# docs/src/reference/provenance.md. Opt-in since most gridoxide users only need
# PGM-JSON input and shouldn't pay for cimmodel's own dependency tree or
# build time.
cgmes = ["dep:cimmodel"]
# Reads UCTE-DEF network files (src/ucte.rs) — the fixed-column format the
# continental-European TSOs exchanged snapshots in before CGMES, and the one
# most published remedial-action test material is still written in.
#
# Unlike `cgmes` this pulls in **no dependency at all**: UCTE-DEF is plain
# fixed-column text, so the importer is pure `std`. It is therefore built and
# tested in CI, alongside `opf` and `capi` rather than alongside the
# system-library features.
#
# It is also the only importer that arrives carrying branch *ratings* and tap
# *tables* — see `crates/gridoxide-core/src/ratings.rs` and `types::TapChanger` for why those two
# were the gap worth closing.
ucte = []
# Reads MATPOWER case files (src/matpower.rs) — the `.m` text format the
# standard transmission test cases (IEEE, PEGASE, RTE, PGLib) are published
# in. Plain text, no dependency, like `ucte`. Read directly rather than
# through `python/gridoxide/matpower.py`'s PGM conversion, which puts the
# reference bus behind a source impedance and rounds phase shifts to 30°.
# The Python bindings always include it.
matpower = []
# Reads PowSyBl's IIDM network format (src/iidm/mod.rs) — `.xiidm` files, the native
# serialization of powsybl-core's network model, and the second of the two
# formats the published remedial-action test material comes in.
#
# Deliberately **version-tolerant**: it accepts any `.../schema/iidm/1_*`
# namespace and skips elements it does not know rather than failing. The 51
# vendored fixtures span eleven schema versions (1_0 through 1_16) and the model
# moved underneath them — limits are spelled `currentLimits1` in the older files
# and `operationalLimitsGroup1` in the newer. A strict parser would rot against
# the next powsybl release; this one is designed not to.
#
# Costs one dependency (`quick-xml`) and no system library, so like `ucte` and
# `opf` it is built and tested in CI.
iidm = ["dep:quick-xml"]
# The remedial-action layer (src/rao/). At present the
# CRAC data model and its readers; the optimizer follows.
#
# Depends on `opf` for the `LinearProgram`/`Solver` boundary the range-action
# optimization is written against — not on any *backend*, so the default
# in-house interior-point solver serves and nothing needs installing. Built and
# tested in CI.
rao = ["opf"]
# The optimization layer, complete and self-contained: `opf::LinearProgram`,
# `opf::Solution` and the `opf::Solver` trait (src/opf/mod.rs), DC-OPF
# (src/opf/dc.rs), and the in-house primal-dual interior-point QP solver
# (src/opf/ipm.rs) that answers them. Pure Rust on this crate's own sparse
# factorization — no system libraries, no C toolchain, nothing to install — so
# unlike `klu`/`pardiso`/`opf-highs` this one *is* built and tested in CI.
#
# This is what `gridoxide opf` and the Python `dc_opf` need. `opf-highs` below
# is an optional second backend, not a requirement.
opf = []
# RMS (phasor-domain) dynamic simulation — src/dynamics/. The DAE, its
# integrator, and the machine/exciter/governor model library; see
# docs/src/dynamics/.
#
# Like `opf`, this is **complete and self-contained**: pure Rust on this
# crate's own sparse factorization and its existing `solver::LinearSolver`
# backends, no system library, no C toolchain, nothing to install. So it is
# built and tested in CI, unlike `klu`/`pardiso`/`opf-highs`.
#
# Feature-gated anyway, for the same reason `opf` is: it is a large module that
# most users of a power-flow crate never call, and the gate is about build cost
# rather than about dependencies. The Dynawo `.dyd`/`.par` reader additionally
# needs `iidm` for `quick-xml`; everything else here needs nothing.
dynamics = []
# Builds the HiGHS backend for `opf` (src/opf/highs.rs). Like `pardiso`,
# nothing is vendored or compiled from source: this links a *system* HiGHS
# install and generates FFI bindings via `bindgen` against that install's own
# `highs_c_api.h`, so nothing HiGHS-derived is redistributed and
# `Cargo.toml`'s `license` field doesn't change (HiGHS is MIT in any case).
#
# Needs `libclang` (for `bindgen`) plus a local HiGHS with its development
# files — on Debian/Ubuntu that is `apt install libhighs-dev`. The install is
# found at `/usr` by default; set `HIGHS_ROOT` for anything else.
#
# Not built or tested in CI, since no runner has HiGHS installed — the same
# local/manual-verification-only position `pardiso` is in. Unlike `pardiso`,
# nothing depends on it: `opf`'s own interior-point solver is the default
# backend, and this one is the *reference* the two are cross-checked against in
# tests/opf_cross_test.rs. On a convex problem the optimum is unique, so a
# disagreement between them is a bug in one — which is a sharper signal than
# either could produce alone, and worth keeping the feature for even though it
# is no longer needed to solve anything.
opf-highs = ["opf", "dep:bindgen"]
# Builds the IPOPT backend for `opf`'s *nonlinear* solver (src/opf/ipopt.rs) —
# the AC-OPF counterpart of what `opf-highs` is for DC. Same arrangement again:
# nothing vendored, nothing compiled from source, bindings generated via
# `bindgen` against a *system* IPOPT's own `IpStdCInterface.h`.
#
# That arrangement is what keeps IPOPT's EPL-2.0 licence off this crate's
# `license` field. Nothing IPOPT-derived is redistributed here; the feature
# links a library the user installed, exactly as `pardiso` links a
# separately-licensed oneMKL. Enabling it in a *distributed binary* is the
# user's licensing decision, not this crate's.
#
# Needs `libclang` (for `bindgen`) plus a local IPOPT with its development
# files — on Debian/Ubuntu that is `apt install coinor-libipopt-dev`. Found at
# `/usr` by default; set `IPOPT_ROOT` for anything else.
#
# Not built or tested in CI, and nothing depends on it: `opf::nlp` is the
# default nonlinear solver and this is the *reference* the two are
# cross-checked against, the same role `opf-highs` plays for `opf::ipm`. On a
# nonconvex problem the two need not agree — they may find different local
# optima — which makes the comparison weaker than the convex one and worth
# reading carefully rather than as a pass/fail.
opf-ipopt = ["opf", "dep:bindgen"]
# Builds the ARPACK cross-check for the sparse small-signal eigensolver
# (src/dynamics/arpack.rs) — the modal-analysis counterpart of what `opf-highs`
# is for DC-OPF and `opf-ipopt` for AC-OPF, and it plays exactly that role: the
# reference `dynamics::arnoldi`'s own iteration is checked against, not
# something anything depends on. Both are handed the *same* shift-invert
# operator, so a disagreement isolates the eigensolver and nothing else.
#
# Unlike its three siblings it runs no `bindgen`: ARPACK's C surface here is two
# functions, declared by hand — see `build.rs`'s `arpack` module for why.
#
# Needs a local ARPACK with its development files (on Debian/Ubuntu: `apt
# install libarpack2-dev` — the runtime package alone ships only
# `libarpack.so.2`, which cannot be linked against). Found at `/usr` by default;
# set `ARPACK_ROOT` for anything else. Not built or tested in CI, since no
# runner has ARPACK installed — the same local/manual-verification-only position
# `pardiso`, `opf-highs` and `opf-ipopt` are in.
smallsignal-arpack = ["dynamics"]
# Exports a C ABI (src/capi/) so a C++ project can embed gridoxide, plus the
# `cbindgen`-generated `include/gridoxide.h` that describes it.
#
# This is the **mirror image** of `klu`/`pardiso`/`opf-highs`/`opf-ipopt`.
# Those four consume a C header to reach a library; this one produces a C
# header so a library can reach us. `build.rs` gained a `capi` module shaped
# like its four siblings, but running cbindgen rather than bindgen.
#
# Unlike them it has **no system dependency** — pure Rust plus a build-time
# crate — so, like `opf`, it is built and tested in CI.
#
# **Never build this together with `python`.** `pyo3`'s `extension-module`
# deliberately leaves libpython unlinked, which is right for something Python
# dlopens and wrong for a library a C++ binary links directly; the two produce
# incompatible cdylibs from one `cargo` invocation. The same trap the `python`
# feature already documents for tests and examples.
capi = ["dep:cbindgen"]

[[test]]
name = "all"
path = "tests/all.rs"

[[example]]
name = "smallsignal_scale"
path = "examples/smallsignal_scale.rs"
required-features = ["dynamics"]

[[example]]
name = "dynamics_scale"
path = "examples/dynamics_scale.rs"
required-features = ["dynamics"]

[[example]]
name = "doc_numbers"
path = "examples/doc_numbers.rs"
required-features = ["rao", "ucte"]

[[example]]
name = "voltage_control_probe"
path = "examples/voltage_control_probe.rs"
required-features = ["cgmes"]

[[example]]
name = "tap_regulation_probe"
path = "examples/tap_regulation_probe.rs"
required-features = ["cgmes"]

[[example]]
name = "tap_control_probe"
path = "examples/tap_control_probe.rs"
required-features = ["cgmes"]

[[example]]
name = "klu_scaling_stress"
path = "examples/klu_scaling_stress.rs"
required-features = ["matpower", "cgmes", "klu"]

[[example]]
name = "pf_profile"
path = "examples/pf_profile.rs"
required-features = ["matpower"]

[[example]]
name = "tap_deadband_probe"
path = "examples/tap_deadband_probe.rs"
required-features = ["cgmes"]

[[example]]
name = "qv_probe"
path = "examples/qv_probe.rs"

[[example]]
name = "base_mismatch_probe"
path = "examples/base_mismatch_probe.rs"
required-features = ["cgmes"]

[[example]]
name = "remote_probe"
path = "examples/remote_probe.rs"
required-features = ["cgmes"]

[[example]]
name = "dispatch_probe"
path = "examples/dispatch_probe.rs"
required-features = ["cgmes"]

[[example]]
name = "cgmes_area_probe"
path = "examples/cgmes_area_probe.rs"
required-features = ["cgmes"]

[[example]]
name = "ucte_area_probe"
path = "examples/ucte_area_probe.rs"
required-features = ["ucte"]

[[example]]
name = "iidm_area_probe"
path = "examples/iidm_area_probe.rs"
required-features = ["iidm"]

[[example]]
name = "iidm_regulation_probe"
path = "examples/iidm_regulation_probe.rs"
required-features = ["iidm"]

[[example]]
name = "bench_node_breaker"
path = "examples/bench_node_breaker.rs"
required-features = ["cgmes"]

[[example]]
name = "cgmes_microgrid_be_dump"
path = "examples/cgmes_microgrid_be_dump.rs"
required-features = ["cgmes"]






# Line tables only: backtraces and panics keep file and line, but a debugger
# loses local variables. Full debug info made `target/debug` tens of gigabytes
# and was most of what the linker had to write. Override for a debugging
# session with `CARGO_PROFILE_DEV_DEBUG=true`.
[profile.dev]
debug = "line-tables-only"