@sdeverywhere/runtime 0.2.5 → 0.2.7

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/README.md CHANGED
@@ -52,7 +52,7 @@ import loadGeneratedModel from './sde-prep/generated-model.js'
52
52
  ### 2. Initialize a `ModelRunner`
53
53
 
54
54
  The next step is to create a `ModelRunner` instance, which simplifies
55
- the process of running a `WasmModel` with a given set of inputs and
55
+ the process of running a generated model with a given set of inputs and
56
56
  parsing the outputs.
57
57
  The `ModelRunner` produces an `Outputs` instance that provides easy
58
58
  access to time series data for each output variable in the model.
package/dist/index.cjs CHANGED
@@ -58,6 +58,7 @@ __export(src_exports, {
58
58
  MockWasmModule: () => MockWasmModule,
59
59
  ModelListing: () => ModelListing,
60
60
  ModelScheduler: () => ModelScheduler,
61
+ MultiContextModelScheduler: () => MultiContextModelScheduler,
61
62
  Outputs: () => Outputs,
62
63
  ReferencedRunModelParams: () => ReferencedRunModelParams,
63
64
  Series: () => Series,
@@ -251,7 +252,7 @@ function encodeVarIndices(varSpecs, indicesArray) {
251
252
  }
252
253
  }
253
254
  function getEncodedLookupBufferLengths(lookupDefs) {
254
- var _a;
255
+ var _a, _b;
255
256
  let lookupIndicesLength = 1;
256
257
  let lookupsLength = 0;
257
258
  for (const lookupDef of lookupDefs) {
@@ -263,7 +264,7 @@ function getEncodedLookupBufferLengths(lookupDefs) {
263
264
  const subCount = ((_a = varSpec.subscriptIndices) == null ? void 0 : _a.length) || 0;
264
265
  lookupIndicesLength += subCount;
265
266
  lookupIndicesLength += 2;
266
- lookupsLength += lookupDef.points.length;
267
+ lookupsLength += ((_b = lookupDef.points) == null ? void 0 : _b.length) || 0;
267
268
  }
268
269
  return {
269
270
  lookupIndicesLength,
@@ -283,10 +284,15 @@ function encodeLookups(lookupDefs, lookupIndicesArray, lookupsArray) {
283
284
  for (let i = 0; i < subCount; i++) {
284
285
  lookupIndicesArray[li++] = subs[i];
285
286
  }
286
- lookupIndicesArray[li++] = lookupDataOffset;
287
- lookupIndicesArray[li++] = lookupDef.points.length;
288
- lookupsArray == null ? void 0 : lookupsArray.set(lookupDef.points, lookupDataOffset);
289
- lookupDataOffset += lookupDef.points.length;
287
+ if (lookupDef.points !== void 0) {
288
+ lookupIndicesArray[li++] = lookupDataOffset;
289
+ lookupIndicesArray[li++] = lookupDef.points.length;
290
+ lookupsArray == null ? void 0 : lookupsArray.set(lookupDef.points, lookupDataOffset);
291
+ lookupDataOffset += lookupDef.points.length;
292
+ } else {
293
+ lookupIndicesArray[li++] = -1;
294
+ lookupIndicesArray[li++] = 0;
295
+ }
290
296
  }
291
297
  }
292
298
  function decodeLookups(lookupIndicesArray, lookupsArray) {
@@ -307,10 +313,14 @@ function decodeLookups(lookupIndicesArray, lookupsArray) {
307
313
  subscriptIndices
308
314
  };
309
315
  let points;
310
- if (lookupsArray) {
311
- points = lookupsArray.slice(lookupDataOffset, lookupDataOffset + lookupDataLength);
316
+ if (lookupDataOffset >= 0) {
317
+ if (lookupsArray) {
318
+ points = lookupsArray.slice(lookupDataOffset, lookupDataOffset + lookupDataLength);
319
+ } else {
320
+ points = new Float64Array(0);
321
+ }
312
322
  } else {
313
- points = new Float64Array(0);
323
+ points = void 0;
314
324
  }
315
325
  lookupDefs.push({
316
326
  varRef: {
@@ -324,11 +334,14 @@ function decodeLookups(lookupIndicesArray, lookupsArray) {
324
334
 
325
335
  // src/_shared/lookup-def.ts
326
336
  function createLookupDef(varRef, points) {
327
- const flatPoints = new Float64Array(points.length * 2);
328
- let i = 0;
329
- for (const p of points) {
330
- flatPoints[i++] = p.x;
331
- flatPoints[i++] = p.y;
337
+ let flatPoints;
338
+ if (points) {
339
+ flatPoints = new Float64Array(points.length * 2);
340
+ let i = 0;
341
+ for (const p of points) {
342
+ flatPoints[i++] = p.x;
343
+ flatPoints[i++] = p.y;
344
+ }
332
345
  }
333
346
  return {
334
347
  varRef,
@@ -884,18 +897,59 @@ var _NA_ = -Number.MAX_VALUE;
884
897
  // src/js-model/js-model-lookup.ts
885
898
  var JsModelLookup = class {
886
899
  /**
887
- * @param n The number of (x,y) pairs in the lookup.
900
+ * @param size The number of (x,y) pairs in the lookup.
888
901
  * @param data The lookup data, as (x,y) pairs. The length of the array must be
889
902
  * >= 2*n. Note that the data will be stored by reference, so if there is a chance
890
903
  * that the array will be reused or modified by other code, be sure to pass in a
891
904
  * copy of the array.
892
905
  */
893
- constructor(n, data) {
894
- this.n = n;
895
- this.data = data;
896
- if (data.length < n * 2) {
897
- throw new Error(`Lookup data array length must be >= 2*size (length=${data.length} size=${n}`);
906
+ constructor(size, data) {
907
+ if (data && data.length < size * 2) {
908
+ throw new Error(`Lookup data array length must be >= 2*size (length=${data.length} size=${size}`);
909
+ }
910
+ this.originalData = data;
911
+ this.originalSize = size;
912
+ this.dynamicData = void 0;
913
+ this.dynamicSize = 0;
914
+ this.activeData = this.originalData;
915
+ this.activeSize = this.originalSize;
916
+ this.lastInput = Number.MAX_VALUE;
917
+ this.lastHitIndex = 0;
918
+ }
919
+ /**
920
+ * Set new data for this lookup instance, or restore the original data.
921
+ *
922
+ * If `data` is undefined, the original data that was supplied to the constructor will
923
+ * be restored as the "active" data. Otherwise, `data` will be copied to an internal
924
+ * data buffer, which will be the "active" data. If `size` is greater than the size
925
+ * passed to previous calls, the internal data buffer will be grown as needed.
926
+ *
927
+ * @param size The number of (x,y) pairs in the lookup.
928
+ * @param data The lookup data, as (x,y) pairs. The length of the array must be
929
+ * >= 2*n. Note that the data will be copied into an internal data buffer, so it
930
+ * is not necessary to defensively copy data before calling this method.
931
+ */
932
+ setData(size, data) {
933
+ if (data) {
934
+ if (data.length < size * 2) {
935
+ throw new Error(`Lookup data array length must be >= 2*size (length=${data.length} size=${size}`);
936
+ }
937
+ const dataLengthInElems = size * 2;
938
+ if (this.dynamicData === void 0 || dataLengthInElems > this.dynamicData.length) {
939
+ this.dynamicData = new Float64Array(dataLengthInElems);
940
+ }
941
+ this.dynamicSize = size;
942
+ if (size > 0) {
943
+ const subarray = data.subarray(0, dataLengthInElems);
944
+ this.dynamicData.set(subarray);
945
+ }
946
+ this.activeData = this.dynamicData;
947
+ this.activeSize = this.dynamicSize;
948
+ } else {
949
+ this.activeData = this.originalData;
950
+ this.activeSize = this.originalSize;
898
951
  }
952
+ this.invertedData = void 0;
899
953
  this.lastInput = Number.MAX_VALUE;
900
954
  this.lastHitIndex = 0;
901
955
  }
@@ -904,8 +958,8 @@ var JsModelLookup = class {
904
958
  }
905
959
  getValueForY(y) {
906
960
  if (this.invertedData === void 0) {
907
- const numValues = this.n * 2;
908
- const normalData = this.data;
961
+ const numValues = this.activeSize * 2;
962
+ const normalData = this.activeData;
909
963
  const invertedData = Array(numValues);
910
964
  for (let i = 0; i < numValues; i += 2) {
911
965
  invertedData[i] = normalData[i + 1];
@@ -920,11 +974,11 @@ var JsModelLookup = class {
920
974
  * NOTE: The x values are assumed to be monotonically increasing.
921
975
  */
922
976
  getValue(input, useInvertedData, mode) {
923
- if (this.n === 0) {
977
+ if (this.activeSize === 0) {
924
978
  return _NA_;
925
979
  }
926
- const data = useInvertedData ? this.invertedData : this.data;
927
- const max = this.n * 2;
980
+ const data = useInvertedData ? this.invertedData : this.activeData;
981
+ const max = this.activeSize * 2;
928
982
  const useCachedValues = !useInvertedData;
929
983
  let startIndex;
930
984
  if (useCachedValues && input >= this.lastInput) {
@@ -983,10 +1037,10 @@ var JsModelLookup = class {
983
1037
  * no points) or if the provided time is earlier than the first data point.
984
1038
  */
985
1039
  getValueForGameTime(time, defaultValue) {
986
- if (this.n <= 0) {
1040
+ if (this.activeSize <= 0) {
987
1041
  return defaultValue;
988
1042
  }
989
- const x0 = this.data[0];
1043
+ const x0 = this.activeData[0];
990
1044
  if (time < x0) {
991
1045
  return defaultValue;
992
1046
  }
@@ -1001,33 +1055,34 @@ var JsModelLookup = class {
1001
1055
  * lookup behavior, so we implement it as a separate method here.
1002
1056
  */
1003
1057
  getValueBetweenTimes(input, mode) {
1004
- if (this.n === 0) {
1058
+ if (this.activeSize === 0) {
1005
1059
  return _NA_;
1006
1060
  }
1007
- const max = this.n * 2;
1061
+ const data = this.activeData;
1062
+ const max = this.activeSize * 2;
1008
1063
  switch (mode) {
1009
1064
  case "forward": {
1010
1065
  input = Math.floor(input);
1011
1066
  for (let xi = 0; xi < max; xi += 2) {
1012
- const x = this.data[xi];
1067
+ const x = data[xi];
1013
1068
  if (x >= input) {
1014
- return this.data[xi + 1];
1069
+ return data[xi + 1];
1015
1070
  }
1016
1071
  }
1017
- return this.data[max - 1];
1072
+ return data[max - 1];
1018
1073
  }
1019
1074
  case "backward": {
1020
1075
  input = Math.floor(input);
1021
1076
  for (let xi = 2; xi < max; xi += 2) {
1022
- const x = this.data[xi];
1077
+ const x = data[xi];
1023
1078
  if (x >= input) {
1024
- return this.data[xi - 1];
1079
+ return data[xi - 1];
1025
1080
  }
1026
1081
  }
1027
1082
  if (max >= 4) {
1028
- return this.data[max - 3];
1083
+ return data[max - 3];
1029
1084
  } else {
1030
- return this.data[1];
1085
+ return data[1];
1031
1086
  }
1032
1087
  }
1033
1088
  case "interpolate":
@@ -1039,17 +1094,17 @@ var JsModelLookup = class {
1039
1094
  throw new Error(msg);
1040
1095
  }
1041
1096
  for (let xi = 2; xi < max; xi += 2) {
1042
- const x = this.data[xi];
1097
+ const x = data[xi];
1043
1098
  if (x >= input) {
1044
- const last_x = this.data[xi - 2];
1045
- const last_y = this.data[xi - 1];
1046
- const y = this.data[xi + 1];
1099
+ const last_x = data[xi - 2];
1100
+ const last_y = data[xi - 1];
1101
+ const y = data[xi + 1];
1047
1102
  const dx = x - last_x;
1048
1103
  const dy = y - last_y;
1049
1104
  return last_y + dy / dx * (input - last_x);
1050
1105
  }
1051
1106
  }
1052
- return this.data[max - 1];
1107
+ return data[max - 1];
1053
1108
  }
1054
1109
  }
1055
1110
  }
@@ -1500,7 +1555,8 @@ var MockJsModel = class {
1500
1555
  if (varId === void 0) {
1501
1556
  throw new Error(`No lookup variable found for spec ${varSpec}`);
1502
1557
  }
1503
- this.lookups.set(varId, new JsModelLookup(points.length / 2, points));
1558
+ const numPoints = points ? points.length / 2 : 0;
1559
+ this.lookups.set(varId, new JsModelLookup(numPoints, points));
1504
1560
  }
1505
1561
  // from JsModel interface
1506
1562
  storeOutputs(storeValue) {
@@ -1629,14 +1685,21 @@ var WasmModel = class {
1629
1685
  } else {
1630
1686
  subIndicesAddress = 0;
1631
1687
  }
1632
- const numLookupElements = lookupDef.points.length;
1633
- if (this.lookupDataBuffer === void 0 || this.lookupDataBuffer.numElements < numLookupElements) {
1634
- (_c = this.lookupDataBuffer) == null ? void 0 : _c.dispose();
1635
- this.lookupDataBuffer = createFloat64WasmBuffer(this.wasmModule, numLookupElements);
1688
+ let pointsAddress;
1689
+ let numPoints;
1690
+ if (lookupDef.points) {
1691
+ const numLookupElements = lookupDef.points.length;
1692
+ if (this.lookupDataBuffer === void 0 || this.lookupDataBuffer.numElements < numLookupElements) {
1693
+ (_c = this.lookupDataBuffer) == null ? void 0 : _c.dispose();
1694
+ this.lookupDataBuffer = createFloat64WasmBuffer(this.wasmModule, numLookupElements);
1695
+ }
1696
+ this.lookupDataBuffer.getArrayView().set(lookupDef.points);
1697
+ pointsAddress = this.lookupDataBuffer.getAddress();
1698
+ numPoints = numLookupElements / 2;
1699
+ } else {
1700
+ pointsAddress = 0;
1701
+ numPoints = 0;
1636
1702
  }
1637
- this.lookupDataBuffer.getArrayView().set(lookupDef.points);
1638
- const pointsAddress = this.lookupDataBuffer.getAddress();
1639
- const numPoints = numLookupElements / 2;
1640
1703
  const varIndex = varSpec.varIndex;
1641
1704
  this.wasmSetLookup(varIndex, subIndicesAddress, pointsAddress, numPoints);
1642
1705
  }
@@ -1844,7 +1907,7 @@ var ModelScheduler = class {
1844
1907
  /** Whether a model run is in progress. */
1845
1908
  this.runInProgress = false;
1846
1909
  const afterSet = () => {
1847
- this.runWasmModelIfNeeded();
1910
+ this.runModelIfNeeded();
1848
1911
  };
1849
1912
  for (const userInput of userInputs) {
1850
1913
  userInput.callbacks.onSet = afterSet;
@@ -1855,24 +1918,24 @@ var ModelScheduler = class {
1855
1918
  }
1856
1919
  }
1857
1920
  /**
1858
- * Schedule a wasm model run (if not already pending). When the run is
1921
+ * Schedule a model run (if not already pending). When the run is
1859
1922
  * complete, save the outputs and call the `onOutputsChanged` callback.
1860
1923
  */
1861
- runWasmModelIfNeeded() {
1924
+ runModelIfNeeded() {
1862
1925
  this.runNeeded = true;
1863
1926
  if (this.runInProgress) {
1864
1927
  return;
1865
1928
  } else {
1866
1929
  this.runInProgress = true;
1867
1930
  setTimeout(() => {
1868
- this.runWasmModelNow();
1931
+ this.runModelNow();
1869
1932
  }, 0);
1870
1933
  }
1871
1934
  }
1872
1935
  /**
1873
- * Run the wasm model asynchronously using the current set of input values.
1936
+ * Run the model asynchronously using the current set of input values.
1874
1937
  */
1875
- runWasmModelNow() {
1938
+ runModelNow() {
1876
1939
  return __async(this, null, function* () {
1877
1940
  var _a;
1878
1941
  for (let i = 0; i < this.userInputs.length; i++) {
@@ -1887,7 +1950,7 @@ var ModelScheduler = class {
1887
1950
  if (this.runNeeded) {
1888
1951
  this.runNeeded = false;
1889
1952
  setTimeout(() => {
1890
- this.runWasmModelNow();
1953
+ this.runModelNow();
1891
1954
  }, 0);
1892
1955
  } else {
1893
1956
  this.runNeeded = false;
@@ -1909,6 +1972,192 @@ function createSimpleInputValue(varId) {
1909
1972
  };
1910
1973
  return { varId, get, set, reset, callbacks: {} };
1911
1974
  }
1975
+
1976
+ // src/model-scheduler/multi-context-model-scheduler.ts
1977
+ var MultiContextModelScheduler = class {
1978
+ /**
1979
+ * @param runner The model runner.
1980
+ * @param options Additional options for the scheduler.
1981
+ * @param options.initialOutputs An optional `Outputs` instance that will be reused
1982
+ * for the initial context. This is useful for saving memory when an `Outputs`
1983
+ * instance was already created for, e.g., a initial baseline/reference run.
1984
+ */
1985
+ constructor(runner, options) {
1986
+ this.runner = runner;
1987
+ /** The contexts that hold distinct sets of inputs and outputs. */
1988
+ this.contexts = [];
1989
+ /** Whether a model run has been scheduled. */
1990
+ this.runNeeded = false;
1991
+ /** Whether a model run is in progress. */
1992
+ this.runInProgress = false;
1993
+ this.initialOutputs = options == null ? void 0 : options.initialOutputs;
1994
+ }
1995
+ /**
1996
+ * Return true if the scheduler has started any model runs.
1997
+ */
1998
+ isStarted() {
1999
+ return this.initialOutputs === void 0;
2000
+ }
2001
+ /**
2002
+ * Add a new context that holds a distinct set of model inputs and outputs.
2003
+ * These inputs and outputs are kept separate from those in other contexts,
2004
+ * which allows an application to use the same underlying model to run with
2005
+ * multiple I/O contexts.
2006
+ *
2007
+ * Note that the contexts created before the first scheduled model run
2008
+ * will inherit the data from `initialOutputs` passed to the constructor,
2009
+ * but contexts created after that will initially have output values set
2010
+ * to zero.
2011
+ *
2012
+ * @param inputs The input values, in the same order as in the spec file passed to `sde`.
2013
+ * @param options Additional options for the context.
2014
+ * @param options.externalData Additional data that is external to the model outputs.
2015
+ * For example, this can contain data that was captured from an initial reference
2016
+ * run, or other static data that is displayed in graphs alongside the model
2017
+ * output data in graphs.
2018
+ */
2019
+ addContext(inputs, options) {
2020
+ let outputs;
2021
+ if (this.initialOutputs !== void 0) {
2022
+ if (this.contexts.length === 0) {
2023
+ outputs = this.initialOutputs;
2024
+ } else {
2025
+ outputs = this.runner.createOutputs();
2026
+ for (const varId of outputs.varIds) {
2027
+ const series0 = this.initialOutputs.getSeriesForVar(varId);
2028
+ const series1 = outputs.getSeriesForVar(varId);
2029
+ for (let i = 0; i < series0.points.length; i++) {
2030
+ series1.points[i].y = series0.points[i].y;
2031
+ }
2032
+ }
2033
+ }
2034
+ } else {
2035
+ outputs = this.runner.createOutputs();
2036
+ }
2037
+ const context = new ModelContextImpl(inputs, outputs, options == null ? void 0 : options.externalData);
2038
+ const afterSet = () => {
2039
+ context.runNeeded = true;
2040
+ this.runModelIfNeeded();
2041
+ };
2042
+ for (const input of inputs) {
2043
+ input.callbacks.onSet = afterSet;
2044
+ }
2045
+ this.contexts.push(context);
2046
+ return context;
2047
+ }
2048
+ /**
2049
+ * Remove the given context from the set of contexts managed by the scheduler.
2050
+ *
2051
+ * @param context The context to remove.
2052
+ */
2053
+ removeContext(context) {
2054
+ const index = this.contexts.findIndex((c) => c === context);
2055
+ if (index >= 0) {
2056
+ this.contexts.splice(index, 1);
2057
+ }
2058
+ }
2059
+ /**
2060
+ * Schedule a model run (if not already pending). When the run is
2061
+ * complete, save the outputs and call the `onOutputsChanged` callback.
2062
+ */
2063
+ runModelIfNeeded() {
2064
+ this.runNeeded = true;
2065
+ if (this.runInProgress) {
2066
+ return;
2067
+ } else {
2068
+ this.runInProgress = true;
2069
+ setTimeout(() => {
2070
+ this.runModelNow();
2071
+ }, 0);
2072
+ }
2073
+ }
2074
+ /**
2075
+ * Run the model asynchronously for all relevant contexts.
2076
+ */
2077
+ runModelNow() {
2078
+ return __async(this, null, function* () {
2079
+ this.initialOutputs = void 0;
2080
+ for (const context of this.contexts) {
2081
+ if (context.runNeeded) {
2082
+ context.runNeeded = false;
2083
+ yield this.runModelNowForContext(context);
2084
+ }
2085
+ }
2086
+ if (this.runNeeded) {
2087
+ this.runNeeded = false;
2088
+ setTimeout(() => {
2089
+ this.runModelNow();
2090
+ }, 0);
2091
+ } else {
2092
+ this.runNeeded = false;
2093
+ this.runInProgress = false;
2094
+ }
2095
+ });
2096
+ }
2097
+ /**
2098
+ * Run the model asynchronously using the current set of input values in the given context.
2099
+ *
2100
+ * @param context The context to use for the model run.
2101
+ */
2102
+ runModelNowForContext(context) {
2103
+ return __async(this, null, function* () {
2104
+ var _a;
2105
+ if (this.currentInputs === void 0) {
2106
+ this.currentInputs = Array(context.inputsArray.length);
2107
+ }
2108
+ for (let i = 0; i < context.inputsArray.length; i++) {
2109
+ this.currentInputs[i] = context.inputsArray[i].get();
2110
+ }
2111
+ try {
2112
+ yield this.runner.runModel(this.currentInputs, context.outputs);
2113
+ (_a = context.onOutputsChanged) == null ? void 0 : _a.call(context);
2114
+ } catch (e) {
2115
+ console.error("ERROR: The scheduler encountered an error when running the model:", e);
2116
+ }
2117
+ });
2118
+ }
2119
+ };
2120
+ var ModelContextImpl = class {
2121
+ /**
2122
+ * @hidden This is intended for use by `MultiContextModelScheduler` only.
2123
+ *
2124
+ * @param inputs The input values, in the same order as in the spec file passed to `sde`.
2125
+ * @param outputs The structure into which the model outputs will be stored.
2126
+ * @param externalData Additional data that is external to the model outputs. For example, this can contain
2127
+ * data that was captured from an initial reference run, or other static data that is displayed in graphs
2128
+ * alongside the model output data in graphs.
2129
+ */
2130
+ constructor(inputs, outputs, externalData) {
2131
+ this.externalData = externalData;
2132
+ /**
2133
+ * Whether a model run is needed for this context.
2134
+ * @hidden This is intended for use by `MultiContextModelScheduler` only.
2135
+ */
2136
+ this.runNeeded = false;
2137
+ this.inputsArray = Array.from(inputs);
2138
+ this.outputs = outputs;
2139
+ }
2140
+ /**
2141
+ * Return the series data for the given model output variable or external
2142
+ * dataset.
2143
+ *
2144
+ * @param varId The ID of the output variable associated with the data.
2145
+ * @param sourceName The external data source name (e.g. "Ref"), or
2146
+ * undefined to use the latest model output data from this context.
2147
+ */
2148
+ getSeriesForVar(varId, sourceName) {
2149
+ if (sourceName === void 0) {
2150
+ return this.outputs.getSeriesForVar(varId);
2151
+ } else {
2152
+ const dataForSource = this.externalData.get(sourceName);
2153
+ if (dataForSource !== void 0) {
2154
+ return dataForSource.get(varId);
2155
+ } else {
2156
+ return void 0;
2157
+ }
2158
+ }
2159
+ }
2160
+ };
1912
2161
  // Annotate the CommonJS export names for ESM import in node:
1913
2162
  0 && (module.exports = {
1914
2163
  BufferedRunModelParams,
@@ -1916,6 +2165,7 @@ function createSimpleInputValue(varId) {
1916
2165
  MockWasmModule,
1917
2166
  ModelListing,
1918
2167
  ModelScheduler,
2168
+ MultiContextModelScheduler,
1919
2169
  Outputs,
1920
2170
  ReferencedRunModelParams,
1921
2171
  Series,