Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
3783dbd
feat: add ConstantDef interface for override constants feature
chrispcampbell Jan 10, 2026
63ef7ef
feat: add RunModelOptions.constants and ModelSpec.customConstants
chrispcampbell Jan 10, 2026
d6026c4
feat: generate setConstant function in JS code generator
chrispcampbell Jan 10, 2026
3394c23
feat: generate setConstant function in C code generator
chrispcampbell Jan 10, 2026
8985785
feat: add constant override support to JS model runtime
chrispcampbell Jan 10, 2026
7bfd3fe
feat: add constant override support to Wasm model runtime
chrispcampbell Jan 10, 2026
6e8df47
feat: add getConstants method to RunModelParams interface
chrispcampbell Jan 10, 2026
23a355a
feat: update BaseRunnableModel to pass constants through
chrispcampbell Jan 10, 2026
11c4efc
test: add override-constants integration test
chrispcampbell Jan 10, 2026
0fe10bf
fix: implement getConstants in RunModelParams implementations
chrispcampbell Jan 10, 2026
9eb2f07
fix: add setConstant to MockJsModel and remove unused import
chrispcampbell Jan 10, 2026
ceabb7e
feat: add constant encoding/decoding for async/worker support
chrispcampbell Jan 10, 2026
a8e0ed6
fix: add customConstants to ResolvedModelSpec and spec.json
chrispcampbell Jan 10, 2026
83e8d71
fix: add customConstants to plugin-config processor
chrispcampbell Jan 10, 2026
3ccacfa
fix: move customConstants code before customLookups instead of after
chrispcampbell Jan 10, 2026
58010f2
build: revert to lock file from main branch
chrispcampbell Jan 10, 2026
4ac06a6
Merge branch 'main' into chris/470-override-constants-v2
chrispcampbell Jan 10, 2026
1e165d9
build: update lock file to include override-constants test package
chrispcampbell Jan 10, 2026
ad463e4
fix: implement constant overrides in C runtime
chrispcampbell Jan 11, 2026
eee9844
fix: move constant override functions before lookup override functions
chrispcampbell Jan 14, 2026
b893f0c
docs: format PLAN.md
chrispcampbell Jan 14, 2026
33001b2
fix: correct order of constants and lookups in updateFromEncodedBuffer
chrispcampbell Jan 14, 2026
35174ce
test: add tests for constant override encode/decode
chrispcampbell Jan 14, 2026
10c222e
build: update vitest and related packages to fix storybook tests
chrispcampbell Jan 15, 2026
eb9a780
test: add tests for constant override handling in model runners
chrispcampbell Jan 15, 2026
9923522
test: add tests for setConstant to mirror what was done for setLookup
chrispcampbell Jan 15, 2026
bb0aaa8
test: add tests for constant buffer encode/decode
chrispcampbell Jan 15, 2026
f83d7af
fix: prettier
chrispcampbell Jan 15, 2026
61a762d
test: update plugin-config tests to check custom constants
chrispcampbell Jan 15, 2026
87f7aa8
docs: update API docs
chrispcampbell Jan 15, 2026
2d3d85c
fix: include `custom constants` column in model.csv files
chrispcampbell Jan 15, 2026
4d533da
fix: move setConstantsFromBuffers before runModelWithBuffers
chrispcampbell Jan 15, 2026
5902a0c
docs: add param docs for runModelWithBuffers
chrispcampbell Jan 21, 2026
ef872db
Merge branch 'main' into chris/470-override-constants-v2
chrispcampbell Jan 21, 2026
4a2562c
build: update lock file
chrispcampbell Jan 21, 2026
7bb6df6
docs: clarify docs for runModelWithBuffers
chrispcampbell Jan 21, 2026
910e096
fix: remove PLAN.md
chrispcampbell Jan 24, 2026
b1a47be
Merge branch 'main' into chris/470-override-constants-v2
chrispcampbell Feb 4, 2026
c5978c2
fix: update order of params passed to runModelWithBuffers call from J…
chrispcampbell Feb 4, 2026
9fc0780
fix: update C runModel to pass NULL for constant override params
chrispcampbell Feb 4, 2026
843c1c9
fix: move setConstant before setLookup in header to be consistent wit…
chrispcampbell Feb 4, 2026
02f2b65
fix: move constant-related code before lookup-related code
chrispcampbell Feb 4, 2026
189547f
fix: move values-related code before indices-related code
chrispcampbell Feb 4, 2026
3a2cce5
fix: update main.c to pass NULL for constant override params
chrispcampbell Feb 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/sir/config/model.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
graph default min time,graph default max time,model dat files,bundle listing,custom lookups,custom outputs
0,100,,false,false,false
graph default min time,graph default max time,model dat files,bundle listing,custom constants,custom lookups,custom outputs
0,100,,false,false,false,false
4 changes: 2 additions & 2 deletions examples/template-jquery/config/model.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
graph default min time,graph default max time,model dat files,bundle listing,custom lookups,custom outputs
0,100,,false,false,false
graph default min time,graph default max time,model dat files,bundle listing,custom constants,custom lookups,custom outputs
0,100,,false,false,false,false
4 changes: 2 additions & 2 deletions examples/template-svelte/config/model.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
graph default min time,graph default max time,model dat files,bundle listing,custom lookups,custom outputs
0,100,,false,false,false
graph default min time,graph default max time,model dat files,bundle listing,custom constants,custom lookups,custom outputs
0,100,,false,false,false,false
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@
"@playwright/test": "^1.56.1",
"@typescript-eslint/eslint-plugin": "^8.46.0",
"@typescript-eslint/parser": "^8.46.0",
"@vitest/browser": "^4.0.16",
"@vitest/coverage-v8": "^4.0.16",
"@vitest/browser": "^4.0.17",
"@vitest/browser-playwright": "^4.0.17",
"@vitest/coverage-v8": "^4.0.17",
"eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-eslint-comments": "^3.2.0",
Expand All @@ -42,7 +43,7 @@
"typedoc": "0.25.0",
"typedoc-plugin-markdown": "3.16.0",
"typescript": "^5.2.2",
"vitest": "^4.0.16"
"vitest": "^4.0.17"
},
"pnpm": {
"peerDependencyRules": {
Expand Down
17 changes: 17 additions & 0 deletions packages/build/docs/interfaces/ModelSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ if it is needed.

___

### customConstants

`Optional` **customConstants**: `boolean` \| `string`[]

Whether to allow constants to be overridden at runtime using `setConstant`.

If undefined or false, the generated model will implement `setConstant`
as a no-op, meaning that constants cannot be overridden at runtime.

If true, all constants in the generated model will be available to be
overridden.

If an array is provided, only those variable names in the array will
be available to be overridden.

___

### customLookups

`Optional` **customLookups**: `boolean` \| `string`[]
Expand Down
17 changes: 17 additions & 0 deletions packages/build/docs/interfaces/ResolvedModelSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ if it is needed.

___

### customConstants

**customConstants**: `boolean` \| `string`[]

Whether to allow constants to be overridden at runtime using `setConstant`.

If false, the generated model will contain a `setConstant` function that
throws an error, meaning that constants cannot be overridden at runtime.

If true, all constants in the generated model will be available to be
overridden.

If an array is provided, only those variable names in the array will
be available to be overridden.

___

### customLookups

**customLookups**: `boolean` \| `string`[]
Expand Down
28 changes: 28 additions & 0 deletions packages/build/src/_shared/model-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,20 @@ export interface ModelSpec {
*/
bundleListing?: boolean

/**
* Whether to allow constants to be overridden at runtime using `setConstant`.
*
* If undefined or false, the generated model will implement `setConstant`
* as a no-op, meaning that constants cannot be overridden at runtime.
*
* If true, all constants in the generated model will be available to be
* overridden.
*
* If an array is provided, only those variable names in the array will
* be available to be overridden.
*/
customConstants?: boolean | VarName[]

/**
* Whether to allow lookups to be overridden at runtime using `setLookup`.
*
Expand Down Expand Up @@ -166,6 +180,20 @@ export interface ResolvedModelSpec {
*/
bundleListing: boolean

/**
* Whether to allow constants to be overridden at runtime using `setConstant`.
*
* If false, the generated model will contain a `setConstant` function that
* throws an error, meaning that constants cannot be overridden at runtime.
*
* If true, all constants in the generated model will be available to be
* overridden.
*
* If an array is provided, only those variable names in the array will
* be available to be overridden.
*/
customConstants: boolean | VarName[]

/**
* Whether to allow lookups to be overridden at runtime using `setLookup`.
*
Expand Down
13 changes: 11 additions & 2 deletions packages/build/src/build/impl/build-once.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ export async function buildOnce(
outputVarNames: modelSpec.outputVarNames,
externalDatfiles: modelSpec.datFiles,
bundleListing: modelSpec.bundleListing,
customLookups: modelSpec.customLookups,
customOutputs: modelSpec.customOutputs,
customConstants: modelSpec.customConstants || false,
customLookups: modelSpec.customLookups || false,
customOutputs: modelSpec.customOutputs || false,
...modelSpec.options
}
const specPath = joinPath(config.prepDir, 'spec.json')
Expand Down Expand Up @@ -220,6 +221,13 @@ function resolveModelSpec(modelSpec: ModelSpec): ResolvedModelSpec {
outputSpecs = []
}

let customConstants: boolean | VarName[]
if (modelSpec.customConstants !== undefined) {
customConstants = modelSpec.customConstants
} else {
customConstants = false
}

let customLookups: boolean | VarName[]
if (modelSpec.customLookups !== undefined) {
customLookups = modelSpec.customLookups
Expand All @@ -241,6 +249,7 @@ function resolveModelSpec(modelSpec: ModelSpec): ResolvedModelSpec {
outputs: outputSpecs,
datFiles: modelSpec.datFiles || [],
bundleListing: modelSpec.bundleListing === true,
customConstants,
customLookups,
customOutputs,
options: modelSpec.options
Expand Down
6 changes: 5 additions & 1 deletion packages/build/tests/build-prod/build-prod.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ describe('build in production mode', () => {
expect(resolvedModelSpec!.outputs).toEqual([{ varName: 'Z' }])
expect(resolvedModelSpec!.datFiles).toEqual([])
expect(resolvedModelSpec!.bundleListing).toBe(false)
expect(resolvedModelSpec!.customConstants).toBe(false)
expect(resolvedModelSpec!.customLookups).toBe(false)
expect(resolvedModelSpec!.customOutputs).toBe(false)
})
Expand Down Expand Up @@ -144,11 +145,12 @@ describe('build in production mode', () => {
expect(resolvedModelSpec!.outputs).toEqual([{ varName: 'Z' }])
expect(resolvedModelSpec!.datFiles).toEqual([])
expect(resolvedModelSpec!.bundleListing).toBe(true)
expect(resolvedModelSpec!.customConstants).toBe(false)
expect(resolvedModelSpec!.customLookups).toEqual(['lookup1'])
expect(resolvedModelSpec!.customOutputs).toEqual(['output1'])
})

it('should resolve model spec (when boolean is provided for customLookups and customOutputs)', async () => {
it('should resolve model spec (when boolean is provided for customConstants, customLookups, and customOutputs)', async () => {
let resolvedModelSpec: ResolvedModelSpec
const userConfig: UserConfig = {
genFormat: 'c',
Expand All @@ -161,6 +163,7 @@ describe('build in production mode', () => {
inputs: ['Y'],
outputs: ['Z'],
bundleListing: true,
customConstants: true,
customLookups: true,
customOutputs: true
}
Expand All @@ -182,6 +185,7 @@ describe('build in production mode', () => {
expect(result.value.exitCode).toBe(0)
expect(resolvedModelSpec!).toBeDefined()
expect(resolvedModelSpec!.bundleListing).toBe(true)
expect(resolvedModelSpec!.customConstants).toEqual(true)
expect(resolvedModelSpec!.customLookups).toEqual(true)
expect(resolvedModelSpec!.customOutputs).toEqual(true)
})
Expand Down
3 changes: 2 additions & 1 deletion packages/check-ui-shell/vitest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from 'node:path'
import { fileURLToPath } from 'node:url'

import { defineConfig, mergeConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'

import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'

Expand Down Expand Up @@ -36,7 +37,7 @@ export default defineConfig(() =>
name: 'storybook',
browser: {
enabled: true,
provider: 'playwright',
provider: playwright(),
headless: true,
instances: [{ browser: 'chromium' }]
},
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/c/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ int main(int argc, char** argv) {
double* outputBuffer = (double*)malloc(numOutputs * numSavePoints * sizeof(double));

// Run the model with the sparse input arrays and output buffer
runModelWithBuffers(inputValues, inputIndices, outputBuffer, NULL);
runModelWithBuffers(inputValues, inputIndices, outputBuffer, NULL, NULL, NULL);

if (!suppress_data_output) {
if (raw_output) {
Expand Down
116 changes: 92 additions & 24 deletions packages/cli/src/c/model.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,39 @@ double getSaveper() {
return _saveper;
}

/**
* Set constant overrides from the given buffers.
*
* The `constantIndices` buffer contains the variable indices and subscript indices
* for each constant to override. The format is:
* [count, varIndex1, subCount1, subIndex1_1, ..., varIndex2, subCount2, ...]
*
* The `constantValues` buffer contains the corresponding values for each constant.
*/
void setConstantOverridesFromBuffers(double* constantValues, int32_t* constantIndices) {
if (constantValues == NULL || constantIndices == NULL) {
return;
}

size_t indexBufferOffset = 0;
size_t valueBufferOffset = 0;
size_t constantCount = (size_t)constantIndices[indexBufferOffset++];

for (size_t i = 0; i < constantCount; i++) {
size_t varIndex = (size_t)constantIndices[indexBufferOffset++];
size_t subCount = (size_t)constantIndices[indexBufferOffset++];
size_t* subIndices;
if (subCount > 0) {
subIndices = (size_t*)(constantIndices + indexBufferOffset);
indexBufferOffset += subCount;
} else {
subIndices = NULL;
}
double value = constantValues[valueBufferOffset++];
setConstant(varIndex, subIndices, value);
}
}

/**
* Run the model, reading inputs from the given `inputs` buffer, and writing outputs
* to the given `outputs` buffer.
Expand Down Expand Up @@ -90,21 +123,28 @@ double getSaveper() {
* values. See above for details on the expected format.
*/
void runModel(double* inputs, double* outputs) {
runModelWithBuffers(inputs, NULL, outputs, NULL);
runModelWithBuffers(inputs, NULL, outputs, NULL, NULL, NULL);
}

/**
* Run the model, reading inputs from the given `inputs` buffer, and writing outputs
* to the given `outputs` buffer.
*
* INPUTS
* ------
*
* If `inputIndices` is NULL, the `inputs` buffer is assumed to have one double value
* for each input variable, in exactly the same order as the variables are listed in
* the spec file.
*
* If `inputIndices` is non-NULL, it specifies which inputs are being set:
* - inputIndices[0] is the count of inputs being specified
* - inputIndices[1..N] are the indices of the inputs to set
* - inputs[0..N-1] are the corresponding values
* - inputIndices[0] is the count (C) of inputs being specified
* - inputIndices[1...C] are the indices of the inputs to set (where each index
* corresponds to the index of the input variable in the spec.json file)
* - inputs[0...C-1] are the corresponding values
*
* OUTPUTS
* -------
*
* After each step of the run, the `outputs` buffer will be updated with the output
* variables. The `outputs` buffer needs to be at least as large as:
Expand All @@ -118,31 +158,59 @@ void runModel(double* inputs, double* outputs) {
* outputs will begin, and so on.
*
* If `outputIndices` is non-NULL, it specifies which outputs are being stored:
* - outputIndices[0] is the count of output variables being stored
* - outputIndices[1..N] are the indices of the output variables to store (unlike
* `inputIndices`, these indices refer to the ones defined in the `{model}.json`
* listing file, NOT the list of output variables spec file)
* - outputs[0..N-1] are the corresponding values
* - outputIndices[0] is the count (C) of output variables being stored
* - outputIndices[1...] are the indices of the output variables to store, in
* the following format:
* [count, varIndex1, subCount1, subIndex1_1, ..., varIndex2, subCount2, ...]
* where `count` is the number of variables to store, `varIndexN` is the index
* of the variable to store (from the {model}.json listing file), `subCountN` is
* the number of subscripts for that variable, and `subIndexN_M` is the index of
* the subscript at the Mth position for that variable
* - outputs[0...C-1] are the corresponding values
*
* @param inputs The buffer that contains the model input values. If NULL,
* no inputs will be set and the model will use the default values for all
* constants as defined in the generated model. If non-NULL, the buffer is
* assumed to have one double value for each input variable. The number of
* values provided depends on `inputIndices`; see above for details on the
* expected format of these two parameters.
* @param inputIndices The optional buffer that specifies which input values
* from the `inputs` buffer are being set. See above for details on the
* expected format.
* @param outputs The required buffer that will receive the model output
* values. See above for details on the expected format.
* @param outputIndices The optional buffer that specifies which output values
* will be stored in the `outputs` buffer. See above for details on the
* expected format.
* CONSTANT OVERRIDES
* ------------------
*
* If `constants` and `constantIndices` are non-NULL, the provided constant values will
* override the default values for those constants as defined in the generated model.
*
* The `constantIndices` buffer specifies which constants are being overridden. The
* format is the same as described above for `outputIndices`:
* - constantIndices[0] is the count (C) of constants being overridden
* - constantIndices[1...] are the indices of the constants to override, in the
* following format:
* [count, varIndex1, subCount1, subIndex1_1, ..., varIndex2, subCount2, ...]
* where `count` is the number of constants to override, `varIndexN` is the index
* of the variable to store (from the {model}.json listing file), `subCountN` is
* the number of subscripts for that variable, and `subIndexN_M` is the index of
* the subscript at the Mth position for that variable
* - constants[0...C-1] are the corresponding values
*
* @param inputs The buffer that contains the model input values. If NULL, no inputs
* will be set and the model will use the default values for all constants as defined
* in the generated model. If non-NULL, the buffer is assumed to have one double value
* for each input variable. The number of values provided depends on `inputIndices`;
* see above for details on the expected format of these two parameters.
* @param inputIndices The optional buffer that specifies which input values from the
* `inputs` buffer are being set. See above for details on the expected format.
* @param outputs The required buffer that will receive the model output values. See
* above for details on the expected format.
* @param outputIndices The optional buffer that specifies which output values will be
* stored in the `outputs` buffer. See above for details on the expected format.
* @param constants An optional buffer that contains the values of the constants to
* override. Pass NULL if not overriding any constants. Each value in the buffer
* corresponds to the value of the constant at the corresponding index.
* @param constantIndices An optional buffer that contains the indices of the constants
* to override. Pass NULL if not overriding any constants. See above for details on
* the expected format.
*/
void runModelWithBuffers(double* inputs, int32_t* inputIndices, double* outputs, int32_t* outputIndices) {
void runModelWithBuffers(double* inputs, int32_t* inputIndices, double* outputs, int32_t* outputIndices, double* constants, int32_t* constantIndices) {
outputBuffer = outputs;
outputIndexBuffer = outputIndices;
initConstants();
if (constants != NULL && constantIndices != NULL) {
setConstantOverridesFromBuffers(constants, constantIndices);
}
if (inputs != NULL) {
setInputs(inputs, inputIndices);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/c/sde.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ double getInitialTime(void);
double getFinalTime(void);
double getSaveper(void);
void runModel(double* inputs, double* outputs);
void runModelWithBuffers(double* inputs, int32_t* inputIndices, double* outputs, int32_t* outputIndices);
void runModelWithBuffers(double* inputs, int32_t* inputIndices, double* outputs, int32_t* outputIndices, double* constants, int32_t* constantIndices);
void run(void);
void startOutput(void);
void outputVar(double value);
Expand All @@ -66,6 +66,7 @@ void finish(void);
void initConstants(void);
void initLevels(void);
void setInputs(double* inputValues, int32_t* inputIndices);
void setConstant(size_t varIndex, size_t* subIndices, double value);
void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints);
void evalAux(void);
void evalLevels(void);
Expand Down
Loading