numbl 0.0.22 → 0.1.0

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.
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Element-wise binary operations on Float64Arrays.
3
+ *
4
+ * Real:
5
+ * elemwise(a: Float64Array, b: Float64Array, op: number): Float64Array
6
+ * op: 0=add, 1=sub, 2=mul, 3=div
7
+ *
8
+ * Complex:
9
+ * elemwiseComplex(aRe: Float64Array, aIm: Float64Array,
10
+ * bRe: Float64Array, bIm: Float64Array,
11
+ * op: number): { re: Float64Array, im: Float64Array }
12
+ * op: 0=add, 1=sub, 2=mul, 3=div
13
+ * Pass null for aIm or bIm to treat as zero (mixed real/complex).
14
+ */
15
+
16
+ #include "numbl_addon_common.h"
17
+
18
+ // ── elemwise() — real element-wise binary op ────────────────────────────────
19
+
20
+ Napi::Value Elemwise(const Napi::CallbackInfo& info) {
21
+ Napi::Env env = info.Env();
22
+
23
+ if (info.Length() < 3
24
+ || !info[0].IsTypedArray()
25
+ || !info[1].IsTypedArray()
26
+ || !info[2].IsNumber()) {
27
+ Napi::TypeError::New(env,
28
+ "elemwise: expected (Float64Array a, Float64Array b, number op)")
29
+ .ThrowAsJavaScriptException();
30
+ return env.Null();
31
+ }
32
+
33
+ auto arrA = info[0].As<Napi::Float64Array>();
34
+ auto arrB = info[1].As<Napi::Float64Array>();
35
+ int op = info[2].As<Napi::Number>().Int32Value();
36
+
37
+ size_t n = arrA.ElementLength();
38
+ if (arrB.ElementLength() != n) {
39
+ Napi::RangeError::New(env, "elemwise: arrays must have same length")
40
+ .ThrowAsJavaScriptException();
41
+ return env.Null();
42
+ }
43
+
44
+ auto result = Napi::Float64Array::New(env, n);
45
+ const double* a = arrA.Data();
46
+ const double* b = arrB.Data();
47
+ double* out = result.Data();
48
+
49
+ switch (op) {
50
+ case 0: // add
51
+ for (size_t i = 0; i < n; i++) out[i] = a[i] + b[i];
52
+ break;
53
+ case 1: // sub
54
+ for (size_t i = 0; i < n; i++) out[i] = a[i] - b[i];
55
+ break;
56
+ case 2: // mul
57
+ for (size_t i = 0; i < n; i++) out[i] = a[i] * b[i];
58
+ break;
59
+ case 3: // div
60
+ for (size_t i = 0; i < n; i++) out[i] = a[i] / b[i];
61
+ break;
62
+ default:
63
+ Napi::RangeError::New(env, "elemwise: op must be 0-3")
64
+ .ThrowAsJavaScriptException();
65
+ return env.Null();
66
+ }
67
+
68
+ return result;
69
+ }
70
+
71
+ // ── elemwiseComplex() — complex element-wise binary op ──────────────────────
72
+
73
+ Napi::Value ElemwiseComplex(const Napi::CallbackInfo& info) {
74
+ Napi::Env env = info.Env();
75
+
76
+ // (aRe, aIm_or_null, bRe, bIm_or_null, op)
77
+ if (info.Length() < 5 || !info[0].IsTypedArray() || !info[2].IsTypedArray()
78
+ || !info[4].IsNumber()) {
79
+ Napi::TypeError::New(env,
80
+ "elemwiseComplex: expected (Float64Array aRe, Float64Array|null aIm, "
81
+ "Float64Array bRe, Float64Array|null bIm, number op)")
82
+ .ThrowAsJavaScriptException();
83
+ return env.Null();
84
+ }
85
+
86
+ auto arrARe = info[0].As<Napi::Float64Array>();
87
+ auto arrBRe = info[2].As<Napi::Float64Array>();
88
+ int op = info[4].As<Napi::Number>().Int32Value();
89
+
90
+ size_t n = arrARe.ElementLength();
91
+ if (arrBRe.ElementLength() != n) {
92
+ Napi::RangeError::New(env, "elemwiseComplex: arrays must have same length")
93
+ .ThrowAsJavaScriptException();
94
+ return env.Null();
95
+ }
96
+
97
+ const double* aRe = arrARe.Data();
98
+ const double* bRe = arrBRe.Data();
99
+
100
+ // aIm and bIm may be null (treat as zero)
101
+ bool hasAIm = info[1].IsTypedArray();
102
+ bool hasBIm = info[3].IsTypedArray();
103
+ const double* aIm = hasAIm ? info[1].As<Napi::Float64Array>().Data() : nullptr;
104
+ const double* bIm = hasBIm ? info[3].As<Napi::Float64Array>().Data() : nullptr;
105
+
106
+ auto outRe = Napi::Float64Array::New(env, n);
107
+ auto outIm = Napi::Float64Array::New(env, n);
108
+ double* oRe = outRe.Data();
109
+ double* oIm = outIm.Data();
110
+
111
+ switch (op) {
112
+ case 0: // add
113
+ for (size_t i = 0; i < n; i++) {
114
+ oRe[i] = aRe[i] + bRe[i];
115
+ oIm[i] = (aIm ? aIm[i] : 0.0) + (bIm ? bIm[i] : 0.0);
116
+ }
117
+ break;
118
+ case 1: // sub
119
+ for (size_t i = 0; i < n; i++) {
120
+ oRe[i] = aRe[i] - bRe[i];
121
+ oIm[i] = (aIm ? aIm[i] : 0.0) - (bIm ? bIm[i] : 0.0);
122
+ }
123
+ break;
124
+ case 2: { // mul: (a+bi)(c+di) = (ac-bd) + (ad+bc)i
125
+ for (size_t i = 0; i < n; i++) {
126
+ double ar = aRe[i], ai = aIm ? aIm[i] : 0.0;
127
+ double br = bRe[i], bi = bIm ? bIm[i] : 0.0;
128
+ oRe[i] = ar * br - ai * bi;
129
+ oIm[i] = ar * bi + ai * br;
130
+ }
131
+ break;
132
+ }
133
+ case 3: { // div: (a+bi)/(c+di) = ((ac+bd) + (bc-ad)i) / (c²+d²)
134
+ for (size_t i = 0; i < n; i++) {
135
+ double ar = aRe[i], ai = aIm ? aIm[i] : 0.0;
136
+ double br = bRe[i], bi = bIm ? bIm[i] : 0.0;
137
+ double denom = br * br + bi * bi;
138
+ if (denom == 0.0) {
139
+ oRe[i] = (ar == 0.0 && ai == 0.0) ? 0.0 / 0.0 /* NaN */
140
+ : (ar > 0 ? 1.0 : ar < 0 ? -1.0 : 0.0) / 0.0 /* ±Inf */;
141
+ oIm[i] = (ar == 0.0 && ai == 0.0) ? 0.0
142
+ : (ai > 0 ? 1.0 : ai < 0 ? -1.0 : 0.0) / 0.0;
143
+ } else {
144
+ oRe[i] = (ar * br + ai * bi) / denom;
145
+ oIm[i] = (ai * br - ar * bi) / denom;
146
+ }
147
+ }
148
+ break;
149
+ }
150
+ default:
151
+ Napi::RangeError::New(env, "elemwiseComplex: op must be 0-3")
152
+ .ThrowAsJavaScriptException();
153
+ return env.Null();
154
+ }
155
+
156
+ // Check if result is purely real
157
+ bool isReal = true;
158
+ for (size_t i = 0; i < n; i++) {
159
+ if (oIm[i] != 0.0) { isReal = false; break; }
160
+ }
161
+
162
+ auto result = Napi::Object::New(env);
163
+ result.Set("re", outRe);
164
+ if (!isReal) {
165
+ result.Set("im", outIm);
166
+ }
167
+ return result;
168
+ }
@@ -13,7 +13,7 @@
13
13
  * Returns the triangular factor and info (0 = success, >0 = not pos def).
14
14
  */
15
15
 
16
- #include "lapack_common.h"
16
+ #include "numbl_addon_common.h"
17
17
 
18
18
  // Zero out the opposite triangle of an n×n column-major matrix.
19
19
  template<typename T>
@@ -12,7 +12,7 @@
12
12
  * The ts-lapack bridge handles the nobalance case.
13
13
  */
14
14
 
15
- #include "lapack_common.h"
15
+ #include "numbl_addon_common.h"
16
16
 
17
17
  // ── eig() ─────────────────────────────────────────────────────────────────────
18
18
 
@@ -12,7 +12,7 @@
12
12
  * Does NOT normalize for inverse (caller handles 1/n scaling).
13
13
  */
14
14
 
15
- #include "lapack_common.h"
15
+ #include "numbl_addon_common.h"
16
16
  #include <fftw3.h>
17
17
 
18
18
  // Shared core: run FFTW on pre-filled input, return {re, im} result object.
@@ -17,7 +17,7 @@
17
17
  * Does NOT normalize for inverse — caller handles 1/n scaling.
18
18
  */
19
19
 
20
- #include "lapack_common.h"
20
+ #include "numbl_addon_common.h"
21
21
  #include <fftw3.h>
22
22
  #include <cmath>
23
23
 
@@ -11,7 +11,7 @@
11
11
  * zgetrf + zgetri. Throws if the matrix is singular.
