@sdeverywhere/runtime 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020-2022 Climate Interactive / New Venture Fund
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # @sdeverywhere/runtime
2
+
3
+ This package provides a simplified API around a system dynamics model that
4
+ has been produced by [SDEverywhere](https://github.com/climateinteractive/SDEverywhere)
5
+ and compiled to a WebAssembly (Wasm) module via [Emscripten](https://emscripten.org).
6
+
7
+ ## Usage
8
+
9
+ ### 1. Initialize the `WasmModel`
10
+
11
+ In your application, load the wasm module using the wrapper produced by
12
+ Emscripten, then pass it to `initWasmModelAndBuffers`.
13
+ This will create the `WasmModel` and `WasmBuffer` instances that will be
14
+ used in the next step to initalize the `ModelRunner`.
15
+
16
+ ```ts
17
+ import { initWasmModelAndBuffers, WasmModelInitResult } from '@sdeverywhere/runtime'
18
+ import loadWasm from './generated/mymodel'
19
+
20
+ // These are the same lists (and must be in the same order) as the spec file passed to `sde`.
21
+ const inputVarNames = [] // from spec.json
22
+ const outputVarNames = [] // from spec.json
23
+
24
+ async function initWasmModel(): Promise<WasmModelInitResult> {
25
+ // Load the wasm module asynchronously
26
+ const wasmModule = await loadWasm()
27
+
28
+ // Initialize the wasm model and its associated buffers
29
+ return initWasmModelAndBuffers(wasmModule, inputVarNames.length, outputVarNames, 2000, 2100)
30
+ }
31
+ ```
32
+
33
+ ### 2. Initialize the `ModelRunner`
34
+
35
+ The next step is to create a `ModelRunner` instance, which simplifies
36
+ the process of running a `WasmModel` with a given set of inputs and
37
+ parsing the outputs.
38
+ The `ModelRunner` produces an `Outputs` instance that provides easy
39
+ access to time series data for each output variable in the model.
40
+ The `createWasmModelRunner` function is the simplest way to create
41
+ a `ModelRunner` that works with your `WasmModel`:
42
+
43
+ ```ts
44
+ import { createWasmModelRunner, createInputValue, Outputs } from '@sdeverywhere/runtime'
45
+
46
+ async function main() {
47
+ // Initialize the `WasmModel` and `ModelRunner`
48
+ const wasmResult = await initWasmModel()
49
+ const modelRunner = createWasmModelRunner(wasmResult)
50
+
51
+ // Create a set of `InputValue` instances corresponding to the inputs in the spec.json file
52
+ const inputs = [createInputValue('_input1', 2), createInputValue('_input2', 10)] // etc
53
+
54
+ // Create an `Outputs` instance to hold the model outputs
55
+ let outputs = new Outputs(wasmResult.outputVarIds, wasmResult.startTime, wasmResult.endTime)
56
+
57
+ // Run the model with those inputs
58
+ outputs = await modelRunner.runModel(inputs, outputs)
59
+
60
+ // Get the time series data and/or a specific value for a given output variable
61
+ const series = outputs.getSeriesForVar('_temperature_change_from_1850')
62
+ const tempChangeIn2100 = series.getValueAtTime(2100)
63
+ console.log(`Temperature change in 2100: ${tempChangeIn2100}`)
64
+ ```
65
+
66
+ See the `@sdeverywhere/runtime-async` package for an alternative
67
+ implementation of `ModelRunner` that allows for running a model in a Web
68
+ Worker or Node.js worker thread.
69
+
70
+ ### 3. Initialize a `ModelScheduler` (optional)
71
+
72
+ If you build a more complex application with a user interface around a
73
+ model, the `ModelScheduler` class takes care of automatically scheduling
74
+ and running the model whenever there are changes to input variables:
75
+
76
+ ```ts
77
+ import { ModelScheduler } from '@sdeverywhere/runtime'
78
+
79
+ async function initModel() {
80
+ // Initialize the `WasmModel`, `ModelRunner`, inputs, and outputs as above
81
+ const modelScheduler = new ModelScheduler(modelRunner, inputs, outputs)
82
+
83
+ // Get notified when new output data is available
84
+ modelScheduler.onOutputsChanged = newOutputs => {
85
+ // Update the user interface to reflect the new output data, etc
86
+ }
87
+
88
+ // When you change the value of an input, the scheduler will automatically
89
+ // run the model and call `onOutputsChanged` when new outputs are ready
90
+ inputs[0].set(3)
91
+ }
92
+ ```
93
+
94
+ ## Emscripten Notes
95
+
96
+ The `@sdeverywhere/runtime` package assumes you have created `<mymodel>.wasm`
97
+ and `<mymodel>.js` files with Emscripten.
98
+ The `emcc` command line options should be similar to the following:
99
+
100
+ ```
101
+ $ emcc \
102
+ build/<mymodel>.c build/macros.c build/model.c build/vensim.c \
103
+ -Ibuild -o ./output/<mymodel>.js -Wall -Os \
104
+ -s STRICT=1 -s MALLOC=emmalloc -s FILESYSTEM=0 -s MODULARIZE=1 \
105
+ -s EXPORTED_FUNCTIONS="['_runModelWithBuffers', '_malloc']" \
106
+ -s EXPORTED_RUNTIME_METHODS="['cwrap']"
107
+ ```
108
+
109
+ (The generated module must export at minimum `_runModelWithBuffers`,
110
+ `_malloc`, and `cwrap`.)
111
+
112
+ ## Documentation
113
+
114
+ API documentation is available in the [`docs`](./docs/index.md) directory.
115
+
116
+ ## License
117
+
118
+ SDEverywhere is distributed under the MIT license. See `LICENSE` for more details.
package/dist/index.cjs ADDED
@@ -0,0 +1,335 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __spreadValues = (a, b) => {
9
+ for (var prop in b || (b = {}))
10
+ if (__hasOwnProp.call(b, prop))
11
+ __defNormalProp(a, prop, b[prop]);
12
+ if (__getOwnPropSymbols)
13
+ for (var prop of __getOwnPropSymbols(b)) {
14
+ if (__propIsEnum.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ }
17
+ return a;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, { get: all[name], enumerable: true });
22
+ };
23
+ var __copyProps = (to, from, except, desc) => {
24
+ if (from && typeof from === "object" || typeof from === "function") {
25
+ for (let key of __getOwnPropNames(from))
26
+ if (!__hasOwnProp.call(to, key) && key !== except)
27
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
28
+ }
29
+ return to;
30
+ };
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // src/index.ts
34
+ var src_exports = {};
35
+ __export(src_exports, {
36
+ ModelScheduler: () => ModelScheduler,
37
+ Outputs: () => Outputs,
38
+ Series: () => Series,
39
+ WasmBuffer: () => WasmBuffer,
40
+ WasmModel: () => WasmModel,
41
+ createInputValue: () => createInputValue,
42
+ createWasmModelRunner: () => createWasmModelRunner,
43
+ initWasmModelAndBuffers: () => initWasmModelAndBuffers,
44
+ perfElapsed: () => perfElapsed,
45
+ perfNow: () => perfNow
46
+ });
47
+ module.exports = __toCommonJS(src_exports);
48
+
49
+ // src/wasm-model/wasm-buffer.ts
50
+ var WasmBuffer = class {
51
+ constructor(wasmModule, numElements) {
52
+ this.wasmModule = wasmModule;
53
+ const sizeOfFloat64 = 8;
54
+ const lengthInBytes = numElements * sizeOfFloat64;
55
+ this.byteOffset = wasmModule._malloc(lengthInBytes);
56
+ const float64Offset = this.byteOffset / sizeOfFloat64;
57
+ this.heapArray = wasmModule.HEAPF64.subarray(float64Offset, float64Offset + numElements);
58
+ }
59
+ getArrayView() {
60
+ return this.heapArray;
61
+ }
62
+ getAddress() {
63
+ return this.byteOffset;
64
+ }
65
+ dispose() {
66
+ if (this.heapArray) {
67
+ this.wasmModule._free(this.byteOffset);
68
+ this.heapArray = void 0;
69
+ this.byteOffset = void 0;
70
+ }
71
+ }
72
+ };
73
+
74
+ // src/wasm-model/wasm-model.ts
75
+ var WasmModel = class {
76
+ constructor(wasmModule) {
77
+ this.wasmRunModel = wasmModule.cwrap("runModelWithBuffers", null, ["number", "number"]);
78
+ }
79
+ runModel(inputs, outputs) {
80
+ this.wasmRunModel(inputs.getAddress(), outputs.getAddress());
81
+ }
82
+ };
83
+ function initWasmModelAndBuffers(wasmModule, numInputs, outputVarIds, startTime, endTime) {
84
+ const model = new WasmModel(wasmModule);
85
+ const inputsBuffer = new WasmBuffer(wasmModule, numInputs);
86
+ const seriesLength = endTime - startTime + 1;
87
+ const outputsBuffer = new WasmBuffer(wasmModule, outputVarIds.length * seriesLength);
88
+ return {
89
+ model,
90
+ inputsBuffer,
91
+ outputsBuffer,
92
+ outputVarIds,
93
+ startTime,
94
+ endTime
95
+ };
96
+ }
97
+
98
+ // src/model-runner/inputs.ts
99
+ function createInputValue(varId, defaultValue, initialValue) {
100
+ let currentValue = initialValue !== void 0 ? initialValue : defaultValue;
101
+ const callbacks = {};
102
+ const get = () => {
103
+ return currentValue;
104
+ };
105
+ const set = (newValue) => {
106
+ var _a;
107
+ if (newValue !== currentValue) {
108
+ currentValue = newValue;
109
+ (_a = callbacks.onSet) == null ? void 0 : _a.call(callbacks);
110
+ }
111
+ };
112
+ const reset = () => {
113
+ set(defaultValue);
114
+ };
115
+ return { varId, get, set, reset, callbacks };
116
+ }
117
+
118
+ // src/model-runner/outputs.ts
119
+ var import_neverthrow = require("neverthrow");
120
+ var Series = class {
121
+ constructor(varId, points) {
122
+ this.varId = varId;
123
+ this.points = points;
124
+ }
125
+ getValueAtTime(time) {
126
+ var _a;
127
+ const startTime = this.points[0].x;
128
+ return (_a = this.points[time - startTime]) == null ? void 0 : _a.y;
129
+ }
130
+ copy() {
131
+ const pointsCopy = this.points.map((p) => __spreadValues({}, p));
132
+ return new Series(this.varId, pointsCopy);
133
+ }
134
+ };
135
+ var Outputs = class {
136
+ constructor(varIds, timeStart, timeEnd) {
137
+ this.varIds = varIds;
138
+ this.timeStart = timeStart;
139
+ this.timeEnd = timeEnd;
140
+ this.seriesLength = timeEnd - timeStart + 1;
141
+ this.varSeries = new Array(varIds.length);
142
+ for (let i = 0; i < varIds.length; i++) {
143
+ const points = new Array(this.seriesLength);
144
+ let time = timeStart;
145
+ for (let j = 0; j < this.seriesLength; j++) {
146
+ points[j] = { x: time++, y: 0 };
147
+ }
148
+ const varId = varIds[i];
149
+ this.varSeries[i] = new Series(varId, points);
150
+ }
151
+ }
152
+ updateFromBuffer(outputsBuffer, rowLength) {
153
+ const result = parseOutputsBuffer(outputsBuffer, rowLength, this);
154
+ if (result.isOk()) {
155
+ return (0, import_neverthrow.ok)(void 0);
156
+ } else {
157
+ return (0, import_neverthrow.err)(result.error);
158
+ }
159
+ }
160
+ getSeriesForVar(varId) {
161
+ const seriesIndex = this.varIds.indexOf(varId);
162
+ if (seriesIndex >= 0) {
163
+ return this.varSeries[seriesIndex];
164
+ } else {
165
+ return void 0;
166
+ }
167
+ }
168
+ };
169
+ function parseOutputsBuffer(outputsBuffer, rowLength, outputs) {
170
+ const varCount = outputs.varIds.length;
171
+ const seriesLength = outputs.seriesLength;
172
+ if (rowLength < seriesLength || outputsBuffer.length < varCount * seriesLength) {
173
+ return (0, import_neverthrow.err)("invalid-point-count");
174
+ }
175
+ for (let outputVarIndex = 0; outputVarIndex < varCount; outputVarIndex++) {
176
+ const series = outputs.varSeries[outputVarIndex];
177
+ let sourceIndex = rowLength * outputVarIndex;
178
+ for (let valueIndex = 0; valueIndex < seriesLength; valueIndex++) {
179
+ series.points[valueIndex].y = validateNumber(outputsBuffer[sourceIndex]);
180
+ sourceIndex++;
181
+ }
182
+ }
183
+ return (0, import_neverthrow.ok)(outputs);
184
+ }
185
+ function validateNumber(x) {
186
+ if (!isNaN(x) && x > -1e32) {
187
+ return x;
188
+ } else {
189
+ return void 0;
190
+ }
191
+ }
192
+
193
+ // src/model-runner/perf.ts
194
+ var isWeb;
195
+ function perfNow() {
196
+ if (isWeb === void 0) {
197
+ isWeb = typeof self !== "undefined" && (self == null ? void 0 : self.performance) !== void 0;
198
+ }
199
+ if (isWeb) {
200
+ return self.performance.now();
201
+ } else {
202
+ return process == null ? void 0 : process.hrtime();
203
+ }
204
+ }
205
+ function perfElapsed(t0) {
206
+ if (isWeb) {
207
+ const t1 = self.performance.now();
208
+ return t1 - t0;
209
+ } else {
210
+ const elapsed = process.hrtime(t0);
211
+ return (elapsed[0] * 1e9 + elapsed[1]) / 1e6;
212
+ }
213
+ }
214
+
215
+ // src/model-runner/model-runner.ts
216
+ function createWasmModelRunner(wasmResult) {
217
+ const wasmModel = wasmResult.model;
218
+ const inputsBuffer = wasmResult.inputsBuffer;
219
+ const inputsArray = inputsBuffer.getArrayView();
220
+ const outputsBuffer = wasmResult.outputsBuffer;
221
+ const outputsArray = outputsBuffer.getArrayView();
222
+ const rowLength = wasmResult.endTime - wasmResult.startTime + 1;
223
+ let terminated = false;
224
+ const runModelSync = (inputs, outputs) => {
225
+ let i = 0;
226
+ for (const input of inputs) {
227
+ inputsArray[i++] = input.get();
228
+ }
229
+ const t0 = perfNow();
230
+ wasmModel.runModel(inputsBuffer, outputsBuffer);
231
+ outputs.runTimeInMillis = perfElapsed(t0);
232
+ outputs.updateFromBuffer(outputsArray, rowLength);
233
+ return outputs;
234
+ };
235
+ return {
236
+ runModel: (inputs, outputs) => {
237
+ if (terminated) {
238
+ return Promise.reject(new Error("Model runner has already been terminated"));
239
+ }
240
+ return Promise.resolve(runModelSync(inputs, outputs));
241
+ },
242
+ runModelSync: (inputs, outputs) => {
243
+ if (terminated) {
244
+ throw new Error("Model runner has already been terminated");
245
+ }
246
+ return runModelSync(inputs, outputs);
247
+ },
248
+ terminate: () => {
249
+ if (!terminated) {
250
+ terminated = true;
251
+ }
252
+ return Promise.resolve();
253
+ }
254
+ };
255
+ }
256
+
257
+ // src/model-scheduler/model-scheduler.ts
258
+ var ModelScheduler = class {
259
+ constructor(runner, userInputs, outputs) {
260
+ this.runner = runner;
261
+ this.userInputs = userInputs;
262
+ this.outputs = outputs;
263
+ this.runNeeded = false;
264
+ this.runInProgress = false;
265
+ const afterSet = () => {
266
+ this.runWasmModelIfNeeded();
267
+ };
268
+ for (const userInput of userInputs) {
269
+ userInput.callbacks.onSet = afterSet;
270
+ }
271
+ this.currentInputs = [];
272
+ for (const userInput of userInputs) {
273
+ this.currentInputs.push(createSimpleInputValue(userInput.varId));
274
+ }
275
+ }
276
+ runWasmModelIfNeeded() {
277
+ this.runNeeded = true;
278
+ if (this.runInProgress) {
279
+ return;
280
+ } else {
281
+ this.runInProgress = true;
282
+ setTimeout(() => {
283
+ this.runWasmModelNow();
284
+ }, 0);
285
+ }
286
+ }
287
+ async runWasmModelNow() {
288
+ var _a;
289
+ for (let i = 0; i < this.userInputs.length; i++) {
290
+ this.currentInputs[i].set(this.userInputs[i].get());
291
+ }
292
+ try {
293
+ this.outputs = await this.runner.runModel(this.currentInputs, this.outputs);
294
+ (_a = this.onOutputsChanged) == null ? void 0 : _a.call(this, this.outputs);
295
+ } catch (e) {
296
+ console.error(`ERROR: Failed to run model: ${e.message}`);
297
+ }
298
+ if (this.runNeeded) {
299
+ this.runNeeded = false;
300
+ setTimeout(() => {
301
+ this.runWasmModelNow();
302
+ }, 0);
303
+ } else {
304
+ this.runNeeded = false;
305
+ this.runInProgress = false;
306
+ }
307
+ }
308
+ };
309
+ function createSimpleInputValue(varId) {
310
+ let currentValue = 0;
311
+ const get = () => {
312
+ return currentValue;
313
+ };
314
+ const set = (newValue) => {
315
+ currentValue = newValue;
316
+ };
317
+ const reset = () => {
318
+ set(0);
319
+ };
320
+ return { varId, get, set, reset, callbacks: {} };
321
+ }
322
+ // Annotate the CommonJS export names for ESM import in node:
323
+ 0 && (module.exports = {
324
+ ModelScheduler,
325
+ Outputs,
326
+ Series,
327
+ WasmBuffer,
328
+ WasmModel,
329
+ createInputValue,
330
+ createWasmModelRunner,
331
+ initWasmModelAndBuffers,
332
+ perfElapsed,
333
+ perfNow
334
+ });
335
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/wasm-model/wasm-buffer.ts","../src/wasm-model/wasm-model.ts","../src/model-runner/inputs.ts","../src/model-runner/outputs.ts","../src/model-runner/perf.ts","../src/model-runner/model-runner.ts","../src/model-scheduler/model-scheduler.ts"],"sourcesContent":["// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nexport type { InputVarId, OutputVarId } from './_shared'\nexport * from './wasm-model'\nexport * from './model-runner'\nexport * from './model-scheduler'\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { WasmModule } from './wasm-module'\n\n/**\n * Wraps a `WebAssembly.Memory` buffer allocated on the wasm heap.\n *\n * When this is used synchronously (in the browser's normal JavaScript thread),\n * the client can use `getArrayView` to write directly into the underlying memory.\n *\n * Note, however, that `WebAssembly.Memory` buffers cannot be transferred to/from\n * a Web Worker. When using this class in a worker thread, create a separate\n * `Float64Array` that can be transferred between the worker and the client running\n * in the browser's normal JS thread, and then use `getArrayView` to copy into and\n * out of the wasm buffer.\n */\nexport class WasmBuffer {\n private byteOffset: number\n private heapArray: Float64Array\n\n /**\n * @param wasmModule The `WasmModule` used to initialize the memory.\n * @param numElements The number of 64-bit `double` elements in the buffer.\n */\n constructor(private readonly wasmModule: WasmModule, numElements: number) {\n const sizeOfFloat64 = 8\n const lengthInBytes = numElements * sizeOfFloat64\n this.byteOffset = wasmModule._malloc(lengthInBytes)\n const float64Offset = this.byteOffset / sizeOfFloat64\n this.heapArray = wasmModule.HEAPF64.subarray(float64Offset, float64Offset + numElements)\n }\n\n /**\n * @return A new `Float64Array` view on the underlying heap buffer.\n */\n getArrayView(): Float64Array {\n return this.heapArray\n }\n\n /**\n * @return The raw address of the underlying heap buffer.\n * @hidden This is intended for use by `WasmModel` only.\n */\n getAddress(): number {\n return this.byteOffset\n }\n\n /**\n * Dispose the buffer by freeing the allocated heap memory.\n */\n dispose(): void {\n if (this.heapArray) {\n this.wasmModule._free(this.byteOffset)\n this.heapArray = undefined\n this.byteOffset = undefined\n }\n }\n}\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { OutputVarId } from '../_shared'\nimport { WasmBuffer } from './wasm-buffer'\nimport type { WasmModule } from './wasm-module'\n\n/**\n * An interface to the En-ROADS model. Allows for running the model with\n * a given set of input values, producing a set of output values.\n */\nexport class WasmModel {\n private readonly wasmRunModel: (inputsAddress: number, outputsAddress: number) => void\n\n /**\n * @param wasmModule The `WasmModule` containing the `runModelWithBuffers` function.\n */\n constructor(wasmModule: WasmModule) {\n this.wasmRunModel = wasmModule.cwrap('runModelWithBuffers', null, ['number', 'number'])\n }\n\n /**\n * Run the model, using inputs from the `inputs` buffer, and writing outputs into\n * the `outputs` buffer.\n *\n * @param inputs The buffer containing inputs in the order expected by the model.\n * @param outputs The buffer into which the model will store output values.\n */\n runModel(inputs: WasmBuffer, outputs: WasmBuffer): void {\n this.wasmRunModel(inputs.getAddress(), outputs.getAddress())\n }\n}\n\n/**\n * The result of model initialization.\n */\nexport interface WasmModelInitResult {\n /** The wasm model. */\n model: WasmModel\n /** The buffer used to pass input values to the model. */\n inputsBuffer: WasmBuffer\n /** The buffer used to receive output values from the model. */\n outputsBuffer: WasmBuffer\n /** The output variable IDs. */\n outputVarIds: OutputVarId[]\n /** The start time (year) for the model. */\n startTime: number\n /** The end time (year) for the model. */\n endTime: number\n}\n\n/**\n * Initialize the wasm model and buffers.\n *\n * @param wasmModule The `WasmModule` that wraps the `wasm` binary.\n * @param numInputs The number of input variables, per the spec file passed to `sde`.\n * @param outputVarIds The output variable IDs, per the spec file passed to `sde`.\n * @param startTime The start time (year) for the model.\n * @param endTime The end time (year) for the model.\n */\nexport function initWasmModelAndBuffers(\n wasmModule: WasmModule,\n numInputs: number,\n outputVarIds: OutputVarId[],\n startTime: number,\n endTime: number\n): WasmModelInitResult {\n // Wrap the native C `runModelWithBuffers` function in a JS function that we can call\n const model = new WasmModel(wasmModule)\n\n // Allocate a buffer that is large enough to hold the input values\n const inputsBuffer = new WasmBuffer(wasmModule, numInputs)\n\n // Each series will include one data point per year, inclusive of the\n // start and end years\n // TODO: We should pull these from the C variables instead of having them passed in;\n // for now we assume `_saveper` is 1 but that should be pulled from the C variable too\n const seriesLength = endTime - startTime + 1\n\n // Allocate a buffer that is large enough to hold the series data for\n // each output variable\n const outputsBuffer = new WasmBuffer(wasmModule, outputVarIds.length * seriesLength)\n\n return {\n model,\n inputsBuffer,\n outputsBuffer,\n outputVarIds,\n startTime,\n endTime\n }\n}\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { InputVarId } from '../_shared'\n\n/** Callback functions that are called when the input value is changed. */\nexport interface InputCallbacks {\n /** Called after a new value is set. */\n onSet?: () => void\n}\n\n/**\n * Represents a writable model input.\n */\nexport interface InputValue {\n /** The ID of the associated input variable, as used in SDEverywhere. */\n varId: InputVarId\n /** Get the current value of the input. */\n get: () => number\n /** Set the input to the given value. */\n set: (value: number) => void\n /** Reset the input to its default value. */\n reset: () => void\n /** Callback functions that are called when the input value is changed. */\n callbacks: InputCallbacks\n}\n\n/**\n * Create a basic `InputValue` instance that notifies when a new value is set.\n *\n * @param varId The input variable ID, as used in SDEverywhere.\n * @param defaultValue The default value of the input.\n * @param initialValue The inital value of the input; if undefined, will use `defaultValue`.\n */\nexport function createInputValue(varId: InputVarId, defaultValue: number, initialValue?: number): InputValue {\n let currentValue = initialValue !== undefined ? initialValue : defaultValue\n\n // The `onSet` callback is initially undefined but will be installed by `ModelScheduler`\n const callbacks: InputCallbacks = {}\n\n const get = () => {\n return currentValue\n }\n\n const set = (newValue: number) => {\n if (newValue !== currentValue) {\n currentValue = newValue\n callbacks.onSet?.()\n }\n }\n\n const reset = () => {\n set(defaultValue)\n }\n\n return { varId, get, set, reset, callbacks }\n}\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { Result } from 'neverthrow'\nimport { ok, err } from 'neverthrow'\nimport type { OutputVarId } from '../_shared'\n\n/** Indicates the type of error encountered when parsing an outputs buffer. */\nexport type ParseError = 'invalid-point-count'\n\n/** A data point. */\nexport interface Point {\n /** The x value (typically a year). */\n x: number\n /** The y value. */\n y: number\n}\n\n/**\n * A time series of data points for an output variable.\n */\nexport class Series {\n /**\n * @param varId The ID for the output variable (as used by SDEverywhere).\n * @param points The data points for the variable, one point per time increment.\n */\n constructor(public readonly varId: OutputVarId, public readonly points: Point[]) {}\n\n /**\n * Return the Y value at the given time.\n *\n * @param time The x (time) value.\n */\n getValueAtTime(time: number): number | undefined {\n // TODO: This assumes one data point per year; we should take `_saveper` into account\n // and if it's not 1 point per year, search for a specific x value\n // TODO: Add option to allow interpolation if the given time value is in between points\n const startTime = this.points[0].x\n return this.points[time - startTime]?.y\n }\n\n /**\n * Create a new `Series` instance that is a copy of this one.\n */\n copy(): Series {\n // Create a deep copy\n const pointsCopy = this.points.map(p => ({ ...p }))\n return new Series(this.varId, pointsCopy)\n }\n}\n\n/** Represents the outputs from a model run. */\nexport class Outputs {\n /** The number of data points in each series. */\n public readonly seriesLength: number\n /** The array of series, one for each output variable. */\n public readonly varSeries: Series[]\n\n /**\n * The latest model run time, in milliseconds.\n * @hidden This is not yet part of the public API; it is exposed here for use\n * in performance testing tools.\n */\n public runTimeInMillis: number\n\n constructor(\n public readonly varIds: OutputVarId[],\n public readonly timeStart: number,\n public readonly timeEnd: number\n ) {\n // Each series will include one data point per year, inclusive of the start and end years\n this.seriesLength = timeEnd - timeStart + 1\n\n // Create an array of arrays, one for each output variable\n this.varSeries = new Array(varIds.length)\n\n // Populate the arrays, filling in the time for each point\n for (let i = 0; i < varIds.length; i++) {\n const points: Point[] = new Array(this.seriesLength)\n let time = timeStart\n for (let j = 0; j < this.seriesLength; j++) {\n points[j] = { x: time++, y: 0 }\n }\n const varId = varIds[i]\n this.varSeries[i] = new Series(varId, points)\n }\n }\n\n /**\n * Parse the given raw float buffer (produced by the model) and store the values\n * into this `Outputs` instance.\n *\n * Note that the length of `outputsBuffer` must be greater than or equal to\n * the capacity of this `Outputs` instance. The `Outputs` instance is allowed\n * to be smaller to support the case where you want to extract a subset of\n * the time range in the buffer produced by the model.\n *\n * @param outputsBuffer The raw outputs buffer produced by the model.\n * @param rowLength The number of elements per row (one element per year or save point).\n * @return An `ok` result if the buffer is valid, otherwise an `err` result.\n */\n updateFromBuffer(outputsBuffer: Float64Array, rowLength: number): Result<void, ParseError> {\n const result = parseOutputsBuffer(outputsBuffer, rowLength, this)\n if (result.isOk()) {\n return ok(undefined)\n } else {\n return err(result.error)\n }\n }\n\n /**\n * Return the series for the given output variable.\n *\n * @param varId The ID of the output variable (as used by SDEverywhere).\n */\n getSeriesForVar(varId: OutputVarId): Series | undefined {\n const seriesIndex = this.varIds.indexOf(varId)\n if (seriesIndex >= 0) {\n return this.varSeries[seriesIndex]\n } else {\n // TODO: Error\n return undefined\n }\n }\n}\n\n/**\n * Parse the raw buffer produced by the model and store the values in the\n * given (reused) `Outputs` object.\n *\n * @param outputsBuffer The raw outputs buffer produced by the model.\n * @param rowLength The number of elements per row (one element per year or save point).\n * @return An `ok` result if the buffer is valid, otherwise an `err` result.\n * @hidden\n */\nfunction parseOutputsBuffer(\n outputsBuffer: Float64Array,\n rowLength: number,\n outputs: Outputs\n): Result<Outputs, ParseError> {\n const varCount = outputs.varIds.length\n const seriesLength = outputs.seriesLength\n if (rowLength < seriesLength || outputsBuffer.length < varCount * seriesLength) {\n return err('invalid-point-count')\n }\n\n // The buffer populated by the C `runModelWithBuffers` function is already\n // transposed, so the first \"row\" contains the values for the first output\n // variable (from start time to end time), and so on.\n for (let outputVarIndex = 0; outputVarIndex < varCount; outputVarIndex++) {\n const series = outputs.varSeries[outputVarIndex]\n let sourceIndex = rowLength * outputVarIndex\n for (let valueIndex = 0; valueIndex < seriesLength; valueIndex++) {\n series.points[valueIndex].y = validateNumber(outputsBuffer[sourceIndex])\n sourceIndex++\n }\n }\n\n return ok(outputs)\n}\n\n/**\n * Return the given number if it is valid, or undefined if it is invalid.\n *\n * SDE converts Vensim's `:NA:` values to `-DBL_MAX`, so if we see a very large negative\n * value, convert it to `undefined`. This is preferable to including extreme values\n * because some charting libraries (e.g. Chart.js) appear to choke on these large values\n * in certain browsers (e.g. Safari), but `undefined` appears to be handled better and\n * does a better job of signaling that the data point is undefined.\n *\n * @hidden\n */\nfunction validateNumber(x: number): number | undefined {\n if (!isNaN(x) && x > -1e32) {\n return x\n } else {\n return undefined\n }\n}\n","// Copyright (c) 2021-2022 Climate Interactive / New Venture Fund\n\nlet isWeb: boolean\n\n/**\n * Return a timestamp that can be passed to `perfElapsed` for calculating the elapsed\n * time of an operation.\n *\n * @hidden This is not part of the public API; exposed only for use in performance testing.\n */\nexport function perfNow(): unknown {\n // Note that `self` resolves to the window (in browser context) or the worker global scope\n // (in a Web Worker context)\n if (isWeb === undefined) {\n isWeb = typeof self !== 'undefined' && self?.performance !== undefined\n }\n if (isWeb) {\n return self.performance.now()\n } else {\n // XXX: We only use `process` in two places; we bypass type checking instead of\n // setting up type declarations\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n return process?.hrtime()\n }\n}\n\n/**\n * Return the elapsed time between the given timestamp (created by `perfNow`) and now.\n *\n * @hidden This is not part of the public API; exposed only for use in performance testing.\n */\nexport function perfElapsed(t0: unknown): number {\n if (isWeb) {\n const t1 = self.performance.now()\n return (t1 as number) - (t0 as number)\n } else {\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n const elapsed = process.hrtime(t0) as number[]\n // Convert from nanoseconds to milliseconds\n return (elapsed[0] * 1000000000 + elapsed[1]) / 1000000\n }\n}\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { WasmModelInitResult } from '../wasm-model'\nimport type { InputValue } from './inputs'\nimport type { Outputs } from './outputs'\nimport { perfElapsed, perfNow } from './perf'\n\n/**\n * Abstraction that allows for running the wasm model on the JS thread\n * or asynchronously (e.g. in a Web Worker), depending on the implementation.\n */\nexport interface ModelRunner {\n /**\n * Run the model.\n *\n * @param inputs The model input values (must be in the same order as in the spec file).\n * @param outputs The structure into which the model outputs will be stored.\n * @return A promise that resolves with the outputs when the model run is complete.\n */\n runModel(inputs: InputValue[], outputs: Outputs): Promise<Outputs>\n\n /**\n * Run the model synchronously.\n *\n * @param inputs The model input values (must be in the same order as in the spec file).\n * @param outputs The structure into which the model outputs will be stored.\n * @return The outputs of the run.\n *\n * @hidden This is only intended for internal use; some implementations may not support\n * running the model synchronously, in which case this will be undefined.\n */\n runModelSync?(inputs: InputValue[], outputs: Outputs): Outputs\n\n /**\n * Terminate the runner by releasing underlying resources (e.g., the worker thread or\n * Wasm module/buffers).\n */\n terminate(): Promise<void>\n}\n\n/**\n * Create a `ModelRunner` that runs the given wasm model on the JS thread.\n *\n * @param wasmResult The result of initializing the wasm model.\n */\nexport function createWasmModelRunner(wasmResult: WasmModelInitResult): ModelRunner {\n // Create views on the wasm buffers\n const wasmModel = wasmResult.model\n const inputsBuffer = wasmResult.inputsBuffer\n const inputsArray = inputsBuffer.getArrayView()\n const outputsBuffer = wasmResult.outputsBuffer\n const outputsArray = outputsBuffer.getArrayView()\n const rowLength = wasmResult.endTime - wasmResult.startTime + 1\n\n // Disallow `runModel` after the runner has been terminated\n let terminated = false\n\n const runModelSync = (inputs: InputValue[], outputs: Outputs) => {\n // Capture the current set of input values into the reusable buffer\n let i = 0\n for (const input of inputs) {\n inputsArray[i++] = input.get()\n }\n\n // Run the model\n const t0 = perfNow()\n wasmModel.runModel(inputsBuffer, outputsBuffer)\n outputs.runTimeInMillis = perfElapsed(t0)\n\n // Capture the outputs array by copying the data into the given `Outputs`\n // data structure\n outputs.updateFromBuffer(outputsArray, rowLength)\n\n return outputs\n }\n\n return {\n runModel: (inputs, outputs) => {\n if (terminated) {\n return Promise.reject(new Error('Model runner has already been terminated'))\n }\n return Promise.resolve(runModelSync(inputs, outputs))\n },\n runModelSync: (inputs, outputs) => {\n if (terminated) {\n throw new Error('Model runner has already been terminated')\n }\n return runModelSync(inputs, outputs)\n },\n terminate: () => {\n if (!terminated) {\n // TODO: Release wasm-related resources (module or buffers)\n terminated = true\n }\n return Promise.resolve()\n }\n }\n}\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { InputVarId } from '../_shared'\nimport type { InputValue, ModelRunner, Outputs } from '../model-runner'\n\n/**\n * A high-level interface that schedules running of the underlying `WasmModel`.\n *\n * When one or more input values are changed, this class will schedule a model\n * run to be completed as soon as possible. When the model run has completed,\n * `onOutputsChanged` is called to notify that new output data is available.\n *\n * The `ModelRunner` is pluggable to allow for running the model synchronously\n * (on the main JavaScript thread) or asynchronously (in a Web Worker or Node.js\n * worker thread).\n */\nexport class ModelScheduler {\n /** The second array that holds a stable copy of the user inputs. */\n private readonly currentInputs: InputValue[]\n\n /** Whether a model run has been scheduled. */\n private runNeeded = false\n\n /** Whether a model run is in progress. */\n private runInProgress = false\n\n /** Called when `outputs` has been updated after a model run. */\n public onOutputsChanged?: (outputs: Outputs) => void\n\n /**\n * @param runner The model runner.\n * @param userInputs The input values, in the same order as in the spec file passed to `sde`.\n * @param outputs The structure into which the model outputs will be stored.\n */\n constructor(\n private readonly runner: ModelRunner,\n private readonly userInputs: InputValue[],\n private outputs: Outputs\n ) {\n // When any input has an updated value, schedule a model run on the next tick\n const afterSet = () => {\n this.runWasmModelIfNeeded()\n }\n for (const userInput of userInputs) {\n userInput.callbacks.onSet = afterSet\n }\n\n // Create a second array to hold a stable copy of the user inputs during model runs\n this.currentInputs = []\n for (const userInput of userInputs) {\n this.currentInputs.push(createSimpleInputValue(userInput.varId))\n }\n }\n\n /**\n * Schedule a wasm model run (if not already pending). When the run is\n * complete, save the outputs and call the `onOutputsChanged` callback.\n */\n private runWasmModelIfNeeded(): void {\n // Set a flag indicating that a new run is needed (even if one is already\n // in progress)\n this.runNeeded = true\n\n if (this.runInProgress) {\n // A run is already in progress; let it finish first\n return\n } else {\n // A run is not already in progress, so schedule it now. We use\n // `setTimeout` so that if a lot of inputs are all changing at once\n // (like after a reset), we wait for all those `set` or `reset`\n // calls to finish before gathering the input values into an array\n // and initiating the run on the next tick.\n this.runInProgress = true\n setTimeout(() => {\n // Kick off the (possibly asynchronous) model run\n this.runWasmModelNow()\n }, 0)\n }\n }\n\n /**\n * Run the wasm model asynchronously using the current set of input values.\n */\n private async runWasmModelNow(): Promise<void> {\n // Copy the current inputs into a separate array; this ensures that the\n // model run uses a stable set of inputs, even if the user continues to\n // change the inputs while the model is being run asynchronously\n for (let i = 0; i < this.userInputs.length; i++) {\n this.currentInputs[i].set(this.userInputs[i].get())\n }\n\n // Run the model with the current set of input values and save the outputs\n try {\n this.outputs = await this.runner.runModel(this.currentInputs, this.outputs)\n this.onOutputsChanged?.(this.outputs)\n } catch (e) {\n console.error(`ERROR: Failed to run model: ${e.message}`)\n }\n\n // See if another run is needed\n if (this.runNeeded) {\n // Keep `runInProgress` set, but clear the `runNeeded` flag\n this.runNeeded = false\n setTimeout(() => {\n this.runWasmModelNow()\n }, 0)\n } else {\n // No run needed, so clear both flags\n this.runNeeded = false\n this.runInProgress = false\n }\n }\n}\n\n/**\n * Create an `InputValue` that is only used to hold a copy of another input (no callbacks).\n * @hidden\n */\nfunction createSimpleInputValue(varId: InputVarId): InputValue {\n let currentValue = 0\n const get = () => {\n return currentValue\n }\n const set = (newValue: number) => {\n currentValue = newValue\n }\n const reset = () => {\n set(0)\n }\n return { varId, get, set, reset, callbacks: {} }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,296 @@
1
+ import { Result } from 'neverthrow';
2
+
3
+ /** An input variable identifier string, as used in SDEverywhere. */
4
+ declare type InputVarId = string;
5
+ /** An output variable identifier string, as used in SDEverywhere. */
6
+ declare type OutputVarId = string;
7
+
8
+ /**
9
+ * Type declaration for a WebAssembly module wrapper produced
10
+ * by the Emscripten compiler. This only declares the minimal
11
+ * set of fields needed by `WasmModel` and `WasmBuffer`.
12
+ */
13
+ interface WasmModule {
14
+ /** @hidden */
15
+ cwrap: (fname: string, rettype: string, argtypes: string[]) => any;
16
+ /** @hidden */
17
+ _malloc: (numBytes: number) => number;
18
+ /** @hidden */
19
+ _free: (byteOffset: number) => void;
20
+ /** @hidden */
21
+ HEAPF64: Float64Array;
22
+ }
23
+
24
+ /**
25
+ * Wraps a `WebAssembly.Memory` buffer allocated on the wasm heap.
26
+ *
27
+ * When this is used synchronously (in the browser's normal JavaScript thread),
28
+ * the client can use `getArrayView` to write directly into the underlying memory.
29
+ *
30
+ * Note, however, that `WebAssembly.Memory` buffers cannot be transferred to/from
31
+ * a Web Worker. When using this class in a worker thread, create a separate
32
+ * `Float64Array` that can be transferred between the worker and the client running
33
+ * in the browser's normal JS thread, and then use `getArrayView` to copy into and
34
+ * out of the wasm buffer.
35
+ */
36
+ declare class WasmBuffer {
37
+ private readonly wasmModule;
38
+ private byteOffset;
39
+ private heapArray;
40
+ /**
41
+ * @param wasmModule The `WasmModule` used to initialize the memory.
42
+ * @param numElements The number of 64-bit `double` elements in the buffer.
43
+ */
44
+ constructor(wasmModule: WasmModule, numElements: number);
45
+ /**
46
+ * @return A new `Float64Array` view on the underlying heap buffer.
47
+ */
48
+ getArrayView(): Float64Array;
49
+ /**
50
+ * @return The raw address of the underlying heap buffer.
51
+ * @hidden This is intended for use by `WasmModel` only.
52
+ */
53
+ getAddress(): number;
54
+ /**
55
+ * Dispose the buffer by freeing the allocated heap memory.
56
+ */
57
+ dispose(): void;
58
+ }
59
+
60
+ /**
61
+ * An interface to the En-ROADS model. Allows for running the model with
62
+ * a given set of input values, producing a set of output values.
63
+ */
64
+ declare class WasmModel {
65
+ private readonly wasmRunModel;
66
+ /**
67
+ * @param wasmModule The `WasmModule` containing the `runModelWithBuffers` function.
68
+ */
69
+ constructor(wasmModule: WasmModule);
70
+ /**
71
+ * Run the model, using inputs from the `inputs` buffer, and writing outputs into
72
+ * the `outputs` buffer.
73
+ *
74
+ * @param inputs The buffer containing inputs in the order expected by the model.
75
+ * @param outputs The buffer into which the model will store output values.
76
+ */
77
+ runModel(inputs: WasmBuffer, outputs: WasmBuffer): void;
78
+ }
79
+ /**
80
+ * The result of model initialization.
81
+ */
82
+ interface WasmModelInitResult {
83
+ /** The wasm model. */
84
+ model: WasmModel;
85
+ /** The buffer used to pass input values to the model. */
86
+ inputsBuffer: WasmBuffer;
87
+ /** The buffer used to receive output values from the model. */
88
+ outputsBuffer: WasmBuffer;
89
+ /** The output variable IDs. */
90
+ outputVarIds: OutputVarId[];
91
+ /** The start time (year) for the model. */
92
+ startTime: number;
93
+ /** The end time (year) for the model. */
94
+ endTime: number;
95
+ }
96
+ /**
97
+ * Initialize the wasm model and buffers.
98
+ *
99
+ * @param wasmModule The `WasmModule` that wraps the `wasm` binary.
100
+ * @param numInputs The number of input variables, per the spec file passed to `sde`.
101
+ * @param outputVarIds The output variable IDs, per the spec file passed to `sde`.
102
+ * @param startTime The start time (year) for the model.
103
+ * @param endTime The end time (year) for the model.
104
+ */
105
+ declare function initWasmModelAndBuffers(wasmModule: WasmModule, numInputs: number, outputVarIds: OutputVarId[], startTime: number, endTime: number): WasmModelInitResult;
106
+
107
+ /** Callback functions that are called when the input value is changed. */
108
+ interface InputCallbacks {
109
+ /** Called after a new value is set. */
110
+ onSet?: () => void;
111
+ }
112
+ /**
113
+ * Represents a writable model input.
114
+ */
115
+ interface InputValue {
116
+ /** The ID of the associated input variable, as used in SDEverywhere. */
117
+ varId: InputVarId;
118
+ /** Get the current value of the input. */
119
+ get: () => number;
120
+ /** Set the input to the given value. */
121
+ set: (value: number) => void;
122
+ /** Reset the input to its default value. */
123
+ reset: () => void;
124
+ /** Callback functions that are called when the input value is changed. */
125
+ callbacks: InputCallbacks;
126
+ }
127
+ /**
128
+ * Create a basic `InputValue` instance that notifies when a new value is set.
129
+ *
130
+ * @param varId The input variable ID, as used in SDEverywhere.
131
+ * @param defaultValue The default value of the input.
132
+ * @param initialValue The inital value of the input; if undefined, will use `defaultValue`.
133
+ */
134
+ declare function createInputValue(varId: InputVarId, defaultValue: number, initialValue?: number): InputValue;
135
+
136
+ /** Indicates the type of error encountered when parsing an outputs buffer. */
137
+ declare type ParseError = 'invalid-point-count';
138
+ /** A data point. */
139
+ interface Point {
140
+ /** The x value (typically a year). */
141
+ x: number;
142
+ /** The y value. */
143
+ y: number;
144
+ }
145
+ /**
146
+ * A time series of data points for an output variable.
147
+ */
148
+ declare class Series {
149
+ readonly varId: OutputVarId;
150
+ readonly points: Point[];
151
+ /**
152
+ * @param varId The ID for the output variable (as used by SDEverywhere).
153
+ * @param points The data points for the variable, one point per time increment.
154
+ */
155
+ constructor(varId: OutputVarId, points: Point[]);
156
+ /**
157
+ * Return the Y value at the given time.
158
+ *
159
+ * @param time The x (time) value.
160
+ */
161
+ getValueAtTime(time: number): number | undefined;
162
+ /**
163
+ * Create a new `Series` instance that is a copy of this one.
164
+ */
165
+ copy(): Series;
166
+ }
167
+ /** Represents the outputs from a model run. */
168
+ declare class Outputs {
169
+ readonly varIds: OutputVarId[];
170
+ readonly timeStart: number;
171
+ readonly timeEnd: number;
172
+ /** The number of data points in each series. */
173
+ readonly seriesLength: number;
174
+ /** The array of series, one for each output variable. */
175
+ readonly varSeries: Series[];
176
+ /**
177
+ * The latest model run time, in milliseconds.
178
+ * @hidden This is not yet part of the public API; it is exposed here for use
179
+ * in performance testing tools.
180
+ */
181
+ runTimeInMillis: number;
182
+ constructor(varIds: OutputVarId[], timeStart: number, timeEnd: number);
183
+ /**
184
+ * Parse the given raw float buffer (produced by the model) and store the values
185
+ * into this `Outputs` instance.
186
+ *
187
+ * Note that the length of `outputsBuffer` must be greater than or equal to
188
+ * the capacity of this `Outputs` instance. The `Outputs` instance is allowed
189
+ * to be smaller to support the case where you want to extract a subset of
190
+ * the time range in the buffer produced by the model.
191
+ *
192
+ * @param outputsBuffer The raw outputs buffer produced by the model.
193
+ * @param rowLength The number of elements per row (one element per year or save point).
194
+ * @return An `ok` result if the buffer is valid, otherwise an `err` result.
195
+ */
196
+ updateFromBuffer(outputsBuffer: Float64Array, rowLength: number): Result<void, ParseError>;
197
+ /**
198
+ * Return the series for the given output variable.
199
+ *
200
+ * @param varId The ID of the output variable (as used by SDEverywhere).
201
+ */
202
+ getSeriesForVar(varId: OutputVarId): Series | undefined;
203
+ }
204
+
205
+ /**
206
+ * Abstraction that allows for running the wasm model on the JS thread
207
+ * or asynchronously (e.g. in a Web Worker), depending on the implementation.
208
+ */
209
+ interface ModelRunner {
210
+ /**
211
+ * Run the model.
212
+ *
213
+ * @param inputs The model input values (must be in the same order as in the spec file).
214
+ * @param outputs The structure into which the model outputs will be stored.
215
+ * @return A promise that resolves with the outputs when the model run is complete.
216
+ */
217
+ runModel(inputs: InputValue[], outputs: Outputs): Promise<Outputs>;
218
+ /**
219
+ * Run the model synchronously.
220
+ *
221
+ * @param inputs The model input values (must be in the same order as in the spec file).
222
+ * @param outputs The structure into which the model outputs will be stored.
223
+ * @return The outputs of the run.
224
+ *
225
+ * @hidden This is only intended for internal use; some implementations may not support
226
+ * running the model synchronously, in which case this will be undefined.
227
+ */
228
+ runModelSync?(inputs: InputValue[], outputs: Outputs): Outputs;
229
+ /**
230
+ * Terminate the runner by releasing underlying resources (e.g., the worker thread or
231
+ * Wasm module/buffers).
232
+ */
233
+ terminate(): Promise<void>;
234
+ }
235
+ /**
236
+ * Create a `ModelRunner` that runs the given wasm model on the JS thread.
237
+ *
238
+ * @param wasmResult The result of initializing the wasm model.
239
+ */
240
+ declare function createWasmModelRunner(wasmResult: WasmModelInitResult): ModelRunner;
241
+
242
+ /**
243
+ * Return a timestamp that can be passed to `perfElapsed` for calculating the elapsed
244
+ * time of an operation.
245
+ *
246
+ * @hidden This is not part of the public API; exposed only for use in performance testing.
247
+ */
248
+ declare function perfNow(): unknown;
249
+ /**
250
+ * Return the elapsed time between the given timestamp (created by `perfNow`) and now.
251
+ *
252
+ * @hidden This is not part of the public API; exposed only for use in performance testing.
253
+ */
254
+ declare function perfElapsed(t0: unknown): number;
255
+
256
+ /**
257
+ * A high-level interface that schedules running of the underlying `WasmModel`.
258
+ *
259
+ * When one or more input values are changed, this class will schedule a model
260
+ * run to be completed as soon as possible. When the model run has completed,
261
+ * `onOutputsChanged` is called to notify that new output data is available.
262
+ *
263
+ * The `ModelRunner` is pluggable to allow for running the model synchronously
264
+ * (on the main JavaScript thread) or asynchronously (in a Web Worker or Node.js
265
+ * worker thread).
266
+ */
267
+ declare class ModelScheduler {
268
+ private readonly runner;
269
+ private readonly userInputs;
270
+ private outputs;
271
+ /** The second array that holds a stable copy of the user inputs. */
272
+ private readonly currentInputs;
273
+ /** Whether a model run has been scheduled. */
274
+ private runNeeded;
275
+ /** Whether a model run is in progress. */
276
+ private runInProgress;
277
+ /** Called when `outputs` has been updated after a model run. */
278
+ onOutputsChanged?: (outputs: Outputs) => void;
279
+ /**
280
+ * @param runner The model runner.
281
+ * @param userInputs The input values, in the same order as in the spec file passed to `sde`.
282
+ * @param outputs The structure into which the model outputs will be stored.
283
+ */
284
+ constructor(runner: ModelRunner, userInputs: InputValue[], outputs: Outputs);
285
+ /**
286
+ * Schedule a wasm model run (if not already pending). When the run is
287
+ * complete, save the outputs and call the `onOutputsChanged` callback.
288
+ */
289
+ private runWasmModelIfNeeded;
290
+ /**
291
+ * Run the wasm model asynchronously using the current set of input values.
292
+ */
293
+ private runWasmModelNow;
294
+ }
295
+
296
+ export { InputCallbacks, InputValue, InputVarId, ModelRunner, ModelScheduler, OutputVarId, Outputs, ParseError, Point, Series, WasmBuffer, WasmModel, WasmModelInitResult, WasmModule, createInputValue, createWasmModelRunner, initWasmModelAndBuffers, perfElapsed, perfNow };
package/dist/index.js ADDED
@@ -0,0 +1,303 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
3
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
5
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
+ var __spreadValues = (a, b) => {
7
+ for (var prop in b || (b = {}))
8
+ if (__hasOwnProp.call(b, prop))
9
+ __defNormalProp(a, prop, b[prop]);
10
+ if (__getOwnPropSymbols)
11
+ for (var prop of __getOwnPropSymbols(b)) {
12
+ if (__propIsEnum.call(b, prop))
13
+ __defNormalProp(a, prop, b[prop]);
14
+ }
15
+ return a;
16
+ };
17
+
18
+ // src/wasm-model/wasm-buffer.ts
19
+ var WasmBuffer = class {
20
+ constructor(wasmModule, numElements) {
21
+ this.wasmModule = wasmModule;
22
+ const sizeOfFloat64 = 8;
23
+ const lengthInBytes = numElements * sizeOfFloat64;
24
+ this.byteOffset = wasmModule._malloc(lengthInBytes);
25
+ const float64Offset = this.byteOffset / sizeOfFloat64;
26
+ this.heapArray = wasmModule.HEAPF64.subarray(float64Offset, float64Offset + numElements);
27
+ }
28
+ getArrayView() {
29
+ return this.heapArray;
30
+ }
31
+ getAddress() {
32
+ return this.byteOffset;
33
+ }
34
+ dispose() {
35
+ if (this.heapArray) {
36
+ this.wasmModule._free(this.byteOffset);
37
+ this.heapArray = void 0;
38
+ this.byteOffset = void 0;
39
+ }
40
+ }
41
+ };
42
+
43
+ // src/wasm-model/wasm-model.ts
44
+ var WasmModel = class {
45
+ constructor(wasmModule) {
46
+ this.wasmRunModel = wasmModule.cwrap("runModelWithBuffers", null, ["number", "number"]);
47
+ }
48
+ runModel(inputs, outputs) {
49
+ this.wasmRunModel(inputs.getAddress(), outputs.getAddress());
50
+ }
51
+ };
52
+ function initWasmModelAndBuffers(wasmModule, numInputs, outputVarIds, startTime, endTime) {
53
+ const model = new WasmModel(wasmModule);
54
+ const inputsBuffer = new WasmBuffer(wasmModule, numInputs);
55
+ const seriesLength = endTime - startTime + 1;
56
+ const outputsBuffer = new WasmBuffer(wasmModule, outputVarIds.length * seriesLength);
57
+ return {
58
+ model,
59
+ inputsBuffer,
60
+ outputsBuffer,
61
+ outputVarIds,
62
+ startTime,
63
+ endTime
64
+ };
65
+ }
66
+
67
+ // src/model-runner/inputs.ts
68
+ function createInputValue(varId, defaultValue, initialValue) {
69
+ let currentValue = initialValue !== void 0 ? initialValue : defaultValue;
70
+ const callbacks = {};
71
+ const get = () => {
72
+ return currentValue;
73
+ };
74
+ const set = (newValue) => {
75
+ var _a;
76
+ if (newValue !== currentValue) {
77
+ currentValue = newValue;
78
+ (_a = callbacks.onSet) == null ? void 0 : _a.call(callbacks);
79
+ }
80
+ };
81
+ const reset = () => {
82
+ set(defaultValue);
83
+ };
84
+ return { varId, get, set, reset, callbacks };
85
+ }
86
+
87
+ // src/model-runner/outputs.ts
88
+ import { ok, err } from "neverthrow";
89
+ var Series = class {
90
+ constructor(varId, points) {
91
+ this.varId = varId;
92
+ this.points = points;
93
+ }
94
+ getValueAtTime(time) {
95
+ var _a;
96
+ const startTime = this.points[0].x;
97
+ return (_a = this.points[time - startTime]) == null ? void 0 : _a.y;
98
+ }
99
+ copy() {
100
+ const pointsCopy = this.points.map((p) => __spreadValues({}, p));
101
+ return new Series(this.varId, pointsCopy);
102
+ }
103
+ };
104
+ var Outputs = class {
105
+ constructor(varIds, timeStart, timeEnd) {
106
+ this.varIds = varIds;
107
+ this.timeStart = timeStart;
108
+ this.timeEnd = timeEnd;
109
+ this.seriesLength = timeEnd - timeStart + 1;
110
+ this.varSeries = new Array(varIds.length);
111
+ for (let i = 0; i < varIds.length; i++) {
112
+ const points = new Array(this.seriesLength);
113
+ let time = timeStart;
114
+ for (let j = 0; j < this.seriesLength; j++) {
115
+ points[j] = { x: time++, y: 0 };
116
+ }
117
+ const varId = varIds[i];
118
+ this.varSeries[i] = new Series(varId, points);
119
+ }
120
+ }
121
+ updateFromBuffer(outputsBuffer, rowLength) {
122
+ const result = parseOutputsBuffer(outputsBuffer, rowLength, this);
123
+ if (result.isOk()) {
124
+ return ok(void 0);
125
+ } else {
126
+ return err(result.error);
127
+ }
128
+ }
129
+ getSeriesForVar(varId) {
130
+ const seriesIndex = this.varIds.indexOf(varId);
131
+ if (seriesIndex >= 0) {
132
+ return this.varSeries[seriesIndex];
133
+ } else {
134
+ return void 0;
135
+ }
136
+ }
137
+ };
138
+ function parseOutputsBuffer(outputsBuffer, rowLength, outputs) {
139
+ const varCount = outputs.varIds.length;
140
+ const seriesLength = outputs.seriesLength;
141
+ if (rowLength < seriesLength || outputsBuffer.length < varCount * seriesLength) {
142
+ return err("invalid-point-count");
143
+ }
144
+ for (let outputVarIndex = 0; outputVarIndex < varCount; outputVarIndex++) {
145
+ const series = outputs.varSeries[outputVarIndex];
146
+ let sourceIndex = rowLength * outputVarIndex;
147
+ for (let valueIndex = 0; valueIndex < seriesLength; valueIndex++) {
148
+ series.points[valueIndex].y = validateNumber(outputsBuffer[sourceIndex]);
149
+ sourceIndex++;
150
+ }
151
+ }
152
+ return ok(outputs);
153
+ }
154
+ function validateNumber(x) {
155
+ if (!isNaN(x) && x > -1e32) {
156
+ return x;
157
+ } else {
158
+ return void 0;
159
+ }
160
+ }
161
+
162
+ // src/model-runner/perf.ts
163
+ var isWeb;
164
+ function perfNow() {
165
+ if (isWeb === void 0) {
166
+ isWeb = typeof self !== "undefined" && (self == null ? void 0 : self.performance) !== void 0;
167
+ }
168
+ if (isWeb) {
169
+ return self.performance.now();
170
+ } else {
171
+ return process == null ? void 0 : process.hrtime();
172
+ }
173
+ }
174
+ function perfElapsed(t0) {
175
+ if (isWeb) {
176
+ const t1 = self.performance.now();
177
+ return t1 - t0;
178
+ } else {
179
+ const elapsed = process.hrtime(t0);
180
+ return (elapsed[0] * 1e9 + elapsed[1]) / 1e6;
181
+ }
182
+ }
183
+
184
+ // src/model-runner/model-runner.ts
185
+ function createWasmModelRunner(wasmResult) {
186
+ const wasmModel = wasmResult.model;
187
+ const inputsBuffer = wasmResult.inputsBuffer;
188
+ const inputsArray = inputsBuffer.getArrayView();
189
+ const outputsBuffer = wasmResult.outputsBuffer;
190
+ const outputsArray = outputsBuffer.getArrayView();
191
+ const rowLength = wasmResult.endTime - wasmResult.startTime + 1;
192
+ let terminated = false;
193
+ const runModelSync = (inputs, outputs) => {
194
+ let i = 0;
195
+ for (const input of inputs) {
196
+ inputsArray[i++] = input.get();
197
+ }
198
+ const t0 = perfNow();
199
+ wasmModel.runModel(inputsBuffer, outputsBuffer);
200
+ outputs.runTimeInMillis = perfElapsed(t0);
201
+ outputs.updateFromBuffer(outputsArray, rowLength);
202
+ return outputs;
203
+ };
204
+ return {
205
+ runModel: (inputs, outputs) => {
206
+ if (terminated) {
207
+ return Promise.reject(new Error("Model runner has already been terminated"));
208
+ }
209
+ return Promise.resolve(runModelSync(inputs, outputs));
210
+ },
211
+ runModelSync: (inputs, outputs) => {
212
+ if (terminated) {
213
+ throw new Error("Model runner has already been terminated");
214
+ }
215
+ return runModelSync(inputs, outputs);
216
+ },
217
+ terminate: () => {
218
+ if (!terminated) {
219
+ terminated = true;
220
+ }
221
+ return Promise.resolve();
222
+ }
223
+ };
224
+ }
225
+
226
+ // src/model-scheduler/model-scheduler.ts
227
+ var ModelScheduler = class {
228
+ constructor(runner, userInputs, outputs) {
229
+ this.runner = runner;
230
+ this.userInputs = userInputs;
231
+ this.outputs = outputs;
232
+ this.runNeeded = false;
233
+ this.runInProgress = false;
234
+ const afterSet = () => {
235
+ this.runWasmModelIfNeeded();
236
+ };
237
+ for (const userInput of userInputs) {
238
+ userInput.callbacks.onSet = afterSet;
239
+ }
240
+ this.currentInputs = [];
241
+ for (const userInput of userInputs) {
242
+ this.currentInputs.push(createSimpleInputValue(userInput.varId));
243
+ }
244
+ }
245
+ runWasmModelIfNeeded() {
246
+ this.runNeeded = true;
247
+ if (this.runInProgress) {
248
+ return;
249
+ } else {
250
+ this.runInProgress = true;
251
+ setTimeout(() => {
252
+ this.runWasmModelNow();
253
+ }, 0);
254
+ }
255
+ }
256
+ async runWasmModelNow() {
257
+ var _a;
258
+ for (let i = 0; i < this.userInputs.length; i++) {
259
+ this.currentInputs[i].set(this.userInputs[i].get());
260
+ }
261
+ try {
262
+ this.outputs = await this.runner.runModel(this.currentInputs, this.outputs);
263
+ (_a = this.onOutputsChanged) == null ? void 0 : _a.call(this, this.outputs);
264
+ } catch (e) {
265
+ console.error(`ERROR: Failed to run model: ${e.message}`);
266
+ }
267
+ if (this.runNeeded) {
268
+ this.runNeeded = false;
269
+ setTimeout(() => {
270
+ this.runWasmModelNow();
271
+ }, 0);
272
+ } else {
273
+ this.runNeeded = false;
274
+ this.runInProgress = false;
275
+ }
276
+ }
277
+ };
278
+ function createSimpleInputValue(varId) {
279
+ let currentValue = 0;
280
+ const get = () => {
281
+ return currentValue;
282
+ };
283
+ const set = (newValue) => {
284
+ currentValue = newValue;
285
+ };
286
+ const reset = () => {
287
+ set(0);
288
+ };
289
+ return { varId, get, set, reset, callbacks: {} };
290
+ }
291
+ export {
292
+ ModelScheduler,
293
+ Outputs,
294
+ Series,
295
+ WasmBuffer,
296
+ WasmModel,
297
+ createInputValue,
298
+ createWasmModelRunner,
299
+ initWasmModelAndBuffers,
300
+ perfElapsed,
301
+ perfNow
302
+ };
303
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/wasm-model/wasm-buffer.ts","../src/wasm-model/wasm-model.ts","../src/model-runner/inputs.ts","../src/model-runner/outputs.ts","../src/model-runner/perf.ts","../src/model-runner/model-runner.ts","../src/model-scheduler/model-scheduler.ts"],"sourcesContent":["// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { WasmModule } from './wasm-module'\n\n/**\n * Wraps a `WebAssembly.Memory` buffer allocated on the wasm heap.\n *\n * When this is used synchronously (in the browser's normal JavaScript thread),\n * the client can use `getArrayView` to write directly into the underlying memory.\n *\n * Note, however, that `WebAssembly.Memory` buffers cannot be transferred to/from\n * a Web Worker. When using this class in a worker thread, create a separate\n * `Float64Array` that can be transferred between the worker and the client running\n * in the browser's normal JS thread, and then use `getArrayView` to copy into and\n * out of the wasm buffer.\n */\nexport class WasmBuffer {\n private byteOffset: number\n private heapArray: Float64Array\n\n /**\n * @param wasmModule The `WasmModule` used to initialize the memory.\n * @param numElements The number of 64-bit `double` elements in the buffer.\n */\n constructor(private readonly wasmModule: WasmModule, numElements: number) {\n const sizeOfFloat64 = 8\n const lengthInBytes = numElements * sizeOfFloat64\n this.byteOffset = wasmModule._malloc(lengthInBytes)\n const float64Offset = this.byteOffset / sizeOfFloat64\n this.heapArray = wasmModule.HEAPF64.subarray(float64Offset, float64Offset + numElements)\n }\n\n /**\n * @return A new `Float64Array` view on the underlying heap buffer.\n */\n getArrayView(): Float64Array {\n return this.heapArray\n }\n\n /**\n * @return The raw address of the underlying heap buffer.\n * @hidden This is intended for use by `WasmModel` only.\n */\n getAddress(): number {\n return this.byteOffset\n }\n\n /**\n * Dispose the buffer by freeing the allocated heap memory.\n */\n dispose(): void {\n if (this.heapArray) {\n this.wasmModule._free(this.byteOffset)\n this.heapArray = undefined\n this.byteOffset = undefined\n }\n }\n}\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { OutputVarId } from '../_shared'\nimport { WasmBuffer } from './wasm-buffer'\nimport type { WasmModule } from './wasm-module'\n\n/**\n * An interface to the En-ROADS model. Allows for running the model with\n * a given set of input values, producing a set of output values.\n */\nexport class WasmModel {\n private readonly wasmRunModel: (inputsAddress: number, outputsAddress: number) => void\n\n /**\n * @param wasmModule The `WasmModule` containing the `runModelWithBuffers` function.\n */\n constructor(wasmModule: WasmModule) {\n this.wasmRunModel = wasmModule.cwrap('runModelWithBuffers', null, ['number', 'number'])\n }\n\n /**\n * Run the model, using inputs from the `inputs` buffer, and writing outputs into\n * the `outputs` buffer.\n *\n * @param inputs The buffer containing inputs in the order expected by the model.\n * @param outputs The buffer into which the model will store output values.\n */\n runModel(inputs: WasmBuffer, outputs: WasmBuffer): void {\n this.wasmRunModel(inputs.getAddress(), outputs.getAddress())\n }\n}\n\n/**\n * The result of model initialization.\n */\nexport interface WasmModelInitResult {\n /** The wasm model. */\n model: WasmModel\n /** The buffer used to pass input values to the model. */\n inputsBuffer: WasmBuffer\n /** The buffer used to receive output values from the model. */\n outputsBuffer: WasmBuffer\n /** The output variable IDs. */\n outputVarIds: OutputVarId[]\n /** The start time (year) for the model. */\n startTime: number\n /** The end time (year) for the model. */\n endTime: number\n}\n\n/**\n * Initialize the wasm model and buffers.\n *\n * @param wasmModule The `WasmModule` that wraps the `wasm` binary.\n * @param numInputs The number of input variables, per the spec file passed to `sde`.\n * @param outputVarIds The output variable IDs, per the spec file passed to `sde`.\n * @param startTime The start time (year) for the model.\n * @param endTime The end time (year) for the model.\n */\nexport function initWasmModelAndBuffers(\n wasmModule: WasmModule,\n numInputs: number,\n outputVarIds: OutputVarId[],\n startTime: number,\n endTime: number\n): WasmModelInitResult {\n // Wrap the native C `runModelWithBuffers` function in a JS function that we can call\n const model = new WasmModel(wasmModule)\n\n // Allocate a buffer that is large enough to hold the input values\n const inputsBuffer = new WasmBuffer(wasmModule, numInputs)\n\n // Each series will include one data point per year, inclusive of the\n // start and end years\n // TODO: We should pull these from the C variables instead of having them passed in;\n // for now we assume `_saveper` is 1 but that should be pulled from the C variable too\n const seriesLength = endTime - startTime + 1\n\n // Allocate a buffer that is large enough to hold the series data for\n // each output variable\n const outputsBuffer = new WasmBuffer(wasmModule, outputVarIds.length * seriesLength)\n\n return {\n model,\n inputsBuffer,\n outputsBuffer,\n outputVarIds,\n startTime,\n endTime\n }\n}\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { InputVarId } from '../_shared'\n\n/** Callback functions that are called when the input value is changed. */\nexport interface InputCallbacks {\n /** Called after a new value is set. */\n onSet?: () => void\n}\n\n/**\n * Represents a writable model input.\n */\nexport interface InputValue {\n /** The ID of the associated input variable, as used in SDEverywhere. */\n varId: InputVarId\n /** Get the current value of the input. */\n get: () => number\n /** Set the input to the given value. */\n set: (value: number) => void\n /** Reset the input to its default value. */\n reset: () => void\n /** Callback functions that are called when the input value is changed. */\n callbacks: InputCallbacks\n}\n\n/**\n * Create a basic `InputValue` instance that notifies when a new value is set.\n *\n * @param varId The input variable ID, as used in SDEverywhere.\n * @param defaultValue The default value of the input.\n * @param initialValue The inital value of the input; if undefined, will use `defaultValue`.\n */\nexport function createInputValue(varId: InputVarId, defaultValue: number, initialValue?: number): InputValue {\n let currentValue = initialValue !== undefined ? initialValue : defaultValue\n\n // The `onSet` callback is initially undefined but will be installed by `ModelScheduler`\n const callbacks: InputCallbacks = {}\n\n const get = () => {\n return currentValue\n }\n\n const set = (newValue: number) => {\n if (newValue !== currentValue) {\n currentValue = newValue\n callbacks.onSet?.()\n }\n }\n\n const reset = () => {\n set(defaultValue)\n }\n\n return { varId, get, set, reset, callbacks }\n}\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { Result } from 'neverthrow'\nimport { ok, err } from 'neverthrow'\nimport type { OutputVarId } from '../_shared'\n\n/** Indicates the type of error encountered when parsing an outputs buffer. */\nexport type ParseError = 'invalid-point-count'\n\n/** A data point. */\nexport interface Point {\n /** The x value (typically a year). */\n x: number\n /** The y value. */\n y: number\n}\n\n/**\n * A time series of data points for an output variable.\n */\nexport class Series {\n /**\n * @param varId The ID for the output variable (as used by SDEverywhere).\n * @param points The data points for the variable, one point per time increment.\n */\n constructor(public readonly varId: OutputVarId, public readonly points: Point[]) {}\n\n /**\n * Return the Y value at the given time.\n *\n * @param time The x (time) value.\n */\n getValueAtTime(time: number): number | undefined {\n // TODO: This assumes one data point per year; we should take `_saveper` into account\n // and if it's not 1 point per year, search for a specific x value\n // TODO: Add option to allow interpolation if the given time value is in between points\n const startTime = this.points[0].x\n return this.points[time - startTime]?.y\n }\n\n /**\n * Create a new `Series` instance that is a copy of this one.\n */\n copy(): Series {\n // Create a deep copy\n const pointsCopy = this.points.map(p => ({ ...p }))\n return new Series(this.varId, pointsCopy)\n }\n}\n\n/** Represents the outputs from a model run. */\nexport class Outputs {\n /** The number of data points in each series. */\n public readonly seriesLength: number\n /** The array of series, one for each output variable. */\n public readonly varSeries: Series[]\n\n /**\n * The latest model run time, in milliseconds.\n * @hidden This is not yet part of the public API; it is exposed here for use\n * in performance testing tools.\n */\n public runTimeInMillis: number\n\n constructor(\n public readonly varIds: OutputVarId[],\n public readonly timeStart: number,\n public readonly timeEnd: number\n ) {\n // Each series will include one data point per year, inclusive of the start and end years\n this.seriesLength = timeEnd - timeStart + 1\n\n // Create an array of arrays, one for each output variable\n this.varSeries = new Array(varIds.length)\n\n // Populate the arrays, filling in the time for each point\n for (let i = 0; i < varIds.length; i++) {\n const points: Point[] = new Array(this.seriesLength)\n let time = timeStart\n for (let j = 0; j < this.seriesLength; j++) {\n points[j] = { x: time++, y: 0 }\n }\n const varId = varIds[i]\n this.varSeries[i] = new Series(varId, points)\n }\n }\n\n /**\n * Parse the given raw float buffer (produced by the model) and store the values\n * into this `Outputs` instance.\n *\n * Note that the length of `outputsBuffer` must be greater than or equal to\n * the capacity of this `Outputs` instance. The `Outputs` instance is allowed\n * to be smaller to support the case where you want to extract a subset of\n * the time range in the buffer produced by the model.\n *\n * @param outputsBuffer The raw outputs buffer produced by the model.\n * @param rowLength The number of elements per row (one element per year or save point).\n * @return An `ok` result if the buffer is valid, otherwise an `err` result.\n */\n updateFromBuffer(outputsBuffer: Float64Array, rowLength: number): Result<void, ParseError> {\n const result = parseOutputsBuffer(outputsBuffer, rowLength, this)\n if (result.isOk()) {\n return ok(undefined)\n } else {\n return err(result.error)\n }\n }\n\n /**\n * Return the series for the given output variable.\n *\n * @param varId The ID of the output variable (as used by SDEverywhere).\n */\n getSeriesForVar(varId: OutputVarId): Series | undefined {\n const seriesIndex = this.varIds.indexOf(varId)\n if (seriesIndex >= 0) {\n return this.varSeries[seriesIndex]\n } else {\n // TODO: Error\n return undefined\n }\n }\n}\n\n/**\n * Parse the raw buffer produced by the model and store the values in the\n * given (reused) `Outputs` object.\n *\n * @param outputsBuffer The raw outputs buffer produced by the model.\n * @param rowLength The number of elements per row (one element per year or save point).\n * @return An `ok` result if the buffer is valid, otherwise an `err` result.\n * @hidden\n */\nfunction parseOutputsBuffer(\n outputsBuffer: Float64Array,\n rowLength: number,\n outputs: Outputs\n): Result<Outputs, ParseError> {\n const varCount = outputs.varIds.length\n const seriesLength = outputs.seriesLength\n if (rowLength < seriesLength || outputsBuffer.length < varCount * seriesLength) {\n return err('invalid-point-count')\n }\n\n // The buffer populated by the C `runModelWithBuffers` function is already\n // transposed, so the first \"row\" contains the values for the first output\n // variable (from start time to end time), and so on.\n for (let outputVarIndex = 0; outputVarIndex < varCount; outputVarIndex++) {\n const series = outputs.varSeries[outputVarIndex]\n let sourceIndex = rowLength * outputVarIndex\n for (let valueIndex = 0; valueIndex < seriesLength; valueIndex++) {\n series.points[valueIndex].y = validateNumber(outputsBuffer[sourceIndex])\n sourceIndex++\n }\n }\n\n return ok(outputs)\n}\n\n/**\n * Return the given number if it is valid, or undefined if it is invalid.\n *\n * SDE converts Vensim's `:NA:` values to `-DBL_MAX`, so if we see a very large negative\n * value, convert it to `undefined`. This is preferable to including extreme values\n * because some charting libraries (e.g. Chart.js) appear to choke on these large values\n * in certain browsers (e.g. Safari), but `undefined` appears to be handled better and\n * does a better job of signaling that the data point is undefined.\n *\n * @hidden\n */\nfunction validateNumber(x: number): number | undefined {\n if (!isNaN(x) && x > -1e32) {\n return x\n } else {\n return undefined\n }\n}\n","// Copyright (c) 2021-2022 Climate Interactive / New Venture Fund\n\nlet isWeb: boolean\n\n/**\n * Return a timestamp that can be passed to `perfElapsed` for calculating the elapsed\n * time of an operation.\n *\n * @hidden This is not part of the public API; exposed only for use in performance testing.\n */\nexport function perfNow(): unknown {\n // Note that `self` resolves to the window (in browser context) or the worker global scope\n // (in a Web Worker context)\n if (isWeb === undefined) {\n isWeb = typeof self !== 'undefined' && self?.performance !== undefined\n }\n if (isWeb) {\n return self.performance.now()\n } else {\n // XXX: We only use `process` in two places; we bypass type checking instead of\n // setting up type declarations\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n return process?.hrtime()\n }\n}\n\n/**\n * Return the elapsed time between the given timestamp (created by `perfNow`) and now.\n *\n * @hidden This is not part of the public API; exposed only for use in performance testing.\n */\nexport function perfElapsed(t0: unknown): number {\n if (isWeb) {\n const t1 = self.performance.now()\n return (t1 as number) - (t0 as number)\n } else {\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n const elapsed = process.hrtime(t0) as number[]\n // Convert from nanoseconds to milliseconds\n return (elapsed[0] * 1000000000 + elapsed[1]) / 1000000\n }\n}\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { WasmModelInitResult } from '../wasm-model'\nimport type { InputValue } from './inputs'\nimport type { Outputs } from './outputs'\nimport { perfElapsed, perfNow } from './perf'\n\n/**\n * Abstraction that allows for running the wasm model on the JS thread\n * or asynchronously (e.g. in a Web Worker), depending on the implementation.\n */\nexport interface ModelRunner {\n /**\n * Run the model.\n *\n * @param inputs The model input values (must be in the same order as in the spec file).\n * @param outputs The structure into which the model outputs will be stored.\n * @return A promise that resolves with the outputs when the model run is complete.\n */\n runModel(inputs: InputValue[], outputs: Outputs): Promise<Outputs>\n\n /**\n * Run the model synchronously.\n *\n * @param inputs The model input values (must be in the same order as in the spec file).\n * @param outputs The structure into which the model outputs will be stored.\n * @return The outputs of the run.\n *\n * @hidden This is only intended for internal use; some implementations may not support\n * running the model synchronously, in which case this will be undefined.\n */\n runModelSync?(inputs: InputValue[], outputs: Outputs): Outputs\n\n /**\n * Terminate the runner by releasing underlying resources (e.g., the worker thread or\n * Wasm module/buffers).\n */\n terminate(): Promise<void>\n}\n\n/**\n * Create a `ModelRunner` that runs the given wasm model on the JS thread.\n *\n * @param wasmResult The result of initializing the wasm model.\n */\nexport function createWasmModelRunner(wasmResult: WasmModelInitResult): ModelRunner {\n // Create views on the wasm buffers\n const wasmModel = wasmResult.model\n const inputsBuffer = wasmResult.inputsBuffer\n const inputsArray = inputsBuffer.getArrayView()\n const outputsBuffer = wasmResult.outputsBuffer\n const outputsArray = outputsBuffer.getArrayView()\n const rowLength = wasmResult.endTime - wasmResult.startTime + 1\n\n // Disallow `runModel` after the runner has been terminated\n let terminated = false\n\n const runModelSync = (inputs: InputValue[], outputs: Outputs) => {\n // Capture the current set of input values into the reusable buffer\n let i = 0\n for (const input of inputs) {\n inputsArray[i++] = input.get()\n }\n\n // Run the model\n const t0 = perfNow()\n wasmModel.runModel(inputsBuffer, outputsBuffer)\n outputs.runTimeInMillis = perfElapsed(t0)\n\n // Capture the outputs array by copying the data into the given `Outputs`\n // data structure\n outputs.updateFromBuffer(outputsArray, rowLength)\n\n return outputs\n }\n\n return {\n runModel: (inputs, outputs) => {\n if (terminated) {\n return Promise.reject(new Error('Model runner has already been terminated'))\n }\n return Promise.resolve(runModelSync(inputs, outputs))\n },\n runModelSync: (inputs, outputs) => {\n if (terminated) {\n throw new Error('Model runner has already been terminated')\n }\n return runModelSync(inputs, outputs)\n },\n terminate: () => {\n if (!terminated) {\n // TODO: Release wasm-related resources (module or buffers)\n terminated = true\n }\n return Promise.resolve()\n }\n }\n}\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { InputVarId } from '../_shared'\nimport type { InputValue, ModelRunner, Outputs } from '../model-runner'\n\n/**\n * A high-level interface that schedules running of the underlying `WasmModel`.\n *\n * When one or more input values are changed, this class will schedule a model\n * run to be completed as soon as possible. When the model run has completed,\n * `onOutputsChanged` is called to notify that new output data is available.\n *\n * The `ModelRunner` is pluggable to allow for running the model synchronously\n * (on the main JavaScript thread) or asynchronously (in a Web Worker or Node.js\n * worker thread).\n */\nexport class ModelScheduler {\n /** The second array that holds a stable copy of the user inputs. */\n private readonly currentInputs: InputValue[]\n\n /** Whether a model run has been scheduled. */\n private runNeeded = false\n\n /** Whether a model run is in progress. */\n private runInProgress = false\n\n /** Called when `outputs` has been updated after a model run. */\n public onOutputsChanged?: (outputs: Outputs) => void\n\n /**\n * @param runner The model runner.\n * @param userInputs The input values, in the same order as in the spec file passed to `sde`.\n * @param outputs The structure into which the model outputs will be stored.\n */\n constructor(\n private readonly runner: ModelRunner,\n private readonly userInputs: InputValue[],\n private outputs: Outputs\n ) {\n // When any input has an updated value, schedule a model run on the next tick\n const afterSet = () => {\n this.runWasmModelIfNeeded()\n }\n for (const userInput of userInputs) {\n userInput.callbacks.onSet = afterSet\n }\n\n // Create a second array to hold a stable copy of the user inputs during model runs\n this.currentInputs = []\n for (const userInput of userInputs) {\n this.currentInputs.push(createSimpleInputValue(userInput.varId))\n }\n }\n\n /**\n * Schedule a wasm model run (if not already pending). When the run is\n * complete, save the outputs and call the `onOutputsChanged` callback.\n */\n private runWasmModelIfNeeded(): void {\n // Set a flag indicating that a new run is needed (even if one is already\n // in progress)\n this.runNeeded = true\n\n if (this.runInProgress) {\n // A run is already in progress; let it finish first\n return\n } else {\n // A run is not already in progress, so schedule it now. We use\n // `setTimeout` so that if a lot of inputs are all changing at once\n // (like after a reset), we wait for all those `set` or `reset`\n // calls to finish before gathering the input values into an array\n // and initiating the run on the next tick.\n this.runInProgress = true\n setTimeout(() => {\n // Kick off the (possibly asynchronous) model run\n this.runWasmModelNow()\n }, 0)\n }\n }\n\n /**\n * Run the wasm model asynchronously using the current set of input values.\n */\n private async runWasmModelNow(): Promise<void> {\n // Copy the current inputs into a separate array; this ensures that the\n // model run uses a stable set of inputs, even if the user continues to\n // change the inputs while the model is being run asynchronously\n for (let i = 0; i < this.userInputs.length; i++) {\n this.currentInputs[i].set(this.userInputs[i].get())\n }\n\n // Run the model with the current set of input values and save the outputs\n try {\n this.outputs = await this.runner.runModel(this.currentInputs, this.outputs)\n this.onOutputsChanged?.(this.outputs)\n } catch (e) {\n console.error(`ERROR: Failed to run model: ${e.message}`)\n }\n\n // See if another run is needed\n if (this.runNeeded) {\n // Keep `runInProgress` set, but clear the `runNeeded` flag\n this.runNeeded = false\n setTimeout(() => {\n this.runWasmModelNow()\n }, 0)\n } else {\n // No run needed, so clear both flags\n this.runNeeded = false\n this.runInProgress = false\n }\n }\n}\n\n/**\n * Create an `InputValue` that is only used to hold a copy of another input (no callbacks).\n * @hidden\n */\nfunction createSimpleInputValue(varId: InputVarId): InputValue {\n let currentValue = 0\n const get = () => {\n return currentValue\n }\n const set = (newValue: number) => {\n currentValue = newValue\n }\n const reset = () => {\n set(0)\n }\n return { varId, get, set, reset, callbacks: {} }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@sdeverywhere/runtime",
3
+ "version": "0.1.0",
4
+ "files": [
5
+ "dist/**"
6
+ ],
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "dependencies": {
19
+ "neverthrow": "^2.7.1"
20
+ },
21
+ "author": "Climate Interactive",
22
+ "license": "MIT",
23
+ "homepage": "https://sdeverywhere.org",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/climateinteractive/SDEverywhere.git",
27
+ "directory": "packages/runtime"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/climateinteractive/SDEverywhere/issues"
31
+ },
32
+ "scripts": {
33
+ "clean": "rm -rf dist",
34
+ "lint": "eslint src --ext .ts --max-warnings 0",
35
+ "prettier:check": "prettier --check .",
36
+ "prettier:fix": "prettier --write .",
37
+ "precommit": "../../scripts/precommit",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "test:ci": "vitest run",
41
+ "type-check": "tsc --noEmit -p tsconfig-build.json",
42
+ "build": "tsup",
43
+ "docs": "../../scripts/gen-docs.js",
44
+ "ci:build": "run-s clean lint prettier:check test:ci type-check build docs"
45
+ }
46
+ }