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
//! Topological reduction: collapsing buses tied together by zero-impedance
//! connections.
//!
//! This is approach 1 of the three surveyed in
//! `docs/src/powerflow/zero_impedance_branches.md` — merge before you
//! formulate, so the admittance matrix never sees an infinite entry. It lives
//! here rather than in an importer because both importers can reach a network
//! needing it, and because the graph pass is the same regardless of what named
//! the connection.
//!
//! # When merging is the right answer
//!
//! Merging is exact and free — no new equations, no new unknowns, no numerical
//! stiffness — but it *destroys identity*: once two buses are one, nothing
//! downstream can report a flow through the connection or an injection at
//! either end separately.
//!
//! So the policy is about identity, not about impedance:
//!
//! - A **CGMES closed switch** has no identity in the bus/branch view — that
//! view is *defined* as the merged one, which is what powsybl's
//! `CalculatedBusImpl` produces too. Merge.
//! - A **PGM `link`** does have identity: power-grid-model's output schema
//! carries a `link` record with its own flows, and its fixtures assert them.
//! Merging deletes the branch those numbers describe, so a link stays a
//! branch.
//!
//! There is a second, empirical reason the CGMES side merges. Stamping its
//! switches as large-admittance branches was tried and *diverged*: the AC
//! Newton-Raphson solve failed on FullGrid with 20-odd such branches active at
//! once. That is the conditioning cost approach 2 carries, met in practice.
/// The admittance gridoxide gives any element it treats as an ideal
/// connection, declared or detected: `2e5 + j2e5` per-unit.
///
/// Three orders below power-grid-model's own `1e8 + j1e8` for a `link`, and
/// chosen by measurement rather than derivation, because the two calculation
/// types pull in opposite directions and their windows barely overlap:
///
/// - **Power flow** wants it large. The drop across the connection is
/// `ΔV = I/y`, and the `dummy-test` fixture checks node voltages and the
/// link's own current at 1e-5 relative. At `1e5` both land exactly on that
/// boundary and fail; from `2e5` up they pass.
/// - **State estimation** wants it small. `G = HᵀWH` *squares* the admittance,
/// so power-grid-model's `1e8` becomes `1e16` in the gain matrix and the
/// `node-injection-*` fixtures come back singular. They stay singular at
/// `1e6`.
///
/// | `y` | power flow | state estimation |
/// |---|---|---|
/// | `1e8` (power-grid-model's) | pass | **singular** |
/// | `1e6` | pass | **singular** |
/// | **`2e5`** | **pass** | **converge** |
/// | `1e5` | **fail**, at tolerance | converge |
///
/// The narrowness of that window is the argument for treating this as a
/// regularization parameter with a measured value rather than a physical
/// constant: a network far outside these fixtures' power scale may need it
/// re-measured, and if no value serves both, the equality-constrained
/// formulation (`docs/src/powerflow/zero_impedance_branches.md`, approach 3) is
/// the exit — it imposes `V_i = V_j` exactly, with no large number anywhere.
pub const IDEAL_CONNECTION_Y: Complex = new;
/// The impedance corresponding to [`IDEAL_CONNECTION_Y`]: `3.54e-6` p.u.
///
/// What a branch caught by [`ZERO_IMPEDANCE_THRESHOLD`] is raised *to*.
/// Below this impedance, per-unit, a branch is treated as an ideal connection
/// rather than as an ordinary one.
///
/// `Y = 1/Z` is unbounded as `Z -> 0`, and a network can reach that by accident
/// — a jumper modelled as a very short line rather than as a declared switch or
/// link. power-grid-model's own `ill-conditioned-by-line-meshed` fixture
/// contains exactly that, a line at `7.07e-9` p.u., and is named for the
/// consequence.
///
/// **Detection and treatment are separate numbers, deliberately.** This
/// threshold only decides *whether* a branch is an ideal connection; what it is
/// clamped to is [`IDEAL_CONNECTION_Y`], the same admittance a declared link
/// gets. Tying the two together would force a single value to satisfy two
/// incompatible constraints:
///
/// - it must sit below every *legitimate* branch, or real data gets mangled —
/// the smallest in the committed fixtures are `1.0e-6` (PGM) and `2.92e-6`
/// (CGMES);
/// - yet clamping only to that value leaves `|Y|` as high as `1e7`, which is
/// inside the range measured as singular for state estimation.
///
/// Separating them satisfies both: detection at `1e-7` touches nothing real,
/// and anything it does catch is raised to a link's stiffness — which is what
/// such a branch *is*, an undeclared link.
///
/// powsybl-open-loadflow's equivalent (`lowImpedanceThreshold`) is `1e-8`. Its
/// default treatment is the equality-constrained formulation rather than a
/// clamp, so it never has to reconcile these two roles in one number.
pub const ZERO_IMPEDANCE_THRESHOLD: f64 = 1e-7;
/// Raises a branch below [`ZERO_IMPEDANCE_THRESHOLD`] to the impedance of
/// [`IDEAL_CONNECTION_Y`], preserving the R/X ratio so it keeps its character.
///
/// A branch at exactly zero has no ratio to preserve and becomes purely
/// reactive, matching how a zero-impedance connection behaves in practice.
///
/// This is powsybl's `REPLACE_BY_MIN_IMPEDANCE_LINE` treatment
/// (`SimplePiModel::setMinZ`). Its *default* is the equality-constrained
/// alternative instead; gridoxide clamps because it has no augmented-system
/// machinery on the power-flow side — see
/// `docs/src/powerflow/zero_impedance_branches.md`.
/// Minimal path-compressing union-find.
///
/// Deliberately without union-by-rank: the inputs here are at most a few
/// hundred buses, and path compression alone already flattens them.
/// Collapses each union-find group into a single index.
///
/// Returns `(remap, n_merged)`, where `remap[old] = new`. Group representatives
/// are chosen by whichever member the root lands on — arbitrary but
/// deterministic, and the resulting order is stable across runs because it
/// follows ascending original index.
///
/// The caller keeps ownership of what a bus *is*: this returns only the index
/// mapping, so an importer can carry across whichever fields the representative
/// should keep. That is safe for the usual case because a zero-impedance
/// connection ties buses at the same nominal voltage, so `u_rated` and friends
/// agree across a group by construction.
/// Builds the union-find for a set of index pairs to be tied together.
///
/// A convenience for callers that already have their pairs in hand; importers
/// with per-element conditions to check (open switches, out-of-service
/// equipment) will usually drive [`UnionFind`] directly instead.