12
12
  */
13
13
 
14
- #include "lapack_common.h"
14
+ #include "numbl_addon_common.h"
15
15
 
16
16
  // ── inv() ─────────────────────────────────────────────────────────────────────
17
17
 
@@ -20,7 +20,7 @@
20
20
  * Underdetermined (m < n): minimum-norm solution minimising ||X||₂.
21
21
  */
22
22
 
23
- #include "lapack_common.h"
23
+ #include "numbl_addon_common.h"
24
24
 
25
25
  // ── linsolve() ────────────────────────────────────────────────────────────────
26
26
 
@@ -12,7 +12,7 @@
12
12
  * Returns the packed LU matrix and 1-based pivot indices.
13
13
  */
14
14
 
15
- #include "lapack_common.h"
15
+ #include "numbl_addon_common.h"
16
16
 
17
17
  // ── lu() ─────────────────────────────────────────────────────────────────────
18
18
 
@@ -10,7 +10,7 @@
10
10
  * C is an m×n matrix returned in column-major order
11
11
  */
12
12
 
13
- #include "lapack_common.h"
13
+ #include "numbl_addon_common.h"
14
14
 
15
15
  // ── matmul() ──────────────────────────────────────────────────────────────────
16
16
 
@@ -0,0 +1,110 @@
1
+ /**
2
+ * matmulComplex() — Complex matrix-matrix multiplication via BLAS zgemm.
3
+ *
4
+ * matmulComplex(ARe: Float64Array, AIm: Float64Array,
5
+ * m: number, k: number,
6
+ * BRe: Float64Array, BIm: Float64Array,
7
+ * n: number): { re: Float64Array, im: Float64Array }
8
+ *
9
+ * Computes C = A * B where:
10
+ * A is an m×k complex matrix (split re/im) stored in column-major order
11
+ * B is a k×n complex matrix (split re/im) stored in column-major order
12
+ * C is an m×n complex matrix returned as {re, im} in column-major order
13
+ */
14
+
15
+ #include "numbl_addon_common.h"
16
+
17
+ Napi::Value MatmulComplex(const Napi::CallbackInfo& info) {
18
+ Napi::Env env = info.Env();
19
+
20
+ // matmulComplex(ARe, AIm, m, k, BRe, BIm, n)
21
+ if (info.Length() < 7
22
+ || !info[0].IsTypedArray() // ARe
23
+ || !info[1].IsTypedArray() // AIm
24
+ || !info[2].IsNumber() // m
25
+ || !info[3].IsNumber() // k
26
+ || !info[4].IsTypedArray() // BRe
27
+ || !info[5].IsTypedArray() // BIm
28
+ || !info[6].IsNumber()) { // n
29
+ Napi::TypeError::New(env,
30
+ "matmulComplex: expected (Float64Array ARe, Float64Array AIm, "
31
+ "number m, number k, Float64Array BRe, Float64Array BIm, number n)")
32
+ .ThrowAsJavaScriptException();
33
+ return env.Null();
34
+ }
35
+
36
+ auto arrARe = info[0].As<Napi::Float64Array>();
37
+ auto arrAIm = info[1].As<Napi::Float64Array>();
38
+ int m = info[2].As<Napi::Number>().Int32Value();
39
+ int k = info[3].As<Napi::Number>().Int32Value();
40
+ auto arrBRe = info[4].As<Napi::Float64Array>();
41
+ auto arrBIm = info[5].As<Napi::Float64Array>();
42
+ int n = info[6].As<Napi::Number>().Int32Value();
43
+
44
+ if (m < 0 || k < 0 || n < 0) {
45
+ Napi::RangeError::New(env, "matmulComplex: m, k, n must be non-negative")
46
+ .ThrowAsJavaScriptException();
47
+ return env.Null();
48
+ }
49
+
50
+ int mk = m * k;
51
+ int kn = k * n;
52
+ int mn = m * n;
53
+
54
+ // Handle empty-dimension multiply
55
+ if (m == 0 || k == 0 || n == 0) {
56
+ auto result = Napi::Object::New(env);
57
+ result.Set("re", Napi::Float64Array::New(env, static_cast<size_t>(mn)));
58
+ result.Set("im", Napi::Float64Array::New(env, static_cast<size_t>(mn)));
59
+ return result;
60
+ }
61
+
62
+ // Interleave into complex arrays for zgemm
63
+ std::vector<lapack_complex_double> a(mk);
64
+ for (int i = 0; i < mk; ++i) {
65
+ a[i].real = arrARe[i];
66
+ a[i].imag = arrAIm[i];
67
+ }
68
+
69
+ std::vector<lapack_complex_double> b(kn);
70
+ for (int i = 0; i < kn; ++i) {
71
+ b[i].real = arrBRe[i];
72
+ b[i].imag = arrBIm[i];
73
+ }
74
+
75
+ std::vector<lapack_complex_double> c(mn, {0.0, 0.0});
76
+
77
+ char transa = 'N';
78
+ char transb = 'N';
79
+ lapack_complex_double alpha = {1.0, 0.0};
80
+ lapack_complex_double beta = {0.0, 0.0};
81
+ int lda = m;
82
+ int ldb = k;
83
+ int ldc = m;
84
+
85
+ zgemm_(&transa, &transb,
86
+ &m, &n, &k,
87
+ &alpha, a.data(), &lda,
88
+ b.data(), &ldb,
89
+ &beta, c.data(), &ldc);
90
+
91
+ // Deinterleave result
92
+ auto result = Napi::Object::New(env);
93
+ auto outRe = Napi::Float64Array::New(env, static_cast<size_t>(mn));
94
+ auto outIm = Napi::Float64Array::New(env, static_cast<size_t>(mn));
95
+ for (int i = 0; i < mn; ++i) {
96
+ outRe[i] = c[i].real;
97
+ outIm[i] = c[i].imag;
98
+ }
99
+
100
+ // Check if result is purely real
101
+ bool isReal = true;
102
+ for (int i = 0; i < mn; ++i) {
103
+ if (outIm[i] != 0.0) { isReal = false; break; }
104
+ }
105
+ result.Set("re", outRe);
106
+ if (!isReal) {
107
+ result.Set("im", outIm);
108
+ }
109
+ return result;
110
+ }
@@ -13,7 +13,7 @@
13
13
  * wantQ=false: skips Q generation.
