numbl 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>numbl - Figures</title>
7
- <script type="module" crossorigin src="/assets/index-BjKyNJgj.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-vtrJ8bml.js"></script>
8
8
  </head>
9
9
  <body style="margin: 0; overflow: hidden">
10
10
  <div id="root"></div>
@@ -0,0 +1,612 @@
1
+ /**
2
+ * gmres() — Restarted GMRES solver using BLAS for matvec and LAPACK for
3
+ * preconditioner solves.
4
+ *
5
+ * gmres(A, n, b, restart, tol, maxit, M1, M2, x0):
6
+ * { x: Float64Array, flag: number, relres: number,
7
+ * iter: Int32Array, resvec: Float64Array }
8
+ *
9
+ * A : Float64Array (n×n column-major)
10
+ * n : number
11
+ * b : Float64Array (n)
12
+ * restart : number (inner iterations per cycle)
13
+ * tol : number
14
+ * maxit : number (max outer iterations)
15
+ * M1 : Float64Array (n×n) | null — preconditioner factor 1
16
+ * M2 : Float64Array (n×n) | null — preconditioner factor 2
17
+ * x0 : Float64Array (n) | null — initial guess
18
+ */
19
+
20
+ #include "numbl_addon_common.h"
21
+ #include <cmath>
22
+
23
+ // ── Local helpers ────────────────────────────────────────────────────────────
24
+
25
+ static inline double vec_dot(int n, const double* a, const double* b) {
26
+ double s = 0;
27
+ for (int i = 0; i < n; i++) s += a[i] * b[i];
28
+ return s;
29
+ }
30
+
31
+ static inline double vec_nrm2(int n, const double* a) {
32
+ double s = 0;
33
+ for (int i = 0; i < n; i++) s += a[i] * a[i];
34
+ return std::sqrt(s);
35
+ }
36
+
37
+ // y = A*x via BLAS dgemv
38
+ static inline void mat_vec(int n, const double* A, const double* x, double* y) {
39
+ char trans = 'N';
40
+ double alpha = 1.0, beta = 0.0;
41
+ int inc = 1;
42
+ dgemv_(&trans, &n, &n, &alpha, const_cast<double*>(A), &n,
43
+ const_cast<double*>(x), &inc, &beta, y, &inc);
44
+ }
45
+
46
+ // Solve using pre-factored LU (dgetrs, in-place on rhs)
47
+ static inline void lu_solve(int n, const double* LU, const int* ipiv, double* rhs) {
48
+ char trans = 'N';
49
+ int nrhs = 1, info_val = 0;
50
+ dgetrs_(&trans, &n, &nrhs, const_cast<double*>(LU), &n,
51
+ const_cast<int*>(ipiv), rhs, &n, &info_val);
52
+ }
53
+
54
+ static inline void givens(double a, double b, double& c, double& s) {
55
+ if (b == 0.0) { c = 1.0; s = 0.0; return; }
56
+ if (std::abs(b) > std::abs(a)) {
57
+ double t = a / b;
58
+ s = 1.0 / std::sqrt(1.0 + t * t);
59
+ c = s * t;
60
+ } else {
61
+ double t = b / a;
62
+ c = 1.0 / std::sqrt(1.0 + t * t);
63
+ s = c * t;
64
+ }
65
+ }
66
+
67
+ // ── Gmres ────────────────────────────────────────────────────────────────────
68
+
69
+ Napi::Value Gmres(const Napi::CallbackInfo& info) {
70
+ Napi::Env env = info.Env();
71
+
72
+ if (info.Length() < 9) {
73
+ Napi::TypeError::New(env, "gmres: expected 9 arguments")
74
+ .ThrowAsJavaScriptException();
75
+ return env.Null();
76
+ }
77
+
78
+ // ── Parse required args ────────────────────────────────────────────────────
79
+ if (!info[0].IsTypedArray() || !info[1].IsNumber() || !info[2].IsTypedArray()
80
+ || !info[3].IsNumber() || !info[4].IsNumber() || !info[5].IsNumber()) {
81
+ Napi::TypeError::New(env,
82
+ "gmres: expected (Float64Array A, number n, Float64Array b, "
83
+ "number restart, number tol, number maxit, M1?, M2?, x0?)")
84
+ .ThrowAsJavaScriptException();
85
+ return env.Null();
86
+ }
87
+
88
+ auto fA = info[0].As<Napi::Float64Array>();
89
+ int n = info[1].As<Napi::Number>().Int32Value();
90
+ auto fB = info[2].As<Napi::Float64Array>();
91
+
92
+ if (n <= 0 || static_cast<int>(fA.ElementLength()) != n * n
93
+ || static_cast<int>(fB.ElementLength()) != n) {
94
+ Napi::RangeError::New(env, "gmres: dimension mismatch")
95
+ .ThrowAsJavaScriptException();
96
+ return env.Null();
97
+ }
98
+
99
+ int restart = info[3].As<Napi::Number>().Int32Value();
100
+ double tol = info[4].As<Napi::Number>().DoubleValue();
101
+ int maxit = info[5].As<Napi::Number>().Int32Value();
102
+
103
+ if (restart <= 0 || restart > n) restart = n;
104
+
105
+ const double* A = fA.Data();
106
+ const double* b = fB.Data();
107
+
108
+ // ── Pre-factor preconditioners ─────────────────────────────────────────────
109
+ bool hasM1 = !info[6].IsNull() && !info[6].IsUndefined();
110
+ bool hasM2 = !info[7].IsNull() && !info[7].IsUndefined();
111
+
112
+ std::vector<double> m1lu, m2lu;
113
+ std::vector<int> m1ipiv, m2ipiv;
114
+
115
+ if (hasM1) {
116
+ auto fM1 = info[6].As<Napi::Float64Array>();
117
+ if (static_cast<int>(fM1.ElementLength()) != n * n) {
118
+ Napi::RangeError::New(env, "gmres: M1 size mismatch")
119
+ .ThrowAsJavaScriptException();
120
+ return env.Null();
121
+ }
122
+ m1lu.assign(fM1.Data(), fM1.Data() + n * n);
123
+ m1ipiv.resize(n);
124
+ int info_val = 0;
125
+ dgetrf_(&n, &n, m1lu.data(), &n, m1ipiv.data(), &info_val);
126
+ if (info_val != 0) {
127
+ Napi::Error::New(env, "gmres: preconditioner M1 is singular")
128
+ .ThrowAsJavaScriptException();
129
+ return env.Null();
130
+ }
131
+ }
132
+ if (hasM2) {
133
+ auto fM2 = info[7].As<Napi::Float64Array>();
134
+ if (static_cast<int>(fM2.ElementLength()) != n * n) {
135
+ Napi::RangeError::New(env, "gmres: M2 size mismatch")
136
+ .ThrowAsJavaScriptException();
137
+ return env.Null();
138
+ }
139
+ m2lu.assign(fM2.Data(), fM2.Data() + n * n);
140
+ m2ipiv.resize(n);
141
+ int info_val = 0;
142
+ dgetrf_(&n, &n, m2lu.data(), &n, m2ipiv.data(), &info_val);
143
+ if (info_val != 0) {
144
+ Napi::Error::New(env, "gmres: preconditioner M2 is singular")
145
+ .ThrowAsJavaScriptException();
146
+ return env.Null();
147
+ }
148
+ }
149
+
150
+ // ── Parse x0 ──────────────────────────────────────────────────────────────
151
+ std::vector<double> x(n, 0.0);
152
+ if (!info[8].IsNull() && !info[8].IsUndefined()) {
153
+ auto fX0 = info[8].As<Napi::Float64Array>();
154
+ if (static_cast<int>(fX0.ElementLength()) != n) {
155
+ Napi::RangeError::New(env, "gmres: x0 size mismatch")
156
+ .ThrowAsJavaScriptException();
157
+ return env.Null();
158
+ }
159
+ std::memcpy(x.data(), fX0.Data(), n * sizeof(double));
160
+ }
161
+
162
+ // ── Helpers ────────────────────────────────────────────────────────────────
163
+ auto apply_precond = [&](double* r) {
164
+ if (hasM1) lu_solve(n, m1lu.data(), m1ipiv.data(), r);
165
+ if (hasM2) lu_solve(n, m2lu.data(), m2ipiv.data(), r);
166
+ };
167
+
168
+ // ── Initial preconditioned residual ────────────────────────────────────────
169
+ std::vector<double> r(n), tmp(n);
170
+ mat_vec(n, A, x.data(), r.data()); // r = A*x
171
+ for (int i = 0; i < n; i++) r[i] = b[i] - r[i]; // r = b - A*x
172
+ apply_precond(r.data()); // r = M\(b - A*x)
173
+ double beta = vec_nrm2(n, r.data());
174
+
175
+ // Preconditioned RHS norm
176
+ std::vector<double> Mb(b, b + n);
177
+ apply_precond(Mb.data());
178
+ double normMb = vec_nrm2(n, Mb.data());
179
+ if (normMb == 0.0) normMb = 1.0;
180
+
181
+ std::vector<double> resvec;
182
+ resvec.push_back(beta);
183
+
184
+ int flag = 1;
185
+ int outerIter = 0, innerIter = 0;
186
+
187
+ // Already converged?
188
+ if (beta / normMb <= tol) {
189
+ flag = 0;
190
+ goto done;
191
+ }
192
+
193
+ {
194
+ // Workspace allocated once and reused across restarts
195
+ std::vector<double> V(n * (restart + 1));
196
+ std::vector<double> H((restart + 1) * restart);
197
+ std::vector<double> cs(restart), sn(restart);
198
+ std::vector<double> g(restart + 1);
199
+ std::vector<double> w(n);
200
+
201
+ for (int outer = 1; outer <= maxit; outer++) {
202
+ outerIter = outer;
203
+
204
+ // V(:,0) = r / beta
205
+ for (int i = 0; i < n; i++) V[i] = r[i] / beta;
206
+
207
+ std::fill(H.begin(), H.end(), 0.0);
208
+ std::fill(cs.begin(), cs.end(), 0.0);
209
+ std::fill(sn.begin(), sn.end(), 0.0);
210
+ std::fill(g.begin(), g.end(), 0.0);
211
+ g[0] = beta;
212
+
213
+ bool converged = false;
214
+ const int ldh = restart + 1;
215
+
216
+ for (int j = 0; j < restart; j++) {
217
+ innerIter = j + 1;
218
+
219
+ // w = A * V(:,j)
220
+ mat_vec(n, A, &V[j * n], w.data());
221
+ apply_precond(w.data());
222
+
223
+ // Modified Gram-Schmidt
224
+ for (int i = 0; i <= j; i++) {
225
+ double hij = vec_dot(n, w.data(), &V[i * n]);
226
+ H[i + j * ldh] = hij;
227
+ for (int k = 0; k < n; k++) w[k] -= hij * V[k + i * n];
228
+ }
229
+
230
+ double wnorm = vec_nrm2(n, w.data());
231
+ H[(j + 1) + j * ldh] = wnorm;
232
+
233
+ if (wnorm > 1e-300) {
234
+ double inv_wnorm = 1.0 / wnorm;
235
+ for (int k = 0; k < n; k++) V[k + (j + 1) * n] = w[k] * inv_wnorm;
236
+ }
237
+
238
+ // Apply previous Givens rotations
239
+ for (int i = 0; i < j; i++) {
240
+ double hi = H[i + j * ldh];
241
+ double hi1 = H[(i + 1) + j * ldh];
242
+ H[i + j * ldh] = cs[i] * hi + sn[i] * hi1;
243
+ H[(i + 1) + j * ldh] = -sn[i] * hi + cs[i] * hi1;
244
+ }
245
+
246
+ // New Givens rotation
247
+ double c, s;
248
+ givens(H[j + j * ldh], H[(j + 1) + j * ldh], c, s);
249
+ cs[j] = c;
250
+ sn[j] = s;
251
+
252
+ H[j + j * ldh] = c * H[j + j * ldh] + s * H[(j + 1) + j * ldh];
253
+ H[(j + 1) + j * ldh] = 0.0;
254
+
255
+ double gj = g[j];
256
+ g[j] = c * gj;
257
+ g[j + 1] = -s * gj;
258
+
259
+ double residNorm = std::abs(g[j + 1]);
260
+ resvec.push_back(residNorm);
261
+
262
+ if (residNorm / normMb <= tol) {
263
+ // Back-solve H(0:j+1, 0:j+1) * y = g(0:j+1)
264
+ std::vector<double> y(j + 1);
265
+ for (int k = 0; k <= j; k++) y[k] = g[k];
266
+ for (int k = j; k >= 0; k--) {
267
+ for (int l = k + 1; l <= j; l++) y[k] -= H[k + l * ldh] * y[l];
268
+ y[k] /= H[k + k * ldh];
269
+ }
270
+ // x += V(:,0:j+1) * y
271
+ for (int k = 0; k < n; k++)
272
+ for (int l = 0; l <= j; l++) x[k] += V[k + l * n] * y[l];
273
+
274
+ flag = 0;
275
+ converged = true;
276
+ break;
277
+ }
278
+ }
279
+
280
+ if (converged) break;
281
+
282
+ // Restart: solve and update x
283
+ {
284
+ std::vector<double> y(restart);
285
+ for (int k = 0; k < restart; k++) y[k] = g[k];
286
+ for (int k = restart - 1; k >= 0; k--) {
287
+ for (int l = k + 1; l < restart; l++) y[k] -= H[k + l * ldh] * y[l];
288
+ y[k] /= H[k + k * ldh];
289
+ }
290
+ for (int k = 0; k < n; k++)
291
+ for (int l = 0; l < restart; l++) x[k] += V[k + l * n] * y[l];
292
+ }
293
+
294
+ // Recompute residual
295
+ mat_vec(n, A, x.data(), r.data());
296
+ for (int i = 0; i < n; i++) r[i] = b[i] - r[i];
297
+ apply_precond(r.data());
298
+ beta = vec_nrm2(n, r.data());
299
+
300
+ if (beta / normMb <= tol) {
301
+ flag = 0;
302
+ innerIter = 0;
303
+ break;
304
+ }
305
+ }
306
+ }
307
+
308
+ done:
309
+ // Final relative residual (recompute for accuracy if converged)
310
+ double relres;
311
+ if (flag == 0) {
312
+ mat_vec(n, A, x.data(), tmp.data());
313
+ for (int i = 0; i < n; i++) tmp[i] = b[i] - tmp[i];
314
+ apply_precond(tmp.data());
315
+ relres = vec_nrm2(n, tmp.data()) / normMb;
316
+ } else {
317
+ relres = beta / normMb;
318
+ }
319
+
320
+ auto result = Napi::Object::New(env);
321
+ result.Set("x", vecToF64(env, x));
322
+ result.Set("flag", Napi::Number::New(env, flag));
323
+ result.Set("relres", Napi::Number::New(env, relres));
324
+ auto iterArr = Napi::Int32Array::New(env, 2);
325
+ iterArr[0] = outerIter;
326
+ iterArr[1] = innerIter;
327
+ result.Set("iter", iterArr);
328
+ result.Set("resvec", vecToF64(env, resvec));
329
+ return result;
330
+ }
331
+
332
+ // ══════════════════════════════════════════════════════════════════════════════
333
+ // GmresComplex — complex version using BLAS zgemv + LAPACK zgetrf/zgetrs
334
+ // ══════════════════════════════════════════════════════════════════════════════
335
+
336
+ using cx = lapack_complex_double;
337
+
338
+ static inline double cx_abs(cx a) { return std::sqrt(a.real*a.real + a.imag*a.imag); }
339
+ static inline cx cx_conj(cx a) { return {a.real, -a.imag}; }
340
+ static inline cx cx_mul(cx a, cx b) { return {a.real*b.real - a.imag*b.imag, a.real*b.imag + a.imag*b.real}; }
341
+ static inline cx cx_add(cx a, cx b) { return {a.real+b.real, a.imag+b.imag}; }
342
+ static inline cx cx_sub(cx a, cx b) { return {a.real-b.real, a.imag-b.imag}; }
343
+ static inline cx cx_scale(double s, cx a) { return {s*a.real, s*a.imag}; }
344
+ static inline cx cx_div(cx a, cx b) {
345
+ double d = b.real*b.real + b.imag*b.imag;
346
+ return {(a.real*b.real + a.imag*b.imag)/d, (a.imag*b.real - a.real*b.imag)/d};
347
+ }
348
+
349
+ static double cvec_nrm2(int n, const cx* a) {
350
+ double s = 0;
351
+ for (int i = 0; i < n; i++) s += a[i].real*a[i].real + a[i].imag*a[i].imag;
352
+ return std::sqrt(s);
353
+ }
354
+
355
+ // Conjugate dot product: sum(conj(a) * b)
356
+ static cx cvec_dot(int n, const cx* a, const cx* b) {
357
+ double re = 0, im = 0;
358
+ for (int i = 0; i < n; i++) {
359
+ re += a[i].real*b[i].real + a[i].imag*b[i].imag;
360
+ im += a[i].real*b[i].imag - a[i].imag*b[i].real;
361
+ }
362
+ return {re, im};
363
+ }
364
+
365
+ // y = A*x using zgemv
366
+ static void cmat_vec(int n, const cx* A, const cx* x, cx* y) {
367
+ char trans = 'N';
368
+ cx alpha = {1,0}, beta_val = {0,0};
369
+ int inc = 1;
370
+ zgemv_(&trans, &n, &n, &alpha, const_cast<cx*>(A), &n,
371
+ const_cast<cx*>(x), &inc, &beta_val, y, &inc);
372
+ }
373
+
374
+ static void clu_solve(int n, const cx* LU, const int* ipiv, cx* rhs) {
375
+ char trans = 'N';
376
+ int nrhs = 1, info_val = 0;
377
+ zgetrs_(&trans, &n, &nrhs, const_cast<cx*>(LU), &n,
378
+ const_cast<int*>(ipiv), rhs, &n, &info_val);
379
+ }
380
+
381
+ // Complex Givens: [c s; -conj(s) c] * [a; b] = [r; 0], c real >= 0
382
+ static void cgivens(cx a, cx b, double& c, cx& s, cx& r) {
383
+ double absB = cx_abs(b);
384
+ if (absB == 0) { c = 1; s = {0,0}; r = a; return; }
385
+ double absA = cx_abs(a);
386
+ if (absA == 0) { c = 0; s = cx_conj({b.real/absB, b.imag/absB}); r = {absB,0}; return; }
387
+ double norm = std::sqrt(absA*absA + absB*absB);
388
+ c = absA / norm;
389
+ cx alpha = {a.real/absA, a.imag/absA};
390
+ s = cx_div(cx_mul(alpha, cx_conj(b)), {norm, 0});
391
+ r = cx_scale(norm, alpha);
392
+ }
393
+
394
+ Napi::Value GmresComplex(const Napi::CallbackInfo& info) {
395
+ Napi::Env env = info.Env();
396
+
397
+ // Args: ARe, AIm, n, bRe, bIm, restart, tol, maxit,
398
+ // M1Re, M1Im, M2Re, M2Im, x0Re, x0Im (14 args)
399
+ if (info.Length() < 14) {
400
+ Napi::TypeError::New(env, "gmresComplex: expected 14 arguments")
401
+ .ThrowAsJavaScriptException();
402
+ return env.Null();
403
+ }
404
+
405
+ auto fARe = info[0].As<Napi::Float64Array>();
406
+ auto fAIm = info[1].As<Napi::Float64Array>();
407
+ int n = info[2].As<Napi::Number>().Int32Value();
408
+ auto fBRe = info[3].As<Napi::Float64Array>();
409
+ auto fBIm = info[4].As<Napi::Float64Array>();
410
+ int restart = info[5].As<Napi::Number>().Int32Value();
411
+ double tol = info[6].As<Napi::Number>().DoubleValue();
412
+ int maxit = info[7].As<Napi::Number>().Int32Value();
413
+
414
+ if (restart <= 0 || restart > n) restart = n;
415
+
416
+ // Interleave A
417
+ auto A = splitToInterleaved(fARe, fAIm, n * n);
418
+
419
+ // b
420
+ std::vector<cx> b(n);
421
+ for (int i = 0; i < n; i++) b[i] = {fBRe[i], fBIm[i]};
422
+
423
+ // Pre-factor preconditioners
424
+ bool hasM1 = !info[8].IsNull() && !info[8].IsUndefined();
425
+ bool hasM2 = !info[10].IsNull() && !info[10].IsUndefined();
426
+
427
+ std::vector<cx> m1lu, m2lu;
428
+ std::vector<int> m1ipiv, m2ipiv;
429
+
430
+ if (hasM1) {
431
+ auto fM1Re = info[8].As<Napi::Float64Array>();
432
+ auto fM1Im = info[9].As<Napi::Float64Array>();
433
+ m1lu = splitToInterleaved(fM1Re, fM1Im, n * n);
434
+ m1ipiv.resize(n);
435
+ int info_val = 0;
436
+ zgetrf_(&n, &n, m1lu.data(), &n, m1ipiv.data(), &info_val);
437
+ if (info_val != 0) {
438
+ Napi::Error::New(env, "gmresComplex: M1 is singular").ThrowAsJavaScriptException();
439
+ return env.Null();
440
+ }
441
+ }
442
+ if (hasM2) {
443
+ auto fM2Re = info[10].As<Napi::Float64Array>();
444
+ auto fM2Im = info[11].As<Napi::Float64Array>();
445
+ m2lu = splitToInterleaved(fM2Re, fM2Im, n * n);
446
+ m2ipiv.resize(n);
447
+ int info_val = 0;
448
+ zgetrf_(&n, &n, m2lu.data(), &n, m2ipiv.data(), &info_val);
449
+ if (info_val != 0) {
450
+ Napi::Error::New(env, "gmresComplex: M2 is singular").ThrowAsJavaScriptException();
451
+ return env.Null();
452
+ }
453
+ }
454
+
455
+ // x0
456
+ std::vector<cx> x(n, {0,0});
457
+ if (!info[12].IsNull() && !info[12].IsUndefined()) {
458
+ auto fX0Re = info[12].As<Napi::Float64Array>();
459
+ auto fX0Im = info[13].As<Napi::Float64Array>();
460
+ for (int i = 0; i < n; i++) x[i] = {fX0Re[i], fX0Im[i]};
461
+ }
462
+
463
+ auto apply_precond = [&](cx* r) {
464
+ if (hasM1) clu_solve(n, m1lu.data(), m1ipiv.data(), r);
465
+ if (hasM2) clu_solve(n, m2lu.data(), m2ipiv.data(), r);
466
+ };
467
+
468
+ // Initial residual
469
+ std::vector<cx> r(n), tmp(n);
470
+ cmat_vec(n, A.data(), x.data(), r.data());
471
+ for (int i = 0; i < n; i++) r[i] = cx_sub(b[i], r[i]);
472
+ apply_precond(r.data());
473
+ double beta = cvec_nrm2(n, r.data());
474
+
475
+ std::vector<cx> Mb(b);
476
+ apply_precond(Mb.data());
477
+ double normMb = cvec_nrm2(n, Mb.data());
478
+ if (normMb == 0.0) normMb = 1.0;
479
+
480
+ std::vector<double> resvec;
481
+ resvec.push_back(beta);
482
+
483
+ int flag = 1, outerIter = 0, innerIter = 0;
484
+
485
+ if (beta / normMb <= tol) { flag = 0; goto cdone; }
486
+
487
+ {
488
+ std::vector<cx> V(n * (restart + 1));
489
+ std::vector<cx> H((restart + 1) * restart, {0,0});
490
+ std::vector<double> cs_arr(restart);
491
+ std::vector<cx> sn_arr(restart);
492
+ std::vector<cx> g(restart + 1, {0,0});
493
+ std::vector<cx> w(n);
494
+
495
+ for (int outer = 1; outer <= maxit; outer++) {
496
+ outerIter = outer;
497
+ for (int i = 0; i < n; i++) V[i] = cx_scale(1.0/beta, r[i]);
498
+
499
+ std::fill(H.begin(), H.end(), cx{0,0});
500
+ std::fill(cs_arr.begin(), cs_arr.end(), 0.0);
501
+ std::fill(sn_arr.begin(), sn_arr.end(), cx{0,0});
502
+ std::fill(g.begin(), g.end(), cx{0,0});
503
+ g[0] = {beta, 0};
504
+
505
+ bool converged = false;
506
+ const int ldh = restart + 1;
507
+
508
+ for (int j = 0; j < restart; j++) {
509
+ innerIter = j + 1;
510
+
511
+ cmat_vec(n, A.data(), &V[j*n], w.data());
512
+ apply_precond(w.data());
513
+
514
+ // Modified Gram-Schmidt (conjugate dot)
515
+ for (int i = 0; i <= j; i++) {
516
+ cx hij = cvec_dot(n, &V[i*n], w.data());
517
+ H[i + j*ldh] = hij;
518
+ for (int k = 0; k < n; k++) w[k] = cx_sub(w[k], cx_mul(hij, V[k + i*n]));
519
+ }
520
+
521
+ double wnorm = cvec_nrm2(n, w.data());
522
+ H[(j+1) + j*ldh] = {wnorm, 0};
523
+ if (wnorm > 1e-300) {
524
+ double inv_w = 1.0 / wnorm;
525
+ for (int k = 0; k < n; k++) V[k + (j+1)*n] = cx_scale(inv_w, w[k]);
526
+ }
527
+
528
+ // Apply previous Givens rotations
529
+ for (int i = 0; i < j; i++) {
530
+ double c = cs_arr[i]; cx s = sn_arr[i];
531
+ cx hi = H[i + j*ldh], hi1 = H[(i+1) + j*ldh];
532
+ H[i + j*ldh] = cx_add(cx_scale(c, hi), cx_mul(s, hi1));
533
+ H[(i+1) + j*ldh] = cx_add(cx_mul({-s.real, s.imag}, hi), cx_scale(c, hi1));
534
+ }
535
+
536
+ // New Givens rotation
537
+ double c; cx s, rr;
538
+ cgivens(H[j + j*ldh], H[(j+1) + j*ldh], c, s, rr);
539
+ cs_arr[j] = c;
540
+ sn_arr[j] = s;
541
+
542
+ H[j + j*ldh] = rr;
543
+ H[(j+1) + j*ldh] = {0,0};
544
+
545
+ cx gj = g[j], gj1 = g[j+1];
546
+ g[j] = cx_add(cx_scale(c, gj), cx_mul(s, gj1));
547
+ g[j+1] = cx_add(cx_mul({-s.real, s.imag}, gj), cx_scale(c, gj1));
548
+
549
+ double residNorm = cx_abs(g[j+1]);
550
+ resvec.push_back(residNorm);
551
+
552
+ if (residNorm / normMb <= tol) {
553
+ // Complex back-solve
554
+ std::vector<cx> y(j+1);
555
+ for (int k = 0; k <= j; k++) y[k] = g[k];
556
+ for (int k = j; k >= 0; k--) {
557
+ for (int l = k+1; l <= j; l++) y[k] = cx_sub(y[k], cx_mul(H[k + l*ldh], y[l]));
558
+ y[k] = cx_div(y[k], H[k + k*ldh]);
559
+ }
560
+ for (int k = 0; k < n; k++)
561
+ for (int l = 0; l <= j; l++) x[k] = cx_add(x[k], cx_mul(V[k + l*n], y[l]));
562
+ flag = 0; converged = true; break;
563
+ }
564
+ }
565
+ if (converged) break;
566
+
567
+ // Restart
568
+ {
569
+ std::vector<cx> y(restart);
570
+ for (int k = 0; k < restart; k++) y[k] = g[k];
571
+ for (int k = restart-1; k >= 0; k--) {
572
+ for (int l = k+1; l < restart; l++) y[k] = cx_sub(y[k], cx_mul(H[k + l*ldh], y[l]));
573
+ y[k] = cx_div(y[k], H[k + k*ldh]);
574
+ }
575
+ for (int k = 0; k < n; k++)
576
+ for (int l = 0; l < restart; l++) x[k] = cx_add(x[k], cx_mul(V[k + l*n], y[l]));
577
+ }
578
+
579
+ cmat_vec(n, A.data(), x.data(), r.data());
580
+ for (int i = 0; i < n; i++) r[i] = cx_sub(b[i], r[i]);
581
+ apply_precond(r.data());
582
+ beta = cvec_nrm2(n, r.data());
583
+ if (beta / normMb <= tol) { flag = 0; innerIter = 0; break; }
584
+ }
585
+ }
586
+
587
+ cdone:
588
+ double relres;
589
+ if (flag == 0) {
590
+ cmat_vec(n, A.data(), x.data(), tmp.data());
591
+ for (int i = 0; i < n; i++) tmp[i] = cx_sub(b[i], tmp[i]);
592
+ apply_precond(tmp.data());
593
+ relres = cvec_nrm2(n, tmp.data()) / normMb;
594
+ } else {
595
+ relres = beta / normMb;
596
+ }
597
+
598
+ auto result = Napi::Object::New(env);
599
+ // Split x into re/im
600
+ auto xReArr = Napi::Float64Array::New(env, n);
601
+ auto xImArr = Napi::Float64Array::New(env, n);
602
+ for (int i = 0; i < n; i++) { xReArr[i] = x[i].real; xImArr[i] = x[i].imag; }
603
+ result.Set("xRe", xReArr);
604
+ result.Set("xIm", xImArr);
605
+ result.Set("flag", Napi::Number::New(env, flag));
606
+ result.Set("relres", Napi::Number::New(env, relres));
607
+ auto iterArr = Napi::Int32Array::New(env, 2);
608
+ iterArr[0] = outerIter; iterArr[1] = innerIter;
609
+ result.Set("iter", iterArr);
610
+ result.Set("resvec", vecToF64(env, resvec));
611
+ return result;
612
+ }
@@ -29,7 +29,7 @@ extern "C" {
29
29
  // ── Addon version ────────────────────────────────────────────────────────────
30
30
  // Bump this integer whenever the addon's API changes (new functions, signature
31
31
  // changes, etc.) so that the JS side can detect stale builds.
32
- static const int ADDON_VERSION = 1;
32
+ static const int ADDON_VERSION = 2;
33
33
 
34
34
  static Napi::Value AddonVersion(const Napi::CallbackInfo& info) {
35
35
  return Napi::Number::New(info.Env(), ADDON_VERSION);
@@ -101,6 +101,10 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
101
101
  Napi::Function::New(env, FillRandn));
102
102
  exports.Set(Napi::String::New(env, "unaryElemwise"),
103
103
  Napi::Function::New(env, UnaryElemwise));
104
+ exports.Set(Napi::String::New(env, "gmres"),
105
+ Napi::Function::New(env, Gmres));
106
+ exports.Set(Napi::String::New(env, "gmresComplex"),
107
+ Napi::Function::New(env, GmresComplex));
104
108
  return exports;
105
109
  }
