@sdeverywhere/compile 0.7.10 → 0.7.11
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/package.json +2 -1
- package/src/generate/code-gen.js +38 -42
- package/src/generate/direct-data-helpers.js +86 -0
- package/src/generate/equation-gen.js +1 -1
- package/src/generate/expand-var-names.js +115 -0
- package/src/generate/gen-const-list.js +89 -0
- package/src/generate/gen-direct-const.js +91 -0
- package/src/generate/gen-equation.js +273 -0
- package/src/generate/gen-expr.js +851 -0
- package/src/generate/gen-lookup-from-direct.js +105 -0
- package/src/generate/gen-lookup-from-external.js +115 -0
- package/src/generate/gen-lookup-from-points.js +85 -0
- package/src/model/equation-reader.js +27 -22
- package/src/model/model.js +84 -19
- package/src/model/read-equation-fn-delay.js +312 -0
- package/src/model/read-equation-fn-npv.js +67 -0
- package/src/model/read-equation-fn-smooth.js +133 -0
- package/src/model/read-equation-fn-trend.js +44 -0
- package/src/model/read-equation-fn-with-lookup.js +37 -0
- package/src/model/read-equations.js +795 -0
- package/src/model/read-subscripts.js +77 -0
- package/src/model/read-variables.js +264 -0
- package/src/model/reduce-variables.js +166 -0
- package/src/model/variable.js +7 -2
- package/src/parse-and-generate.js +65 -12
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdeverywhere/compile",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.11",
|
|
4
4
|
"description": "The core Vensim to C compiler for the SDEverywhere tool suite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"dependencies": {
|
|
8
|
+
"@sdeverywhere/parse": "^0.1.0",
|
|
8
9
|
"antlr4": "4.12.0",
|
|
9
10
|
"antlr4-vensim": "0.6.2",
|
|
10
11
|
"bufx": "^1.0.5",
|
package/src/generate/code-gen.js
CHANGED
|
@@ -4,15 +4,16 @@ import { asort, lines, strlist, abend, mapIndexed } from '../_shared/helpers.js'
|
|
|
4
4
|
import { sub, allDimensions, allMappings, subscriptFamilies } from '../_shared/subscript.js'
|
|
5
5
|
import Model from '../model/model.js'
|
|
6
6
|
|
|
7
|
+
import { generateEquation } from './gen-equation.js'
|
|
7
8
|
import EquationGen from './equation-gen.js'
|
|
8
|
-
import
|
|
9
|
+
import { expandVarNames } from './expand-var-names.js'
|
|
9
10
|
|
|
10
|
-
export function generateCode(
|
|
11
|
-
return codeGenerator(
|
|
11
|
+
export function generateCode(parsedModel, opts) {
|
|
12
|
+
return codeGenerator(parsedModel, opts).generate()
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
let codeGenerator = (
|
|
15
|
-
const { spec,
|
|
15
|
+
let codeGenerator = (parsedModel, opts) => {
|
|
16
|
+
const { spec, operations, extData, directData, modelDirname } = opts
|
|
16
17
|
// Set to 'decl', 'init-lookups', 'eval', etc depending on the section being generated.
|
|
17
18
|
let mode = ''
|
|
18
19
|
// Set to true to output all variables when there is no model run spec.
|
|
@@ -25,21 +26,30 @@ let codeGenerator = (parseTree, opts) => {
|
|
|
25
26
|
outputAllVars = true
|
|
26
27
|
}
|
|
27
28
|
// Function to generate a section of the code
|
|
28
|
-
let generateSection = R.map(v =>
|
|
29
|
+
let generateSection = R.map(v => {
|
|
30
|
+
if (parsedModel.kind === 'vensim-legacy') {
|
|
31
|
+
return new EquationGen(v, extData, directData, mode, modelDirname).generate()
|
|
32
|
+
} else {
|
|
33
|
+
return generateEquation(v, mode, extData, directData, modelDirname)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
29
36
|
let section = R.pipe(generateSection, R.flatten, lines)
|
|
30
37
|
function generate() {
|
|
31
38
|
// Read variables and subscript ranges from the model parse tree.
|
|
32
39
|
// This is the main entry point for code generation and is called just once.
|
|
33
40
|
try {
|
|
34
|
-
Model.read(
|
|
41
|
+
Model.read(parsedModel, spec, extData, directData, modelDirname)
|
|
35
42
|
// In list mode, print variables to the console instead of generating code.
|
|
36
|
-
if (
|
|
43
|
+
if (operations.includes('printRefIdTest')) {
|
|
37
44
|
Model.printRefIdTest()
|
|
38
|
-
}
|
|
45
|
+
}
|
|
46
|
+
if (operations.includes('printRefGraph')) {
|
|
39
47
|
Model.printRefGraph(opts.varname)
|
|
40
|
-
}
|
|
48
|
+
}
|
|
49
|
+
if (operations.includes('convertNames')) {
|
|
41
50
|
// Do not generate output, but leave the results of model analysis.
|
|
42
|
-
}
|
|
51
|
+
}
|
|
52
|
+
if (operations.includes('generateC')) {
|
|
43
53
|
// Generate code for each variable in the proper order.
|
|
44
54
|
let code = emitDeclCode()
|
|
45
55
|
code += emitInitLookupsCode()
|
|
@@ -198,10 +208,21 @@ void ${name}${idx}() {
|
|
|
198
208
|
}
|
|
199
209
|
let funcCalls = R.pipe(mapIndexed(funcCall), lines)
|
|
200
210
|
|
|
201
|
-
// Break the vars into chunks of 30
|
|
202
|
-
// determined by looking at runtime performance and memory usage
|
|
203
|
-
//
|
|
204
|
-
let
|
|
211
|
+
// Break the vars into chunks. The default value of 30 was empirically
|
|
212
|
+
// determined by looking at runtime performance and memory usage of the
|
|
213
|
+
// En-ROADS model on various devices.
|
|
214
|
+
let chunkSize
|
|
215
|
+
if (process.env.SDE_CODE_GEN_CHUNK_SIZE) {
|
|
216
|
+
chunkSize = parseInt(process.env.SDE_CODE_GEN_CHUNK_SIZE)
|
|
217
|
+
} else {
|
|
218
|
+
chunkSize = 30
|
|
219
|
+
}
|
|
220
|
+
let chunks
|
|
221
|
+
if (chunkSize > 0) {
|
|
222
|
+
chunks = R.splitEvery(chunkSize, vars)
|
|
223
|
+
} else {
|
|
224
|
+
chunks = [vars]
|
|
225
|
+
}
|
|
205
226
|
|
|
206
227
|
if (!preStep) {
|
|
207
228
|
preStep = ''
|
|
@@ -296,33 +317,8 @@ ${postStep}
|
|
|
296
317
|
// Return a list of var names for all variables except lookups and data variables.
|
|
297
318
|
// The names are in Vensim format if vensimNames is true, otherwise they are in C format.
|
|
298
319
|
// Expand subscripted vars into separate var names with each index.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return R.sortBy(v => {
|
|
302
|
-
let modelLHSReader = new ModelLHSReader()
|
|
303
|
-
modelLHSReader.read(v.modelLHS)
|
|
304
|
-
return modelLHSReader.varName.toUpperCase()
|
|
305
|
-
}, Model.variables)
|
|
306
|
-
}
|
|
307
|
-
return R.uniq(
|
|
308
|
-
R.reduce(
|
|
309
|
-
(a, v) => {
|
|
310
|
-
if (v.varType !== 'lookup' && v.varType !== 'data' && v.includeInOutput) {
|
|
311
|
-
let modelLHSReader = new ModelLHSReader()
|
|
312
|
-
modelLHSReader.read(v.modelLHS)
|
|
313
|
-
if (vensimNames) {
|
|
314
|
-
return R.concat(a, modelLHSReader.names())
|
|
315
|
-
} else {
|
|
316
|
-
return R.concat(a, R.map(Model.cName, modelLHSReader.names()))
|
|
317
|
-
}
|
|
318
|
-
} else {
|
|
319
|
-
return a
|
|
320
|
-
}
|
|
321
|
-
},
|
|
322
|
-
[],
|
|
323
|
-
sortedVars()
|
|
324
|
-
)
|
|
325
|
-
)
|
|
320
|
+
const canonicalNames = !vensimNames
|
|
321
|
+
return expandVarNames(canonicalNames)
|
|
326
322
|
}
|
|
327
323
|
//
|
|
328
324
|
// Input/output section helpers
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import XLSX from 'xlsx'
|
|
4
|
+
|
|
5
|
+
import { cdbl, readCsv, readXlsx } from '../_shared/helpers.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Return a `getCellValue` function that reads the CSV or XLS[X] content.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} fileOrTag The filename (e.g., 'data.xlsx') or tag name (e.g., '?data').
|
|
11
|
+
* @param {string} tabOrDelimiter
|
|
12
|
+
* @param {'data' | 'constants'} dataKind The kind of `GET DIRECT ...` being used.
|
|
13
|
+
* @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA` call (e.g.,
|
|
14
|
+
* `?data`) to the tabular data contained in the loaded data file.
|
|
15
|
+
* @param {string} modelDir The path to the directory containing the model (used for resolving data files).
|
|
16
|
+
* @returns A `getCellValue` function.
|
|
17
|
+
*/
|
|
18
|
+
export function handleExcelOrCsvFile(fileOrTag, tabOrDelimiter, dataKind, directData, modelDir) {
|
|
19
|
+
if (fileOrTag.startsWith('?')) {
|
|
20
|
+
// The file is a tag for an Excel file with data in the directData map.
|
|
21
|
+
const workbook = directData.get(fileOrTag)
|
|
22
|
+
return handleExcelWorkbook(fileOrTag, workbook, tabOrDelimiter, dataKind, 'tagged')
|
|
23
|
+
} else {
|
|
24
|
+
// The file is a CSV or XLS[X] pathname. Read it now.
|
|
25
|
+
const dataPathname = path.resolve(modelDir, fileOrTag)
|
|
26
|
+
if (dataPathname.toLowerCase().endsWith('csv')) {
|
|
27
|
+
return handleCsvFile(fileOrTag, dataPathname, tabOrDelimiter, dataKind)
|
|
28
|
+
} else {
|
|
29
|
+
const workbook = readXlsx(dataPathname)
|
|
30
|
+
return handleExcelWorkbook(fileOrTag, workbook, tabOrDelimiter, dataKind, 'file')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Return a `getCellValue` function for the given Excel workbook parsed from an XLS[X] file.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} fileOrTag The filename (e.g., 'data.xlsx') or tag name (e.g., '?data').
|
|
39
|
+
* @param {*} workbook The workbook data loaded from the file.
|
|
40
|
+
* @param {string} tab The name of the tab within the workbook.
|
|
41
|
+
* @param {'data' | 'constants'} dataKind The kind of `GET DIRECT ...` being used.
|
|
42
|
+
* @param {'file' | 'tagged'} dataSource The reference kind, either 'file' or 'tagged'.
|
|
43
|
+
* @returns A `getCellValue` function.
|
|
44
|
+
*/
|
|
45
|
+
function handleExcelWorkbook(fileOrTag, workbook, tab, dataKind, dataSource) {
|
|
46
|
+
if (workbook) {
|
|
47
|
+
let sheet = workbook.Sheets[tab]
|
|
48
|
+
if (sheet) {
|
|
49
|
+
return (c, r) => {
|
|
50
|
+
let cell = sheet[XLSX.utils.encode_cell({ c, r })]
|
|
51
|
+
return cell != null ? cdbl(cell.v) : null
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
throw new Error(`Direct ${dataKind} worksheet ${tab} in ${dataSource} ${fileOrTag} not found`)
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
throw new Error(`Direct ${dataKind} workbook ${dataSource} ${fileOrTag} not found`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Return a `getCellValue` function for the given CSV file.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} file The filename of the data file.
|
|
65
|
+
* @param {string} dataFilename The full path to the data file.
|
|
66
|
+
* @param {string} delimiter The delimiter for the tabular data.
|
|
67
|
+
* @param {'data' | 'constants'} dataKind The kind of `GET DIRECT ...` being used.
|
|
68
|
+
* @returns A `getCellValue` function.
|
|
69
|
+
*/
|
|
70
|
+
function handleCsvFile(file, dataPathname, delimiter, dataKind) {
|
|
71
|
+
// Return a `getCellValue` function for the given CSV file.
|
|
72
|
+
let data = readCsv(dataPathname, delimiter)
|
|
73
|
+
if (data) {
|
|
74
|
+
return (c, r) => {
|
|
75
|
+
let value = '0.0'
|
|
76
|
+
try {
|
|
77
|
+
value = data[r] != null && data[r][c] != null ? cdbl(data[r][c]) : null
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error(`${error.message} in ${dataPathname}`)
|
|
80
|
+
}
|
|
81
|
+
return value
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
throw new Error(`Direct ${dataKind} file ${file} could not be read`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -673,7 +673,7 @@ export default class EquationGen extends ModelReader {
|
|
|
673
673
|
throw new Error(`ERROR: lookup size = ${this.var.points.length} in ${this.var.refId}`)
|
|
674
674
|
}
|
|
675
675
|
let lookupData = R.reduce((a, p) => listConcat(a, `${cdbl(p[0])}, ${cdbl(p[1])}`, true), '', this.var.points)
|
|
676
|
-
this.emit(`__new_lookup(${this.var.points.length}, /*copy=*/true, (double[]){ ${lookupData} })
|
|
676
|
+
this.emit(`__new_lookup(${this.var.points.length}, /*copy=*/true, (double[]){ ${lookupData} })`)
|
|
677
677
|
}
|
|
678
678
|
} else {
|
|
679
679
|
super.visitEquation(ctx)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as R from 'ramda'
|
|
2
|
+
|
|
3
|
+
import { cartesianProductOf, canonicalName } from '../_shared/helpers.js'
|
|
4
|
+
import { sub, isDimension } from '../_shared/subscript.js'
|
|
5
|
+
|
|
6
|
+
import Model from '../model/model.js'
|
|
7
|
+
|
|
8
|
+
import ModelLHSReader from './model-lhs-reader.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Return an array of names for all variable in the model, sorted alphabetically and expanded to
|
|
12
|
+
* include the full set of subscripted variants for variables that include subscripts.
|
|
13
|
+
*
|
|
14
|
+
* @param canonical If true, convert names to canonical representation (variable identifiers), otherwise
|
|
15
|
+
* return the original name of each variable as it appears in the model.
|
|
16
|
+
* @returns {string[]} An array of variable names or identifiers.
|
|
17
|
+
*/
|
|
18
|
+
export function expandVarNames(canonical) {
|
|
19
|
+
const sortedVars = R.sortBy(v => v.varName, Model.variables)
|
|
20
|
+
return R.uniq(
|
|
21
|
+
R.reduce(
|
|
22
|
+
(a, v) => {
|
|
23
|
+
if (v.varType !== 'lookup' && v.varType !== 'data' && v.includeInOutput) {
|
|
24
|
+
if (canonical) {
|
|
25
|
+
return R.concat(a, R.map(Model.cName, namesForVar(v)))
|
|
26
|
+
} else {
|
|
27
|
+
return R.concat(a, namesForVar(v))
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
return a
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
[],
|
|
34
|
+
sortedVars
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Return an array of names for the given variable including all subscript variants.
|
|
41
|
+
*
|
|
42
|
+
* @param {*} v A `Variable` instance.
|
|
43
|
+
* @returns {string[]} An array of expanded names for the given variable.
|
|
44
|
+
*/
|
|
45
|
+
function namesForVar(v) {
|
|
46
|
+
if (process.env.SDE_NONPUBLIC_USE_NEW_PARSE !== '1') {
|
|
47
|
+
// TODO: When the old parsing code is active, use the old ModelLHSReader. This code path
|
|
48
|
+
// will be removed when the old parsing code is removed.
|
|
49
|
+
let modelLHSReader = new ModelLHSReader()
|
|
50
|
+
modelLHSReader.read(v.modelLHS)
|
|
51
|
+
return modelLHSReader.names()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (v.parsedEqn === undefined) {
|
|
55
|
+
// XXX: The special `Time` variable does not have a `parsedEqn`, so use the raw LHS
|
|
56
|
+
return [v.modelLHS]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Expand each variable to get the names of all subscripted variants
|
|
60
|
+
const lhsVarDef = v.parsedEqn.lhs.varDef
|
|
61
|
+
const lhsSubRefs = lhsVarDef.subscriptRefs
|
|
62
|
+
if (lhsSubRefs?.length > 0) {
|
|
63
|
+
// At each position, expand any dimensions or use a subscript (index) directly
|
|
64
|
+
const subOrDimNames = lhsSubRefs.map(subRef => subRef.subName)
|
|
65
|
+
return expandDims(lhsVarDef.varName, subOrDimNames)
|
|
66
|
+
} else {
|
|
67
|
+
// No subscripts, so include a single variable name
|
|
68
|
+
return [lhsVarDef.varName]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Return an array of all expanded subscript combinations.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} baseVarName The base name of the variable.
|
|
76
|
+
* @param {string[]} subOrDimNames The array of subscript or dimension names.
|
|
77
|
+
* @returns {string[]} An array of string representations of subscripted references,
|
|
78
|
+
* e.g., `'x[A1,B1]' ,'x[A1,B2]', ...]`.
|
|
79
|
+
*/
|
|
80
|
+
function expandDims(baseVarName, subOrDimNames) {
|
|
81
|
+
// Expand the dimension for each position
|
|
82
|
+
const expanded = subOrDimNames.map(name => expandDim(name).flat(Infinity))
|
|
83
|
+
|
|
84
|
+
// Expand these into the set of all combinations of subscripts for the variable
|
|
85
|
+
const origCombos = cartesianProductOf(expanded)
|
|
86
|
+
return origCombos.map(combo => {
|
|
87
|
+
const subs = combo.join(',')
|
|
88
|
+
return `${baseVarName}[${subs}]`
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Return an array containing all subscript (index) names in the given dimension. If
|
|
94
|
+
* the given name is a subscript, it will return a single-element array with that
|
|
95
|
+
* subscript name.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} subOrDimName A subscript or dimension name.
|
|
98
|
+
* @returns {string[]} A (possibly nested) array of subscript names.
|
|
99
|
+
*/
|
|
100
|
+
function expandDim(subOrDimName) {
|
|
101
|
+
// Convert the name to an ID
|
|
102
|
+
const subOrDimId = canonicalName(subOrDimName)
|
|
103
|
+
|
|
104
|
+
if (isDimension(subOrDimId)) {
|
|
105
|
+
// Get the object for the dimension
|
|
106
|
+
const dimObj = sub(subOrDimId)
|
|
107
|
+
|
|
108
|
+
// The dimension may contain a mix of individual subscripts (indices) and/or subdimensions,
|
|
109
|
+
// so recursively expand them
|
|
110
|
+
return dimObj.modelValue.map(expandDim)
|
|
111
|
+
} else {
|
|
112
|
+
// This is an individual subscript (index), so return it directly
|
|
113
|
+
return [subOrDimName]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { cartesianProductOf, cdbl } from '../_shared/helpers.js'
|
|
2
|
+
import { isDimension, normalizeSubscripts, sub } from '../_shared/subscript.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate code for a single element in a const list definition.
|
|
6
|
+
*
|
|
7
|
+
* @param {*} variable The `Variable` instance to process.
|
|
8
|
+
* @param {*} parsedEqn The parsed equation.
|
|
9
|
+
* @return {string[]} An array of strings containing the generated C code for the variable,
|
|
10
|
+
* one string per line of code.
|
|
11
|
+
*/
|
|
12
|
+
export function generateConstListElement(variable, parsedEqn) {
|
|
13
|
+
// In the "read variables" phase, const lists are expanded into separated variable
|
|
14
|
+
// definitions, so `variable` here will have `subscripts` that represent specific
|
|
15
|
+
// subscript indices in normalized order (alphabetized by parent dimension/family
|
|
16
|
+
// name). However, we need to consult the LHS subscripts/dimensions, which will
|
|
17
|
+
// be in the original order from the model equation.
|
|
18
|
+
//
|
|
19
|
+
// In the following example,
|
|
20
|
+
// we have a 2D variable whose original dimensions are not in normal order:
|
|
21
|
+
// DimA: A1, A2 ~~|
|
|
22
|
+
// DimB: B1, B2, B3 ~~|
|
|
23
|
+
// x[DimB, DimA] = 1, 2; 3, 4; 5, 6; ~~|
|
|
24
|
+
//
|
|
25
|
+
// The variable `x` will have been separated into:
|
|
26
|
+
// x[B1,A1]
|
|
27
|
+
// x[B1,A2]
|
|
28
|
+
// x[B2,A1]
|
|
29
|
+
// ...
|
|
30
|
+
//
|
|
31
|
+
// Each one will refer to a single element from the original const list. To determine
|
|
32
|
+
// which element in the const list goes with which variable instance, we build an array
|
|
33
|
+
// of all subscript combinations and then find the index of the one that matches the
|
|
34
|
+
// combination used for the separated variable instance.
|
|
35
|
+
const lhsSubRefs = variable.parsedEqn.lhs.varDef.subscriptRefs
|
|
36
|
+
const lhsSubIds = lhsSubRefs.map(subRef => subRef.subId)
|
|
37
|
+
const subIdArrays = lhsSubIds.map(subOrDimId => {
|
|
38
|
+
if (isDimension(subOrDimId)) {
|
|
39
|
+
// Use the full array of subscripts (indexes) for the dimension at this position
|
|
40
|
+
return sub(subOrDimId).value
|
|
41
|
+
} else {
|
|
42
|
+
// This is a single subscript (index), so use an array with a single element
|
|
43
|
+
return [subOrDimId]
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Continuing with the above example, at this point we will have a 2D array:
|
|
48
|
+
// [
|
|
49
|
+
// [_b1,_b2,_b3],
|
|
50
|
+
// [_a1,_a2]
|
|
51
|
+
// ]
|
|
52
|
+
// We expand these into the set of all combinations of subscripts in the original
|
|
53
|
+
// order of the dimensions from the equation LHS.
|
|
54
|
+
const origCombos = cartesianProductOf(subIdArrays)
|
|
55
|
+
|
|
56
|
+
// Now we have the combinations in original order:
|
|
57
|
+
// [_b1,_a1]
|
|
58
|
+
// [_b1,_a2]
|
|
59
|
+
// [_b2,_a1]
|
|
60
|
+
// ...
|
|
61
|
+
// But we need to put them into normalized order so that we can find the index of
|
|
62
|
+
// `variable.subscripts` (which is already in normalized order).
|
|
63
|
+
const normalizedCombos = origCombos.map(normalizeSubscripts)
|
|
64
|
+
|
|
65
|
+
// Convert to strings to make matching easier. Now we have the strings in normalized order:
|
|
66
|
+
// [_a1,_b1]
|
|
67
|
+
// [_a2,_b1]
|
|
68
|
+
// [_a1,_b2]
|
|
69
|
+
// ...
|
|
70
|
+
const comboStrings = normalizedCombos.map(combo => combo.map(subId => `[${subId}]`).join(''))
|
|
71
|
+
|
|
72
|
+
// Convert `variable.subscripts` into the same format so that we can do an array lookup,
|
|
73
|
+
// for example if this separated variable instance is x[_a2,_b1], this will be:
|
|
74
|
+
// [_a2,_b1]
|
|
75
|
+
const lhsComboString = variable.subscripts.map(subId => `[${subId}]`).join('')
|
|
76
|
+
|
|
77
|
+
// Find the index of the combination that matches `variable.subscripts`
|
|
78
|
+
const constIndex = comboStrings.indexOf(lhsComboString)
|
|
79
|
+
if (constIndex < 0) {
|
|
80
|
+
throw new Error(`Failed to determine index of const list element for ${variable.refId}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Determine the LHS and RHS of the const assignment
|
|
84
|
+
const lhsVarId = variable.varName
|
|
85
|
+
const lhsIndicesString = variable.subscripts.map(subId => `[${sub(subId).value}]`).join('')
|
|
86
|
+
const lhsRef = `${lhsVarId}${lhsIndicesString}`
|
|
87
|
+
const rhsConstValue = cdbl(parsedEqn.rhs.constants[constIndex].value)
|
|
88
|
+
return ` ${lhsRef} = ${rhsConstValue};`
|
|
89
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import XLSX from 'xlsx'
|
|
2
|
+
|
|
3
|
+
import { cartesianProductOf } from '../_shared/helpers.js'
|
|
4
|
+
import { indexInSepDim, isDimension, sub } from '../_shared/subscript.js'
|
|
5
|
+
|
|
6
|
+
import { handleExcelOrCsvFile } from './direct-data-helpers.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate code for a variable that uses `GET DIRECT CONSTANTS` to source constant values from an external
|
|
10
|
+
* file (in CSV or Excel format).
|
|
11
|
+
*
|
|
12
|
+
* @param {*} variable The `Variable` instance to process.
|
|
13
|
+
* @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT CONSTANTS` call (e.g.,
|
|
14
|
+
* `?data`) to the tabular data contained in the loaded data file.
|
|
15
|
+
* @param {string} modelDir The path to the directory containing the model (used for resolving data files).
|
|
16
|
+
* @return {string[]} An array of strings containing the generated C code for the variable,
|
|
17
|
+
* one string per line of code.
|
|
18
|
+
*/
|
|
19
|
+
export function generateDirectConstInit(variable, directData, modelDir) {
|
|
20
|
+
// Create a function that reads the CSV or XLS[X] content
|
|
21
|
+
let { file, tab, startCell } = variable.directConstArgs
|
|
22
|
+
let getCellValue = handleExcelOrCsvFile(file, tab, 'constants', directData, modelDir)
|
|
23
|
+
|
|
24
|
+
// Get C subscripts in text form for the LHS in normal order.
|
|
25
|
+
let lhsSubIds = variable.parsedEqn.lhs.varDef.subscriptRefs?.map(s => s.subId) || []
|
|
26
|
+
let modelDimNames = lhsSubIds.filter(s => isDimension(s))
|
|
27
|
+
|
|
28
|
+
// Generate offsets from the start cell in the table corresponding to LHS indices.
|
|
29
|
+
let cellOffsets = []
|
|
30
|
+
let cSubscripts = variable.subscripts.map(s => (isDimension(s) ? sub(s).value : [s]))
|
|
31
|
+
let lhsIndexSubscripts = cartesianProductOf(cSubscripts)
|
|
32
|
+
|
|
33
|
+
// Find the table cell offset for each LHS index tuple.
|
|
34
|
+
let lines = []
|
|
35
|
+
for (let indexSubscripts of lhsIndexSubscripts) {
|
|
36
|
+
let entry = [null, null]
|
|
37
|
+
for (let i = 0; i < variable.subscripts.length; i++) {
|
|
38
|
+
// LHS dimensions or indices in a separated dimension map to table cells.
|
|
39
|
+
let lhsSubscript = variable.subscripts[i]
|
|
40
|
+
if (isDimension(lhsSubscript) || indexInSepDim(lhsSubscript, variable)) {
|
|
41
|
+
// Consider the LHS index subscript at this position.
|
|
42
|
+
let indexSubscript = indexSubscripts[i]
|
|
43
|
+
let ind = sub(indexSubscript)
|
|
44
|
+
// Find the model subscript position corresponding to the LHS index subscript.
|
|
45
|
+
for (let iModelDim = 0; iModelDim < modelDimNames.length; iModelDim++) {
|
|
46
|
+
// Only fill an entry position once.
|
|
47
|
+
if (entry[iModelDim] === null) {
|
|
48
|
+
let modelDim = sub(modelDimNames[iModelDim])
|
|
49
|
+
if (modelDim.family === ind.family) {
|
|
50
|
+
// Set the numeric index for the model dimension in the cell offset entry.
|
|
51
|
+
// Use the position within the dimension to map subdimensions onto cell offsets.
|
|
52
|
+
let pos = modelDim.value.indexOf(indexSubscript)
|
|
53
|
+
// Vectors use a 2D cell offset that maps to columns in the first row.
|
|
54
|
+
// Tables use a 2D cell offset with the row or column matching the model dimension.
|
|
55
|
+
let entryRowOrCol = modelDimNames.length > 1 ? iModelDim : 1
|
|
56
|
+
entry[entryRowOrCol] = pos
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Replace unfilled entry positions with zero.
|
|
64
|
+
entry = entry.map(x => (x === null ? 0 : x))
|
|
65
|
+
// Read values by column first when the start cell ends with an asterisk.
|
|
66
|
+
// Ref: https://www.vensim.com/documentation/fn_get_direct_constants.html
|
|
67
|
+
if (startCell.endsWith('*')) {
|
|
68
|
+
entry.reverse()
|
|
69
|
+
}
|
|
70
|
+
cellOffsets.push(entry)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Read tabular data into an indexed variable for each cell.
|
|
74
|
+
let numericSubscripts = lhsIndexSubscripts.map(idx => idx.map(s => sub(s).value))
|
|
75
|
+
let lhsSubscripts = numericSubscripts.map(s => s.reduce((a, v) => a.concat(`[${v}]`), ''))
|
|
76
|
+
let dataAddress = XLSX.utils.decode_cell(startCell.toUpperCase())
|
|
77
|
+
let startCol = dataAddress.c
|
|
78
|
+
let startRow = dataAddress.r
|
|
79
|
+
if (startCol < 0 || startRow < 0) {
|
|
80
|
+
throw new Error(`Failed to parse 'cell' argument for GET DIRECT CONSTANTS call for ${variable.refId}: ${startCell}`)
|
|
81
|
+
}
|
|
82
|
+
for (let i = 0; i < cellOffsets.length; i++) {
|
|
83
|
+
let rowOffset = cellOffsets[i][0] ? cellOffsets[i][0] : 0
|
|
84
|
+
let colOffset = cellOffsets[i][1] ? cellOffsets[i][1] : 0
|
|
85
|
+
let dataValue = getCellValue(startCol + colOffset, startRow + rowOffset)
|
|
86
|
+
let lhs = `${variable.varName}${lhsSubscripts[i] || ''}`
|
|
87
|
+
lines.push(` ${lhs} = ${dataValue};`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return lines
|
|
91
|
+
}
|