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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
/* gridoxide — a C ABI for the gridoxide power-systems library.
 *
 * GENERATED BY cbindgen FROM src/capi/. DO NOT EDIT BY HAND.
 * Regenerate with:  cargo build --features capi
 *
 * A C++ convenience wrapper over this header lives beside it in
 * gridoxide.hpp — RAII handles, std::vector results and exceptions.
 *
 * THREADING: a gridoxide_powerflow_t is single-thread-affine. Separate
 * handles on separate threads are fine; one handle touched from two threads
 * is not.
 */

#ifndef GRIDOXIDE_H
#define GRIDOXIDE_H
/**
 * The ABI this header describes. Compare it against `gridoxide_abi_version()`
 * at run time when linking a prebuilt library, so a layout mismatch fails
 * early instead of quietly corrupting a struct.
 */
#define GRIDOXIDE_ABI_VERSION 1


#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>

/**
 * How a call ended.
 *
 * The split between [`InvalidArgument`](Self::InvalidArgument) and the
 * numerical codes mirrors the `PyValueError`/`PyRuntimeError` distinction
 * `src/python.rs` already draws: the first says *you* passed
 * something wrong, the rest say the computation or its environment did not
 * work out.
 */
typedef enum gridoxide_status {
    /**
     * The call succeeded.
     */
    GRIDOXIDE_STATUS_OK = 0,
    /**
     * A caller-supplied argument was wrong: a null pointer, an out-of-range
     * index, a buffer of the wrong length, an unknown enum value.
     */
    GRIDOXIDE_STATUS_INVALID_ARGUMENT = 1,
    /**
     * A file could not be read.
     */
    GRIDOXIDE_STATUS_IO = 2,
    /**
     * A document could not be parsed.
     */
    GRIDOXIDE_STATUS_PARSE = 3,
    /**
     * The solve ran out of iterations. **The state is still readable** — the
     * voltages are whatever the last iteration produced, and the per-island
     * detail says which components failed to settle.
     */
    GRIDOXIDE_STATUS_NOT_CONVERGED = 4,
    /**
     * A matrix was singular. As above, the state is readable but meaningless.
     */
    GRIDOXIDE_STATUS_SINGULAR = 5,
    /**
     * The question does not apply here — see the module docs. Not a failure.
     */
    GRIDOXIDE_STATUS_NO_ANSWER = 6,
    /**
     * A panic was caught at the boundary. Always a bug in gridoxide; the
     * message from [`gridoxide_last_error_message`] should go in the report.
     */
    GRIDOXIDE_STATUS_PANIC = 7,
    /**
     * Anything else.
     */
    GRIDOXIDE_STATUS_INTERNAL = 8,
} gridoxide_status;

/**
 * Which sparse-LU backend factorizes the Jacobian.
 *
 * The two beyond `Scalar` and `Block` need their cargo features compiled in;
 * asking for one that is not produces
 * [`Status::InvalidArgument`](super::Status::InvalidArgument) rather than a
 * silent downgrade, because a caller selecting a backend usually has a reason.
 */
typedef enum gridoxide_backend {
    /**
     * faer's sparse LU. Always available.
     */
    GRIDOXIDE_BACKEND_SCALAR = 0,
    /**
     * Per-bus 2×2 real blocks. Always available.
     */
    GRIDOXIDE_BACKEND_BLOCK = 1,
    /**
     * SuiteSparse KLU. Needs the `klu` feature.
     */
    GRIDOXIDE_BACKEND_KLU = 2,
    /**
     * The pure-Rust KLU port. Needs no feature; the default.
     */
    GRIDOXIDE_BACKEND_KLU_NATIVE = 3,
    /**
     * Intel oneMKL PARDISO. Needs the `pardiso` feature.
     */
    GRIDOXIDE_BACKEND_PARDISO = 4,
} gridoxide_backend;

/**
 * What a bus is for the solve. Values are fixed by the ABI, not by the
 * discriminants of [`BusType`].
 */
