Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: initialize control parameters prior to access #304

Merged
merged 3 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 43 additions & 0 deletions packages/cli/src/c/model.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,49 @@ size_t savePointIndex = 0;

int step = 0;

void initControlParamsIfNeeded() {
static bool controlParamsInitialized = false;
if (controlParamsInitialized) {
return;
}

// Some models may define the control parameters as variables that are
// dependent on other values that are only known at runtime (after running
// the initializers and/or one step of the model), so we need to perform
// those steps once before the parameters are accessed
// TODO: This approach doesn't work if one or more control parameters are
// defined in terms of some value that is provided at runtime as an input
initConstants();
initLevels();
_time = _initial_time;
evalAux();
controlParamsInitialized = true;
}

/**
* Return the constant or computed value of `INITIAL TIME`.
*/
double getInitialTime() {
initControlParamsIfNeeded();
return _initial_time;
}

/**
* Return the constant or computed value of `FINAL TIME`.
*/
double getFinalTime() {
initControlParamsIfNeeded();
return _final_time;
}

/**
* Return the constant or computed value of `SAVEPER`.
*/
double getSaveper() {
initControlParamsIfNeeded();
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.
Expand Down
10 changes: 4 additions & 6 deletions packages/cli/src/c/sde.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,17 @@ EXTERN double _final_time;
EXTERN double _time_step;
EXTERN double _saveper;

// API (defined in model.h)
// API (defined in model.c)
double getInitialTime(void);
double getFinalTime(void);
double getSaveper(void);
char* run_model(const char* inputs);
void runModelWithBuffers(double* inputs, double* outputs);
void run(void);
void startOutput(void);
void outputVar(double value);
void finish(void);

// API (defined by the generated model)
double getInitialTime(void);
double getFinalTime(void);
double getSaveper(void);

// Functions implemented by the generated model
void initConstants(void);
void initLevels(void);
Expand Down
50 changes: 0 additions & 50 deletions packages/compile/src/generate/code-gen.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ let codeGenerator = (parseTree, opts) => {
} else if (operation === 'generateC') {
// Generate code for each variable in the proper order.
let code = emitDeclCode()
code += emitGetControlValuesCode()
code += emitInitLookupsCode()
code += emitInitConstantsCode()
code += emitInitLevelsCode()
Expand Down Expand Up @@ -80,55 +79,6 @@ ${dimensionMappingsSection()}
${section(Model.lookupVars())}
${section(Model.dataVars())}

`
}

//
// Control value getters section
//
function emitGetControlValuesCode() {
function getConstTimeValue(vensimName) {
const v = Model.varWithName(Model.cName(vensimName))
if (v && v.varType === 'const') {
return v.modelFormula
} else {
throw new Error(`SDE only supports ${vensimName} defined as a constant value`)
}
}

function getSaveperValue() {
const v = Model.varWithName('_saveper')
if (v && v.varType === 'const') {
return v.modelFormula
} else if (v && v.varType === 'aux' && v.modelFormula === 'TIME STEP') {
return getConstTimeValue('TIME STEP')
} else {
throw new Error(`SDE only supports SAVEPER defined as TIME STEP or a constant value`)
}
}

// For now we only allow models that have:
// INITIAL TIME = a constant value
// FINAL TIME = a constant value
// SAVEPER = a constant value or constant `TIME STEP`
// If the model does not meet these expectations, we throw an error.
// TODO: Loosen this up to allow for certain common constant expressions
const initialTimeValue = getConstTimeValue('INITIAL TIME')
const finalTimeValue = getConstTimeValue('FINAL TIME')
const saveperValue = getSaveperValue('SAVEPER')

return `
// Control parameter accessors
double getInitialTime() {
return ${initialTimeValue};
}
double getFinalTime() {
return ${finalTimeValue};
}
double getSaveper() {
return ${saveperValue};
}

`
}

Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions tests/integration/ext-control-params/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
baselines
sde-prep
2 changes: 2 additions & 0 deletions tests/integration/ext-control-params/ext-control-params.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
initial time,time step
2000,0.25
16 changes: 16 additions & 0 deletions tests/integration/ext-control-params/ext-control-params.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{UTF-8}

X = TIME ~~|

Y = 0
~ [-10,10,0.1]
~
|

Z = X + Y
~~|

INITIAL TIME = GET DIRECT CONSTANTS('ext-control-params.csv', ',', 'A2') ~~|
FINAL TIME = INITIAL TIME + 2 ~~|
TIME STEP = GET DIRECT CONSTANTS('ext-control-params.csv', ',', 'B2') ~~|
SAVEPER = TIME STEP * 2 ~~|
22 changes: 22 additions & 0 deletions tests/integration/ext-control-params/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "ext-control-params",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"clean": "rm -rf sde-prep",
"build": "sde bundle",
"dev": "sde dev",
"run-tests": "./run-tests.js",
"test": "run-s build run-tests",
"ci:int-test": "run-s clean test"
},
"dependencies": {
"@sdeverywhere/build": "workspace:*",
"@sdeverywhere/cli": "workspace:*",
"@sdeverywhere/plugin-wasm": "workspace:*",
"@sdeverywhere/plugin-worker": "workspace:*",
"@sdeverywhere/runtime": "workspace:*",
"@sdeverywhere/runtime-async": "workspace:*"
}
}
85 changes: 85 additions & 0 deletions tests/integration/ext-control-params/run-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env node

import { readFile } from 'fs/promises'
import { join as joinPath } from 'path'

import { createInputValue, createWasmModelRunner, initWasmModelAndBuffers } from '@sdeverywhere/runtime'
import { spawnAsyncModelRunner } from '@sdeverywhere/runtime-async'

import initWasm from './sde-prep/wasm-model.js'

/*
* This is a JS-level integration test that verifies that both the synchronous
* and asynchronous `ModelRunner` implementations work with a wasm model that
* uses a `SAVEPER` value that is not equal to 1 (defined in a separate csv file).
*/

function verify(runnerKind, outputs, inputY) {
const series = outputs.getSeriesForVar('_z')
for (let time = 2000; time <= 2002; time += 0.5) {
const actualZ = series.getValueAtTime(time)
const expectedZ = time + inputY
if (actualZ !== expectedZ) {
console.error(
`Test failed for ${runnerKind} runner at time=${time} with y=${inputY}: expected z=${expectedZ}, got z=${actualZ}`
)
process.exit(1)
}
}
}

async function runTests(runnerKind, modelRunner) {
// Create the set of inputs
const inputY = createInputValue('_y', 0)
const inputs = [inputY]

// Create the buffer to hold the outputs
let outputs = modelRunner.createOutputs()

// Run the model with input at default (0)
outputs = await modelRunner.runModel(inputs, outputs)

// Verify outputs
verify(runnerKind, outputs, 0)

// Run the model with input at 1
inputY.set(1)
outputs = await modelRunner.runModel(inputs, outputs)

// Verify outputs
verify(runnerKind, outputs, 1)

// Terminate the model runner
await modelRunner.terminate()
}

async function createSynchronousRunner() {
// TODO: This test app is using ESM-style modules, and `__dirname` is not defined
// in an ESM context. The `wasm-model.js` file (containing the embedded wasm model)
// contains a reference to `__dirname`, so we need to define it here. We should
// fix the generated `wasm-model.js` file so that it works for either ESM or CommonJS.
global.__dirname = '.'

const wasmModule = await initWasm()
const wasmResult = initWasmModelAndBuffers(wasmModule, 1, ['_z'])
return createWasmModelRunner(wasmResult)
}

async function createAsynchronousRunner() {
const modelWorkerJs = await readFile(joinPath('sde-prep', 'worker.js'), 'utf8')
return await spawnAsyncModelRunner({ source: modelWorkerJs })
}

async function main() {
// Verify with the synchronous model runner
const syncRunner = await createSynchronousRunner()
await runTests('synchronous', syncRunner)

// Verify with the asynchronous model runner
const asyncRunner = await createAsynchronousRunner()
await runTests('asynchronous', asyncRunner)

console.log('Tests passed!\n')
}

main()
32 changes: 32 additions & 0 deletions tests/integration/ext-control-params/sde.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { wasmPlugin } from '@sdeverywhere/plugin-wasm'
import { workerPlugin } from '@sdeverywhere/plugin-worker'

export async function config() {
return {
modelFiles: ['ext-control-params.mdl'],

modelSpec: async () => {
return {
inputs: [{ varName: 'Y', defaultValue: 0, minValue: -10, maxValue: 10 }],
outputs: [{ varName: 'Z' }],
datFiles: []
}
},

plugins: [
// XXX: Include a custom plugin that applies post-processing steps. This is
// a workaround for issue #303 where external data files can't be resolved.
{
postProcessMdl: (_, mdlContent) => {
return mdlContent.replaceAll('ext-control-params.csv', '../ext-control-params.csv')
}
},

// Generate a `wasm-model.js` file containing the Wasm model
wasmPlugin(),

// Generate a `worker.js` file that runs the Wasm model in a worker
workerPlugin()
]
}
}
3 changes: 2 additions & 1 deletion tests/integration/saveper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"build": "sde bundle",
"dev": "sde dev",
"run-tests": "./run-tests.js",
"test": "run-s build run-tests"
"test": "run-s build run-tests",
"ci:int-test": "run-s clean test"
},
"dependencies": {
"@sdeverywhere/build": "workspace:*",
Expand Down
3 changes: 0 additions & 3 deletions tests/integration/saveper/sde.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ export async function config() {

modelSpec: async () => {
return {
// TODO: Remove these values once they are no longer required
startTime: 2000,
endTime: 2005,
inputs: [{ varName: 'Y', defaultValue: 0, minValue: -10, maxValue: 10 }],
outputs: [{ varName: 'Z' }],
datFiles: []
Expand Down
4 changes: 2 additions & 2 deletions tests/run-js-int-tests
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
set -e # fail on error
set -x # include all commands in logs

pnpm -F saveper clean
pnpm -F saveper test
# Run "ci:int-test" script for all JS-level integration test packages
pnpm -r ci:int-test