From f8e7c506464ce333b067656388be16d933656942 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 23 Jan 2026 14:37:20 -0800 Subject: [PATCH 1/6] fix: remove `setInputs(char*)` function in generated model + remove `run_model` + add `runModel` --- packages/cli/src/c/main.c | 103 ++++++++++++++---- packages/cli/src/c/model.c | 62 ++++------- packages/cli/src/c/sde.h | 6 +- packages/compile/src/generate/gen-code-c.js | 43 ++------ .../compile/src/generate/gen-code-c.spec.ts | 21 +--- 5 files changed, 116 insertions(+), 119 deletions(-) diff --git a/packages/cli/src/c/main.c b/packages/cli/src/c/main.c index 4559637b..ffed2d46 100644 --- a/packages/cli/src/c/main.c +++ b/packages/cli/src/c/main.c @@ -1,8 +1,38 @@ #include "sde.h" +/** + * Parse input data string in the format "varIndex:value varIndex:value ..." + * and populate the input buffer. + * + * @param inputData The input string to parse. + * @param inputBuffer The buffer to populate with parsed values. + */ +static void parseInputs(const char* inputData, double* inputBuffer) { + if (inputData == NULL || *inputData == '\0') { + return; + } + // Make a copy since strtok modifies the string + char* inputs = (char*)inputData; + char* inputsCopy = (char*)malloc(strlen(inputs) + 1); + strcpy(inputsCopy, inputs); + + char* token = strtok(inputsCopy, " "); + while (token) { + char* p = strchr(token, ':'); + if (p) { + *p = '\0'; + int modelVarIndex = atoi(token); + double value = atof(p + 1); + inputBuffer[modelVarIndex] = value; + } + token = strtok(NULL, " "); + } + free(inputsCopy); +} + int main(int argc, char** argv) { // TODO make the input buffer size dynamic - char inputs[500000]; + char inputString[500000]; // When true, output data without newlines or a header, suitable for embedding reference data. bool raw_output = false; // When true, suppress data output when using PR* macros. @@ -10,11 +40,11 @@ int main(int argc, char** argv) { // Try to read input from a file named in the argument. if (argc > 1) { FILE* instream = fopen(argv[1], "r"); - if (instream && fgets(inputs, sizeof inputs, instream) != NULL) { + if (instream && fgets(inputString, sizeof inputString, instream) != NULL) { fclose(instream); - size_t len = strlen(inputs); - if (inputs[len - 1] == '\n') { - inputs[len - 1] = '\0'; + size_t len = strlen(inputString); + if (inputString[len - 1] == '\n') { + inputString[len - 1] = '\0'; } } if (argc > 2) { @@ -24,35 +54,62 @@ int main(int argc, char** argv) { } } } else { - *inputs = '\0'; + *inputString = '\0'; + } + + // Allocate input buffer and parse string inputs into it. + // Only allocate a buffer if there are inputs to parse; otherwise pass NULL + // to runModel so that the model uses its default values from initConstants. + double* inputBuffer = NULL; + if (numInputs > 0 && *inputString != '\0') { + inputBuffer = (double*)calloc(numInputs, sizeof(double)); + parseInputs(inputString, inputBuffer); } - // Run the model and get output for all time steps. - char* outputs = run_model(inputs); + + // Calculate the number of save points for the output buffer + double initialTime = getInitialTime(); + double finalTime = getFinalTime(); + double saveper = getSaveper(); + size_t numSavePoints = (size_t)(round((finalTime - initialTime) / saveper)) + 1; + + // Allocate output buffer + double* outputBuffer = (double*)malloc(numOutputs * numSavePoints * sizeof(double)); + + // Run the model with the input and output buffers + runModel(inputBuffer, outputBuffer); + if (!suppress_data_output) { if (raw_output) { - // Write raw output data directly. - fputs(outputs, stdout); + // Write raw output data directly (tab-separated, no newlines) + for (size_t t = 0; t < numSavePoints; t++) { + for (size_t v = 0; v < numOutputs; v++) { + // Output buffer is organized by variable (each variable has numSavePoints values) + double value = outputBuffer[v * numSavePoints + t]; + printf("%g\t", value); + } + } } else { // Write a header for output data. printf("%s\n", getHeader()); // Write tab-delimited output data, one line per output time step. - if (outputs != NULL) { - char* p = outputs; - while (*p) { - char* line = p; - for (size_t i = 0; i < numOutputs; i++) { - if (i > 0) { - p++; - } - while (*p && *p != '\t') { - p++; - } + for (size_t t = 0; t < numSavePoints; t++) { + for (size_t v = 0; v < numOutputs; v++) { + // Output buffer is organized by variable (each variable has numSavePoints values) + double value = outputBuffer[v * numSavePoints + t]; + if (v > 0) { + printf("\t"); } - *p++ = '\0'; - printf("%s\n", line); + printf("%g", value); } + printf("\n"); } } } + + // Clean up + if (inputBuffer != NULL) { + free(inputBuffer); + } + free(outputBuffer); finish(); } diff --git a/packages/cli/src/c/model.c b/packages/cli/src/c/model.c index 7c48a785..579b771b 100644 --- a/packages/cli/src/c/model.c +++ b/packages/cli/src/c/model.c @@ -11,11 +11,7 @@ struct timespec startTime, finishTime; // The special _time variable is not included in .mdl files. double _time; -// Output data buffer used by `run_model` -char* outputData = NULL; -size_t outputIndex = 0; - -// Output data buffer used by `runModelWithBuffers` +// Output data buffer used by `runModel` / `runModelWithBuffers` double* outputBuffer = NULL; int32_t* outputIndexBuffer = NULL; size_t outputVarIndex = 0; @@ -67,25 +63,23 @@ double getSaveper() { return _saveper; } -char* run_model(const char* inputs) { - // run_model does everything necessary to run the model with the given inputs. - // It may be called multiple times. Call finish() after all runs are complete. - // Initialize the state to default values in the model at the start of each run. - initConstants(); - // Set inputs for this run that override default values. - // fprintf(stderr, "run_model inputs = %s\n", inputs); - setInputs(inputs); - initLevels(); - run(); - return outputData; +/** + * Run the model, reading inputs from the given `inputs` buffer, and writing outputs + * to the given `outputs` buffer. + * + * This is a simplified version of `runModelWithBuffers` that passes NULL for + * `outputIndices`, which means the outputs will be stored in the order defined + * in the spec file. + */ +void runModel(double* inputs, double* outputs) { + runModelWithBuffers(inputs, outputs, NULL); } /** * Run the model, reading inputs from the given `inputs` buffer, and writing outputs * to the given `outputs` buffer. * - * This function performs the same steps as the original `run_model` function, - * except that it uses the provided pre-allocated buffers. + * This function uses the provided pre-allocated buffers for inputs and outputs. * * The `inputs` buffer is assumed to have one double value for each input variable; * they must be in exactly the same order as the variables are listed in the spec file. @@ -107,7 +101,9 @@ void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices outputBuffer = outputs; outputIndexBuffer = outputIndices; initConstants(); - setInputsFromBuffer(inputs); + if (inputs != NULL) { + setInputs(inputs); + } initLevels(); run(); outputBuffer = NULL; @@ -121,7 +117,6 @@ void run() { // Restart fresh output for all steps in this run. savePointIndex = 0; - outputIndex = 0; // Initialize time with the required INITIAL TIME control variable. _time = _initial_time; @@ -171,25 +166,11 @@ void run() { } void outputVar(double value) { - if (outputBuffer != NULL) { - // Write each value into the preallocated buffer; each variable has a "row" that - // contains `numSavePoints` values, one value for each save point - double* outputPtr = outputBuffer + (outputVarIndex * numSavePoints) + savePointIndex; - *outputPtr = value; - outputVarIndex++; - } else { - // Allocate an output buffer for all output steps as a single block. - // Add one character for a null terminator. - if (outputData == NULL) { - int numOutputSteps = (int)(round((_final_time - _initial_time) / _saveper)) + 1; - size_t size = numOutputSteps * (OUTPUT_STRING_LEN * numOutputs) + 1; - // fprintf(stderr, "output data size = %zu\n", size); - outputData = (char*)malloc(size); - } - // Format the value as a string in the output data buffer. - int numChars = snprintf(outputData + outputIndex, OUTPUT_STRING_LEN + 1, "%g\t", value); - outputIndex += numChars; - } + // Write each value into the preallocated buffer; each variable has a "row" that + // contains `numSavePoints` values, one value for each save point + double* outputPtr = outputBuffer + (outputVarIndex * numSavePoints) + savePointIndex; + *outputPtr = value; + outputVarIndex++; } void finish() { @@ -199,7 +180,4 @@ void finish() { 1000.0 * finishTime.tv_sec + 1e-6 * finishTime.tv_nsec - (1000.0 * startTime.tv_sec + 1e-6 * startTime.tv_nsec); fprintf(stderr, "calculation runtime = %.0f ms\n", runtime); #endif - if (outputData != NULL) { - free(outputData); - } } diff --git a/packages/cli/src/c/sde.h b/packages/cli/src/c/sde.h index 4d3e583c..f8bcac34 100644 --- a/packages/cli/src/c/sde.h +++ b/packages/cli/src/c/sde.h @@ -41,6 +41,7 @@ EXTERN double _epsilon; #define OUTPUT_STRING_LEN 14 // Internal variables +EXTERN const int numInputs; EXTERN const int numOutputs; // Standard simulation control parameters @@ -54,7 +55,7 @@ EXTERN double _saveper; double getInitialTime(void); double getFinalTime(void); double getSaveper(void); -char* run_model(const char* inputs); +void runModel(double* inputs, double* outputs); void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices); void run(void); void startOutput(void); @@ -64,8 +65,7 @@ void finish(void); // Functions implemented by the generated model void initConstants(void); void initLevels(void); -void setInputs(const char* inputData); -void setInputsFromBuffer(double *inputData); +void setInputs(double *inputData); void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints); void evalAux(void); void evalLevels(void); diff --git a/packages/compile/src/generate/gen-code-c.js b/packages/compile/src/generate/gen-code-c.js index d2b2c0b9..2b9b2a5a 100644 --- a/packages/compile/src/generate/gen-code-c.js +++ b/packages/compile/src/generate/gen-code-c.js @@ -200,11 +200,7 @@ ${customOutputSection(Model.varIndexInfo(), spec.customOutputs)} mode = 'io' return `\ -void setInputs(const char* inputData) { -${inputsFromStringImpl()} -} - -void setInputsFromBuffer(double* inputData) { +void setInputs(double* inputData) { ${inputsFromBufferImpl()} } @@ -322,13 +318,19 @@ ${section(chunk)} } function internalVarsSection() { // Declare internal variables to run the model. - let decls + let numInputsDecl + if (spec.inputVars && spec.inputVars.length > 0) { + numInputsDecl = `const int numInputs = ${spec.inputVars.length};` + } else { + numInputsDecl = `const int numInputs = 0;` + } + let numOutputsDecl if (outputAllVars) { - decls = `const int numOutputs = ${expandedVarNames().length};` + numOutputsDecl = `const int numOutputs = ${expandedVarNames().length};` } else { - decls = `const int numOutputs = ${spec.outputVars.length};` + numOutputsDecl = `const int numOutputs = ${spec.outputVars.length};` } - return decls + return `${numInputsDecl}\n${numOutputsDecl}` } function arrayDimensionsSection() { // Emit a declaration for each array dimension's index numbers. @@ -402,29 +404,6 @@ ${section(chunk)} const section = R.pipe(outputVars, code, lines) return section(varIndexInfo) } - function inputsFromStringImpl() { - // If there was an I/O spec file, then emit code to parse input variables. - // The user can replace this with a parser for a different serialization format. - let inputVars = '' - if (spec.inputVars && spec.inputVars.length > 0) { - let inputVarPtrs = R.reduce((a, inputVar) => R.concat(a, ` &${inputVar},\n`), '', spec.inputVars) - inputVars = `\ - static double* inputVarPtrs[] = {\n${inputVarPtrs} }; - char* inputs = (char*)inputData; - char* token = strtok(inputs, " "); - while (token) { - char* p = strchr(token, ':'); - if (p) { - *p = '\\0'; - int modelVarIndex = atoi(token); - double value = atof(p+1); - *inputVarPtrs[modelVarIndex] = value; - } - token = strtok(NULL, " "); - }` - } - return inputVars - } function inputsFromBufferImpl() { let inputVars = [] if (spec.inputVars && spec.inputVars.length > 0) { diff --git a/packages/compile/src/generate/gen-code-c.spec.ts b/packages/compile/src/generate/gen-code-c.spec.ts index bcfb8f4f..ec25a28f 100644 --- a/packages/compile/src/generate/gen-code-c.spec.ts +++ b/packages/compile/src/generate/gen-code-c.spec.ts @@ -126,6 +126,7 @@ double _y; double _z; // Internal variables +const int numInputs = 1; const int numOutputs = 8; // Array dimensions @@ -245,25 +246,7 @@ void evalLevels() { // Evaluate levels. } -void setInputs(const char* inputData) { - static double* inputVarPtrs[] = { - &_input, - }; - char* inputs = (char*)inputData; - char* token = strtok(inputs, " "); - while (token) { - char* p = strchr(token, ':'); - if (p) { - *p = '\\0'; - int modelVarIndex = atoi(token); - double value = atof(p+1); - *inputVarPtrs[modelVarIndex] = value; - } - token = strtok(NULL, " "); - } -} - -void setInputsFromBuffer(double* inputData) { +void setInputs(double* inputData) { _input = inputData[0]; } From d48bb80cb27903fc6dfa749180f92f254dfe71b7 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 23 Jan 2026 14:37:46 -0800 Subject: [PATCH 2/6] fix: remove old/unused perf test file --- notes/main-perftest.c | 53 ------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 notes/main-perftest.c diff --git a/notes/main-perftest.c b/notes/main-perftest.c deleted file mode 100644 index d4500f60..00000000 --- a/notes/main-perftest.c +++ /dev/null @@ -1,53 +0,0 @@ -#include -#include "sde.h" - -int main(int argc, char** argv) { - // TODO make the input buffer size dynamic - char inputs[1000]; - // Try to read input from a file named in the argument. - if (argc > 1) { - FILE* instream = fopen(argv[1], "r"); - if (instream && fgets(inputs, sizeof inputs, instream) != NULL) { - fclose(instream); - size_t len = strlen(inputs); - if (inputs[len-1] == '\n') { - inputs[len-1] = '\0'; - } - } - } - else { - *inputs = '\0'; - } - -struct timespec startTime, finishTime; -clock_gettime(CLOCK_MONOTONIC, &startTime); - - // Run the model and get output for all time steps. - char* outputs = run_model(inputs); - -clock_gettime(CLOCK_MONOTONIC, &finishTime); -double runtime = 1000.0 * finishTime.tv_sec + 1e-6 * finishTime.tv_nsec - - (1000.0 * startTime.tv_sec + 1e-6 * startTime.tv_nsec); -fprintf(stderr, "%g ms\n", runtime); - - // Write a header for output data. - printf("%s\n", getHeader()); - // Write tab-delimited output data, one line per output time step. - if (outputs != NULL) { - char* p = outputs; - while (*p) { - char* line = p; - for (size_t i = 0; i < numOutputs; i++) { - if (i > 0) { - p++; - } - while (*p && *p != '\t') { - p++; - } - } - *p++ = '\0'; - printf("%s\n", line); - } - } - finish(); -} From 50876addf037eadefc3357c1c9093ff8f4cb5d99 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 23 Jan 2026 15:40:22 -0800 Subject: [PATCH 3/6] fix: update setInputs to take optional inputIndices buffer --- packages/cli/src/c/main.c | 72 ++++++++++++++----- packages/cli/src/c/model.c | 67 ++++++++++++----- packages/cli/src/c/sde.h | 4 +- packages/compile/src/generate/gen-code-c.js | 36 +++++++--- .../compile/src/generate/gen-code-c.spec.ts | 22 +++++- .../src/wasm-model/_mocks/mock-wasm-module.ts | 7 +- packages/runtime/src/wasm-model/wasm-model.ts | 12 +++- 7 files changed, 167 insertions(+), 53 deletions(-) diff --git a/packages/cli/src/c/main.c b/packages/cli/src/c/main.c index ffed2d46..1b7a7827 100644 --- a/packages/cli/src/c/main.c +++ b/packages/cli/src/c/main.c @@ -1,21 +1,48 @@ #include "sde.h" +/** + * Count the number of input pairs in the input string. + */ +static size_t countInputs(const char* inputData) { + if (inputData == NULL || *inputData == '\0') { + return 0; + } + + // Make a copy since strtok modifies the string + char* inputsCopy = (char*)malloc(strlen(inputData) + 1); + strcpy(inputsCopy, inputData); + + size_t count = 0; + char* token = strtok(inputsCopy, " "); + while (token) { + if (strchr(token, ':') != NULL) { + count++; + } + token = strtok(NULL, " "); + } + free(inputsCopy); + + return count; +} + /** * Parse input data string in the format "varIndex:value varIndex:value ..." - * and populate the input buffer. + * and populate the inputValues and inputIndices arrays for sparse input setting. * * @param inputData The input string to parse. - * @param inputBuffer The buffer to populate with parsed values. + * @param inputValues The array to populate with input values. + * @param inputIndices The array to populate with input indices (first element is count). */ -static void parseInputs(const char* inputData, double* inputBuffer) { +static void parseInputs(const char* inputData, double* inputValues, int32_t* inputIndices) { if (inputData == NULL || *inputData == '\0') { return; } + // Make a copy since strtok modifies the string - char* inputs = (char*)inputData; - char* inputsCopy = (char*)malloc(strlen(inputs) + 1); - strcpy(inputsCopy, inputs); + char* inputsCopy = (char*)malloc(strlen(inputData) + 1); + strcpy(inputsCopy, inputData); + size_t i = 0; char* token = strtok(inputsCopy, " "); while (token) { char* p = strchr(token, ':'); @@ -23,10 +50,13 @@ static void parseInputs(const char* inputData, double* inputBuffer) { *p = '\0'; int modelVarIndex = atoi(token); double value = atof(p + 1); - inputBuffer[modelVarIndex] = value; + inputIndices[i + 1] = modelVarIndex; + inputValues[i] = value; + i++; } token = strtok(NULL, " "); } + inputIndices[0] = (int32_t)i; free(inputsCopy); } @@ -57,13 +87,16 @@ int main(int argc, char** argv) { *inputString = '\0'; } - // Allocate input buffer and parse string inputs into it. - // Only allocate a buffer if there are inputs to parse; otherwise pass NULL - // to runModel so that the model uses its default values from initConstants. - double* inputBuffer = NULL; - if (numInputs > 0 && *inputString != '\0') { - inputBuffer = (double*)calloc(numInputs, sizeof(double)); - parseInputs(inputString, inputBuffer); + // Parse input string and create sparse input arrays. Only allocate buffers if there + // are inputs to parse; otherwise pass NULL to runModelWithBuffers so that the model + // uses its default values from initConstants. + double* inputValues = NULL; + int32_t* inputIndices = NULL; + size_t inputCount = countInputs(inputString); + if (inputCount > 0) { + inputValues = (double*)malloc(inputCount * sizeof(double)); + inputIndices = (int32_t*)malloc((inputCount + 1) * sizeof(int32_t)); + parseInputs(inputString, inputValues, inputIndices); } // Calculate the number of save points for the output buffer @@ -75,8 +108,8 @@ int main(int argc, char** argv) { // Allocate output buffer double* outputBuffer = (double*)malloc(numOutputs * numSavePoints * sizeof(double)); - // Run the model with the input and output buffers - runModel(inputBuffer, outputBuffer); + // Run the model with the sparse input arrays and output buffer + runModelWithBuffers(inputValues, inputIndices, outputBuffer, NULL); if (!suppress_data_output) { if (raw_output) { @@ -107,8 +140,11 @@ int main(int argc, char** argv) { } // Clean up - if (inputBuffer != NULL) { - free(inputBuffer); + if (inputValues != NULL) { + free(inputValues); + } + if (inputIndices != NULL) { + free(inputIndices); } free(outputBuffer); finish(); diff --git a/packages/cli/src/c/model.c b/packages/cli/src/c/model.c index 579b771b..d993c39b 100644 --- a/packages/cli/src/c/model.c +++ b/packages/cli/src/c/model.c @@ -11,7 +11,7 @@ struct timespec startTime, finishTime; // The special _time variable is not included in .mdl files. double _time; -// Output data buffer used by `runModel` / `runModelWithBuffers` +// Output data buffer and parameters used by `runModelWithBuffers` double* outputBuffer = NULL; int32_t* outputIndexBuffer = NULL; size_t outputVarIndex = 0; @@ -68,41 +68,72 @@ double getSaveper() { * to the given `outputs` buffer. * * This is a simplified version of `runModelWithBuffers` that passes NULL for - * `outputIndices`, which means the outputs will be stored in the order defined - * in the spec file. + * all parameters other than `inputs` and `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: + * `number of output variables` * `number of save points` + * + * @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 in exactly the + * same order that the variables are listed in the spec file. + * @param outputs The required buffer that will receive the model output + * values. See above for details on the expected format. */ void runModel(double* inputs, double* outputs) { - runModelWithBuffers(inputs, outputs, NULL); + runModelWithBuffers(inputs, NULL, outputs, NULL); } /** * Run the model, reading inputs from the given `inputs` buffer, and writing outputs * to the given `outputs` buffer. * - * This function uses the provided pre-allocated buffers for inputs and outputs. + * 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. * - * The `inputs` buffer is assumed to have one double value for each input variable; - * they must be 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 * * After each step of the run, the `outputs` buffer will be updated with the output - * variables. The buffer needs to be at least as large as: + * variables. The `outputs` buffer needs to be at least as large as: * `number of output variables` * `number of save points` - * where `number of save points` is typically one point for each year inclusive of - * the start and end times. * - * The outputs will be stored in the same order as the outputs are defined in the - * spec file, with one "row" for each variable. For example, the first value in - * the buffer will be the output value at t0 for the first output variable, followed - * by the output value for that variable at t1, and so on. After the value for tN - * (where tN is the last time in the range), the second variable outputs will begin, - * and so on. + * If `outputIndices` is NULL, outputs will be stored in the same order as the outputs + * are defined in the spec file, with one "row" for each variable. + * + * 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 + * + * @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. */ -void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices) { +void runModelWithBuffers(double* inputs, int32_t* inputIndices, double* outputs, int32_t* outputIndices) { outputBuffer = outputs; outputIndexBuffer = outputIndices; initConstants(); if (inputs != NULL) { - setInputs(inputs); + setInputs(inputs, inputIndices); } initLevels(); run(); diff --git a/packages/cli/src/c/sde.h b/packages/cli/src/c/sde.h index f8bcac34..098c900d 100644 --- a/packages/cli/src/c/sde.h +++ b/packages/cli/src/c/sde.h @@ -56,7 +56,7 @@ double getInitialTime(void); double getFinalTime(void); double getSaveper(void); void runModel(double* inputs, double* outputs); -void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices); +void runModelWithBuffers(double* inputs, int32_t* inputIndices, double* outputs, int32_t* outputIndices); void run(void); void startOutput(void); void outputVar(double value); @@ -65,7 +65,7 @@ void finish(void); // Functions implemented by the generated model void initConstants(void); void initLevels(void); -void setInputs(double *inputData); +void setInputs(double* inputValues, int32_t* inputIndices); void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints); void evalAux(void); void evalLevels(void); diff --git a/packages/compile/src/generate/gen-code-c.js b/packages/compile/src/generate/gen-code-c.js index 2b9b2a5a..b4020c71 100644 --- a/packages/compile/src/generate/gen-code-c.js +++ b/packages/compile/src/generate/gen-code-c.js @@ -200,8 +200,8 @@ ${customOutputSection(Model.varIndexInfo(), spec.customOutputs)} mode = 'io' return `\ -void setInputs(double* inputData) { -${inputsFromBufferImpl()} +void setInputs(double* inputValues, int32_t* inputIndices) { +${setInputsImpl()} } void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints) { @@ -404,15 +404,31 @@ ${section(chunk)} const section = R.pipe(outputVars, code, lines) return section(varIndexInfo) } - function inputsFromBufferImpl() { - let inputVars = [] - if (spec.inputVars && spec.inputVars.length > 0) { - for (let i = 0; i < spec.inputVars.length; i++) { - const inputVar = spec.inputVars[i] - inputVars.push(` ${inputVar} = inputData[${i}];`) - } + function setInputsImpl() { + if (!spec.inputVars || spec.inputVars.length === 0) { + return '' + } + // Build the pointer table for input variables + let inputVarPtrs = R.reduce((a, inputVar) => R.concat(a, ` &${inputVar},\n`), '', spec.inputVars) + return `\ + static double* inputVarPtrs[] = { +${inputVarPtrs} }; + if (inputIndices == NULL) { + // When inputIndices is NULL, assume that inputValues contains all input values + // in the same order that the variables are defined in the model spec + for (size_t i = 0; i < numInputs; i++) { + *inputVarPtrs[i] = inputValues[i]; + } + } else { + // When inputIndices is non-NULL, set the input values according to the indices + // in the inputIndices array, where each index corresponds to the index of the + // variable in the model spec + size_t numInputsToSet = (size_t)inputIndices[0]; + for (size_t i = 0; i < numInputsToSet; i++) { + size_t inputVarIndex = (size_t)inputIndices[i + 1]; + *inputVarPtrs[inputVarIndex] = inputValues[i]; } - return inputVars.join('\n') + }` } function setLookupImpl(varIndexInfo, customLookups) { // Emit case statements for all lookups and data variables that can be overridden diff --git a/packages/compile/src/generate/gen-code-c.spec.ts b/packages/compile/src/generate/gen-code-c.spec.ts index ec25a28f..725aa791 100644 --- a/packages/compile/src/generate/gen-code-c.spec.ts +++ b/packages/compile/src/generate/gen-code-c.spec.ts @@ -246,8 +246,26 @@ void evalLevels() { // Evaluate levels. } -void setInputs(double* inputData) { - _input = inputData[0]; +void setInputs(double* inputValues, int32_t* inputIndices) { + static double* inputVarPtrs[] = { + &_input, + }; + if (inputIndices == NULL) { + // When inputIndices is NULL, assume that inputValues contains all input values + // in the same order that the variables are defined in the model spec + for (size_t i = 0; i < numInputs; i++) { + *inputVarPtrs[i] = inputValues[i]; + } + } else { + // When inputIndices is non-NULL, set the input values according to the indices + // in the inputIndices array, where each index corresponds to the index of the + // variable in the model spec + size_t numInputsToSet = (size_t)inputIndices[0]; + for (size_t i = 0; i < numInputsToSet; i++) { + size_t inputVarIndex = (size_t)inputIndices[i + 1]; + *inputVarPtrs[inputVarIndex] = inputValues[i]; + } + } } void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints) { diff --git a/packages/runtime/src/wasm-model/_mocks/mock-wasm-module.ts b/packages/runtime/src/wasm-model/_mocks/mock-wasm-module.ts index 5c3674c4..7968d108 100644 --- a/packages/runtime/src/wasm-model/_mocks/mock-wasm-module.ts +++ b/packages/runtime/src/wasm-model/_mocks/mock-wasm-module.ts @@ -104,7 +104,12 @@ export class MockWasmModule implements WasmModule { this.lookups.set(varId, new JsModelLookup(numPoints, points)) } case 'runModelWithBuffers': - return (inputsAddress: number, outputsAddress: number, outputIndicesAddress: number) => { + return ( + inputsAddress: number, + _inputIndicesAddress: number, + outputsAddress: number, + outputIndicesAddress: number + ) => { const inputs = this.getHeapView('float64', inputsAddress) as Float64Array const outputs = this.getHeapView('float64', outputsAddress) as Float64Array const outputIndices = this.getHeapView('int32', outputIndicesAddress) as Int32Array diff --git a/packages/runtime/src/wasm-model/wasm-model.ts b/packages/runtime/src/wasm-model/wasm-model.ts index 01ea5dec..012af10b 100644 --- a/packages/runtime/src/wasm-model/wasm-model.ts +++ b/packages/runtime/src/wasm-model/wasm-model.ts @@ -40,7 +40,12 @@ class WasmModel implements RunnableModel { pointsAddress: number, numPoints: number ) => void - private readonly wasmRunModel: (inputsAddress: number, outputsAddress: number, outputIndicesAddress: number) => void + private readonly wasmRunModel: ( + inputsAddress: number, + inputIndicesAddress: number, + outputsAddress: number, + outputIndicesAddress: number + ) => void /** * @param wasmModule The `WasmModule` that provides access to the native functions. @@ -65,7 +70,7 @@ class WasmModel implements RunnableModel { // Make the native functions callable this.wasmSetLookup = wasmModule.cwrap('setLookup', null, ['number', 'number', 'number', 'number']) - this.wasmRunModel = wasmModule.cwrap('runModelWithBuffers', null, ['number', 'number', 'number']) + this.wasmRunModel = wasmModule.cwrap('runModelWithBuffers', null, ['number', 'number', 'number', 'number']) } // from RunnableModel interface @@ -158,6 +163,9 @@ class WasmModel implements RunnableModel { const t0 = perfNow() this.wasmRunModel( this.inputsBuffer?.getAddress() || 0, + // Always pass 0 (NULL) for input indices, since we assume that all input values are + // provided and are in the same order as the input variables defined in the model spec + 0, this.outputsBuffer.getAddress(), outputIndicesBuffer?.getAddress() || 0 ) From f6ed329886ca0b65fdc44f7d15adc724068d5cc7 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 23 Jan 2026 15:42:58 -0800 Subject: [PATCH 4/6] fix: clarify format of input string --- packages/cli/src/c/main.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/c/main.c b/packages/cli/src/c/main.c index 1b7a7827..ad7b0463 100644 --- a/packages/cli/src/c/main.c +++ b/packages/cli/src/c/main.c @@ -1,7 +1,8 @@ #include "sde.h" /** - * Count the number of input pairs in the input string. + * Count the number of input pairs in the input string in the format + * "varIndex:value varIndex:value ..." (for example, "0:3.14 6:42"). */ static size_t countInputs(const char* inputData) { if (inputData == NULL || *inputData == '\0') { @@ -26,8 +27,9 @@ static size_t countInputs(const char* inputData) { } /** - * Parse input data string in the format "varIndex:value varIndex:value ..." - * and populate the inputValues and inputIndices arrays for sparse input setting. + * Parse an input data string in the format "varIndex:value varIndex:value ..." + * (for example, "0:3.14 6:42") and populate the inputValues and inputIndices + * arrays for sparse input setting. * * @param inputData The input string to parse. * @param inputValues The array to populate with input values. From 13f95cdb5f272b8198bd499305b19b049ea60bf7 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 23 Jan 2026 15:46:51 -0800 Subject: [PATCH 5/6] docs: clarify output buffer format --- packages/cli/src/c/model.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/c/model.c b/packages/cli/src/c/model.c index d993c39b..2d4f4ef9 100644 --- a/packages/cli/src/c/model.c +++ b/packages/cli/src/c/model.c @@ -74,6 +74,13 @@ double getSaveper() { * variables. The `outputs` buffer needs to be at least as large as: * `number of output variables` * `number of save points` * + * The outputs will be stored in the same order as the outputs are defined in the + * spec file, with one "row" for each variable. For example, the first value in + * the buffer will be the output value at t0 for the first output variable, + * followed by the output value for that variable at t1, and so on. After the + * value for tN (where tN is the last time in the range), the second variable + * outputs will begin, and so on. + * * @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 @@ -104,7 +111,11 @@ void runModel(double* inputs, double* outputs) { * `number of output variables` * `number of save points` * * If `outputIndices` is NULL, outputs will be stored in the same order as the outputs - * are defined in the spec file, with one "row" for each variable. + * are defined in the spec file, with one "row" for each variable. For example, the + * first value in the buffer will be the output value at t0 for the first output + * variable, followed by the output value for that variable at t1, and so on. After + * the value for tN (where tN is the last time in the range), the second variable + * 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 From fc6ff2728d1b5197e224a61671d753b760d4f70e Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 23 Jan 2026 16:19:12 -0800 Subject: [PATCH 6/6] test: add test for running a generated model with input values --- models/prune/prune_check.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/models/prune/prune_check.sh b/models/prune/prune_check.sh index cc6359ae..bba87ddf 100755 --- a/models/prune/prune_check.sh +++ b/models/prune/prune_check.sh @@ -92,4 +92,24 @@ expect_not_present "_test_12_f" expect_not_present "_test_13_cond" expect_not_present "_test_13_f" +# Test sparse input setting in `main.c` by running the generated binary with custom +# input values: +# Input 2 (index 1) = 120.123 +# Input 1 (index 0) = 110 +# Expected: +# Input 1 and 2 Total = 230.123 +INPUT_FILE=output/prune_inputs.txt +echo "1:120.123 0:110" > "$INPUT_FILE" +OUTPUT=$(./build/prune "$INPUT_FILE") + +# Extract "Input 1 and 2 Total" (column 2) from the t=0 row (line 2) +ACTUAL=$(echo "$OUTPUT" | sed -n '2p' | cut -f2) +EXPECTED="230.123" +if [[ "$ACTUAL" != "$EXPECTED" ]]; then + echo "ERROR: Expected 'Input 1 and 2 Total' at t=0 to be '$EXPECTED' but got '$ACTUAL'" + echo "Output was:" + echo "$OUTPUT" + exit 1 +fi + echo "All validation checks passed!"