106
110
 
@@ -146,6 +146,30 @@ extern "C" {
146
146
  // A = U^H * U (uplo='U') or A = L * L^H (uplo='L')
147
147
  void zpotrf_(char* uplo, int* n, lapack_complex_double* a, int* lda, int* info);
148
148
 
149
+ // ── Matrix-vector multiply (BLAS) ─────────────────────────────────────────
150
+ // y = alpha * op(A) * x + beta * y
151
+ void dgemv_(char* trans, int* m, int* n,
152
+ double* alpha, double* a, int* lda,
153
+ double* x, int* incx,
154
+ double* beta, double* y, int* incy);
155
+
156
+ // ── Triangular solve using LU factors (LAPACK) ──────────────────────────────
157
+ // Solve A * X = B using the LU factorization from dgetrf
158
+ void dgetrs_(char* trans, int* n, int* nrhs,
159
+ double* a, int* lda, int* ipiv,
160
+ double* b, int* ldb, int* info);
161
+
162
+ // Complex matrix-vector multiply: y = alpha * op(A) * x + beta * y
163
+ void zgemv_(char* trans, int* m, int* n,
164
+ lapack_complex_double* alpha, lapack_complex_double* a, int* lda,
165
+ lapack_complex_double* x, int* incx,
166
+ lapack_complex_double* beta, lapack_complex_double* y, int* incy);
167
+
168
+ // Complex triangular solve using LU factors from zgetrf
169
+ void zgetrs_(char* trans, int* n, int* nrhs,
170
+ lapack_complex_double* a, int* lda, int* ipiv,
171
+ lapack_complex_double* b, int* ldb, int* info);
172
+
149
173
  // ── QZ factorization (generalized Schur decomposition) ──────────────────────
150
174
  // Compute the generalized real Schur form of (A, B):
151
175
  // A = VSL * S * VSR^T, B = VSL * T * VSR^T
@@ -286,3 +310,5 @@ Napi::Value ElemwiseScalar(const Napi::CallbackInfo& info);
286
310
  Napi::Value ElemwiseComplex(const Napi::CallbackInfo& info);
287
311
  Napi::Value FillRandn(const Napi::CallbackInfo& info);
288
312
  Napi::Value UnaryElemwise(const Napi::CallbackInfo& info);
313
+ Napi::Value Gmres(const Napi::CallbackInfo& info);
314
+ Napi::Value GmresComplex(const Napi::CallbackInfo& info);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numbl",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Run .m source files in the browser and on the command line by compiling to JavaScript",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",