Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
fed74ce
feat: add initial XMILE parsing support
chrispcampbell Nov 24, 2023
0ab29b4
fix: update XMILE parser to account for SubscriptRange -> DimensionDe…
chrispcampbell Nov 25, 2023
180e87a
fix: checkpoint work on parsing gf elements
chrispcampbell Nov 27, 2023
5182e02
fix: flesh out parsing of gf elements
chrispcampbell Apr 25, 2024
2c21f75
fix: expose xmile parsing functions in index
chrispcampbell Apr 25, 2024
6436b97
Merge branch 'main' into chris/468-xmile
chrispcampbell Aug 16, 2025
b0343a1
fix: update XMILE code in parse package to use new canonicalId function
chrispcampbell Aug 16, 2025
89b98d2
fix: add initial support for parsing XMILE models in compile package
chrispcampbell Aug 16, 2025
6916d5f
Merge branch 'main' into chris/468-xmile
chrispcampbell Aug 18, 2025
d7c15c0
test: add initial support for testing stmx files
chrispcampbell Aug 18, 2025
22cb090
fix: update cli package to accept stmx and xmile files + infer model …
chrispcampbell Aug 18, 2025
a56e839
test: add helper function for generating XMILE content
chrispcampbell Aug 18, 2025
bff3837
test: create separate files for vensim and xmile paths for read-equat…
chrispcampbell Aug 18, 2025
a56cb1a
fix: add temporary workaround for XMILE dimension wildcard in parse p…
chrispcampbell Aug 18, 2025
feefb1f
test: restore comment in test
chrispcampbell Aug 19, 2025
7ffe1f6
fix: update compile package to handle XMILE wildcards
chrispcampbell Aug 19, 2025
90c2051
test: update test expectations to account for differences in arrays.s…
chrispcampbell Aug 19, 2025
b42ab63
test: add separate tests for non-apply-to-all with separate defs and …
chrispcampbell Aug 19, 2025
48532e3
fix: correct handling of wildcards to work in the case where referenc…
chrispcampbell Aug 19, 2025
8ef4605
test: convert a batch of readEquations tests to verify XMILE
chrispcampbell Aug 19, 2025
2032ae5
test: convert a batch of readEquations tests for XMILE
chrispcampbell Aug 20, 2025
8227ded
test: convert the last batch and remove all remaining tests that read…
chrispcampbell Aug 20, 2025
3a26509
fix: update readEquations so that it handles Vensim and Stella functi…
chrispcampbell Aug 22, 2025
7748cb9
fix: convert XMILE logical operators to Vensim syntax before parsing
chrispcampbell Aug 23, 2025
3f797c3
docs: fix comment
chrispcampbell Aug 23, 2025
2c8ca56
fix: restore handling of INTEG function calls and update tests
chrispcampbell Aug 23, 2025
c7d9744
fix: include itmx as one of the supported xmile file extensions
chrispcampbell Aug 23, 2025
4f384bd
fix: update XMILE parser to ignore non_negative elements in stocks
chrispcampbell Aug 23, 2025
0033fca
fix: improve error message when parsing fails for XMILE variable defi…
chrispcampbell Aug 23, 2025
fe73b21
fix: include line number from original XML input in XMILE parsing errors
chrispcampbell Aug 23, 2025
1be73de
fix: include the element from the original XML in the error message
chrispcampbell Aug 23, 2025
9576ce6
test: update test for parse error to not rely on IF THEN ELSE
chrispcampbell Aug 23, 2025
44547b9
fix: handle nested IF THEN ELSE
chrispcampbell Aug 23, 2025
99cc6de
fix: update XMILE parser to ignore non_negative elements in flows
chrispcampbell Aug 23, 2025
6c92510
test: update test comments
chrispcampbell Aug 23, 2025
0bc1444
fix: update XMILE stock parser to handle multiple inflows and/or outf…
chrispcampbell Aug 23, 2025
434366c
fix: include STEP as supported XMILE function
chrispcampbell Aug 23, 2025
37704f3
fix: replace newline sequences in variable names with spaces
chrispcampbell Aug 23, 2025
d4c7f7d
fix: parse sim_specs parameters and include in Model AST type
chrispcampbell Aug 23, 2025
2097ccc
fix: remove debug logging
chrispcampbell Aug 23, 2025
f252338
fix: synthesize variables for XMILE control parameters
chrispcampbell Aug 23, 2025
c349a04
fix: expose parseInlineXmileModel in compile package
chrispcampbell Aug 24, 2025
d6808b6
Merge branch 'main' into chris/468-xmile
chrispcampbell Aug 31, 2025
c5d897c
build: update lock file
chrispcampbell Aug 31, 2025
e103751
test: ignore SAVEPER in XMILE tests
chrispcampbell Aug 31, 2025
c8dd5fd
test: implement SMTH{1,3} tests
chrispcampbell Aug 31, 2025
90d523e
docs: update comment
chrispcampbell Aug 31, 2025
5d45392
fix: update create package to include model kind in parseAndGenerate …
chrispcampbell Aug 31, 2025
8d29605
test: convert the remaining SMOOTH3[I] tests
chrispcampbell Sep 1, 2025
c3ba489
test: skip more read-subscripts-xmile tests for now
chrispcampbell Sep 2, 2025
51d2b44
test: append "-from-vensim" to existing code gen test file names
chrispcampbell Sep 5, 2025
2cb691a
test: copy gen-equation-js-from-vensim in preparation for adding "fro…
chrispcampbell Sep 5, 2025
5f574b0
test: convert the first few code gen tests to XMILE syntax
chrispcampbell Sep 5, 2025
4106b82
test: convert arithmetic tests
chrispcampbell Sep 5, 2025
ffbc7be
test: convert conditional expression and constant definition tests
chrispcampbell Sep 5, 2025
425552f
test: remove tests for subscripted variables that are covered in othe…
chrispcampbell Sep 5, 2025
a0a0c04
test: remove tests for functions not supported in XMILE/Stella
chrispcampbell Sep 5, 2025
2968846
test: convert tests for subscripted equations
chrispcampbell Sep 5, 2025
b65e528
test: convert tests for supported XMILE/Stella functions
chrispcampbell Sep 5, 2025
7d00640
fix: update generateFunctionCall to handle differences between Vensim…
chrispcampbell Sep 5, 2025
f7bd6b5
fix: update IF THEN ELSE parsing to handle case where it is nested in…
chrispcampbell Sep 5, 2025
bd8cb56
fix: improve indentation in xmile function
chrispcampbell Sep 5, 2025
cf8f063
fix: update create package to support XMILE file types + change insta…
chrispcampbell Sep 7, 2025
f24f43f
test: update step-config tests to verify handling of XMILE files in a…
chrispcampbell Sep 7, 2025
29b37f6
Merge branch 'main' into chris/468-xmile
chrispcampbell Jan 16, 2026
47be15a
build: update lock file
chrispcampbell Jan 16, 2026
0da1d4f
fix: add tooldat option for `sde test` command
chrispcampbell Jan 16, 2026
af199ba
test: update modeltests script to convert Stella-generated CSV to DAT…
chrispcampbell Jan 16, 2026
7e4aa61
test: disable lint warnings for use of any in tests
chrispcampbell Jan 16, 2026
f28013f
fix: use canonicalVarId instead of canonicalId (which doesn't handle …
chrispcampbell Jan 16, 2026
940d069
feat: add SAFEDIV function support for XMILE models
chrispcampbell Jan 17, 2026
631cb38
feat: add DELAY function support for XMILE models
chrispcampbell Jan 17, 2026
5099835
feat: add SIZE function support for XMILE models
chrispcampbell Jan 17, 2026
ef19899
feat: add DEPRECIATE_STRAIGHTLINE and ACTIVE_INITIAL support for XMIL…
chrispcampbell Jan 18, 2026
e325d66
docs: update comments
chrispcampbell Feb 10, 2026
be3038d
Merge branch 'main' into chris/468-xmile
chrispcampbell Feb 10, 2026
34bf1d9
docs: update comment
chrispcampbell Feb 10, 2026
653bb3f
docs: update PLAN.md to account for latest test fixes
chrispcampbell Feb 10, 2026
546d07f
test: skip two test models that are not yet supported by XMILE backend
chrispcampbell Feb 10, 2026
034ed4d
fix: prettier
chrispcampbell Feb 10, 2026
ff27a52
fix: update copyright years in parse package
chrispcampbell Feb 10, 2026
dc97cca
docs: remove PLAN.md (I added this to an issue comment for the record)
chrispcampbell Feb 13, 2026
8b169f7
Merge branch 'main' into chris/468-xmile
chrispcampbell Mar 9, 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@vitest/browser": "^4.0.17",
"@vitest/browser-playwright": "^4.0.17",
"@vitest/coverage-v8": "^4.0.17",
"csv-parse": "^5.3.3",
"eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-eslint-comments": "^3.2.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/sde-causes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ let handler = argv => {
}
let causes = async (model, varname, opts) => {
// Get the model name and directory from the model argument.
let { modelDirname, modelPathname, modelName } = modelPathProps(model)
let { modelDirname, modelPathname, modelName, modelKind } = modelPathProps(model)
let spec = parseSpec(opts.spec)
// Parse the model to get variable and subscript information.
let input = readFileSync(modelPathname, 'utf8')
await parseAndGenerate(input, spec, ['printRefGraph'], modelDirname, modelName, '', varname)
await parseAndGenerate(input, modelKind, spec, ['printRefGraph'], modelDirname, modelName, '', varname)
}
export default {
command,
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/sde-generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export let handler = async argv => {

export let generate = async (model, opts) => {
// Get the model name and directory from the model argument.
let { modelDirname, modelName, modelPathname } = modelPathProps(model)
let { modelDirname, modelName, modelPathname, modelKind } = modelPathProps(model)
// Ensure the build directory exists.
let buildDirname = buildDir(opts.builddir, modelDirname)
let spec = parseSpec(opts.spec)
Expand Down Expand Up @@ -87,7 +87,7 @@ export let generate = async (model, opts) => {
if (opts.refidtest) {
operations.push('printRefIdTest')
}
await parseAndGenerate(mdlContent, spec, operations, modelDirname, modelName, buildDirname)
await parseAndGenerate(mdlContent, modelKind, spec, operations, modelDirname, modelName, buildDirname)
}

export default {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/sde-names.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ let handler = argv => {
}
let names = async (model, namesPathname, opts) => {
// Get the model name and directory from the model argument.
let { modelDirname, modelPathname, modelName } = modelPathProps(model)
let { modelDirname, modelPathname, modelName, modelKind } = modelPathProps(model)
let spec = parseSpec(opts.spec)
// Parse the model to get variable and subscript information.
let input = readFileSync(modelPathname, 'utf8')
await parseAndGenerate(input, spec, ['convertNames'], modelDirname, modelName, '')
await parseAndGenerate(input, modelKind, spec, ['convertNames'], modelDirname, modelName, '')
// Read each variable name from the names file and convert it.
printNames(namesPathname, opts.toc ? 'to-c' : 'to-vensim')
}
Expand Down
21 changes: 16 additions & 5 deletions packages/cli/src/sde-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export let builder = {
choices: ['js', 'c'],
default: 'js'
},
tooldat: {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this option for sde test so that we can pass a custom name for the dat file. Before this, it assumed that the file is called {model}.dat.

I could've also updated sde test to support reading csv files in addition to dat, but I didn't want to go deep on something that's probably not used much outside of our own testing framework. So instead I just added some code to our test scripts to convert the csv files (for expected Stella results) to dat format as part of the testing process.

describe: 'pathname of the tool DAT file to compare to SDE output',
type: 'string'
},
builddir: {
describe: 'build directory',
type: 'string',
Expand Down Expand Up @@ -53,12 +57,19 @@ export let test = async (model, opts) => {
// Convert the TSV log file to a DAT file in the same directory.
opts.dat = true
await log(logPathname, opts)
// Assume there is a Vensim-created DAT file named {modelName}.dat in the model directory.
// Compare it to the SDE DAT file.
let vensimPathname = path.join(modelDirname, `${modelName}.dat`)
let toolDatPathname
if (opts.tooldat) {
// Use the provided DAT file for comparison
toolDatPathname = opts.tooldat
} else {
// Assume there is a DAT file created by the modeling tool named {modelName}.dat
// in the model directory
toolDatPathname = path.join(modelDirname, `${modelName}.dat`)
}
let p = path.parse(logPathname)
let sdePathname = path.format({ dir: p.dir, name: p.name, ext: '.dat' })
let noDiffs = await compare(vensimPathname, sdePathname, opts)
let sdeDatPathname = path.format({ dir: p.dir, name: p.name, ext: '.dat' })
// Compare SDE-generated DAT file to the tool-generated DAT file
let noDiffs = await compare(toolDatPathname, sdeDatPathname, opts)
if (!noDiffs) {
// Exit with a non-zero error code if differences were detected
console.error()
Expand Down
26 changes: 23 additions & 3 deletions packages/cli/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,45 @@ export function execCmd(cmd) {
}

/**
* Normalize a model pathname that may or may not include the .mdl extension.
* Normalize a model pathname that may or may not include the .mdl or .xmile/.stmx/.itmx extension.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sde CLI commands previously automatically found the corresponding mdl file for a given model name. Now we do the same thing for xmile/stmx/itmx files. If there are multiple options, it throws an error and the user can specify which one by providing a full file name with extension.

* If the pathname does not end with .mdl, .xmile, .stmx, or .itmx, this will attempt to find a
* file with one of those extensions.
* If there is not a path in the model argument, default to the current working directory.
*
* Return an object with properties that look like this:
* modelDirname: '/Users/todd/src/models/arrays'
* modelName: 'arrays'
* modelPathname: '/Users/todd/src/models/arrays/arrays.mdl'
* modelKind: 'vensim'
*
* @param model A path to a Vensim model file.
* @return An object with the properties specified above.
*/
export function modelPathProps(model) {
let p = R.merge({ ext: '.mdl' }, R.pick(['dir', 'name'], path.parse(model)))
const parsedPath = path.parse(model)
if (parsedPath.ext === '') {
const exts = ['.mdl', '.xmile', '.stmx', '.itmx']
const paths = exts.map(ext => path.join(parsedPath.dir, parsedPath.name + ext))
const existingPaths = paths.filter(path => fs.existsSync(path))
if (existingPaths.length > 1) {
throw new Error(
`Found multiple files that match '${model}'; please specify a file with a .mdl, .xmile, .stmx, or .itmx extension`
)
}
if (existingPaths.length === 0) {
throw new Error(`No {mdl,xmile,stmx,itmx} file found for ${model}`)
}
parsedPath.ext = path.extname(existingPaths[0])
}
let p = R.merge({ ext: parsedPath.ext }, R.pick(['dir', 'name'], parsedPath))
if (R.isEmpty(p.dir)) {
p.dir = process.cwd()
}
return {
modelDirname: p.dir,
modelName: p.name,
modelPathname: path.format(p)
modelPathname: path.format(p),
modelKind: p.ext === '.mdl' ? 'vensim' : 'xmile'
}
}

Expand Down
62 changes: 60 additions & 2 deletions packages/compile/src/_tests/test-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,69 @@ export function parseVensimModel(modelName: string): ParsedModel {
const modelDir = sampleModelDir(modelName)
const modelFile = resolve(modelDir, `${modelName}.mdl`)
const mdlContent = readFileSync(modelFile, 'utf8')
return parseModel(mdlContent, modelDir)
return parseModel(mdlContent, 'vensim', modelDir)
}

export function parseInlineVensimModel(mdlContent: string, modelDir?: string): ParsedModel {
return parseModel(mdlContent, modelDir)
return parseModel(mdlContent, 'vensim', modelDir)
}

export function parseXmileModel(modelName: string): ParsedModel {
const modelDir = sampleModelDir(modelName)
const modelFile = resolve(modelDir, `${modelName}.stmx`)
const mdlContent = readFileSync(modelFile, 'utf8')
return parseModel(mdlContent, 'xmile', modelDir)
}

export function parseInlineXmileModel(mdlContent: string, modelDir?: string): ParsedModel {
return parseModel(mdlContent, 'xmile', modelDir)
}

export function xmile(dimensions: string, variables: string): string {
function indent(text: string, indent: string): string {
return text
.split('\n')
.map(line => indent + line)
.join('\n')
}

let dims: string
if (dimensions.length > 0) {
dims = `\
<dimensions>
${indent(dimensions, ' ')}
</dimensions>`
} else {
dims = ''
}

let vars: string
if (variables.length > 0) {
vars = `\
<variables>
${indent(variables, ' ')}
</variables>`
} else {
vars = ''
}

return `\
<xmile xmlns="http://docs.oasis-open.org/xmile/ns/XMILE/v1.0" version="1.0">
<header>
<options namespace="std"/>
<vendor>Ventana Systems, xmutil</vendor>
<product lang="en">Vensim, xmutil</product>
</header>
<sim_specs isee:simulation_delay="0" method="Euler" time_units="Months">
<start>0</start>
<stop>100</stop>
<dt>1</dt>
</sim_specs>
${dims}
<model>
${vars}
</model>
</xmile>`
}

function prettyVar(variable: Variable): string {
Expand Down
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing code gen tests were written using Vensim format as the input model format, but we need new tests that cover similar things except for XMILE format (since the code gen can be different in certain cases), so I renamed these files accordingly.

File renamed without changes.
Loading