@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/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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }