@necpp-engine/wasm 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.
package/dist/model.js ADDED
@@ -0,0 +1,702 @@
1
+ import { NecConditioningError, NecGeometryError, NecInputError, NecPortError, NecRuntimeError, NecSolverError, NecStateError, } from "./errors.js";
2
+ import { transitionModelState } from "./state-machine.js";
3
+ const STATUS_OK = 0;
4
+ const STATUS_STATE = 1;
5
+ const STATUS_INPUT = 2;
6
+ const STATUS_GEOMETRY = 3;
7
+ const STATUS_PORT = 4;
8
+ const STATUS_CONDITIONING = 5;
9
+ const STATUS_SOLVER = 6;
10
+ const STATUS_RUNTIME = 7;
11
+ const INT32_MAX = 2_147_483_647;
12
+ const FLOAT64_BYTES = Float64Array.BYTES_PER_ELEMENT;
13
+ const INT32_BYTES = Int32Array.BYTES_PER_ELEMENT;
14
+ const BUFFER = {
15
+ impedanceReal: 0,
16
+ impedanceImag: 1,
17
+ admittanceReal: 2,
18
+ admittanceImag: 3,
19
+ solutionRequestedReal: 4,
20
+ solutionRequestedImag: 5,
21
+ solutionVoltagesReal: 6,
22
+ solutionVoltagesImag: 7,
23
+ solutionCurrentsReal: 8,
24
+ solutionCurrentsImag: 9,
25
+ solutionActiveImpedancesReal: 10,
26
+ solutionActiveImpedancesImag: 11,
27
+ solutionPowersW: 12,
28
+ farFieldThetaDeg: 13,
29
+ farFieldPhiDeg: 14,
30
+ farFieldEThetaReal: 15,
31
+ farFieldEThetaImag: 16,
32
+ farFieldEPhiReal: 17,
33
+ farFieldEPhiImag: 18,
34
+ embeddedThetaDeg: 19,
35
+ embeddedPhiDeg: 20,
36
+ embeddedEThetaReal: 21,
37
+ embeddedEThetaImag: 22,
38
+ embeddedEPhiReal: 23,
39
+ embeddedEPhiImag: 24,
40
+ };
41
+ const textDecoder = new TextDecoder();
42
+ function inputError(message, details) {
43
+ throw new NecInputError(message, details === undefined ? {} : { details });
44
+ }
45
+ function finiteNumber(value, name) {
46
+ if (typeof value !== "number" || !Number.isFinite(value)) {
47
+ inputError(`${name} must be a finite number`, { name, value });
48
+ }
49
+ return value;
50
+ }
51
+ function positiveNumber(value, name) {
52
+ const number = finiteNumber(value, name);
53
+ if (!(number > 0)) {
54
+ inputError(`${name} must be greater than zero`, { name, value });
55
+ }
56
+ return number;
57
+ }
58
+ function integerInRange(value, name, minimum) {
59
+ if (typeof value !== "number"
60
+ || !Number.isInteger(value)
61
+ || value < minimum
62
+ || value > INT32_MAX) {
63
+ inputError(`${name} must be an integer from ${minimum} through ${INT32_MAX}`, { name, value });
64
+ }
65
+ return value;
66
+ }
67
+ function requireRecord(value, name) {
68
+ if (typeof value !== "object" || value === null) {
69
+ inputError(`${name} must be an object`, { name });
70
+ }
71
+ return value;
72
+ }
73
+ function validatePoint(value, name) {
74
+ if (!Array.isArray(value) || value.length !== 3) {
75
+ inputError(`${name} must contain exactly three coordinates`, { name });
76
+ }
77
+ return [
78
+ finiteNumber(value[0], `${name}[0]`),
79
+ finiteNumber(value[1], `${name}[1]`),
80
+ finiteNumber(value[2], `${name}[2]`),
81
+ ];
82
+ }
83
+ function isFloat64Array(value) {
84
+ return Object.prototype.toString.call(value) === "[object Float64Array]";
85
+ }
86
+ function validateComplexVector(vector, expectedLength, name) {
87
+ const record = requireRecord(vector, name);
88
+ const real = record.real;
89
+ const imag = record.imag;
90
+ if (!isFloat64Array(real) || !isFloat64Array(imag)) {
91
+ inputError(`${name}.real and ${name}.imag must be Float64Array instances`);
92
+ }
93
+ if (real.length !== imag.length || real.length !== expectedLength) {
94
+ inputError(`${name} must contain exactly one value per port`, {
95
+ expectedLength,
96
+ realLength: real.length,
97
+ imagLength: imag.length,
98
+ });
99
+ }
100
+ for (let index = 0; index < expectedLength; index += 1) {
101
+ if (!Number.isFinite(real[index]) || !Number.isFinite(imag[index])) {
102
+ inputError(`${name} values must be finite`, { index });
103
+ }
104
+ }
105
+ return { real, imag };
106
+ }
107
+ function validateGrid(request, portCountForEmbedded = 1) {
108
+ const record = requireRecord(request, "request");
109
+ const theta = requireRecord(record.theta, "request.theta");
110
+ const phi = requireRecord(record.phi, "request.phi");
111
+ const radiusM = record.radiusM === undefined
112
+ ? 1
113
+ : positiveNumber(record.radiusM, "request.radiusM");
114
+ const thetaStartDeg = finiteNumber(theta.startDeg, "request.theta.startDeg");
115
+ const thetaCount = integerInRange(theta.count, "request.theta.count", 1);
116
+ const thetaStepDeg = finiteNumber(theta.stepDeg, "request.theta.stepDeg");
117
+ const phiStartDeg = finiteNumber(phi.startDeg, "request.phi.startDeg");
118
+ const phiCount = integerInRange(phi.count, "request.phi.count", 1);
119
+ const phiStepDeg = finiteNumber(phi.stepDeg, "request.phi.stepDeg");
120
+ const thetaEnd = thetaStartDeg + (thetaCount - 1) * thetaStepDeg;
121
+ const phiEnd = phiStartDeg + (phiCount - 1) * phiStepDeg;
122
+ if (!Number.isFinite(thetaEnd) || !Number.isFinite(phiEnd)) {
123
+ inputError("The requested angle sweep overflows");
124
+ }
125
+ const sampleCount = thetaCount * phiCount;
126
+ if (!Number.isSafeInteger(sampleCount)
127
+ || sampleCount > INT32_MAX
128
+ || sampleCount * portCountForEmbedded > INT32_MAX) {
129
+ inputError("The requested far-field array is too large");
130
+ }
131
+ return {
132
+ radiusM,
133
+ thetaStartDeg,
134
+ thetaCount,
135
+ thetaStepDeg,
136
+ phiStartDeg,
137
+ phiCount,
138
+ phiStepDeg,
139
+ sampleCount,
140
+ };
141
+ }
142
+ function validateTarget(target) {
143
+ const record = requireRecord(target, "load.target");
144
+ const tag = integerInRange(record.tag, "load.target.tag", 0);
145
+ if (record.firstSegment === undefined) {
146
+ if (record.lastSegment !== undefined) {
147
+ inputError("load.target.lastSegment requires firstSegment");
148
+ }
149
+ return { tag, firstSegment: 0, lastSegment: 0 };
150
+ }
151
+ const firstSegment = integerInRange(record.firstSegment, "load.target.firstSegment", 1);
152
+ const lastSegment = record.lastSegment === undefined
153
+ ? firstSegment
154
+ : integerInRange(record.lastSegment, "load.target.lastSegment", 1);
155
+ if (lastSegment < firstSegment) {
156
+ inputError("load.target.lastSegment cannot precede firstSegment");
157
+ }
158
+ return { tag, firstSegment, lastSegment };
159
+ }
160
+ function snapshotPorts(ports) {
161
+ return Object.freeze(ports.map((port) => Object.freeze(port.name === undefined
162
+ ? { tag: port.tag, segment: port.segment }
163
+ : { tag: port.tag, segment: port.segment, name: port.name })));
164
+ }
165
+ function nativeState(value) {
166
+ switch (value) {
167
+ case 0:
168
+ return "empty";
169
+ case 1:
170
+ return "geometry-building";
171
+ case 2:
172
+ return "geometry-complete";
173
+ case 3:
174
+ return "prepared";
175
+ case 4:
176
+ return "solved";
177
+ default:
178
+ throw new NecRuntimeError(`The native model returned invalid state ${value}`);
179
+ }
180
+ }
181
+ export class WasmNecModel {
182
+ #moduleStorage;
183
+ #handle;
184
+ #state = "empty";
185
+ #ports = Object.freeze([]);
186
+ constructor(module, handle) {
187
+ this.#moduleStorage = module;
188
+ this.#handle = handle;
189
+ try {
190
+ this.#state = nativeState(module._necpp_wasm_v1_model_state(handle));
191
+ }
192
+ catch (cause) {
193
+ if (cause instanceof NecRuntimeError) {
194
+ throw cause;
195
+ }
196
+ throw new NecRuntimeError("Failed to read the new native model state", {
197
+ cause,
198
+ });
199
+ }
200
+ if (this.#state !== "empty") {
201
+ throw new NecRuntimeError(`A newly created native model started in unexpected state ${this.#state}`);
202
+ }
203
+ }
204
+ get state() {
205
+ return this.#state;
206
+ }
207
+ get #module() {
208
+ if (this.#moduleStorage === undefined) {
209
+ throw new NecRuntimeError("The disposed model no longer has a WASM module");
210
+ }
211
+ return this.#moduleStorage;
212
+ }
213
+ #assertOperation(operation) {
214
+ transitionModelState(this.#state, operation);
215
+ }
216
+ #syncState() {
217
+ if (this.#handle === 0) {
218
+ this.#state = "disposed";
219
+ return;
220
+ }
221
+ this.#state = nativeState(this.#module._necpp_wasm_v1_model_state(this.#handle));
222
+ }
223
+ #decodeBytes(pointer, length) {
224
+ if (!Number.isSafeInteger(pointer)
225
+ || pointer < 0
226
+ || !Number.isSafeInteger(length)
227
+ || length < 0
228
+ || pointer + length > this.#module.HEAPU8.length) {
229
+ throw new NecRuntimeError("The native module returned an invalid string buffer");
230
+ }
231
+ return textDecoder.decode(this.#module.HEAPU8.slice(pointer, pointer + length));
232
+ }
233
+ #decodeCString(pointer) {
234
+ if (!Number.isSafeInteger(pointer)
235
+ || pointer <= 0
236
+ || pointer >= this.#module.HEAPU8.length) {
237
+ return "";
238
+ }
239
+ const end = this.#module.HEAPU8.indexOf(0, pointer);
240
+ if (end < 0) {
241
+ throw new NecRuntimeError("The native module returned an unterminated string");
242
+ }
243
+ return this.#decodeBytes(pointer, end - pointer);
244
+ }
245
+ #lastError() {
246
+ try {
247
+ return this.#decodeCString(this.#module._necpp_wasm_v1_last_error(this.#handle));
248
+ }
249
+ catch {
250
+ return "";
251
+ }
252
+ }
253
+ #statusError(status, operation) {
254
+ const message = this.#lastError() || `${operation} failed with native status ${status}`;
255
+ const details = { operation, nativeStatus: status };
256
+ switch (status) {
257
+ case STATUS_STATE:
258
+ throw new NecStateError(operation, this.#state, message);
259
+ case STATUS_INPUT:
260
+ throw new NecInputError(message, { details });
261
+ case STATUS_GEOMETRY:
262
+ throw new NecGeometryError(message, { details });
263
+ case STATUS_PORT:
264
+ throw new NecPortError(message, { details });
265
+ case STATUS_CONDITIONING:
266
+ throw new NecConditioningError(message, { details });
267
+ case STATUS_SOLVER:
268
+ throw new NecSolverError(message, { details });
269
+ case STATUS_RUNTIME:
270
+ throw new NecRuntimeError(message, { details });
271
+ default:
272
+ throw new NecRuntimeError(`${operation} returned unknown native status ${status}`, { details });
273
+ }
274
+ }
275
+ #invokeStatus(operation, call) {
276
+ let status;
277
+ try {
278
+ status = call();
279
+ this.#syncState();
280
+ }
281
+ catch (cause) {
282
+ try {
283
+ this.#syncState();
284
+ }
285
+ catch {
286
+ // Preserve the original boundary failure.
287
+ }
288
+ if (cause instanceof NecRuntimeError) {
289
+ throw cause;
290
+ }
291
+ throw new NecRuntimeError(`${operation} failed at the WASM boundary`, {
292
+ cause,
293
+ details: { operation },
294
+ });
295
+ }
296
+ if (status !== STATUS_OK) {
297
+ this.#statusError(status, operation);
298
+ }
299
+ }
300
+ #readResult(operation, read) {
301
+ try {
302
+ return read();
303
+ }
304
+ catch (cause) {
305
+ if (cause instanceof NecRuntimeError) {
306
+ throw cause;
307
+ }
308
+ throw new NecRuntimeError(`${operation} returned an invalid native result`, { cause, details: { operation } });
309
+ }
310
+ }
311
+ #allocate(bytes) {
312
+ let pointer;
313
+ try {
314
+ pointer = this.#module._malloc(bytes);
315
+ }
316
+ catch (cause) {
317
+ throw new NecRuntimeError("WASM memory allocation failed", { cause });
318
+ }
319
+ if (!Number.isSafeInteger(pointer) || pointer <= 0) {
320
+ throw new NecRuntimeError("WASM memory allocation failed");
321
+ }
322
+ return pointer;
323
+ }
324
+ #free(pointer) {
325
+ if (pointer === 0) {
326
+ return;
327
+ }
328
+ try {
329
+ this.#module._free(pointer);
330
+ }
331
+ catch {
332
+ // Emscripten free is not expected to throw; cleanup remains best-effort.
333
+ }
334
+ }
335
+ #withInt32Pair(first, second, call) {
336
+ let firstPointer = 0;
337
+ let secondPointer = 0;
338
+ try {
339
+ firstPointer = this.#allocate(first.byteLength);
340
+ secondPointer = this.#allocate(second.byteLength);
341
+ this.#module.HEAP32.set(first, firstPointer / INT32_BYTES);
342
+ this.#module.HEAP32.set(second, secondPointer / INT32_BYTES);
343
+ return call(firstPointer, secondPointer);
344
+ }
345
+ finally {
346
+ this.#free(secondPointer);
347
+ this.#free(firstPointer);
348
+ }
349
+ }
350
+ #withFloat64Pair(first, second, call) {
351
+ let firstPointer = 0;
352
+ let secondPointer = 0;
353
+ try {
354
+ firstPointer = this.#allocate(first.byteLength);
355
+ secondPointer = this.#allocate(second.byteLength);
356
+ this.#module.HEAPF64.set(first, firstPointer / FLOAT64_BYTES);
357
+ this.#module.HEAPF64.set(second, secondPointer / FLOAT64_BYTES);
358
+ return call(firstPointer, secondPointer);
359
+ }
360
+ finally {
361
+ this.#free(secondPointer);
362
+ this.#free(firstPointer);
363
+ }
364
+ }
365
+ #copyBuffer(kind, expectedLength) {
366
+ try {
367
+ const length = this.#module._necpp_wasm_v1_result_buffer_length(this.#handle, kind);
368
+ const pointer = this.#module._necpp_wasm_v1_result_buffer(this.#handle, kind);
369
+ if (length !== expectedLength
370
+ || !Number.isSafeInteger(pointer)
371
+ || pointer < 0
372
+ || pointer % FLOAT64_BYTES !== 0
373
+ || (length > 0 && pointer === 0)) {
374
+ throw new NecRuntimeError(`Native result buffer ${kind} has invalid dimensions`, { details: { kind, expectedLength, actualLength: length, pointer } });
375
+ }
376
+ const start = pointer / FLOAT64_BYTES;
377
+ const end = start + length;
378
+ if (start < 0 || end > this.#module.HEAPF64.length) {
379
+ throw new NecRuntimeError(`Native result buffer ${kind} is out of bounds`);
380
+ }
381
+ return this.#module.HEAPF64.slice(start, end);
382
+ }
383
+ catch (cause) {
384
+ if (cause instanceof NecRuntimeError) {
385
+ throw cause;
386
+ }
387
+ throw new NecRuntimeError(`Failed to copy native result buffer ${kind}`, {
388
+ cause,
389
+ details: { kind },
390
+ });
391
+ }
392
+ }
393
+ #matrix(realKind, imagKind, order) {
394
+ const length = order * order;
395
+ return {
396
+ rows: order,
397
+ columns: order,
398
+ order: "row-major",
399
+ real: this.#copyBuffer(realKind, length),
400
+ imag: this.#copyBuffer(imagKind, length),
401
+ };
402
+ }
403
+ addWire(wire) {
404
+ this.#assertOperation("addWire");
405
+ const record = requireRecord(wire, "wire");
406
+ const tag = integerInRange(record.tag, "wire.tag", 1);
407
+ const segments = integerInRange(record.segments, "wire.segments", 1);
408
+ const start = validatePoint(record.start, "wire.start");
409
+ const end = validatePoint(record.end, "wire.end");
410
+ if (start[0] === end[0] && start[1] === end[1] && start[2] === end[2]) {
411
+ inputError("wire.start and wire.end must be distinct");
412
+ }
413
+ const radiusM = positiveNumber(record.radiusM, "wire.radiusM");
414
+ this.#invokeStatus("addWire", () => this.#module._necpp_wasm_v1_add_wire(this.#handle, tag, segments, start[0], start[1], start[2], end[0], end[1], end[2], radiusM));
415
+ }
416
+ completeGeometry(options = {}) {
417
+ this.#assertOperation("completeGeometry");
418
+ const record = requireRecord(options, "options");
419
+ const connection = record.groundConnection ?? "none";
420
+ const nativeConnection = connection === "none"
421
+ ? 0
422
+ : connection === "interpolate"
423
+ ? 1
424
+ : connection === "zero-current"
425
+ ? 2
426
+ : inputError("Unknown ground connection", { connection });
427
+ this.#invokeStatus("completeGeometry", () => this.#module._necpp_wasm_v1_complete_geometry(this.#handle, nativeConnection));
428
+ }
429
+ definePorts(ports) {
430
+ this.#assertOperation("definePorts");
431
+ if (!Array.isArray(ports) || ports.length === 0) {
432
+ throw new NecPortError("At least one port is required");
433
+ }
434
+ if (ports.length > INT32_MAX) {
435
+ inputError("Too many ports");
436
+ }
437
+ const tags = new Int32Array(ports.length);
438
+ const segments = new Int32Array(ports.length);
439
+ const copies = [];
440
+ for (let index = 0; index < ports.length; index += 1) {
441
+ const record = requireRecord(ports[index], `ports[${index}]`);
442
+ const tag = integerInRange(record.tag, `ports[${index}].tag`, 1);
443
+ const segment = integerInRange(record.segment, `ports[${index}].segment`, 1);
444
+ if (record.name !== undefined && typeof record.name !== "string") {
445
+ inputError(`ports[${index}].name must be a string`);
446
+ }
447
+ tags[index] = tag;
448
+ segments[index] = segment;
449
+ copies.push(record.name === undefined
450
+ ? { tag, segment }
451
+ : { tag, segment, name: record.name });
452
+ }
453
+ this.#invokeStatus("definePorts", () => this.#withInt32Pair(tags, segments, (tagsPointer, segmentsPointer) => this.#module._necpp_wasm_v1_define_ports(this.#handle, tagsPointer, segmentsPointer, ports.length)));
454
+ this.#ports = snapshotPorts(copies);
455
+ }
456
+ addLoad(load) {
457
+ this.#assertOperation("addLoad");
458
+ const record = requireRecord(load, "load");
459
+ const target = validateTarget(record.target);
460
+ let kind;
461
+ let value1;
462
+ let value2 = 0;
463
+ let value3 = 0;
464
+ switch (record.kind) {
465
+ case "series-rlc":
466
+ case "parallel-rlc": {
467
+ if (record.perMeter !== undefined
468
+ && record.perMeter !== true
469
+ && record.perMeter !== false) {
470
+ inputError("load.perMeter must be boolean when supplied");
471
+ }
472
+ const distributed = record.perMeter === true;
473
+ kind = record.kind === "series-rlc"
474
+ ? (distributed ? 2 : 0)
475
+ : (distributed ? 3 : 1);
476
+ value1 = finiteNumber(record.resistanceOhm, "load.resistanceOhm");
477
+ value2 = finiteNumber(record.inductanceH, "load.inductanceH");
478
+ value3 = finiteNumber(record.capacitanceF, "load.capacitanceF");
479
+ break;
480
+ }
481
+ case "impedance":
482
+ kind = 4;
483
+ value1 = finiteNumber(record.resistanceOhm, "load.resistanceOhm");
484
+ value2 = finiteNumber(record.reactanceOhm, "load.reactanceOhm");
485
+ break;
486
+ case "conductivity":
487
+ kind = 5;
488
+ value1 = positiveNumber(record.conductivitySPerM, "load.conductivitySPerM");
489
+ break;
490
+ default:
491
+ return inputError("Unknown load kind", { kind: record.kind });
492
+ }
493
+ this.#invokeStatus("addLoad", () => this.#module._necpp_wasm_v1_add_load(this.#handle, kind, target.tag, target.firstSegment, target.lastSegment, value1, value2, value3));
494
+ }
495
+ clearLoads() {
496
+ this.#assertOperation("clearLoads");
497
+ this.#invokeStatus("clearLoads", () => this.#module._necpp_wasm_v1_clear_loads(this.#handle));
498
+ }
499
+ setGround(ground) {
500
+ this.#assertOperation("setGround");
501
+ const record = requireRecord(ground, "ground");
502
+ let kind;
503
+ let relativePermittivity = 0;
504
+ let conductivitySPerM = 0;
505
+ switch (record.kind) {
506
+ case "free-space":
507
+ kind = 0;
508
+ break;
509
+ case "perfect":
510
+ kind = 1;
511
+ break;
512
+ case "finite":
513
+ kind = record.method === "reflection-coefficient"
514
+ ? 2
515
+ : record.method === "sommerfeld-norton"
516
+ ? 3
517
+ : inputError("Unknown finite-ground method", { method: record.method });
518
+ relativePermittivity = positiveNumber(record.relativePermittivity, "ground.relativePermittivity");
519
+ conductivitySPerM = positiveNumber(record.conductivitySPerM, "ground.conductivitySPerM");
520
+ break;
521
+ default:
522
+ return inputError("Unknown ground kind", { kind: record.kind });
523
+ }
524
+ this.#invokeStatus("setGround", () => this.#module._necpp_wasm_v1_set_ground(this.#handle, kind, relativePermittivity, conductivitySPerM));
525
+ }
526
+ prepare(options) {
527
+ this.#assertOperation("prepare");
528
+ const record = requireRecord(options, "options");
529
+ const frequencyMHz = positiveNumber(record.frequencyMHz, "options.frequencyMHz");
530
+ this.#invokeStatus("prepare", () => this.#module._necpp_wasm_v1_prepare(this.#handle, frequencyMHz));
531
+ }
532
+ computeImpedanceMatrix() {
533
+ this.#assertOperation("computeImpedanceMatrix");
534
+ this.#invokeStatus("computeImpedanceMatrix", () => this.#module._necpp_wasm_v1_compute_impedance(this.#handle));
535
+ return this.#readResult("computeImpedanceMatrix", () => {
536
+ const order = this.#module._necpp_wasm_v1_impedance_order(this.#handle);
537
+ if (order !== this.#ports.length || !Number.isSafeInteger(order)) {
538
+ throw new NecRuntimeError("The native impedance matrix has invalid order");
539
+ }
540
+ const conditionEstimate = this.#module._necpp_wasm_v1_impedance_condition_estimate(this.#handle);
541
+ if (!Number.isFinite(conditionEstimate) || conditionEstimate < 0) {
542
+ throw new NecRuntimeError("The native condition estimate is invalid");
543
+ }
544
+ return {
545
+ impedance: this.#matrix(BUFFER.impedanceReal, BUFFER.impedanceImag, order),
546
+ admittance: this.#matrix(BUFFER.admittanceReal, BUFFER.admittanceImag, order),
547
+ conditionEstimate,
548
+ frequencyMHz: this.#module._necpp_wasm_v1_impedance_frequency_mhz(this.#handle),
549
+ factorizationGeneration: this.#module._necpp_wasm_v1_impedance_factorization_generation(this.#handle),
550
+ };
551
+ });
552
+ }
553
+ #solution(drive) {
554
+ const count = this.#module._necpp_wasm_v1_solution_count(this.#handle);
555
+ const nativeDrive = this.#module._necpp_wasm_v1_solution_drive(this.#handle);
556
+ if (count !== this.#ports.length
557
+ || !Number.isSafeInteger(count)
558
+ || nativeDrive !== (drive === "voltage" ? 0 : 1)) {
559
+ throw new NecRuntimeError("The native port solution has invalid metadata");
560
+ }
561
+ const complex = (realKind, imagKind) => ({
562
+ real: this.#copyBuffer(realKind, count),
563
+ imag: this.#copyBuffer(imagKind, count),
564
+ });
565
+ return {
566
+ drive,
567
+ frequencyMHz: this.#module._necpp_wasm_v1_solution_frequency_mhz(this.#handle),
568
+ ports: snapshotPorts(this.#ports),
569
+ requested: complex(BUFFER.solutionRequestedReal, BUFFER.solutionRequestedImag),
570
+ voltages: complex(BUFFER.solutionVoltagesReal, BUFFER.solutionVoltagesImag),
571
+ currents: complex(BUFFER.solutionCurrentsReal, BUFFER.solutionCurrentsImag),
572
+ activeImpedances: complex(BUFFER.solutionActiveImpedancesReal, BUFFER.solutionActiveImpedancesImag),
573
+ powersW: this.#copyBuffer(BUFFER.solutionPowersW, count),
574
+ factorizationGeneration: this.#module._necpp_wasm_v1_solution_factorization_generation(this.#handle),
575
+ solveGeneration: this.#module._necpp_wasm_v1_solution_generation(this.#handle),
576
+ };
577
+ }
578
+ solveVoltages(voltages) {
579
+ this.#assertOperation("solveVoltages");
580
+ const vector = validateComplexVector(voltages, this.#ports.length, "voltages");
581
+ this.#invokeStatus("solveVoltages", () => this.#withFloat64Pair(vector.real, vector.imag, (realPointer, imagPointer) => this.#module._necpp_wasm_v1_solve_voltages(this.#handle, realPointer, imagPointer, vector.real.length)));
582
+ return this.#readResult("solveVoltages", () => this.#solution("voltage"));
583
+ }
584
+ solveCurrents(currents) {
585
+ this.#assertOperation("solveCurrents");
586
+ const vector = validateComplexVector(currents, this.#ports.length, "currents");
587
+ this.#invokeStatus("solveCurrents", () => this.#withFloat64Pair(vector.real, vector.imag, (realPointer, imagPointer) => this.#module._necpp_wasm_v1_solve_currents(this.#handle, realPointer, imagPointer, vector.real.length)));
588
+ return this.#readResult("solveCurrents", () => this.#solution("current"));
589
+ }
590
+ #farFieldResult(grid) {
591
+ const thetaCount = this.#module._necpp_wasm_v1_far_field_theta_count(this.#handle);
592
+ const phiCount = this.#module._necpp_wasm_v1_far_field_phi_count(this.#handle);
593
+ if (thetaCount !== grid.thetaCount
594
+ || phiCount !== grid.phiCount
595
+ || thetaCount * phiCount !== grid.sampleCount) {
596
+ throw new NecRuntimeError("The native far-field result has invalid dimensions");
597
+ }
598
+ return {
599
+ radiusM: this.#module._necpp_wasm_v1_far_field_radius_m(this.#handle),
600
+ frequencyMHz: this.#module._necpp_wasm_v1_far_field_frequency_mhz(this.#handle),
601
+ thetaDeg: this.#copyBuffer(BUFFER.farFieldThetaDeg, thetaCount),
602
+ phiDeg: this.#copyBuffer(BUFFER.farFieldPhiDeg, phiCount),
603
+ eThetaReal: this.#copyBuffer(BUFFER.farFieldEThetaReal, grid.sampleCount),
604
+ eThetaImag: this.#copyBuffer(BUFFER.farFieldEThetaImag, grid.sampleCount),
605
+ ePhiReal: this.#copyBuffer(BUFFER.farFieldEPhiReal, grid.sampleCount),
606
+ ePhiImag: this.#copyBuffer(BUFFER.farFieldEPhiImag, grid.sampleCount),
607
+ };
608
+ }
609
+ computeFarField(request) {
610
+ this.#assertOperation("computeFarField");
611
+ const grid = validateGrid(request);
612
+ this.#invokeStatus("computeFarField", () => this.#module._necpp_wasm_v1_compute_far_field(this.#handle, grid.radiusM, grid.thetaStartDeg, grid.thetaCount, grid.thetaStepDeg, grid.phiStartDeg, grid.phiCount, grid.phiStepDeg));
613
+ return this.#readResult("computeFarField", () => this.#farFieldResult(grid));
614
+ }
615
+ computeEmbeddedFarFields(request, normalization = {
616
+ kind: "unit-voltage",
617
+ valueV: 1,
618
+ }) {
619
+ this.#assertOperation("computeEmbeddedFarFields");
620
+ const grid = validateGrid(request, this.#ports.length);
621
+ const record = requireRecord(normalization, "normalization");
622
+ const nativeNormalization = record.kind === "unit-voltage"
623
+ && record.valueV === 1
624
+ ? 0
625
+ : record.kind === "unit-current" && record.valueA === 1
626
+ ? 1
627
+ : inputError("normalization must request exactly one volt or one ampere");
628
+ this.#invokeStatus("computeEmbeddedFarFields", () => this.#module._necpp_wasm_v1_compute_embedded_far_fields(this.#handle, grid.radiusM, grid.thetaStartDeg, grid.thetaCount, grid.thetaStepDeg, grid.phiStartDeg, grid.phiCount, grid.phiStepDeg, nativeNormalization));
629
+ return this.#readResult("computeEmbeddedFarFields", () => {
630
+ const thetaCount = this.#module._necpp_wasm_v1_embedded_theta_count(this.#handle);
631
+ const phiCount = this.#module._necpp_wasm_v1_embedded_phi_count(this.#handle);
632
+ const portCount = this.#module._necpp_wasm_v1_embedded_port_count(this.#handle);
633
+ const samplesPerPort = this.#module._necpp_wasm_v1_embedded_samples_per_port(this.#handle);
634
+ const returnedNormalization = this.#module._necpp_wasm_v1_embedded_normalization(this.#handle);
635
+ if (thetaCount !== grid.thetaCount
636
+ || phiCount !== grid.phiCount
637
+ || portCount !== this.#ports.length
638
+ || samplesPerPort !== grid.sampleCount
639
+ || returnedNormalization !== nativeNormalization) {
640
+ throw new NecRuntimeError("The native embedded far-field result has invalid metadata");
641
+ }
642
+ const totalSamples = samplesPerPort * portCount;
643
+ const resultNormalization = nativeNormalization === 0
644
+ ? Object.freeze({ kind: "unit-voltage", valueV: 1 })
645
+ : Object.freeze({ kind: "unit-current", valueA: 1 });
646
+ return {
647
+ radiusM: this.#module._necpp_wasm_v1_embedded_radius_m(this.#handle),
648
+ frequencyMHz: this.#module._necpp_wasm_v1_embedded_frequency_mhz(this.#handle),
649
+ thetaDeg: this.#copyBuffer(BUFFER.embeddedThetaDeg, thetaCount),
650
+ phiDeg: this.#copyBuffer(BUFFER.embeddedPhiDeg, phiCount),
651
+ eThetaReal: this.#copyBuffer(BUFFER.embeddedEThetaReal, totalSamples),
652
+ eThetaImag: this.#copyBuffer(BUFFER.embeddedEThetaImag, totalSamples),
653
+ ePhiReal: this.#copyBuffer(BUFFER.embeddedEPhiReal, totalSamples),
654
+ ePhiImag: this.#copyBuffer(BUFFER.embeddedEPhiImag, totalSamples),
655
+ ports: snapshotPorts(this.#ports),
656
+ normalization: resultNormalization,
657
+ samplesPerPort,
658
+ };
659
+ });
660
+ }
661
+ dispose() {
662
+ if (this.#state === "disposed") {
663
+ return;
664
+ }
665
+ const handle = this.#handle;
666
+ const module = this.#moduleStorage;
667
+ this.#handle = 0;
668
+ this.#moduleStorage = undefined;
669
+ this.#state = "disposed";
670
+ this.#ports = Object.freeze([]);
671
+ try {
672
+ module?._necpp_wasm_v1_model_delete(handle);
673
+ }
674
+ catch {
675
+ // The ABI promises contained, deterministic cleanup.
676
+ }
677
+ }
678
+ }
679
+ export function createModelFromModule(module) {
680
+ let handle;
681
+ try {
682
+ handle = module._necpp_wasm_v1_model_create();
683
+ }
684
+ catch (cause) {
685
+ throw new NecRuntimeError("Failed to create the native NEC model", { cause });
686
+ }
687
+ if (!Number.isSafeInteger(handle) || handle <= 0) {
688
+ throw new NecRuntimeError("Failed to create the native NEC model");
689
+ }
690
+ try {
691
+ return new WasmNecModel(module, handle);
692
+ }
693
+ catch (error) {
694
+ try {
695
+ module._necpp_wasm_v1_model_delete(handle);
696
+ }
697
+ catch {
698
+ // Preserve the initialization error.
699
+ }
700
+ throw error;
701
+ }
702
+ }