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
//! Second derivatives of the bus power injections.
//!
//! [`ac_sensitivity`](crate::ac_sensitivity) differentiates a converged
//! operating point once. This differentiates it twice — the piece any
//! interior-point method needs to solve AC-OPF, where the Hessian of the
//! Lagrangian is
//!
//! \\[ \nabla^2_{xx} L = \nabla^2 f
//! + \sum_i \lambda_i \nabla^2 P_i + \sum_i \mu_i \nabla^2 Q_i \\]
//!
//! The objective's own \\(\nabla^2 f\\) is trivial; the constraint terms are
//! not, and they are what lives here.
//!
//! # What it computes, and why in that form
//!
//! Never one \\(\nabla^2 P_i\\) at a time — always the **multiplier-weighted
//! sum** over every bus at once, which is the only form an optimizer asks
//! for. Each individual \\(\nabla^2 P_i\\) is a sparse \\(2n \times 2n\\)
//! matrix and there are \\(2n\\) of them; materializing them separately would
//! cost \\(O(n)\\) matrices to build one. Summing as we go makes the whole
//! thing one pass over the Y-bus, the same cost as assembling the Jacobian.
//!
//! # State layout
//!
//! The **full** \\(2n\\) space: \\(\theta_0 \dots \theta_{n-1}\\) at indices
//! \\(0 \dots n-1\\), then \\(|V|_0 \dots |V|_{n-1}\\) at \\(n \dots 2n-1\\).
//! No slack angle removed, no PV magnitude removed.
//!
//! That is deliberately *not* the power flow's reduced state. Newton solves
//! with bus types fixed, so it drops the slack angle and every PV magnitude;
//! AC-OPF makes generator voltages **decision variables**, so the rows Newton
//! discards are exactly the ones the optimizer needs. Emitting the full space
//! and letting the caller select is both more general and easier to check —
//! the reduced form is a submatrix, whereas the reverse would require
//! rebuilding.
//!
//! # Conventions
//!
//! Both functions return **full symmetric** `(row, col, value)` triplets, with
//! each off-diagonal appearing twice. That is the opposite of
//! gridoxide's `opf::LinearProgram::hessian`, which wants
//! the lower triangle only — a caller crossing into the optimizer must filter
//! to `row >= col`, and dropping that step silently doubles every off-diagonal
//! term. Full symmetric is right *here* because these are matrices to
//! multiply and finite-difference, not yet an objective to minimize.
//!
//! Duplicate `(row, col)` pairs may be emitted and **sum**, the same
//! accumulation semantics [`YBus`](crate::network::YBus) documents.
use crateYBusSparse;
use crateBus;
/// The polar injection equations' shared per-pair trigonometry.
///
/// For an ordered pair \\((i, k)\\) with \\(\theta_{ik} = \theta_i -
/// \theta_k\\):
///
/// \\[ c_{ik} = G_{ik}\cos\theta_{ik} + B_{ik}\sin\theta_{ik}, \qquad
/// s_{ik} = G_{ik}\sin\theta_{ik} - B_{ik}\cos\theta_{ik} \\]
///
/// Every derivative below is a product of these with voltage magnitudes,
/// because differentiating in \\(\theta\\) just rotates one into the other:
/// \\(\partial c/\partial\theta_i = -s\\), \\(\partial s/\partial\theta_i =
/// c\\), and the \\(\theta_k\\) derivatives are their negatives. Naming them
/// once is what keeps the second-derivative bookkeeping below tractable.
/// First derivatives of every injection, over the full \\(2n\\) state.
///
/// Rows are the equations — \\(P_0 \dots P_{n-1}\\) then \\(Q_0 \dots
/// Q_{n-1}\\) — and columns the state, so entry \\((i, j)\\) is
/// \\(\partial P_i/\partial x_j\\) for \\(i < n\\).
///
/// This duplicates what [`JacobianPattern`](crate::jacobian::JacobianPattern)
/// computes, and duplicating it is the point. That one is written for Newton:
/// it emits only the reduced rows and columns, and it reads the diagonal
/// blocks out of precomputed `p_calc`/`q_calc` (`H_ii = −Q_i − V_i²B_ii` and
/// friends). This one sums over neighbours directly and shares no line of code
/// with it. Where they overlap they must agree — which makes the mature,
/// heavily exercised assembler an independent oracle for this one, and this
/// one in turn the thing the Hessian is finite-differenced against. Neither
/// check would mean much if the two shared an implementation.
/// \\(\sum_i \lambda_i \nabla^2 P_i + \sum_i \mu_i \nabla^2 Q_i\\), over the
/// full \\(2n\\) state.
///
/// `lambda` weights the active-power equations and `mu` the reactive ones;
/// both are indexed by bus and must have length `buses.len()`. In an AC-OPF
/// these are the balance constraints' multipliers, so passing the ones a
/// solver hands back gives the constraint half of the Lagrangian's Hessian
/// directly.
///
/// # Structure
///
/// The result is symmetric and has the Jacobian's sparsity — no more. That is
/// worth stating because it is not obvious: \\(\nabla^2 P_i\\) has *no*
/// entry at \\((\theta_k, \theta_m)\\) for two distinct neighbours \\(k, m\\)
/// of \\(i\\), even though both appear in \\(P_i\\). Each term of \\(P_i\\)
/// involves exactly one neighbour, so differentiating twice can never couple
/// two of them. A second-order method therefore needs no more fill than the
/// first-order one, which is the reason AC-OPF stays tractable on a network's
/// natural sparsity.
///
/// # Panics
///
/// If `lambda` or `mu` is not `buses.len()` long — a mismatch there would
/// otherwise silently weight the wrong bus.
/// Sums triplets into a dense matrix. For tests and small problems only —
/// the whole point of the sparse form is not to do this.