14
14
  */
15
15
 
16
- #include "lapack_common.h"
16
+ #include "numbl_addon_common.h"
17
17
 
18
18
  // ── qr() ─────────────────────────────────────────────────────────────────────
19
19
 
@@ -115,6 +115,218 @@ Napi::Value Qr(const Napi::CallbackInfo& info) {
115
115
  return result;
116
116
  }
117
117
 
118
+ // ── qrPivot() ────────────────────────────────────────────────────────────────
119
+ // Column-pivoted QR: A*P = Q*R via dgeqp3 + dorgqr.
120
+ // Returns {Q, R, jpvt} where jpvt is a 1-based permutation vector.
121
+
122
+ Napi::Value QrPivot(const Napi::CallbackInfo& info) {
123
+ Napi::Env env = info.Env();
124
+
125
+ if (info.Length() < 4
126
+ || !info[0].IsTypedArray()
127
+ || !info[1].IsNumber()
128
+ || !info[2].IsNumber()
129
+ || !info[3].IsBoolean()) {
130
+ Napi::TypeError::New(env,
131
+ "qrPivot: expected (Float64Array data, number m, number n, boolean econ)")
132
+ .ThrowAsJavaScriptException();
133
+ return env.Null();
134
+ }
135
+
136
+ auto arr = info[0].As<Napi::TypedArray>();
137
+ if (arr.TypedArrayType() != napi_float64_array) {
138
+ Napi::TypeError::New(env, "qrPivot: data must be a Float64Array")
139
+ .ThrowAsJavaScriptException();
140
+ return env.Null();
141
+ }
142
+
143
+ int m = info[1].As<Napi::Number>().Int32Value();
144
+ int n = info[2].As<Napi::Number>().Int32Value();
145
+ bool econ = info[3].As<Napi::Boolean>().Value();
146
+
147
+ if (m <= 0 || n <= 0 || static_cast<int>(arr.ElementLength()) != m * n) {
148
+ Napi::RangeError::New(env, "qrPivot: data.length must equal m*n")
149
+ .ThrowAsJavaScriptException();
150
+ return env.Null();
151
+ }
152
+
153
+ auto float64arr = info[0].As<Napi::Float64Array>();
154
+ int k = m < n ? m : n;
155
+
156
+ // Copy input data
157
+ std::vector<double> a(m * n);
158
+ std::memcpy(a.data(), float64arr.Data(), m * n * sizeof(double));
159
+
160
+ // jpvt initialised to 0 → all columns are free
161
+ std::vector<int> jpvt(n, 0);
162
+ std::vector<double> tau(k);
163
+ int info_val = 0;
164
+
165
+ // Workspace query
166
+ int lwork = -1;
167
+ double work_query = 0.0;
168
+ dgeqp3_(&m, &n, a.data(), &m, jpvt.data(), tau.data(),
169
+ &work_query, &lwork, &info_val);
170
+ lwork = static_cast<int>(work_query);
171
+ if (lwork < 1) lwork = 3 * n + 1;
172
+
173
+ std::vector<double> work(lwork);
174
+ dgeqp3_(&m, &n, a.data(), &m, jpvt.data(), tau.data(),
175
+ work.data(), &lwork, &info_val);
176
+
177
+ if (!checkLapackInfo(env, info_val, "qrPivot", "dgeqp3"))
178
+ return env.Null();
179
+
180
+ // Extract R from the upper triangle
181
+ int r_rows = econ ? k : m;
182
+ std::vector<double> R(r_rows * n, 0.0);
183
+ for (int j = 0; j < n; j++) {
184
+ int ilim = j < k ? j : k - 1;
185
+ for (int i = 0; i <= ilim; i++) {
186
+ R[i + j * r_rows] = a[i + j * m];
187
+ }
188
+ }
189
+
190
+ // Generate Q via dorgqr
191
+ int q_cols = econ ? k : m;
192
+ std::vector<double> q_buf(m * q_cols, 0.0);
193
+ int cols_to_copy = n < q_cols ? n : q_cols;
194
+ for (int j = 0; j < cols_to_copy; j++) {
195
+ for (int i = 0; i < m; i++) {
196
+ q_buf[i + j * m] = a[i + j * m];
197
+ }
198
+ }
199
+
200
+ lwork = -1;
201
+ dorgqr_(&m, &q_cols, &k, q_buf.data(), &m, tau.data(),
202
+ &work_query, &lwork, &info_val);
203
+ lwork = static_cast<int>(work_query);
204
+ if (lwork < 1) lwork = q_cols;
205
+
206
+ work.assign(lwork, 0.0);
207
+ dorgqr_(&m, &q_cols, &k, q_buf.data(), &m, tau.data(),
208
+ work.data(), &lwork, &info_val);
209
+
210
+ if (!checkLapackInfo(env, info_val, "qrPivot", "dorgqr"))
211
+ return env.Null();
212
+
213
+ auto result = Napi::Object::New(env);
214
+ result.Set("Q", vecToF64(env, q_buf));
215
+ result.Set("R", vecToF64(env, R));
216
+ result.Set("jpvt", vecToI32(env, jpvt));
217
+ return result;
218
+ }
219
+
220
+ // ── qrPivotComplex() ─────────────────────────────────────────────────────────
221
+ // Column-pivoted complex QR: A*P = Q*R via zgeqp3 + zungqr.
222
+
223
+ Napi::Value QrPivotComplex(const Napi::CallbackInfo& info) {
224
+ Napi::Env env = info.Env();
225
+
226
+ if (info.Length() < 5
227
+ || !info[0].IsTypedArray()
228
+ || !info[1].IsTypedArray()
229
+ || !info[2].IsNumber()
230
+ || !info[3].IsNumber()
231
+ || !info[4].IsBoolean()) {
232
+ Napi::TypeError::New(env,
233
+ "qrPivotComplex: expected (Float64Array dataRe, Float64Array dataIm, "
234
+ "number m, number n, boolean econ)")
235
+ .ThrowAsJavaScriptException();
236
+ return env.Null();
237
+ }
238
+
239
+ auto arrRe = info[0].As<Napi::TypedArray>();
240
+ auto arrIm = info[1].As<Napi::TypedArray>();
241
+ if (arrRe.TypedArrayType() != napi_float64_array ||
242
+ arrIm.TypedArrayType() != napi_float64_array) {
243
+ Napi::TypeError::New(env, "qrPivotComplex: dataRe and dataIm must be Float64Arrays")
244
+ .ThrowAsJavaScriptException();
245
+ return env.Null();
246
+ }
247
+
248
+ int m = info[2].As<Napi::Number>().Int32Value();
249
+ int n = info[3].As<Napi::Number>().Int32Value();
250
+ bool econ = info[4].As<Napi::Boolean>().Value();
251
+
252
+ if (m <= 0 || n <= 0 ||
253
+ static_cast<int>(arrRe.ElementLength()) != m * n ||
254
+ static_cast<int>(arrIm.ElementLength()) != m * n) {
255
+ Napi::RangeError::New(env,
256
+ "qrPivotComplex: dataRe.length and dataIm.length must equal m*n")
257
+ .ThrowAsJavaScriptException();
258
+ return env.Null();
259
+ }
260
+
261
+ int k = m < n ? m : n;
262
+
263
+ auto a = splitToInterleaved(
264
+ info[0].As<Napi::Float64Array>(),
265
+ info[1].As<Napi::Float64Array>(), m * n);
266
+
267
+ std::vector<int> jpvt(n, 0);
268
+ std::vector<lapack_complex_double> tau(k);
269
+ std::vector<double> rwork(2 * n);
270
+ int info_val = 0;
271
+
272
+ // Workspace query
273
+ int lwork = -1;
274
+ lapack_complex_double work_query;
275
+ zgeqp3_(&m, &n, a.data(), &m, jpvt.data(), tau.data(),
276
+ &work_query, &lwork, rwork.data(), &info_val);
277
+ lwork = static_cast<int>(work_query.real);
278
+ if (lwork < 1) lwork = n + 1;
279
+
280
+ std::vector<lapack_complex_double> work(lwork);
281
+ zgeqp3_(&m, &n, a.data(), &m, jpvt.data(), tau.data(),
282
+ work.data(), &lwork, rwork.data(), &info_val);
283
+
284
+ if (!checkLapackInfo(env, info_val, "qrPivotComplex", "zgeqp3"))
285
+ return env.Null();
286
+
287
+ // Extract R
288
+ int r_rows = econ ? k : m;
289
+ std::vector<double> R_re(r_rows * n, 0.0);
290
+ std::vector<double> R_im(r_rows * n, 0.0);
291
+ for (int j = 0; j < n; j++) {
292
+ int ilim = j < k ? j : k - 1;
293
+ for (int i = 0; i <= ilim; i++) {
294
+ R_re[i + j * r_rows] = a[i + j * m].real;
295
+ R_im[i + j * r_rows] = a[i + j * m].imag;
296
+ }
297
+ }
298
+
299
+ // Generate Q via zungqr
300
+ int q_cols = econ ? k : m;
301
+ std::vector<lapack_complex_double> q_buf(m * q_cols, {0.0, 0.0});
302
+ int cols_to_copy = n < q_cols ? n : q_cols;
303
+ for (int j = 0; j < cols_to_copy; j++) {
304
+ for (int i = 0; i < m; i++) {
305
+ q_buf[i + j * m] = a[i + j * m];
306
+ }
307
+ }
308
+
309
+ lwork = -1;
310
+ zungqr_(&m, &q_cols, &k, q_buf.data(), &m, tau.data(),
311
+ &work_query, &lwork, &info_val);
312
+ lwork = static_cast<int>(work_query.real);
313
+ if (lwork < 1) lwork = q_cols;
314
+
315
+ work.assign(lwork, {0.0, 0.0});
316
+ zungqr_(&m, &q_cols, &k, q_buf.data(), &m, tau.data(),
317
+ work.data(), &lwork, &info_val);
318
+
319
+ if (!checkLapackInfo(env, info_val, "qrPivotComplex", "zungqr"))
320
+ return env.Null();
321
+
322
+ auto result = Napi::Object::New(env);
323
+ setSplitComplex(env, result, "QRe", "QIm", q_buf.data(), m * q_cols);
324
+ result.Set("RRe", vecToF64(env, R_re));
325
+ result.Set("RIm", vecToF64(env, R_im));
326
+ result.Set("jpvt", vecToI32(env, jpvt));
327
+ return result;
328
+ }
329
+
118
330
  // ── qrComplex() ──────────────────────────────────────────────────────────────
