@sdeverywhere/cli 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +14 -0
- package/package.json +41 -0
- package/src/c/macros.c +4 -0
- package/src/c/macros.h +0 -0
- package/src/c/main.c +58 -0
- package/src/c/makefile +15 -0
- package/src/c/model.c +134 -0
- package/src/c/model.h +12 -0
- package/src/c/sde.h +73 -0
- package/src/c/vensim.c +466 -0
- package/src/c/vensim.h +89 -0
- package/src/main.js +77 -0
- package/src/sde-build.js +39 -0
- package/src/sde-bundle.js +54 -0
- package/src/sde-causes.js +35 -0
- package/src/sde-clean.js +33 -0
- package/src/sde-compare.js +106 -0
- package/src/sde-compile.js +57 -0
- package/src/sde-dev.js +52 -0
- package/src/sde-exec.js +48 -0
- package/src/sde-flatten.js +195 -0
- package/src/sde-generate.js +86 -0
- package/src/sde-log.js +104 -0
- package/src/sde-names.js +41 -0
- package/src/sde-run.js +37 -0
- package/src/sde-test.js +70 -0
- package/src/sde-which.js +20 -0
- package/src/utils.js +103 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Copyright (c) 2022 Climate Interactive / New Venture Fund
|
|
2
|
+
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
import { build as runBuild } from '@sdeverywhere/build'
|
|
6
|
+
|
|
7
|
+
export let command = 'bundle [options]'
|
|
8
|
+
export let describe = 'build and bundle a model as specified by a config file'
|
|
9
|
+
export let builder = {
|
|
10
|
+
config: {
|
|
11
|
+
describe: 'path to the config file (defaults to `sde.config.js` in the current directory)',
|
|
12
|
+
type: 'string',
|
|
13
|
+
alias: 'c'
|
|
14
|
+
},
|
|
15
|
+
verbose: {
|
|
16
|
+
describe: 'enable verbose log messages',
|
|
17
|
+
type: 'boolean'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export let handler = argv => {
|
|
21
|
+
bundle(argv.config, argv.verbose)
|
|
22
|
+
}
|
|
23
|
+
export let bundle = async (configPath, verbose) => {
|
|
24
|
+
const logLevels = ['error', 'info']
|
|
25
|
+
if (verbose) {
|
|
26
|
+
logLevels.push('verbose')
|
|
27
|
+
}
|
|
28
|
+
const srcDir = new URL('.', import.meta.url).pathname
|
|
29
|
+
const sdeDir = path.resolve(srcDir, '..')
|
|
30
|
+
const sdeCmdPath = path.resolve(srcDir, 'main.js')
|
|
31
|
+
const result = await runBuild('production', {
|
|
32
|
+
config: configPath,
|
|
33
|
+
logLevels,
|
|
34
|
+
sdeDir,
|
|
35
|
+
sdeCmdPath
|
|
36
|
+
})
|
|
37
|
+
if (result.isOk()) {
|
|
38
|
+
// Exit with the specified code
|
|
39
|
+
console.log()
|
|
40
|
+
process.exit(result.exitCode)
|
|
41
|
+
} else {
|
|
42
|
+
// Exit with a non-zero code if any step failed
|
|
43
|
+
console.error(`ERROR: ${result.error.message}\n`)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default {
|
|
49
|
+
command,
|
|
50
|
+
describe,
|
|
51
|
+
builder,
|
|
52
|
+
handler,
|
|
53
|
+
bundle
|
|
54
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { generateCode, parseModel, preprocessModel } from '@sdeverywhere/compile'
|
|
2
|
+
|
|
3
|
+
import { modelPathProps, parseSpec } from './utils.js'
|
|
4
|
+
|
|
5
|
+
let command = 'causes [options] <model> <c_varname>'
|
|
6
|
+
let describe = 'print dependencies for a C variable name'
|
|
7
|
+
let builder = {
|
|
8
|
+
spec: {
|
|
9
|
+
describe: 'pathname of the I/O specification JSON file',
|
|
10
|
+
type: 'string',
|
|
11
|
+
alias: 's'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
let handler = argv => {
|
|
15
|
+
causes(argv.model, argv.c_varname, argv)
|
|
16
|
+
}
|
|
17
|
+
let causes = (model, varname, opts) => {
|
|
18
|
+
// Get the model name and directory from the model argument.
|
|
19
|
+
let { modelDirname, modelPathname } = modelPathProps(model)
|
|
20
|
+
let extData = new Map()
|
|
21
|
+
let directData = new Map()
|
|
22
|
+
let spec = parseSpec(opts.spec)
|
|
23
|
+
// Preprocess model text into parser input.
|
|
24
|
+
let input = preprocessModel(modelPathname, spec)
|
|
25
|
+
// Parse the model to get variable and subscript information.
|
|
26
|
+
let parseTree = parseModel(input)
|
|
27
|
+
let operation = 'printRefGraph'
|
|
28
|
+
generateCode(parseTree, { spec, operation, extData, directData, modelDirname, varname })
|
|
29
|
+
}
|
|
30
|
+
export default {
|
|
31
|
+
command,
|
|
32
|
+
describe,
|
|
33
|
+
builder,
|
|
34
|
+
handler
|
|
35
|
+
}
|
package/src/sde-clean.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import sh from 'shelljs'
|
|
3
|
+
|
|
4
|
+
let command = 'clean [options]'
|
|
5
|
+
let describe = 'clean out the build and output directories for a model'
|
|
6
|
+
let builder = {
|
|
7
|
+
modeldir: {
|
|
8
|
+
describe: 'model directory',
|
|
9
|
+
type: 'string',
|
|
10
|
+
alias: 'm'
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
let handler = argv => {
|
|
14
|
+
clean(argv)
|
|
15
|
+
}
|
|
16
|
+
let clean = opts => {
|
|
17
|
+
// Remove the directories generated by SDEverywhere.
|
|
18
|
+
// Default to the current directory if no model directory is given.
|
|
19
|
+
let buildDirname = path.join(opts.modeldir || '.', 'build')
|
|
20
|
+
let outputDirname = path.join(opts.modeldir || '.', 'output')
|
|
21
|
+
let silentState = sh.config.silent
|
|
22
|
+
sh.config.silent = true
|
|
23
|
+
sh.rm('-r', buildDirname)
|
|
24
|
+
sh.rm('-r', outputDirname)
|
|
25
|
+
sh.config.silent = silentState
|
|
26
|
+
}
|
|
27
|
+
export default {
|
|
28
|
+
command,
|
|
29
|
+
describe,
|
|
30
|
+
builder,
|
|
31
|
+
handler,
|
|
32
|
+
clean
|
|
33
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync } from 'fs'
|
|
2
|
+
|
|
3
|
+
import { pr } from 'bufx'
|
|
4
|
+
import R from 'ramda'
|
|
5
|
+
|
|
6
|
+
import { readDat } from '@sdeverywhere/compile'
|
|
7
|
+
|
|
8
|
+
// The epsilon value determines the required precision for value comparisons.
|
|
9
|
+
let ε = 1e-5
|
|
10
|
+
|
|
11
|
+
export let command = 'compare [options] <vensimlog> <sdelog>'
|
|
12
|
+
export let describe = 'compare Vensim and SDEverywhere log files in DAT format'
|
|
13
|
+
export let builder = {
|
|
14
|
+
precision: {
|
|
15
|
+
describe: 'precision to which values must agree (default 1e-5)',
|
|
16
|
+
type: 'number',
|
|
17
|
+
alias: 'p'
|
|
18
|
+
},
|
|
19
|
+
name: {
|
|
20
|
+
describe: 'single Vensim variable name to compare',
|
|
21
|
+
type: 'string',
|
|
22
|
+
alias: 'n'
|
|
23
|
+
},
|
|
24
|
+
times: {
|
|
25
|
+
describe: 'limit comparisons to times separated by spaces',
|
|
26
|
+
type: 'array',
|
|
27
|
+
alias: 't'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export let handler = argv => {
|
|
31
|
+
compare(argv.vensimlog, argv.sdelog, argv)
|
|
32
|
+
}
|
|
33
|
+
export let compare = async (vensimfile, sdefile, opts) => {
|
|
34
|
+
if (!existsSync(vensimfile)) {
|
|
35
|
+
throw new Error(`Vensim DAT file not found: ${vensimfile}`)
|
|
36
|
+
}
|
|
37
|
+
if (!existsSync(sdefile)) {
|
|
38
|
+
throw new Error(`SDEverywhere DAT file not found: ${sdefile}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let vensimLog = await readDat(vensimfile)
|
|
42
|
+
let sdeLog = await readDat(sdefile)
|
|
43
|
+
|
|
44
|
+
if (vensimLog.size === 0) {
|
|
45
|
+
throw new Error(`Vensim DAT file did not contain data: ${vensimfile}`)
|
|
46
|
+
}
|
|
47
|
+
if (sdeLog.size === 0) {
|
|
48
|
+
throw new Error(`SDEverywhere DAT file did not contain data: ${sdefile}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (opts.precision) {
|
|
52
|
+
ε = opts.precision
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let noDATDifference = true
|
|
56
|
+
for (let varName of vensimLog.keys()) {
|
|
57
|
+
let sdeValues = sdeLog.get(varName)
|
|
58
|
+
// Ignore variables that are not found in the SDE log file.
|
|
59
|
+
if (sdeValues && (!opts.name || varName === opts.name)) {
|
|
60
|
+
let vensimValue = undefined
|
|
61
|
+
let vensimValues = vensimLog.get(varName)
|
|
62
|
+
// Filter on time t, the key in the values list.
|
|
63
|
+
for (let t of vensimValues.keys()) {
|
|
64
|
+
if (!opts.times || R.find(time => isEqual(time, t), opts.times)) {
|
|
65
|
+
// In Vensim log files, const vars only have one value at the initial time.
|
|
66
|
+
if (vensimValues.size > 1 || !vensimValue) {
|
|
67
|
+
vensimValue = vensimValues.get(t)
|
|
68
|
+
}
|
|
69
|
+
let sdeValue = sdeValues.get(t)
|
|
70
|
+
let diff = difference(sdeValue, vensimValue)
|
|
71
|
+
if (diff > ε) {
|
|
72
|
+
let diffPct = (diff * 100).toFixed(6)
|
|
73
|
+
pr(`${varName} time=${t.toFixed(2)} vensim=${vensimValue} sde=${sdeValue} diff=${diffPct}%`)
|
|
74
|
+
noDATDifference = false
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (noDATDifference) {
|
|
81
|
+
pr(`Data were the same for ${vensimfile} and ${sdefile}`)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
let isZero = value => {
|
|
85
|
+
return Math.abs(value) < ε
|
|
86
|
+
}
|
|
87
|
+
let difference = (x, y) => {
|
|
88
|
+
let diff = 0
|
|
89
|
+
if (isZero(x) || isZero(y)) {
|
|
90
|
+
diff = Math.abs(x - y)
|
|
91
|
+
} else {
|
|
92
|
+
diff = Math.abs(1 - x / y)
|
|
93
|
+
}
|
|
94
|
+
return diff
|
|
95
|
+
}
|
|
96
|
+
let isEqual = (x, y) => {
|
|
97
|
+
return difference(x, y) < ε
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default {
|
|
101
|
+
command,
|
|
102
|
+
describe,
|
|
103
|
+
builder,
|
|
104
|
+
handler,
|
|
105
|
+
compare
|
|
106
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import sh from 'shelljs'
|
|
4
|
+
|
|
5
|
+
import { buildDir, execCmd, modelPathProps } from './utils.js'
|
|
6
|
+
|
|
7
|
+
export let command = 'compile [options] <model>'
|
|
8
|
+
export let describe = 'compile the generated model to an executable file'
|
|
9
|
+
export let builder = {
|
|
10
|
+
builddir: {
|
|
11
|
+
describe: 'build directory',
|
|
12
|
+
type: 'string',
|
|
13
|
+
alias: 'b'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export let handler = argv => {
|
|
17
|
+
compile(argv.model, argv)
|
|
18
|
+
}
|
|
19
|
+
export let compile = (model, opts) => {
|
|
20
|
+
let { modelDirname, modelName } = modelPathProps(model)
|
|
21
|
+
// Ensure the build directory exists.
|
|
22
|
+
let buildDirname = buildDir(opts.builddir, modelDirname)
|
|
23
|
+
// Link SDEverywhere C source files into the build directory.
|
|
24
|
+
linkCSourceFiles(modelDirname, buildDirname)
|
|
25
|
+
// Run make to compile the model C code.
|
|
26
|
+
let silentState = sh.config.silent
|
|
27
|
+
sh.config.silent = true
|
|
28
|
+
sh.pushd(buildDirname)
|
|
29
|
+
let exitCode = execCmd(`make P=${modelName}`)
|
|
30
|
+
sh.popd()
|
|
31
|
+
sh.config.silent = silentState
|
|
32
|
+
if (exitCode > 0) {
|
|
33
|
+
process.exit(exitCode)
|
|
34
|
+
}
|
|
35
|
+
return 0
|
|
36
|
+
}
|
|
37
|
+
export default {
|
|
38
|
+
command,
|
|
39
|
+
describe,
|
|
40
|
+
builder,
|
|
41
|
+
handler,
|
|
42
|
+
compile
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let linkCSourceFiles = (modelDirname, buildDirname) => {
|
|
46
|
+
let cDirname = path.join(new URL('.', import.meta.url).pathname, 'c')
|
|
47
|
+
sh.ls(cDirname).forEach(filename => {
|
|
48
|
+
// If a C source file is present in the model directory, link to it instead
|
|
49
|
+
// as an override.
|
|
50
|
+
let srcPathname = path.join(modelDirname, filename)
|
|
51
|
+
if (!fs.existsSync(srcPathname)) {
|
|
52
|
+
srcPathname = path.join(cDirname, filename)
|
|
53
|
+
}
|
|
54
|
+
let dstPathname = path.join(buildDirname, filename)
|
|
55
|
+
fs.ensureSymlinkSync(srcPathname, dstPathname)
|
|
56
|
+
})
|
|
57
|
+
}
|
package/src/sde-dev.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Copyright (c) 2022 Climate Interactive / New Venture Fund
|
|
2
|
+
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
import { build as runBuild } from '@sdeverywhere/build'
|
|
6
|
+
|
|
7
|
+
export let command = 'dev [options]'
|
|
8
|
+
export let describe = 'run a model in a live development environment'
|
|
9
|
+
export let builder = {
|
|
10
|
+
config: {
|
|
11
|
+
describe: 'path to the config file (defaults to `sde.config.js` in the current directory)',
|
|
12
|
+
type: 'string',
|
|
13
|
+
alias: 'c'
|
|
14
|
+
},
|
|
15
|
+
verbose: {
|
|
16
|
+
describe: 'enable verbose log messages',
|
|
17
|
+
type: 'boolean'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export let handler = argv => {
|
|
21
|
+
dev(argv.config, argv.verbose)
|
|
22
|
+
}
|
|
23
|
+
export let dev = async (configPath, verbose) => {
|
|
24
|
+
const logLevels = ['error', 'info']
|
|
25
|
+
if (verbose) {
|
|
26
|
+
logLevels.push('verbose')
|
|
27
|
+
}
|
|
28
|
+
const srcDir = new URL('.', import.meta.url).pathname
|
|
29
|
+
const sdeDir = path.resolve(srcDir, '..')
|
|
30
|
+
const sdeCmdPath = path.resolve(srcDir, 'main.js')
|
|
31
|
+
const result = await runBuild('development', {
|
|
32
|
+
config: configPath,
|
|
33
|
+
logLevels,
|
|
34
|
+
sdeDir,
|
|
35
|
+
sdeCmdPath
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// Exit with a non-zero code if any step failed, otherwise keep the
|
|
39
|
+
// builder process alive
|
|
40
|
+
if (!result.isOk()) {
|
|
41
|
+
console.error(`ERROR: ${result.error.message}\n`)
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default {
|
|
47
|
+
command,
|
|
48
|
+
describe,
|
|
49
|
+
builder,
|
|
50
|
+
handler,
|
|
51
|
+
dev
|
|
52
|
+
}
|
package/src/sde-exec.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
|
|
3
|
+
import { buildDir, execCmd, modelPathProps, outputDir } from './utils.js'
|
|
4
|
+
|
|
5
|
+
export let command = 'exec [options] <model>'
|
|
6
|
+
export let describe = 'execute the model and capture its output to a file'
|
|
7
|
+
export let builder = {
|
|
8
|
+
builddir: {
|
|
9
|
+
describe: 'build directory',
|
|
10
|
+
type: 'string',
|
|
11
|
+
alias: 'b'
|
|
12
|
+
},
|
|
13
|
+
outfile: {
|
|
14
|
+
describe: 'output pathname',
|
|
15
|
+
type: 'string',
|
|
16
|
+
alias: 'o'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export let handler = argv => {
|
|
20
|
+
exec(argv.model, argv)
|
|
21
|
+
}
|
|
22
|
+
export let exec = (model, opts) => {
|
|
23
|
+
let { modelDirname, modelName } = modelPathProps(model)
|
|
24
|
+
// Ensure the build and output directories exist.
|
|
25
|
+
let buildDirname = buildDir(opts.builddir, modelDirname)
|
|
26
|
+
let outputDirname = outputDir(opts.outfile, modelDirname)
|
|
27
|
+
// Run the model and capture output in the model directory.
|
|
28
|
+
let modelCmd = `${buildDirname}/${modelName}`
|
|
29
|
+
let outputPathname
|
|
30
|
+
if (opts.outfile) {
|
|
31
|
+
outputPathname = opts.outfile
|
|
32
|
+
} else {
|
|
33
|
+
outputPathname = path.join(outputDirname, `${modelName}.txt`)
|
|
34
|
+
}
|
|
35
|
+
let exitCode = execCmd(`${modelCmd} >${outputPathname}`)
|
|
36
|
+
if (exitCode > 0) {
|
|
37
|
+
process.exit(exitCode)
|
|
38
|
+
}
|
|
39
|
+
return 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default {
|
|
43
|
+
command,
|
|
44
|
+
describe,
|
|
45
|
+
builder,
|
|
46
|
+
handler,
|
|
47
|
+
exec
|
|
48
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
|
|
3
|
+
import B from 'bufx'
|
|
4
|
+
|
|
5
|
+
import { preprocessModel } from '@sdeverywhere/compile'
|
|
6
|
+
|
|
7
|
+
import { buildDir, modelPathProps } from './utils.js'
|
|
8
|
+
|
|
9
|
+
const command = 'flatten [options] <outmodel>'
|
|
10
|
+
|
|
11
|
+
// TODO: Set to false for now to keep it hidden from the help menu, since
|
|
12
|
+
// this is an experimental feature
|
|
13
|
+
//const describe = 'flatten submodels into a single preprocessed mdl file'
|
|
14
|
+
const describe = false
|
|
15
|
+
|
|
16
|
+
const builder = {
|
|
17
|
+
builddir: {
|
|
18
|
+
describe: 'build directory',
|
|
19
|
+
type: 'string',
|
|
20
|
+
alias: 'b'
|
|
21
|
+
},
|
|
22
|
+
inputs: {
|
|
23
|
+
describe: 'input mdl files',
|
|
24
|
+
demand: true,
|
|
25
|
+
type: 'array'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const handler = argv => {
|
|
30
|
+
flatten(argv.outmodel, argv.inputs, argv)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const flatten = async (outFile, inFiles, opts) => {
|
|
34
|
+
// Ensure the build directory exists
|
|
35
|
+
// TODO: Since the input mdl files can technically exist in more than
|
|
36
|
+
// one directory, we don't want to guess, so just use the current directory
|
|
37
|
+
// if one isn't provided on the command line
|
|
38
|
+
const buildDirname = buildDir(opts.builddir, '.')
|
|
39
|
+
|
|
40
|
+
// Extract the equations and declarations from each parent or submodel file
|
|
41
|
+
// and store them into a map keyed by source file
|
|
42
|
+
const fileDecls = {}
|
|
43
|
+
for (const inFile of inFiles) {
|
|
44
|
+
// Get the path and short name of the input mdl file
|
|
45
|
+
const inModelProps = modelPathProps(inFile)
|
|
46
|
+
|
|
47
|
+
// Preprocess the mdl file and extract the equations
|
|
48
|
+
const decls = []
|
|
49
|
+
preprocessModel(inModelProps.modelPathname, undefined, 'genc', false, decls)
|
|
50
|
+
|
|
51
|
+
// Associate each declaration with the name of the model from which it came
|
|
52
|
+
for (const decl of decls) {
|
|
53
|
+
decl.sourceModelName = inModelProps.modelName
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fileDecls[inModelProps.modelName] = decls
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Collapse so that we have only one equation or declaration per key.
|
|
60
|
+
// Equations that are defined in a different submodel will appear like
|
|
61
|
+
// a data variable in the model file where it is used (i.e., no equals
|
|
62
|
+
// sign). If we see an equation, that takes priority over a "data"
|
|
63
|
+
// declaration.
|
|
64
|
+
const collapsed = {}
|
|
65
|
+
let hasErrors = false
|
|
66
|
+
for (const modelName of Object.keys(fileDecls)) {
|
|
67
|
+
const decls = fileDecls[modelName]
|
|
68
|
+
for (const decl of decls) {
|
|
69
|
+
const existingDecl = collapsed[decl.key]
|
|
70
|
+
if (existingDecl) {
|
|
71
|
+
// We've already seen a declaration with this key (from another file)
|
|
72
|
+
if (decl.kind === existingDecl.kind) {
|
|
73
|
+
if (decl.kind === 'sub') {
|
|
74
|
+
// The subscript keys are the same, so see if the contents are the same
|
|
75
|
+
if (decl.processedDecl !== existingDecl.processedDecl) {
|
|
76
|
+
// TODO: If we see two subscript declarations that differ, it's
|
|
77
|
+
// probably because a submodel uses a simplified set of dimension
|
|
78
|
+
// mappings, while the parent includes a superset of mappings.
|
|
79
|
+
// We should add more checks here, like see if the mappings in one
|
|
80
|
+
// are a subset of the other, but for now, we will emit a warning
|
|
81
|
+
// and just take the longer of the two.
|
|
82
|
+
let longerDecl
|
|
83
|
+
if (decl.processedDecl.length > existingDecl.processedDecl.length) {
|
|
84
|
+
longerDecl = decl
|
|
85
|
+
} else {
|
|
86
|
+
longerDecl = existingDecl
|
|
87
|
+
}
|
|
88
|
+
collapsed[decl.key] = longerDecl
|
|
89
|
+
let warnMsg = 'Subscript declarations are different; '
|
|
90
|
+
warnMsg += `will use the one from '${longerDecl.sourceModelName}.mdl' because it is longer`
|
|
91
|
+
console.warn('----------')
|
|
92
|
+
console.warn(`\nWARNING: ${warnMsg}:`)
|
|
93
|
+
console.warn(`\nIn ${existingDecl.sourceModelName}.mdl:`)
|
|
94
|
+
console.warn(`${existingDecl.processedDecl}`)
|
|
95
|
+
console.warn(`\nIn ${decl.sourceModelName}.mdl:`)
|
|
96
|
+
console.warn(`${decl.processedDecl}\n`)
|
|
97
|
+
}
|
|
98
|
+
} else if (decl.kind === 'eqn') {
|
|
99
|
+
// The equation keys are the same, so see if the contents are the same
|
|
100
|
+
if (decl.processedDecl !== existingDecl.processedDecl) {
|
|
101
|
+
hasErrors = true
|
|
102
|
+
console.error('----------')
|
|
103
|
+
console.error(`\nERROR: Differing equations:`)
|
|
104
|
+
console.error(`\nIn ${existingDecl.sourceModelName}.mdl:`)
|
|
105
|
+
console.error(`${existingDecl.processedDecl}`)
|
|
106
|
+
console.error(`\nIn ${decl.sourceModelName}.mdl:`)
|
|
107
|
+
console.error(`${decl.processedDecl}\n`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} else if (decl.kind === 'eqn' && existingDecl.kind === 'decl') {
|
|
111
|
+
// Replace the declaration with this equation
|
|
112
|
+
collapsed[decl.key] = decl
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
// We haven't seen this key yet, so save the declaration
|
|
116
|
+
collapsed[decl.key] = decl
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// XXX: For any subscripted (non-equation) declarations that remain, see if
|
|
122
|
+
// there are any corresponding equations. This can happen in the case of
|
|
123
|
+
// subscripted variables that are defined in multiple parts in one submodel,
|
|
124
|
+
// but are declared using the full set in the consuming model.
|
|
125
|
+
// For example, in the submodel:
|
|
126
|
+
// Variable[SubscriptA_Subset1] = ... ~~|
|
|
127
|
+
// Variable[SubscriptA_Subset2] = ... ~~|
|
|
128
|
+
// In the consuming model:
|
|
129
|
+
// Variable[SubscriptA_All] ~~|
|
|
130
|
+
// In this case, if we see Variable[SubscriptA_All], we can remove it and
|
|
131
|
+
// use the other `Variable` definitions.
|
|
132
|
+
for (const key of Object.keys(collapsed)) {
|
|
133
|
+
const decl = collapsed[key]
|
|
134
|
+
if (decl.kind === 'decl') {
|
|
135
|
+
const declKeyWithoutSubscript = key.split('[')[0]
|
|
136
|
+
|
|
137
|
+
// See if there is another equation that has the same key (minus subscript)
|
|
138
|
+
const otherDeclsThatMatch = []
|
|
139
|
+
for (const otherDecl of Object.values(collapsed)) {
|
|
140
|
+
if (otherDecl.kind === 'eqn') {
|
|
141
|
+
const otherDeclKeyWithoutSubscript = otherDecl.key.split('[')[0]
|
|
142
|
+
if (declKeyWithoutSubscript === otherDeclKeyWithoutSubscript) {
|
|
143
|
+
otherDeclsThatMatch.push(otherDecl)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (otherDeclsThatMatch.length > 0) {
|
|
149
|
+
// Remove this declaration and emit a warning
|
|
150
|
+
delete collapsed[key]
|
|
151
|
+
console.warn('----------')
|
|
152
|
+
console.warn(`\nWARNING: Skipping declaration:`)
|
|
153
|
+
console.warn(`\nIn ${decl.sourceModelName}.mdl:`)
|
|
154
|
+
console.warn(`${decl.processedDecl}`)
|
|
155
|
+
console.warn(`\nThe following equations are assumed to provide the necessary data:`)
|
|
156
|
+
for (const otherDecl of otherDeclsThatMatch) {
|
|
157
|
+
console.warn(`\nIn ${otherDecl.sourceModelName}.mdl:`)
|
|
158
|
+
console.warn(`${otherDecl.processedDecl}`)
|
|
159
|
+
}
|
|
160
|
+
console.warn()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Exit with a non-zero error code if there are any conflicting declarations
|
|
166
|
+
if (hasErrors) {
|
|
167
|
+
process.exit(1)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Sort the declarations alphabetically by LHS variable name
|
|
171
|
+
const sorted = Object.values(collapsed).sort((a, b) => {
|
|
172
|
+
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// Build a single buffer containing the sorted declarations
|
|
176
|
+
B.open('pp')
|
|
177
|
+
const ENCODING = '{UTF-8}'
|
|
178
|
+
B.emitLine(ENCODING, 'pp')
|
|
179
|
+
B.emitLine('', 'pp')
|
|
180
|
+
for (const decl of sorted) {
|
|
181
|
+
B.emitLine(`${decl.processedDecl}\n`, 'pp')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Write the flattened mdl file to the build directory
|
|
185
|
+
const outModelProps = modelPathProps(outFile)
|
|
186
|
+
let outputPathname = path.join(buildDirname, `${outModelProps.modelName}.mdl`)
|
|
187
|
+
B.writeBuf(outputPathname, 'pp')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default {
|
|
191
|
+
command,
|
|
192
|
+
describe,
|
|
193
|
+
builder,
|
|
194
|
+
handler
|
|
195
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import B from 'bufx'
|
|
3
|
+
|
|
4
|
+
import { parseAndGenerate, preprocessModel } from '@sdeverywhere/compile'
|
|
5
|
+
|
|
6
|
+
import { buildDir, modelPathProps, parseSpec } from './utils.js'
|
|
7
|
+
|
|
8
|
+
export let command = 'generate [options] <model>'
|
|
9
|
+
export let describe = 'generate model code'
|
|
10
|
+
export let builder = {
|
|
11
|
+
genc: {
|
|
12
|
+
describe: 'generate C code for the model',
|
|
13
|
+
type: 'boolean'
|
|
14
|
+
},
|
|
15
|
+
list: {
|
|
16
|
+
describe: 'list model variables',
|
|
17
|
+
type: 'boolean',
|
|
18
|
+
alias: 'l'
|
|
19
|
+
},
|
|
20
|
+
preprocess: {
|
|
21
|
+
describe: 'write a preprocessed model that runs in Vensim',
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
alias: 'p'
|
|
24
|
+
},
|
|
25
|
+
analysis: {
|
|
26
|
+
describe: 'write a nonexecutable preprocessed model for analysis',
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
alias: 'a'
|
|
29
|
+
},
|
|
30
|
+
spec: {
|
|
31
|
+
describe: 'pathname of the I/O specification JSON file',
|
|
32
|
+
type: 'string',
|
|
33
|
+
alias: 's'
|
|
34
|
+
},
|
|
35
|
+
builddir: {
|
|
36
|
+
describe: 'build directory',
|
|
37
|
+
type: 'string',
|
|
38
|
+
alias: 'b'
|
|
39
|
+
},
|
|
40
|
+
refidtest: {
|
|
41
|
+
describe: 'test reference ids',
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
alias: 'r'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export let handler = argv => {
|
|
47
|
+
generate(argv.model, argv)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export let generate = async (model, opts) => {
|
|
51
|
+
// Get the model name and directory from the model argument.
|
|
52
|
+
let { modelDirname, modelName, modelPathname } = modelPathProps(model)
|
|
53
|
+
// Ensure the build directory exists.
|
|
54
|
+
let buildDirname = buildDir(opts.builddir, modelDirname)
|
|
55
|
+
// Preprocess model text into parser input. Stop now if that's all we're doing.
|
|
56
|
+
let spec = parseSpec(opts.spec)
|
|
57
|
+
// Produce a runnable model with the "genc" and "preprocess" options.
|
|
58
|
+
let profile = opts.analysis ? 'analysis' : 'genc'
|
|
59
|
+
// Write the preprocessed model and removals if the option is "analysis" or "preprocess".
|
|
60
|
+
let writeFiles = opts.analysis || opts.preprocess
|
|
61
|
+
let input = preprocessModel(modelPathname, spec, profile, writeFiles)
|
|
62
|
+
if (writeFiles) {
|
|
63
|
+
let outputPathname = path.join(buildDirname, `${modelName}.mdl`)
|
|
64
|
+
B.write(input, outputPathname)
|
|
65
|
+
process.exit(0)
|
|
66
|
+
}
|
|
67
|
+
// Parse the model and generate code. If no operation is specified, the code generator will
|
|
68
|
+
// read the model and do nothing else. This is required for the list operation.
|
|
69
|
+
let operation = ''
|
|
70
|
+
if (opts.genc) {
|
|
71
|
+
operation = 'generateC'
|
|
72
|
+
} else if (opts.list) {
|
|
73
|
+
operation = 'printVarList'
|
|
74
|
+
} else if (opts.refidtest) {
|
|
75
|
+
operation = 'printRefIdTest'
|
|
76
|
+
}
|
|
77
|
+
await parseAndGenerate(input, spec, operation, modelDirname, modelName, buildDirname)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default {
|
|
81
|
+
command,
|
|
82
|
+
describe,
|
|
83
|
+
builder,
|
|
84
|
+
handler,
|
|
85
|
+
generate
|
|
86
|
+
}
|