numbl 0.0.21 → 0.0.23

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
 
@@ -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,11 +19,32 @@
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"),
@@ -42,6 +63,8 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
42
63
  Napi::Function::New(env, SvdComplex));
43
64
  exports.Set(Napi::String::New(env, "matmul"),
44
65
  Napi::Function::New(env, Matmul));
66
+ exports.Set(Napi::String::New(env, "matmulComplex"),
67
+ Napi::Function::New(env, MatmulComplex));
45
68
  exports.Set(Napi::String::New(env, "linsolve"),
46
69
  Napi::Function::New(env, Linsolve));
47
70
  exports.Set(Napi::String::New(env, "linsolveComplex"),
@@ -64,7 +87,11 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
64
87
  Napi::Function::New(env, Fft1dComplex));
65
88
  exports.Set(Napi::String::New(env, "fftAlongDim"),
66
89
  Napi::Function::New(env, FftAlongDim));
90
+ exports.Set(Napi::String::New(env, "elemwise"),
91
+ Napi::Function::New(env, Elemwise));
92
+ exports.Set(Napi::String::New(env, "elemwiseComplex"),
93
+ Napi::Function::New(env, ElemwiseComplex));
67
94
  return exports;
68
95
  }
69
96
 
70
- NODE_API_MODULE(lapack_addon, Init)
97
+ NODE_API_MODULE(numbl_addon, Init)
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Common includes, type definitions, LAPACK/BLAS declarations, and function
3
- * prototypes shared across the lapack_addon source files.
3
+ * prototypes shared across the numbl_addon source files.
4
4
  */
5
5
 
6
6
  #pragma once
@@ -83,6 +83,13 @@ extern "C" {
83
83
  double* b, int* ldb,
84
84
  double* beta, double* c, int* ldc);
85
85
 
86
+ // Complex matrix-matrix multiplication: C = alpha * op(A) * op(B) + beta * C
87
+ void zgemm_(char* transa, char* transb,
88
+ int* m, int* n, int* k,
89
+ lapack_complex_double* alpha, lapack_complex_double* a, int* lda,
90
+ lapack_complex_double* b, int* ldb,
91
+ lapack_complex_double* beta, lapack_complex_double* c, int* ldc);
92
+
86
93
  // ── Linear solve (square) ─────────────────────────────────────────────────
87
94
  // LU factorisation + solve: A * X = B (A is n×n, B is n×nrhs)
88
95
  // On exit A contains the LU factors; B contains X.
@@ -253,6 +260,7 @@ Napi::Value LuComplex(const Napi::CallbackInfo& info);
253
260
  Napi::Value Svd(const Napi::CallbackInfo& info);
254
261
  Napi::Value SvdComplex(const Napi::CallbackInfo& info);
255
262
  Napi::Value Matmul(const Napi::CallbackInfo& info);
263
+ Napi::Value MatmulComplex(const Napi::CallbackInfo& info);
256
264
  Napi::Value Linsolve(const Napi::CallbackInfo& info);
257
265
  Napi::Value LinsolveComplex(const Napi::CallbackInfo& info);
258
266
  Napi::Value Eig(const Napi::CallbackInfo& info);
@@ -264,3 +272,5 @@ Napi::Value QzComplex(const Napi::CallbackInfo& info);
264
272
  Napi::Value Fft1d(const Napi::CallbackInfo& info);
265
273
  Napi::Value Fft1dComplex(const Napi::CallbackInfo& info);
266
274
  Napi::Value FftAlongDim(const Napi::CallbackInfo& info);
275
+ Napi::Value Elemwise(const Napi::CallbackInfo& info);
276
+ Napi::Value ElemwiseComplex(const Napi::CallbackInfo& info);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numbl",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
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",
@@ -106,10 +106,11 @@
106
106
  "vitest": "^4.0.18"
107
107
  },
108
108
  "lint-staged": {
109
- "*.{js,jsx,ts,tsx}": [
109
+ "*.{ts,tsx}": [
110
110
  "prettier --write",
111
111
  "eslint"
112
112
  ],
113
+ "*.{js,jsx}": "prettier --write",
113
114
  "*.{json,css,md,yml}": "prettier --write"
114
115
  },
115
116
  "optionalDependencies": {