Skip to content
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
8 changes: 7 additions & 1 deletion packages/compile/src/model/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -1023,7 +1023,13 @@ function allListedVars() {
vars.push(...constVars())
vars.push(...lookupVars())
vars.push(...dataVars())
vars.push(varWithName('_time'))
// The special exogenous `Time` variable may have already been removed by
// `removeUnusedVariables` if it is not referenced explicitly in the model,
// so we will only include it in the listing if it is defined here
const timeVar = varWithName('_time')
if (timeVar) {
vars.push(timeVar)
}
vars.push(...initVars())
vars.push(...auxVars())
// TODO: Also levelVars not covered by initVars?
Expand Down
21 changes: 21 additions & 0 deletions pnpm-lock.yaml

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{UTF-8}

DimA: A1, A2 ~~|
DimB: B1, B2, B3 ~~|

T = 10
~ years
~ This is an internal/impl variable.
|

X = 0
~ dmnl [-10,10,0.1]
~ This is an input variable.
|

Y = X * 3
~ dmnl
~ This is an internal/impl variable.
|

Z = T + Y
~ dmnl
~ This is an output variable.
|

A[DimA] = 1, 2
~ dmnl
~ This is a subscripted internal/impl variable.
|

B[DimB] = 100, 200, 300
~ dmnl
~ This is a subscripted internal/impl variable.
|

C[DimA, DimB] = A[DimA] + B[DimB]
~ dmnl
~ This is a subscripted internal/impl variable.
|

D[DimA] = X + SUM(C[DimA, DimB!])
~ dmnl
~ This is an output variable.
|

INITIAL TIME = 2000 ~~|
FINAL TIME = 2002 ~~|
TIME STEP = 1 ~~|
SAVEPER = TIME STEP ~~|
22 changes: 22 additions & 0 deletions tests/integration/impl-var-access-no-time/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "impl-var-access-no-time",
"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:*"
}
}
126 changes: 126 additions & 0 deletions tests/integration/impl-var-access-no-time/run-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env node

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

import { createInputValue, createWasmModelRunner, initWasmModelAndBuffers, ModelListing } 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 is largely the same as the `impl-var-access`
* test, except that this one does not explicitly reference the exogenous `Time` variable.
* This helps verify that the JSON listing can be created without errors when `Time` is
* not explicitly referenced.
*/

function verify(runnerKind, outputs, inputX, varId, checkValue) {
const varName = varId.replaceAll('_', '')
const series = outputs.getSeriesForVar(varId)
for (let time = 2000; time <= 2002; time++) {
const actual = series.getValueAtTime(time)
const expected = checkValue(time, inputX)
if (actual !== expected) {
console.error(
`Test failed for ${runnerKind} runner at time=${time} with x=${inputX}: expected ${varName}=${expected}, got ${varName}=${actual}`
)
process.exit(1)
}
}
}

function verifyDeclaredOutputs(runnerKind, outputs, inputX) {
// T = 10
// X = 0 [-10,10]
// Y = X * 3
// Z = T + Y
verify(runnerKind, outputs, inputX, '_z', (_, inputX) => 10 + inputX * 3)

// A[DimA] = 1, 2
// B[DimB] = 100, 200, 300
// C[DimA, DimB] = A[DimA] + B[DimB]
// D[DimA] = X + SUM(C[DimA, DimB!])
// D[A1] = X + (A[A1] + B[B1]) + (A[A1] + B[B2]) + (A[A1] + B[B3])
verify(runnerKind, outputs, inputX, '_d[_a1]', (_, inputX) => inputX + 1 + 100 + 1 + 200 + 1 + 300)
}

function verifyImplOutputs(runnerKind, outputs, inputX) {
// Y = X * 3
verify(runnerKind, outputs, inputX, '_y', () => inputX * 3)

// A[DimA] = 1, 2
// B[DimB] = 100, 200, 300
// C[DimA, DimB] = A[DimA] + B[DimB]
verify(runnerKind, outputs, inputX, '_c[_a2,_b2]', () => 2 + 200)
verify(runnerKind, outputs, inputX, '_c[_a2,_b3]', () => 2 + 300)
}

async function runTests(runnerKind, modelRunner) {
// Read the JSON model listing
const listingJson = await readFile(joinPath('sde-prep', 'build', 'processed.json'), 'utf8')
const listing = new ModelListing(listingJson)

// Create the set of inputs
const inputX = createInputValue('_x', 0)
const inputs = [inputX]

// 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 declared output variables
verifyDeclaredOutputs(runnerKind, outputs, 0)

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

// Verify declared output variables
verifyDeclaredOutputs(runnerKind, outputs, 1)

// Run the model again and capture impl variables
let outputsWithImpls1 = listing.deriveOutputs(outputs, ['_y', '_c[_a2,_b2]', '_c[_a2,_b3]'])
outputsWithImpls1 = await modelRunner.runModel(inputs, outputsWithImpls1)

// Verify impl variables
verifyImplOutputs(runnerKind, outputsWithImpls1, 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', '_d[_a1]'])
return createWasmModelRunner(wasmResult)
}

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

async function main() {
// TODO: Verify JSON

// 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()
33 changes: 33 additions & 0 deletions tests/integration/impl-var-access-no-time/sde.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { wasmPlugin } from '@sdeverywhere/plugin-wasm'
import { workerPlugin } from '@sdeverywhere/plugin-worker'

export async function config() {
return {
modelFiles: ['impl-var-access-no-time.mdl'],

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

plugins: [
// Include a custom plugin that applies post-processing steps
{
postGenerateC: (_, cContent) => {
// Edit the generated C code so that it enables the `SDE_USE_OUTPUT_INDICES` flag; this is
// required in order to access impl (non-exported) model variables
return cContent.replace('#define SDE_USE_OUTPUT_INDICES 0', '#define SDE_USE_OUTPUT_INDICES 1')
}
},

// 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()
]
}
}