119
331
 
120
332
  Napi::Value QrComplex(const Napi::CallbackInfo& info) {
@@ -9,7 +9,7 @@
9
9
  * where Q and Z are unitary (orthogonal for real case).
10
10
  */
11
11
 
12
- #include "lapack_common.h"
12
+ #include "numbl_addon_common.h"
13
13
 
14
14
  Napi::Value Qz(const Napi::CallbackInfo& info) {
15
15
  Napi::Env env = info.Env();
@@ -13,7 +13,7 @@
13
13
  * (k = min(m, n))
14
14
  */
15
15
 
16
- #include "lapack_common.h"
16
+ #include "numbl_addon_common.h"
17
17
  #include <string>
18
18
 
19
19
  // ── svd() ─────────────────────────────────────────────────────────────────────
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Native Node.js addon exposing LAPACK/BLAS routines for efficient linear algebra.
2
+ * numbl native addon LAPACK/BLAS, FFT, element-wise arithmetic, and more.
3
3
  *
4
4
  * Exported functions (see individual .cpp files for full documentation):
5
5
  *
@@ -19,17 +19,42 @@
19
19
  * cholComplex(dataRe, dataIm, n, upper) — complex Cholesky (lapack_chol.cpp)
20
20
  */
21
21
 
22
- #include "lapack_common.h"
22
+ #include "numbl_addon_common.h"
23
+ #include <cstdlib>
24
+
25
+ extern "C" {
26
+ void openblas_set_num_threads(int num_threads);
27
+ }
28
+
29
+ // ── Addon version ────────────────────────────────────────────────────────────
30
+ // Bump this integer whenever the addon's API changes (new functions, signature
31
+ // changes, etc.) so that the JS side can detect stale builds.
32
+ static const int ADDON_VERSION = 1;
33
+
34
+ static Napi::Value AddonVersion(const Napi::CallbackInfo& info) {
35
+ return Napi::Number::New(info.Env(), ADDON_VERSION);
36
+ }
23
37
 
24
38
  // ── Module initialisation ─────────────────────────────────────────────────────
25
39
 
26
40
  Napi::Object Init(Napi::Env env, Napi::Object exports) {
41
+ // Use single-threaded BLAS unless the user explicitly set the env var.
42
+ // Multi-threaded BLAS adds overhead for the many small matmuls in numbl.
43
+ if (!std::getenv("OPENBLAS_NUM_THREADS")) {
44
+ openblas_set_num_threads(1);
45
+ }
46
+ exports.Set(Napi::String::New(env, "addonVersion"),
47
+ Napi::Function::New(env, AddonVersion));
27
48
  exports.Set(Napi::String::New(env, "inv"),
28
49
  Napi::Function::New(env, Inv));
29
50
  exports.Set(Napi::String::New(env, "invComplex"),
30
51
  Napi::Function::New(env, InvComplex));
31
52
  exports.Set(Napi::String::New(env, "qr"),
32
53
  Napi::Function::New(env, Qr));
54
+ exports.Set(Napi::String::New(env, "qrPivot"),
55
+ Napi::Function::New(env, QrPivot));
56
+ exports.Set(Napi::String::New(env, "qrPivotComplex"),
57
+ Napi::Function::New(env, QrPivotComplex));
33
58
  exports.Set(Napi::String::New(env, "qrComplex"),
34
59
  Napi::Function::New(env, QrComplex));
35
60
  exports.Set(Napi::String::New(env, "lu"),
@@ -42,6 +67,8 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
42
67
  Napi::Function::New(env, SvdComplex));
43
68
  exports.Set(Napi::String::New(env, "matmul"),
44
69
  Napi::Function::New(env, Matmul));
70
+ exports.Set(Napi::String::New(env, "matmulComplex"),
71
+ Napi::Function::New(env, MatmulComplex));
45
72
  exports.Set(Napi::String::New(env, "linsolve"),
46
73
  Napi::Function::New(env, Linsolve));
47
74
  exports.Set(Napi::String::New(env, "linsolveComplex"),
@@ -64,7 +91,11 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
64
91
  Napi::Function::New(env, Fft1dComplex));
65
92
  exports.Set(Napi::String::New(env, "fftAlongDim"),
66
93
  Napi::Function::New(env, FftAlongDim));
94
+ exports.Set(Napi::String::New(env, "elemwise"),
95
+ Napi::Function::New(env, Elemwise));
96
+ exports.Set(Napi::String::New(env, "elemwiseComplex"),
97
+ Napi::Function::New(env, ElemwiseComplex));
67
98
  return exports;
68
99
  }
69
100
 
70
- NODE_API_MODULE(lapack_addon, Init)
101
+ NODE_API_MODULE(numbl_addon, Init)