RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
sparse_jacobian.cpp
Go to the documentation of this file.
1
6#include "sparse_jacobian.h"
7
8#include <algorithm>
9#include <cmath>
10#include <cstdint>
11#include <iostream>
12#include <limits>
13#include <vector>
14
15// ─────────────────────────────────────────────────────────────────────────────
16// Sparsity pattern
17// ─────────────────────────────────────────────────────────────────────────────
18//
19// State layout (full_CD, see parameters.h):
20// y[0..I-1] SIA c_n, n=1..I
21// y[I..I+V-1] VAC c_m, m=1..V
22// y[I+V..N_eq-6] He extras:
23// Case 2 (he_mode=0): Q_tot at I+V, c_h at I+V+1 (dynamic)
24// Case 1 (he_mode=1): Q_m at I+V..I+2V-1, c_h at I+2V (dynamic)
25// y[N_eq-5..N_eq-1] conservation accounting (J_SIA_fixed, J_SIA_mutual,
26// J_VAC_fixed, J_VAC_mutual, J_He_sink)
27//
28// Mobile-cluster columns couple to many rows via global sums and are treated
29// as dense columns:
30// SIA mobile : 0..i_mobile-1
31// VAC mobile : I..I+v_mobile-1
32// He extras : I+V..N_eq-6
33//
34// Non-mobile cluster columns have local (banded) row coupling driven by
35// coalescence/annihilation/emission. Conservation columns have no incoming
36// coupling (only the diagonal is stored, KLU requires a structurally non-
37// zero diagonal).
38
40{
41 if (P.physics_option != 0 && P.physics_option != 1) return -1;
42
43 const int I = P.I;
44 const int V = P.V;
45 const int N_eq = P.N_eq;
46 const int i_mob = std::max(1, P.i_mobile);
47 const int v_mob = std::max(1, P.v_mobile);
48 const int he_off = I + V;
49 // cons_off is stable under the optional appended ⟨100⟩ block (P.cons_off =
50 // N_eq-5 when loop conversion is off). n_he is the He-extra count between
51 // the VAC block and the conservation block.
52 const int cons_off = P.cons_off; // 5 conservation entries
53 const int n_he = cons_off - he_off; // Q vars + (maybe) c_h
54
55 // Build pattern as list of nonzero rows per column (CSC).
56 std::vector<std::vector<int>> col_rows(N_eq);
57
58 auto add = [&](int row, int col) {
59 if (row < 0 || row >= N_eq || col < 0 || col >= N_eq) return;
60 col_rows[col].push_back(row);
61 };
62
63 // Mark column j as a global "dense" column: every row depends on it.
64 auto mark_dense_col = [&](int col) {
65 for (int r = 0; r < N_eq; ++r) add(r, col);
66 };
67
68 // ── Mobile SIA columns (0..i_mobile-1): dense ───────────────────────────
69 for (int j = 0; j < std::min(i_mob, I); ++j) mark_dense_col(j);
70
71 // ── Mobile VAC columns (I..I+v_mobile-1): dense ─────────────────────────
72 for (int j = I; j < I + std::min(v_mob, V); ++j) mark_dense_col(j);
73
74 // ── He-extra columns (Q_tot, Q_m, c_h): dense ───────────────────────────
75 for (int j = he_off; j < he_off + n_he; ++j) mark_dense_col(j);
76
77 // ── SIA rows: local band coupling among non-mobile SIA columns ─────────
78 // Row r = n - 1 (size n = 1..I). Couples to:
79 // self
80 // n - 1 (thermal emission predecessor) and n + 1 (successor)
81 // coalescence reach: backward by i_mobile (gain), forward 0
82 // V-I annihilation reach: forward by v_mobile (gain c_{m'} c_{n+m'})
83 // Non-mobile cluster columns are added here; mobile cluster cols and
84 // VAC band cols are handled below or via mark_dense_col above.
85 for (int r = 0; r < I; ++r) {
86 const int j_lo = std::max(0, r - i_mob);
87 const int j_hi = std::min(I - 1, r + v_mob);
88 for (int j = j_lo; j <= j_hi; ++j) {
89 if (j < i_mob) continue; // already dense
90 add(r, j);
91 }
92 // Thermal emission band of width 1 beyond the above (defensive)
93 if (r - 1 >= i_mob) add(r, r - 1);
94 if (r + 1 < I) add(r, r + 1);
95
96 // Mobile SIA rows additionally couple to all VAC cols (cluster–
97 // cavity capture) — already covered: VAC mobile cols are dense and
98 // the V-I annihilation gain reaches non-mobile VAC cols below.
99 if (r < i_mob) {
100 for (int j = I + v_mob; j < I + V; ++j) add(r, j);
101 }
102 }
103
104 // ── VAC rows: local band coupling among non-mobile VAC columns ─────────
105 // Row r = I + m - 1 (size m = 1..V). Couples to:
106 // self
107 // V-V coalescence backward by v_mobile (gain) and forward 0
108 // SIA-induced shrinkage gain forward by i_mobile (c_n c_{m+n})
109 // thermal emission ±1
110 for (int m = 1; m <= V; ++m) {
111 const int r = I + m - 1;
112 const int m_lo = std::max(1, m - v_mob);
113 const int m_hi = std::min(V, m + i_mob);
114 for (int mm = m_lo; mm <= m_hi; ++mm) {
115 const int j = I + mm - 1;
116 if (j < I + v_mob) continue; // already dense
117 add(r, j);
118 }
119 if (m - 1 >= 1 + v_mob) add(r, I + m - 2);
120 if (m + 1 <= V) add(r, I + m);
121 }
122
123 // ── He-extra rows: depend on all VAC columns and all He extras ──────────
124 // (Mobile SIA cols and self are already dense.)
125 for (int r = he_off; r < he_off + n_he; ++r) {
126 for (int j = I; j < I + V; ++j) add(r, j);
127 for (int j = he_off; j < he_off + n_he; ++j) add(r, j);
128 }
129
130 // ── Conservation rows (the 5 at cons_off): aggregates over all clusters ──
131 // Bounded by cons_off+5 (NOT N_eq): the appended ⟨100⟩ block sits after the
132 // conservation block, so r<N_eq would spill across all ⟨100⟩ rows.
133 for (int r = cons_off; r < cons_off + 5; ++r) {
134 for (int j = 0; j < I; ++j) add(r, j); // SIA
135 for (int j = I; j < I + V; ++j) add(r, j); // VAC
136 for (int j = he_off; j < he_off + n_he; ++j) add(r, j);
137 }
138
139 // ── Loop-conversion ⟨100⟩ block (appended at sia100_off) ────────────────
140 // Mobile SIA columns (0..i_mob-1) and mobile VAC columns (I..I+v_mob-1) are
141 // already dense over ALL rows (incl. the appended ⟨100⟩ rows), which covers
142 // the junction / absorption / growth / shrink dependencies on the mobile
143 // ½⟨111⟩ and vacancy monomer pools. Here we add the remaining structure:
144 // ⟨100⟩ self band (ladders ±1, absorption product reach +i_mob backward),
145 // the unary same-size ½⟨111⟩→⟨100⟩ diagonal coupling, and the ⟨100⟩
146 // columns feeding the monomer / mobile-½⟨111⟩ / conservation rows.
147 if (P.loop_conversion) {
148 const int s0 = P.sia100_off;
149 const int i_mob_c = std::min(i_mob, I);
150 for (int r = 0; r < I; ++r) {
151 const int r100 = s0 + r; // ⟨100⟩ row, size r+1
152 add(r100, r100); // self (diagonal)
153 if (r - 1 >= 0) add(r100, s0 + (r - 1)); // growth predecessor
154 if (r + 1 < I) add(r100, s0 + (r + 1)); // shrink/emission predecessor
155 for (int n = 1; n <= i_mob && r - n >= 0; ++n)
156 add(r100, s0 + (r - n)); // absorption product reach
157 add(r100, r); // unary: dci100[r] <- c_i[r]
158
159 // ⟨100⟩ column r100 feeds: ½⟨111⟩ & vacancy monomer rows, the mobile
160 // ½⟨111⟩ rows (absorption consumes them), and the conservation rows.
161 add(0, r100); // ½⟨111⟩ monomer row
162 add(I, r100); // vacancy monomer row
163 for (int n = 0; n < i_mob_c; ++n) add(n, r100);
164 for (int rc = cons_off; rc < cons_off + 5; ++rc) add(rc, r100);
165 }
166 }
167
168 // ── Diagonal (structural, KLU requirement) ──────────────────────────────
169 for (int j = 0; j < N_eq; ++j) add(j, j);
170
171 // ── Deduplicate + sort each column's rows (CSC requires row indices
172 // sorted within each column) ───────────────────────────────────────
173 int nnz = 0;
174 for (int j = 0; j < N_eq; ++j) {
175 auto& v = col_rows[j];
176 std::sort(v.begin(), v.end());
177 v.erase(std::unique(v.begin(), v.end()), v.end());
178 nnz += static_cast<int>(v.size());
179 }
180
181 // ── Flatten into CSC arrays ────────────────────────────────────────────
182 P.jac_col_ptr.assign(N_eq + 1, 0);
183 P.jac_row_idx.assign(nnz, 0);
184 int pos = 0;
185 for (int j = 0; j < N_eq; ++j) {
186 P.jac_col_ptr[j] = pos;
187 for (int r : col_rows[j]) P.jac_row_idx[pos++] = r;
188 }
189 P.jac_col_ptr[N_eq] = pos;
190
191 return nnz;
192}
193
194// ─────────────────────────────────────────────────────────────────────────────
195// Greedy CPR coloring. Two columns share a color iff they share no row.
196// Greedy first-fit ordering by descending column degree gives a near-optimal
197// number of colors for banded + dense-border patterns.
198// ─────────────────────────────────────────────────────────────────────────────
200{
201 const int N = static_cast<int>(P.jac_col_ptr.size()) - 1;
202 P.jac_colors.assign(N, -1);
203
204 // For each row, build the list of columns that hit it (CSR-like).
205 int nnz = P.jac_col_ptr[N];
206 std::vector<int> row_count(N, 0);
207 for (int k = 0; k < nnz; ++k) row_count[P.jac_row_idx[k]]++;
208 std::vector<int> row_ptr(N + 1, 0);
209 for (int i = 0; i < N; ++i) row_ptr[i + 1] = row_ptr[i] + row_count[i];
210 std::vector<int> row_cols(nnz);
211 std::vector<int> row_fill(N, 0);
212 for (int j = 0; j < N; ++j) {
213 for (int k = P.jac_col_ptr[j]; k < P.jac_col_ptr[j + 1]; ++k) {
214 int i = P.jac_row_idx[k];
215 row_cols[row_ptr[i] + row_fill[i]++] = j;
216 }
217 }
218
219 // Process columns in descending-degree order so dense columns get colored
220 // first (each takes its own color), and band columns share colors after.
221 std::vector<int> order(N);
222 for (int j = 0; j < N; ++j) order[j] = j;
223 std::sort(order.begin(), order.end(), [&](int a, int b) {
224 int da = P.jac_col_ptr[a + 1] - P.jac_col_ptr[a];
225 int db = P.jac_col_ptr[b + 1] - P.jac_col_ptr[b];
226 return da > db;
227 });
228
229 std::vector<int> forbidden; // colors used by neighbours of current col
230 int n_colors = 0;
231 for (int j : order) {
232 forbidden.assign(n_colors, 0);
233 for (int k = P.jac_col_ptr[j]; k < P.jac_col_ptr[j + 1]; ++k) {
234 int i = P.jac_row_idx[k];
235 // Any column that shares row i with j is a "neighbour" in the
236 // intersection graph — its color is forbidden for j.
237 for (int p = row_ptr[i]; p < row_ptr[i + 1]; ++p) {
238 int jj = row_cols[p];
239 int c = P.jac_colors[jj];
240 if (c >= 0) {
241 if (c >= static_cast<int>(forbidden.size()))
242 forbidden.resize(c + 1, 0);
243 forbidden[c] = 1;
244 }
245 }
246 }
247 int chosen = -1;
248 for (int c = 0; c < static_cast<int>(forbidden.size()); ++c)
249 if (!forbidden[c]) { chosen = c; break; }
250 if (chosen < 0) chosen = n_colors++;
251 P.jac_colors[j] = chosen;
252 if (chosen >= n_colors) n_colors = chosen + 1;
253 }
254 P.jac_n_colors = n_colors;
255}
256
258{
259 const int N = static_cast<int>(P.jac_colors.size());
260 const int nc = P.jac_n_colors;
261
262 P.jac_color_offsets.assign(nc + 1, 0);
263 for (int j = 0; j < N; ++j) P.jac_color_offsets[P.jac_colors[j] + 1]++;
264 for (int c = 0; c < nc; ++c)
265 P.jac_color_offsets[c + 1] += P.jac_color_offsets[c];
266
267 P.jac_color_groups.assign(N, 0);
268 std::vector<int> fill(nc, 0);
269 for (int j = 0; j < N; ++j) {
270 int c = P.jac_colors[j];
271 P.jac_color_groups[P.jac_color_offsets[c] + fill[c]++] = j;
272 }
273}
274
275// ─────────────────────────────────────────────────────────────────────────────
276// Sparse Jacobian via colored finite differences.
277//
278// For each color group, build a perturbation vector with one increment per
279// column in the group. Within a group no two columns share a row, so the
280// resulting (f(y+δ) - f(y))_i corresponds unambiguously to the unique column
281// of that color which has i in its sparsity pattern.
282// ─────────────────────────────────────────────────────────────────────────────
283int sparse_fd_jac(sunrealtype /*t*/, N_Vector y, N_Vector fy,
284 SUNMatrix Jac, void* user_data,
285 N_Vector tmp1, N_Vector tmp2, N_Vector tmp3)
286{
287 UserData* ud = static_cast<UserData*>(user_data);
288 Parameters& P = *(ud->P);
289 const int N = P.N_eq;
290
291 double* y_p = N_VGetArrayPointer(y);
292 double* f0 = N_VGetArrayPointer(fy);
293 double* y_pert = N_VGetArrayPointer(tmp1);
294 double* f1 = N_VGetArrayPointer(tmp2);
295 double* delta = N_VGetArrayPointer(tmp3);
296
297 // ── CSC arrays of the SUNSparseMatrix ───────────────────────────────────
298 // We assume the sparsity pattern is already installed; only fill values.
299 sunindextype* col_ptr = SUNSparseMatrix_IndexPointers(Jac);
300 sunindextype* row_idx = SUNSparseMatrix_IndexValues (Jac);
301 sunrealtype* data = SUNSparseMatrix_Data (Jac);
302
303 SUNMatZero(Jac);
304
305 // Per-column FD perturbation size, scaled with current state.
306 const double sqrt_eps = std::sqrt(std::numeric_limits<double>::epsilon());
307
308 for (int c = 0; c < P.jac_n_colors; ++c) {
309 // Reset perturbation vector to zero.
310 std::fill(delta, delta + N, 0.0);
311
312 // Build delta for all columns in this color group.
313 // FD step is scaled by max(|y|, atol): using atol (not an unrelated
314 // 1e-12 floor) avoids catastrophic cancellation for near-floor columns
315 // and keeps this consistent with the Woodbury preconditioner's
316 // fd_delta() in rate_equations.cpp.
317 const int g_lo = P.jac_color_offsets[c];
318 const int g_hi = P.jac_color_offsets[c + 1];
319 for (int g = g_lo; g < g_hi; ++g) {
320 int j = P.jac_color_groups[g];
321 double scale = std::max(std::abs(y_p[j]),
322 std::max(P.atol, 1e-30));
323 delta[j] = sqrt_eps * scale;
324 }
325
326 // y_pert = y + delta
327 for (int i = 0; i < N; ++i) y_pert[i] = y_p[i] + delta[i];
328
329 // f1 = f(y_pert)
330 N_Vector y_pert_vec = tmp1;
331 N_Vector f1_vec = tmp2;
332 if (ud->rhs_fn(0.0, y_pert_vec, f1_vec, user_data) != 0)
333 return -1;
334
335 // For each column in the group, fill its sparse entries.
336 for (int g = g_lo; g < g_hi; ++g) {
337 int j = P.jac_color_groups[g];
338 double inv_d = 1.0 / delta[j];
339 for (sunindextype k = col_ptr[j]; k < col_ptr[j + 1]; ++k) {
340 int i = static_cast<int>(row_idx[k]);
341 data[k] = (f1[i] - f0[i]) * inv_d;
342 }
343 }
344 }
345
346 return 0;
347}
int sparse_fd_jac(sunrealtype, N_Vector y, N_Vector fy, SUNMatrix Jac, void *user_data, N_Vector tmp1, N_Vector tmp2, N_Vector tmp3)
void color_columns_greedy(Parameters &P)
int build_sparsity_pattern_full_CD(Parameters &P)
void build_color_groups(Parameters &P)
CVRhsFn_t rhs_fn
Parameters * P