@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
package/src/sde-log.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
import B from 'bufx'
|
|
5
|
+
import byline from 'byline'
|
|
6
|
+
import R from 'ramda'
|
|
7
|
+
|
|
8
|
+
import { canonicalName } from '@sdeverywhere/compile'
|
|
9
|
+
|
|
10
|
+
export let command = 'log [options] <logfile>'
|
|
11
|
+
export let describe = 'process an SDEverywhere log file'
|
|
12
|
+
export let builder = {
|
|
13
|
+
dat: {
|
|
14
|
+
describe: 'convert a TSV log file to a Vensim DAT file',
|
|
15
|
+
type: 'boolean',
|
|
16
|
+
alias: 'd'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export let handler = argv => {
|
|
20
|
+
log(argv.logfile, argv)
|
|
21
|
+
}
|
|
22
|
+
export let log = async (logPathname, opts) => {
|
|
23
|
+
if (opts.dat) {
|
|
24
|
+
let p = path.parse(logPathname)
|
|
25
|
+
let datPathname = path.format({ dir: p.dir, name: p.name, ext: '.dat' })
|
|
26
|
+
await exportDat(logPathname, datPathname)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
let exportDat = async (logPathname, datPathname) => {
|
|
30
|
+
// Vensim var names found in the log file
|
|
31
|
+
let varNames = {}
|
|
32
|
+
// Vensim var names in canonical format serving as data keys
|
|
33
|
+
let varKeys = []
|
|
34
|
+
// Values at each time step
|
|
35
|
+
let steps = []
|
|
36
|
+
|
|
37
|
+
let readLog = async () => {
|
|
38
|
+
return new Promise(resolve => {
|
|
39
|
+
// Accumulate var values at each time step.
|
|
40
|
+
let stream = byline(fs.createReadStream(logPathname, 'utf8'))
|
|
41
|
+
stream.on('data', line => {
|
|
42
|
+
if (R.isEmpty(varNames)) {
|
|
43
|
+
// Turn the var names in the header line into acceptable keys in canonical format.
|
|
44
|
+
let header = line.split('\t')
|
|
45
|
+
varKeys = R.map(v => canonicalName(v), header)
|
|
46
|
+
varNames = R.zipObj(varKeys, header)
|
|
47
|
+
} else if (!R.isEmpty(line)) {
|
|
48
|
+
// Add an object with values at this time step.
|
|
49
|
+
steps.push(R.zipObj(varKeys, line.split('\t')))
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
stream.on('end', () => {
|
|
53
|
+
resolve()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
let writeDat = async () => {
|
|
58
|
+
// Emit all time step values for each log var as (time, value) pairs.
|
|
59
|
+
let stream = fs.createWriteStream(datPathname, 'utf8')
|
|
60
|
+
let iVarKey = 0
|
|
61
|
+
let finished = () => {}
|
|
62
|
+
stream.on('finish', () => {
|
|
63
|
+
// Resolve the promise after `stream.end()` has been called and all data has been flushed
|
|
64
|
+
// to the output file
|
|
65
|
+
finished()
|
|
66
|
+
})
|
|
67
|
+
let writeContinuation = () => {
|
|
68
|
+
while (iVarKey < varKeys.length) {
|
|
69
|
+
let varKey = varKeys[iVarKey++]
|
|
70
|
+
if (varKey !== '_time') {
|
|
71
|
+
B.clearBuf()
|
|
72
|
+
B.emitLine(varNames[varKey])
|
|
73
|
+
for (let step of steps) {
|
|
74
|
+
B.emitLine(`${step['_time']}\t${step[varKey]}`)
|
|
75
|
+
}
|
|
76
|
+
let status = stream.write(B.getBuf())
|
|
77
|
+
// Handle backpressure by waiting for the drain event to continue when the write buffer is congested.
|
|
78
|
+
if (!status) {
|
|
79
|
+
stream.once('drain', writeContinuation)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// End the stream after the last line is emitted; this will trigger a `finish` event after
|
|
85
|
+
// all data has been flushed to the output file
|
|
86
|
+
stream.end()
|
|
87
|
+
}
|
|
88
|
+
return new Promise(resolve => {
|
|
89
|
+
// Start the write on the next event loop tick so we can return immediately.
|
|
90
|
+
process.nextTick(writeContinuation)
|
|
91
|
+
// Hold on to the resolve function to call when we are finished.
|
|
92
|
+
finished = resolve
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
await readLog()
|
|
96
|
+
await writeDat()
|
|
97
|
+
}
|
|
98
|
+
export default {
|
|
99
|
+
command,
|
|
100
|
+
describe,
|
|
101
|
+
builder,
|
|
102
|
+
handler,
|
|
103
|
+
log
|
|
104
|
+
}
|
package/src/sde-names.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parseAndGenerate, preprocessModel, printNames } from '@sdeverywhere/compile'
|
|
2
|
+
|
|
3
|
+
import { modelPathProps, parseSpec } from './utils.js'
|
|
4
|
+
|
|
5
|
+
let command = 'names [options] <model> <namesfile>'
|
|
6
|
+
let describe = 'convert variable names in a model'
|
|
7
|
+
let builder = {
|
|
8
|
+
toc: {
|
|
9
|
+
describe: 'convert a file with Vensim variable names to C names',
|
|
10
|
+
type: 'boolean'
|
|
11
|
+
},
|
|
12
|
+
tovml: {
|
|
13
|
+
describe: 'convert a file with C variable names to Vensim names',
|
|
14
|
+
type: 'boolean'
|
|
15
|
+
},
|
|
16
|
+
spec: {
|
|
17
|
+
describe: 'pathname of the I/O specification JSON file',
|
|
18
|
+
type: 'string',
|
|
19
|
+
alias: 's'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
let handler = argv => {
|
|
23
|
+
names(argv.model, argv.namesfile, argv)
|
|
24
|
+
}
|
|
25
|
+
let names = async (model, namesPathname, opts) => {
|
|
26
|
+
// Get the model name and directory from the model argument.
|
|
27
|
+
let { modelDirname, modelPathname, modelName } = modelPathProps(model)
|
|
28
|
+
let spec = parseSpec(opts.spec)
|
|
29
|
+
// Preprocess model text into parser input.
|
|
30
|
+
let input = preprocessModel(modelPathname, spec)
|
|
31
|
+
// Parse the model to get variable and subscript information.
|
|
32
|
+
await parseAndGenerate(input, spec, 'convertNames', modelDirname, modelName, '')
|
|
33
|
+
// Read each variable name from the names file and convert it.
|
|
34
|
+
printNames(namesPathname, opts.toc ? 'to-c' : 'to-vensim')
|
|
35
|
+
}
|
|
36
|
+
export default {
|
|
37
|
+
command,
|
|
38
|
+
describe,
|
|
39
|
+
builder,
|
|
40
|
+
handler
|
|
41
|
+
}
|
package/src/sde-run.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { build } from './sde-build.js'
|
|
2
|
+
import { exec } from './sde-exec.js'
|
|
3
|
+
|
|
4
|
+
export let command = 'run [options] <model>'
|
|
5
|
+
export let describe = 'build a model, run it, and capture its output to a file'
|
|
6
|
+
export let builder = {
|
|
7
|
+
spec: {
|
|
8
|
+
describe: 'pathname of the I/O specification JSON file',
|
|
9
|
+
type: 'string',
|
|
10
|
+
alias: 's'
|
|
11
|
+
},
|
|
12
|
+
builddir: {
|
|
13
|
+
describe: 'build directory',
|
|
14
|
+
type: 'string',
|
|
15
|
+
alias: 'b'
|
|
16
|
+
},
|
|
17
|
+
outfile: {
|
|
18
|
+
describe: 'output pathname',
|
|
19
|
+
type: 'string',
|
|
20
|
+
alias: 'o'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export let handler = argv => {
|
|
24
|
+
run(argv.model, argv)
|
|
25
|
+
}
|
|
26
|
+
export let run = async (model, opts) => {
|
|
27
|
+
await build(model, opts)
|
|
28
|
+
exec(model, opts)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
command,
|
|
33
|
+
describe,
|
|
34
|
+
builder,
|
|
35
|
+
handler,
|
|
36
|
+
run
|
|
37
|
+
}
|
package/src/sde-test.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
|
|
3
|
+
import { run } from './sde-run.js'
|
|
4
|
+
import { log } from './sde-log.js'
|
|
5
|
+
import { compare } from './sde-compare.js'
|
|
6
|
+
import { modelPathProps, outputDir } from './utils.js'
|
|
7
|
+
|
|
8
|
+
export let command = 'test [options] <model>'
|
|
9
|
+
export let describe = 'build the model, run it, process the log, and compare to Vensim data'
|
|
10
|
+
export let builder = {
|
|
11
|
+
spec: {
|
|
12
|
+
describe: 'pathname of the I/O specification JSON file',
|
|
13
|
+
type: 'string',
|
|
14
|
+
alias: 's'
|
|
15
|
+
},
|
|
16
|
+
builddir: {
|
|
17
|
+
describe: 'build directory',
|
|
18
|
+
type: 'string',
|
|
19
|
+
alias: 'b'
|
|
20
|
+
},
|
|
21
|
+
outfile: {
|
|
22
|
+
describe: 'output pathname',
|
|
23
|
+
type: 'string',
|
|
24
|
+
alias: 'o'
|
|
25
|
+
},
|
|
26
|
+
precision: {
|
|
27
|
+
describe: 'precision to which values must agree (default 1e-5)',
|
|
28
|
+
type: 'number',
|
|
29
|
+
alias: 'p'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export let handler = argv => {
|
|
33
|
+
test(argv.model, argv)
|
|
34
|
+
}
|
|
35
|
+
export let test = async (model, opts) => {
|
|
36
|
+
try {
|
|
37
|
+
// Run the model and save output to an SDE log file.
|
|
38
|
+
let { modelDirname, modelName } = modelPathProps(model)
|
|
39
|
+
let logPathname
|
|
40
|
+
if (opts.outfile) {
|
|
41
|
+
logPathname = opts.outfile
|
|
42
|
+
} else {
|
|
43
|
+
let outputDirname = outputDir(opts.outfile, modelDirname)
|
|
44
|
+
logPathname = path.join(outputDirname, `${modelName}.txt`)
|
|
45
|
+
opts.outfile = logPathname
|
|
46
|
+
}
|
|
47
|
+
await run(model, opts)
|
|
48
|
+
// Convert the TSV log file to a DAT file in the same directory.
|
|
49
|
+
opts.dat = true
|
|
50
|
+
await log(logPathname, opts)
|
|
51
|
+
// Assume there is a Vensim-created DAT file named {modelName}.dat in the model directory.
|
|
52
|
+
// Compare it to the SDE DAT file.
|
|
53
|
+
let vensimPathname = path.join(modelDirname, `${modelName}.dat`)
|
|
54
|
+
let p = path.parse(logPathname)
|
|
55
|
+
let sdePathname = path.format({ dir: p.dir, name: p.name, ext: '.dat' })
|
|
56
|
+
await compare(vensimPathname, sdePathname, opts)
|
|
57
|
+
} catch (e) {
|
|
58
|
+
// Exit with a non-zero error code if any step failed
|
|
59
|
+
console.error(`ERROR: ${e.message}\n`)
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default {
|
|
65
|
+
command,
|
|
66
|
+
describe,
|
|
67
|
+
builder,
|
|
68
|
+
handler,
|
|
69
|
+
test
|
|
70
|
+
}
|
package/src/sde-which.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
|
|
3
|
+
let command = 'which'
|
|
4
|
+
let describe = 'print the SDEverywhere home directory'
|
|
5
|
+
let builder = {}
|
|
6
|
+
let handler = argv => {
|
|
7
|
+
which(argv)
|
|
8
|
+
}
|
|
9
|
+
let which = () => {
|
|
10
|
+
// The SDEverywhere home directory is one level above the src directory where this code runs.
|
|
11
|
+
let homeDir = path.resolve(new URL('..', import.meta.url).pathname)
|
|
12
|
+
console.log(homeDir)
|
|
13
|
+
}
|
|
14
|
+
export default {
|
|
15
|
+
command,
|
|
16
|
+
describe,
|
|
17
|
+
builder,
|
|
18
|
+
handler,
|
|
19
|
+
which
|
|
20
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
import B from 'bufx'
|
|
5
|
+
import R from 'ramda'
|
|
6
|
+
import sh from 'shelljs'
|
|
7
|
+
|
|
8
|
+
import { canonicalName } from '@sdeverywhere/compile'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Run a command line silently in the "sh" shell. Print error output on error.
|
|
12
|
+
*
|
|
13
|
+
* @return The exit code of the exec'd process.
|
|
14
|
+
*/
|
|
15
|
+
export function execCmd(cmd) {
|
|
16
|
+
let exitCode = 0
|
|
17
|
+
let result = sh.exec(cmd, { silent: true })
|
|
18
|
+
if (result.code !== 0) {
|
|
19
|
+
console.error(result.stderr)
|
|
20
|
+
exitCode = result.code
|
|
21
|
+
}
|
|
22
|
+
return exitCode
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Normalize a model pathname that may or may not include the .mdl extension.
|
|
27
|
+
* If there is not a path in the model argument, default to the current working directory.
|
|
28
|
+
* Return an object with properties that look like this:
|
|
29
|
+
* modelDirname: '/Users/todd/src/models/arrays'
|
|
30
|
+
* modelName: 'arrays'
|
|
31
|
+
* modelPathname: '/Users/todd/src/models/arrays/arrays.mdl'
|
|
32
|
+
*
|
|
33
|
+
* @param model A path to a Vensim model file.
|
|
34
|
+
* @return An object with the properties specified above.
|
|
35
|
+
*/
|
|
36
|
+
export function modelPathProps(model) {
|
|
37
|
+
let p = R.merge({ ext: '.mdl' }, R.pick(['dir', 'name'], path.parse(model)))
|
|
38
|
+
if (R.isEmpty(p.dir)) {
|
|
39
|
+
p.dir = process.cwd()
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
modelDirname: p.dir,
|
|
43
|
+
modelName: p.name,
|
|
44
|
+
modelPathname: path.format(p)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read and parse a model spec JSON file.
|
|
50
|
+
*
|
|
51
|
+
* @param specFilename The name of the model spec JSON file, relative to the
|
|
52
|
+
* current working directory.
|
|
53
|
+
* @return An object containing the model spec properties, or an empty object if
|
|
54
|
+
* the spec file does not exist.
|
|
55
|
+
*/
|
|
56
|
+
export function parseSpec(specFilename) {
|
|
57
|
+
// Parse the JSON file if it exists.
|
|
58
|
+
let spec = {}
|
|
59
|
+
try {
|
|
60
|
+
let json = B.read(specFilename)
|
|
61
|
+
spec = JSON.parse(json)
|
|
62
|
+
} catch (ex) {
|
|
63
|
+
// If the file doesn't exist, use an empty object without complaining.
|
|
64
|
+
// TODO: This needs to be fixed to fail fast instead of staying silent
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Translate dimension families in the spec to canonical form.
|
|
68
|
+
if (spec.dimensionFamilies) {
|
|
69
|
+
let f = {}
|
|
70
|
+
for (let dimName in spec.dimensionFamilies) {
|
|
71
|
+
let family = spec.dimensionFamilies[dimName]
|
|
72
|
+
f[canonicalName(dimName)] = canonicalName(family)
|
|
73
|
+
}
|
|
74
|
+
spec.dimensionFamilies = f
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return spec
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function outputDir(outfile, modelDirname) {
|
|
81
|
+
if (outfile) {
|
|
82
|
+
outfile = path.dirname(outfile)
|
|
83
|
+
}
|
|
84
|
+
return ensureDir(outfile, 'output', modelDirname)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Ensure the given build directory or {modelDir}/build exists.
|
|
89
|
+
*/
|
|
90
|
+
export function buildDir(build, modelDirname) {
|
|
91
|
+
return ensureDir(build, 'build', modelDirname)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Ensure the directory exists as given or under the model directory.
|
|
96
|
+
*/
|
|
97
|
+
function ensureDir(dir, defaultDir, modelDirname) {
|
|
98
|
+
let dirName = dir || path.join(modelDirname, defaultDir)
|
|
99
|
+
if (!fs.existsSync(dirName)) {
|
|
100
|
+
fs.mkdirSync(dirName, { recursive: true })
|
|
101
|
+
}
|
|
102
|
+
return dirName
|
|
103
|
+
}
|