typedef enum gridoxide_bus_type {
    /**
     * Angle and magnitude fixed; active power is the answer.
     */
    GRIDOXIDE_BUS_TYPE_SLACK = 0,
    /**
     * Magnitude and active power fixed; reactive power is the answer.
     */
    GRIDOXIDE_BUS_TYPE_PV = 1,
    /**
     * Both powers fixed; the voltage is the answer.
     */
    GRIDOXIDE_BUS_TYPE_PQ = 2,
} gridoxide_bus_type;

/**
 * How one connected component of the network turned out.
 *
 * The last two are **not failures**. A real network routinely contains a
 * de-energized pocket, and a solve containing one still returns
 * [`Status::Ok`] — so this is the only place such a pocket becomes visible.
 */
typedef enum gridoxide_island_status {
    /**
     * This component's own mismatch is below tolerance.
     */
    GRIDOXIDE_ISLAND_STATUS_CONVERGED = 0,
    /**
     * The solve ran out of iterations with this component still unsettled.
     */
    GRIDOXIDE_ISLAND_STATUS_MAX_ITERATIONS_REACHED = 1,
    /**
     * The factorization was singular. Attribution is best-effort: one
     * combined matrix is factorized, so every unconverged component is marked
     * this way whether or not it was the cause.
     */
    GRIDOXIDE_ISLAND_STATUS_SINGULAR = 2,
    /**
     * No source in this component, so nothing pins its voltage. Reported, not
     * solved.
     */
    GRIDOXIDE_ISLAND_STATUS_NO_REFERENCE_BUS = 3,
    /**
     * More than one source, so the reference is ambiguous.
     */
    GRIDOXIDE_ISLAND_STATUS_AMBIGUOUS_REFERENCE_BUS = 4,
} gridoxide_island_status;

/**
 * An opaque power-flow model. **Single-thread-affine** — see the module docs
 * on [`capi`](super).
 *
 * In C this is `gridoxide_powerflow`, reached only through a pointer.
 */
typedef struct gridoxide_powerflow gridoxide_powerflow;

/**
 * Everything that shapes a solve, in one struct.
 *
 * A struct rather than eight positional parameters because the equivalent
 * Python entry points already carry `#[allow(clippy::too_many_arguments)]`
 * twice — and because adding a field here is source-compatible for a C++
 * caller who zero-initializes, where adding a parameter is not.
 *
 * Zero-initializing gives an unusable configuration (`tol = 0`,
 * `max_iter = 0`); call
 * [`gridoxide_options_default`] first and then
 * override what you care about.
 */
typedef struct gridoxide_options {
    enum gridoxide_backend backend;
    /**
     * Convergence tolerance on the largest power mismatch, per-unit.
     */
    double tolerance;
    size_t max_iterations;
    /**
     * System power base in VA, used when reading a PGM document. Ignored by
     * the in-memory path, which is per-unit already.
     */
    double s_base_va;
    /**
     * Nominal frequency in Hz, used when reading a PGM document.
     */
    double frequency_hz;
} gridoxide_options;

/**
 * A bus.
 *
 * # What is missing, and why
 *
 * **ZIP terms.** [`Bus`] can carry voltage-dependent load
 * components, which are a `Vec` and so have no place in a POD struct. A
 * network needing them must come in through the PGM document path, where they
 * are read as a matter of course.
 *
 * This is a real limitation and is stated in the header rather than left to be
 * discovered: a caller who has ZIP loads and uses this path gets them silently
 * treated as constant power, which converges perfectly well to the wrong
 * answer.
 */
typedef struct gridoxide_bus {
    enum gridoxide_bus_type bus_type;
    /**
     * Per-unit. For `Slack` and `Pv` this is the setpoint; elsewhere the
     * starting guess.
     */
    double voltage_magnitude;
    /**
     * Radians. Only meaningful for `Slack`.
     */
    double voltage_angle;
    /**
     * Per-unit specified injection, **generation minus load** — so a load bus
     * is negative.
     */
    double p_spec;
    double q_spec;
    /**
     * Per-unit reactive limits, used only by the Q-limit-enforcing solve.
     * Pass `-INFINITY` / `INFINITY` for an unlimited machine.
     */
    double q_min;
    double q_max;
    /**
     * Rated line-to-line voltage in volts, used to report `voltage_kv`.
     * Zero if you do not need it.
     */
    double u_rated;
} gridoxide_bus;

/**
 * A line, in per-unit on the system base.
 *
 * `b_shunt`/`g_shunt` are the **total** charging of the π-model, split evenly
 * across the two terminals — not the per-terminal half.
 */
typedef struct gridoxide_line {
    size_t from;
    size_t to;
    double r;
    double x;
    double b_shunt;
    double g_shunt;
} gridoxide_line;

/**
 * A complex number, as C sees it. Layout-compatible with `double[2]`,
 * `std::complex<double>` and `double _Complex`.
 */
typedef struct gridoxide_complex {
    double re;
    double im;
} gridoxide_complex;

/**
 * A two-winding transformer, in per-unit on the system base and the *to*-side
 * voltage base.
 *
 * `tap` is complex: its magnitude is the off-nominal ratio `k`, its argument
 * the vector-group phase shift. A plain ratio with no shift is
 * `{ re: k, im: 0 }`.
 */
typedef struct gridoxide_transformer {
    size_t from;
    size_t to;
    /**
     * Nonzero if the winding is connected. Both must be nonzero for the
     * branch to carry power.
     */
    uint8_t from_status;
    uint8_t to_status;
    struct gridoxide_complex y_series;
    struct gridoxide_complex y_shunt;
    struct gridoxide_complex tap;
} gridoxide_transformer;

/**
 * A shunt admittance at a bus, per-unit.
 */
typedef struct gridoxide_shunt {
    size_t at;
    struct gridoxide_complex y;
} gridoxide_shunt;

#ifdef __cplusplus
extern "C" {
#endif // __cplusplus

/**
 * The last error message on this thread, or `NULL` if the last call succeeded.
 *
 * # Safety
 *
 * The returned pointer is owned by gridoxide and stays valid **only until the
 * next gridoxide call on this thread**. Copy it before calling anything else.
 * Never free it.
 */
const char *gridoxide_last_error_message(void);

/**
 * The ABI version this library was built with — see [`ABI_VERSION`].
 */
uint32_t gridoxide_abi_version(void);

/**
 * The crate version, as a NUL-terminated string. Static; never freed.
 */
const char *gridoxide_version(void);

/**
 * Builds a model from a power-grid-model JSON file.
 *
 * # Safety
 *
 * `path` must be a NUL-terminated string; `options` may be null (defaults are
 * used); `out` must be a writable pointer. On success `*out` owns a handle the
 * caller must release with [`gridoxide_powerflow_free`].
 */
enum gridoxide_status gridoxide_powerflow_from_pgm_file(const char *path,
                                                        const struct gridoxide_options *options,
                                                        struct gridoxide_powerflow **out);

/**
 * Builds a model from a power-grid-model JSON document held in memory.
 *
 * `len` is the byte length, so the document need not be NUL-terminated.
 *
 * # Safety
 *
 * `json` must point to `len` readable bytes; `out` must be writable.
 */
enum gridoxide_status gridoxide_powerflow_from_pgm_string(const char *json,
                                                          size_t len,
                                                          const struct gridoxide_options *options,
                                                          struct gridoxide_powerflow **out);

/**
 * Builds a model from arrays the caller already holds, in per-unit.
 *
 * Any array may be null if its count is zero. Bus indices in `lines`,
 * `transformers` and `shunts` refer to positions in `buses`.
 *
 * Note that ZIP (voltage-dependent) load terms cannot be expressed here — see
 * [`GridoxideBus`]. Use a PGM document if the network has them.
 *
 * # Safety
 *
 * Each pointer must point to at least its stated count of elements, and `out`
 * must be writable.
 */
enum gridoxide_status gridoxide_powerflow_from_arrays(const struct gridoxide_bus *buses,
                                                      size_t n_buses,
                                                      const struct gridoxide_line *lines,
                                                      size_t n_lines,
                                                      const struct gridoxide_transformer *transformers,
                                                      size_t n_transformers,
                                                      const struct gridoxide_shunt *shunts,
                                                      size_t n_shunts,
                                                      const struct gridoxide_options *options,
                                                      struct gridoxide_powerflow **out);

/**
 * Releases a handle. Null is accepted and ignored.
 *
 * # Safety
 *
 * `handle` must have come from a constructor here and must not be used again.
 */
void gridoxide_powerflow_free(struct gridoxide_powerflow *handle);

/**
 * Solves an ordinary AC power flow.
 *
 * # Safety
 *
 * `handle` must be a live handle from a constructor here.
 */
enum gridoxide_status gridoxide_powerflow_solve(struct gridoxide_powerflow *handle);

/**
 * Solves with the PV→PQ outer loop, so generators respect their reactive
 * limits.
 *
 * # Safety
 *
 * As [`gridoxide_powerflow_solve`].
 */
enum gridoxide_status gridoxide_powerflow_solve_enforcing_q_limits(struct gridoxide_powerflow *handle,
                                                                   size_t max_outer_iterations);

/**
 * Solves with the system imbalance shared across generators rather than left
 * on one slack bus.
 *
 * `factors` is one participation weight per bus, normalized per island; zero
 * means the bus does not participate. Passing null uses an equal share for
 * every `Slack` and `PV` bus.
 *
 * After this returns,
 * [`gridoxide_powerflow_slack_shift`] reports
 * how far each bus's schedule moved.
 *
 * # Safety
 *
 * As [`gridoxide_powerflow_solve`]; `factors`, if non-null, must point to
 * `n_factors` readable doubles.
 */
enum gridoxide_status gridoxide_powerflow_solve_distributing_slack(struct gridoxide_powerflow *handle,
                                                                   const double *factors,
                                                                   size_t n_factors);

/**
 * Solves with **both** controls active: reactive limits and distributed slack
 * in one solve.
 *
 * This is the capability the two single-control entry points above cannot
 * express between them — before the outer-loop layer existed a caller picked
 * one, and a network that needs both (a real transmission grid usually does)
 * could not be solved correctly at all.
 *
 * `enforce_q_limits` is a boolean. `slack_factors` may be null, which means
 * "no distributed slack" when `distribute_slack` is 0 and "an equal share for
 * every `Slack` and `PV` bus" when it is 1. Both controls off is an ordinary
 * solve.
 *
 * # Safety
 *
 * As [`gridoxide_powerflow_solve`]; `slack_factors`, if non-null, must point
 * to `n_factors` readable doubles.
 */
enum gridoxide_status gridoxide_powerflow_solve_with_controls(struct gridoxide_powerflow *handle,
                                                              int32_t enforce_q_limits,
                                                              int32_t distribute_slack,
                                                              const double *slack_factors,
                                                              size_t n_factors,
                                                              size_t max_outer_iterations);

/**
 * Number of buses.
 *
 * # Safety
 *
 * `handle` must be live, or null (which returns 0).
 */
size_t gridoxide_powerflow_bus_count(const struct gridoxide_powerflow *handle);

/**
 * Number of branches — **lines first, then transformers**, which is the order
 * every per-branch array here uses.
 *
 * # Safety
 *
 * As [`gridoxide_powerflow_bus_count`].
 */
size_t gridoxide_powerflow_branch_count(const struct gridoxide_powerflow *handle);

/**
 * Bus voltage magnitudes, per-unit.
 *
 * # Safety
 *
 * `out` must point to `len` writable doubles, `len` equal to
 * [`gridoxide_powerflow_bus_count`].
 */
enum gridoxide_status gridoxide_powerflow_voltage_magnitude(const struct gridoxide_powerflow *handle,
                                                            double *out,
                                                            size_t len);

/**
 * Bus voltage angles, radians.
 *
 * # Safety
 *
 * As [`gridoxide_powerflow_voltage_magnitude`].
 */
enum gridoxide_status gridoxide_powerflow_voltage_angle(const struct gridoxide_powerflow *handle,
                                                        double *out,
                                                        size_t len);

/**
 * Active and reactive power entering each branch at the chosen terminal, in
 * per-unit — `terminal` is 0 for *from*, 1 for *to*.
 *
 * Either output may be null if you want only the other. Both arrays are
 * indexed by branch, lines before transformers.
 *
 * This has no counterpart in the Python binding, which exposes branch flows
 * for DC only. It is here because a caller who has just solved an AC power
 * flow almost always wants them, and computing them from the voltages by hand
 * means reimplementing the π-model and its tap handling.
 *
 * # Safety
 *
 * Each non-null output must point to `len` writable doubles, with `len` equal
 * to [`gridoxide_powerflow_branch_count`].
 */
enum gridoxide_status gridoxide_powerflow_branch_flow(const struct gridoxide_powerflow *handle,
                                                      int terminal,
                                                      double *p_out,
                                                      double *q_out,
                                                      size_t len);

/**
 * How far each bus's active schedule moved in the last distributed-slack
 * solve, per-unit.
 *
 * Returns [`Status::NoAnswer`] if the last solve did not distribute slack —
 * there is no shift to report, which is not an error.
 *
 * # Safety
 *
 * `out` must point to `len` writable doubles, `len` equal to
 * [`gridoxide_powerflow_bus_count`].
 */
enum gridoxide_status gridoxide_powerflow_slack_shift(const struct gridoxide_powerflow *handle,
                                                      double *out,
                                                      size_t len);

/**
 * Newton iterations taken by the last solve, or 0 if none has run.
 *
 * # Safety
 *
 * As [`gridoxide_powerflow_bus_count`].
 */
size_t gridoxide_powerflow_iterations(const struct gridoxide_powerflow *handle);

/**
 * Largest power mismatch at the last solve, or NaN if none has run.
 *
 * # Safety
 *
 * As [`gridoxide_powerflow_bus_count`].
 */
double gridoxide_powerflow_max_mismatch(const struct gridoxide_powerflow *handle);

/**
 * Number of connected components the last solve found.
 *
 * # Safety
 *
 * As [`gridoxide_powerflow_bus_count`].
 */
size_t gridoxide_powerflow_island_count(const struct gridoxide_powerflow *handle);

/**
 * One island's outcome.
 *
 * Worth reading even when the solve returned `GRIDOXIDE_OK`: an island with no
 * source reports `NO_REFERENCE_BUS` and is *not* a failure of the call, so the
 * only way to notice a de-energized part of the network is here.
 *
 * # Safety
 *
 * `out` must be writable.
 */
enum gridoxide_status gridoxide_powerflow_island_status(const struct gridoxide_powerflow *handle,
                                                        size_t island,
                                                        enum gridoxide_island_status *out);

/**
 * How many buses are in one island.
 *
 * # Safety
 *
 * As [`gridoxide_powerflow_bus_count`].
 */
size_t gridoxide_powerflow_island_bus_count(const struct gridoxide_powerflow *handle,
                                            size_t island);

/**
 * The bus indices in one island.
 *
 * # Safety
 *
 * `out` must point to `len` writable `size_t`s, `len` equal to
 * [`gridoxide_powerflow_island_bus_count`] for the same island.
 */
enum gridoxide_status gridoxide_powerflow_island_buses(const struct gridoxide_powerflow *handle,
                                                       size_t island,
                                                       size_t *out,
                                                       size_t len);

/**
 * Fills `out` with the defaults. Call this before overriding fields, so a
 * later-added field does not leave you with a zero in it.
 *
 * # Safety
 *
 * `out` must point to a writable [`GridoxideOptions`].
 */
void gridoxide_options_default(struct gridoxide_options *out);

#ifdef __cplusplus
}  // extern "C"
#endif  // __cplusplus

#endif  /* GRIDOXIDE_H */