@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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as R from 'ramda'
|
|
2
|
+
import XLSX from 'xlsx'
|
|
3
|
+
|
|
4
|
+
import { listConcat } from '../_shared/helpers.js'
|
|
5
|
+
import { sub } from '../_shared/subscript.js'
|
|
6
|
+
|
|
7
|
+
import { handleExcelOrCsvFile } from './direct-data-helpers.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate code for a variable that uses `GET DIRECT DATA` to source data from an external file
|
|
11
|
+
* (in CSV or Excel format).
|
|
12
|
+
*
|
|
13
|
+
* @param {*} variable The `Variable` instance to process.
|
|
14
|
+
* @param {'decl' | 'init-lookups'} mode The code generation mode.
|
|
15
|
+
* @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA` call (e.g.,
|
|
16
|
+
* `?data`) to the tabular data contained in the loaded data file.
|
|
17
|
+
* @param {string} modelDir The path to the directory containing the model (used for resolving data files).
|
|
18
|
+
* @param {string} varLhs The C code for the LHS variable reference.
|
|
19
|
+
* @return {string[]} An array of strings containing the generated C code for the variable,
|
|
20
|
+
* one string per line of code.
|
|
21
|
+
*/
|
|
22
|
+
export function generateLookupsFromDirectData(variable, mode, directData, modelDir, varLhs) {
|
|
23
|
+
if (mode === 'decl') {
|
|
24
|
+
// Nothing to emit in decl mode
|
|
25
|
+
return []
|
|
26
|
+
} else if (mode !== 'init-lookups') {
|
|
27
|
+
throw new Error(`Invalid code gen mode '${mode}' for data variable ${variable.modelLHS}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create a function that reads the CSV or XLS[X] content
|
|
31
|
+
const { file, tab, timeRowOrCol, startCell } = variable.directDataArgs
|
|
32
|
+
const getCellValue = handleExcelOrCsvFile(file, tab, 'data', directData, modelDir)
|
|
33
|
+
|
|
34
|
+
// If direct data exists for this variable, copy it from the workbook into one or more lookups
|
|
35
|
+
let indexNum = 0
|
|
36
|
+
if (!R.isEmpty(variable.separationDims)) {
|
|
37
|
+
// Generate a lookup for a separated index in the variable's dimension.
|
|
38
|
+
if (variable.separationDims.length > 1) {
|
|
39
|
+
console.error(`WARNING: direct data variable ${variable.varName} separated on more than one dimension`)
|
|
40
|
+
}
|
|
41
|
+
let dimName = variable.separationDims[0]
|
|
42
|
+
for (let subscript of variable.subscripts) {
|
|
43
|
+
if (sub(subscript).family === dimName) {
|
|
44
|
+
// Use the index value in the subscript family when that is the separation dimension.
|
|
45
|
+
indexNum = sub(subscript).value
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
if (sub(dimName).value.includes(subscript)) {
|
|
49
|
+
// Look up the index when the separation dimension is a subdimension.
|
|
50
|
+
indexNum = sub(dimName).value.indexOf(subscript)
|
|
51
|
+
break
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [generateDirectDataLookup(varLhs, getCellValue, timeRowOrCol, startCell, indexNum)]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function generateDirectDataLookup(varLhs, getCellValue, timeRowOrCol, startCell, indexNum) {
|
|
59
|
+
// Read a row or column of data as (time, value) pairs from the worksheet.
|
|
60
|
+
// The cell(c,r) function wraps data access by column and row.
|
|
61
|
+
let lookupData = ''
|
|
62
|
+
let lookupSize = 0
|
|
63
|
+
let dataAddress = XLSX.utils.decode_cell(startCell.toUpperCase())
|
|
64
|
+
let dataCol = dataAddress.c
|
|
65
|
+
let dataRow = dataAddress.r
|
|
66
|
+
if (dataCol < 0 || dataRow < 0) {
|
|
67
|
+
throw new Error(`Failed to parse 'cell' argument for GET DIRECT {DATA,LOOKUPS} call for ${varLhs}: ${startCell}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let timeCol, timeRow, nextCell
|
|
71
|
+
if (isNaN(parseInt(timeRowOrCol))) {
|
|
72
|
+
// Time values are in a column.
|
|
73
|
+
timeCol = XLSX.utils.decode_col(timeRowOrCol.toUpperCase())
|
|
74
|
+
timeRow = dataRow
|
|
75
|
+
dataCol += indexNum
|
|
76
|
+
nextCell = () => {
|
|
77
|
+
dataRow++
|
|
78
|
+
timeRow++
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
// Time values are in a row.
|
|
82
|
+
timeCol = dataCol
|
|
83
|
+
timeRow = XLSX.utils.decode_row(timeRowOrCol)
|
|
84
|
+
dataRow += indexNum
|
|
85
|
+
nextCell = () => {
|
|
86
|
+
dataCol++
|
|
87
|
+
timeCol++
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let timeValue = getCellValue(timeCol, timeRow)
|
|
92
|
+
let dataValue = getCellValue(dataCol, dataRow)
|
|
93
|
+
while (timeValue != null && dataValue != null) {
|
|
94
|
+
lookupData = listConcat(lookupData, `${timeValue}, ${dataValue}`, true)
|
|
95
|
+
lookupSize++
|
|
96
|
+
nextCell()
|
|
97
|
+
dataValue = getCellValue(dataCol, dataRow)
|
|
98
|
+
timeValue = getCellValue(timeCol, timeRow)
|
|
99
|
+
}
|
|
100
|
+
if (lookupSize === 0) {
|
|
101
|
+
throw new Error(`Empty lookup data array for ${varLhs}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return ` ${varLhs} = __new_lookup(${lookupSize}, /*copy=*/true, (double[]){ ${lookupData} });`
|
|
105
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as R from 'ramda'
|
|
2
|
+
|
|
3
|
+
import { cartesianProductOf, permutationsOf } from '../_shared/helpers.js'
|
|
4
|
+
import { isDimension, sub } from '../_shared/subscript.js'
|
|
5
|
+
import { pointsString } from './gen-lookup-from-points.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate code for a data variable whose data is sourced from an external file (in DAT format).
|
|
9
|
+
*
|
|
10
|
+
* @param {*} variable The `Variable` instance to process.
|
|
11
|
+
* @param {'decl' | 'init-lookups'} mode The code generation mode.
|
|
12
|
+
* @param {Map<string, any>} extData The map of datasets from external `.dat` files.
|
|
13
|
+
* @param {string} varLhs The C code for the LHS variable reference.
|
|
14
|
+
* @return {string[]} An array of strings containing the generated C code for the variable,
|
|
15
|
+
* one string per line of code.
|
|
16
|
+
*/
|
|
17
|
+
export function generateLookupsFromExternalData(variable, mode, extData, varLhs) {
|
|
18
|
+
if (mode !== 'decl' && mode !== 'init-lookups') {
|
|
19
|
+
throw new Error(`Invalid code gen mode '${mode}' for data variable ${variable.modelLHS}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// If there is external data for this variable, copy it from an external file to a lookup.
|
|
23
|
+
// Just like in `generateLookupFromPoints`, we declare static arrays to hold the data points in
|
|
24
|
+
// the first pass ("decl" mode), then initialize each `Lookup` using that data in the second pass
|
|
25
|
+
// ("init" mode).
|
|
26
|
+
const newLookup = (name, lhs, data, subscriptIndexes) => {
|
|
27
|
+
if (!data) {
|
|
28
|
+
throw new Error(`Data for ${name} not found in external data sources`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (data.size === 0) {
|
|
32
|
+
throw new Error(`Empty lookup data array for ${lhs}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dataName = variable.varName + '_data_' + R.map(i => `_${i}_`, subscriptIndexes).join('')
|
|
36
|
+
if (mode === 'decl') {
|
|
37
|
+
// In decl mode, declare a static data array that will be used to create the associated `Lookup`
|
|
38
|
+
// at init time
|
|
39
|
+
const points = pointsString(Array.from(data.entries()))
|
|
40
|
+
return `double ${dataName}[${data.size * 2}] = { ${points} };`
|
|
41
|
+
} else if (mode === 'init-lookups') {
|
|
42
|
+
// In init mode, create the `Lookup`, passing in a pointer to the static data array declared in decl mode.
|
|
43
|
+
return ` ${lhs} = __new_lookup(${data.size}, /*copy=*/false, ${dataName});`
|
|
44
|
+
} else {
|
|
45
|
+
return []
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// There are three common cases that we handle:
|
|
50
|
+
// - variable has no subscripts (C variable _thing = _thing from dat file)
|
|
51
|
+
// - variable has subscript(s) (C variable with index _thing[0] = _thing[_subscript] from dat file)
|
|
52
|
+
// - variable has dimension(s) (C variable in for loop, _thing[i] = _thing[_subscript_i] from dat file)
|
|
53
|
+
|
|
54
|
+
if (!variable.subscripts || variable.subscripts.length === 0) {
|
|
55
|
+
// No subscripts
|
|
56
|
+
const data = extData.get(variable.varName)
|
|
57
|
+
return [newLookup(variable.varName, varLhs, data, [])]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (variable.subscripts.length === 1 && !isDimension(variable.subscripts[0])) {
|
|
61
|
+
// There is exactly one subscript
|
|
62
|
+
const subscript = variable.subscripts[0]
|
|
63
|
+
const nameInDat = `${variable.varName}[${subscript}]`
|
|
64
|
+
const data = extData.get(nameInDat)
|
|
65
|
+
const subIndex = sub(subscript).value
|
|
66
|
+
return [newLookup(nameInDat, varLhs, data, [subIndex])]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!R.all(s => isDimension(s), variable.subscripts)) {
|
|
70
|
+
// We don't yet handle the case where there are more than one subscript or a mix of
|
|
71
|
+
// subscripts and dimensions
|
|
72
|
+
// TODO: Remove this restriction
|
|
73
|
+
throw new Error(`Data variable ${variable.varName} has >= 2 subscripts; not yet handled`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// At this point, we know that we have one or more dimensions; compute all combinations
|
|
77
|
+
// of the dimensions that we will iterate over
|
|
78
|
+
const lines = []
|
|
79
|
+
const allDims = R.map(s => sub(s).value, variable.subscripts)
|
|
80
|
+
const dimTuples = cartesianProductOf(allDims)
|
|
81
|
+
for (const dims of dimTuples) {
|
|
82
|
+
// Note: It appears that the dat file can have the subscripts in a different order
|
|
83
|
+
// than what SDE uses when declaring the C array. If we don't find data for one
|
|
84
|
+
// order, we try the other possible permutations.
|
|
85
|
+
const dimNamePermutations = permutationsOf(dims)
|
|
86
|
+
let nameInDat, data
|
|
87
|
+
for (const dimNames of dimNamePermutations) {
|
|
88
|
+
nameInDat = `${variable.varName}[${dimNames.join(',')}]`
|
|
89
|
+
data = extData.get(nameInDat)
|
|
90
|
+
if (data) {
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!data) {
|
|
95
|
+
// We currently treat this as a warning, not an error, since there can sometimes be
|
|
96
|
+
// datasets that are a sparse matrix, i.e., data is not defined for certain dimensions.
|
|
97
|
+
// For these cases, the lookup will not be initialized (the Lookup pointer will remain
|
|
98
|
+
// NULL, and any calls to `LOOKUP` will return `:NA:`.
|
|
99
|
+
if (mode === 'decl') {
|
|
100
|
+
console.error(`WARNING: Data for ${nameInDat} not found in external data sources`)
|
|
101
|
+
}
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const subscriptIndexes = R.map(dim => sub(dim).value, dims)
|
|
106
|
+
const varSubscripts = R.map(index => `[${index}]`, subscriptIndexes).join('')
|
|
107
|
+
const lhs = `${variable.varName}${varSubscripts}`
|
|
108
|
+
const lookup = newLookup(nameInDat, lhs, data, subscriptIndexes)
|
|
109
|
+
if (lookup) {
|
|
110
|
+
lines.push(lookup)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return lines
|
|
115
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { cdbl } from '../_shared/helpers.js'
|
|
2
|
+
import { isDimension, isTrivialDimension, sub } from '../_shared/subscript.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate code for a data or lookup variable that contains an explicit set of points.
|
|
6
|
+
*
|
|
7
|
+
* @param {*} variable The `Variable` instance to process.
|
|
8
|
+
* @param {'decl' | 'init-lookups'} mode The code generation mode.
|
|
9
|
+
* @param {boolean} copy If false, a static data array will be used (good for larger data sets).
|
|
10
|
+
* If true, the data will inlined and copied when initializing the lookup (good for smaller data sets).
|
|
11
|
+
* @param {string} varLhs The C code for the LHS variable reference.
|
|
12
|
+
* @param {LoopIndexVars} loopIndexVars The loop index state.
|
|
13
|
+
* @return {string[]} An array of strings containing the generated C code for the variable,
|
|
14
|
+
* one string per line of code.
|
|
15
|
+
*/
|
|
16
|
+
export function generateLookupFromPoints(variable, mode, copy, varLhs, loopIndexVars) {
|
|
17
|
+
if (variable.points.length === 0) {
|
|
18
|
+
throw new Error(`Empty lookup data array for ${variable.modelLHS}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (copy) {
|
|
22
|
+
// Inline the data points and copy them when initializing the lookup
|
|
23
|
+
if (mode === 'decl') {
|
|
24
|
+
// Nothing to emit in decl mode
|
|
25
|
+
return []
|
|
26
|
+
} else if (mode === 'init-lookups') {
|
|
27
|
+
// In init mode, generate a new lookup using the data points from the variable
|
|
28
|
+
const points = pointsString(variable.points)
|
|
29
|
+
return [` ${varLhs} = __new_lookup(${variable.points.length}, /*copy=*/true, (double[]){ ${points} });`]
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
// Construct the name of the data array, which is based on the associated lookup var name,
|
|
33
|
+
// with any subscripts tacked on the end.
|
|
34
|
+
const dataName = variable.varName + '_data_' + generateLookupDataName(variable.subscripts, loopIndexVars)
|
|
35
|
+
if (mode === 'decl') {
|
|
36
|
+
// In decl mode, declare a static data array that will be used to create the associated `Lookup`
|
|
37
|
+
// at init time. Using static arrays is better for code size, helps us avoid creating a copy of
|
|
38
|
+
// the data in memory, and seems to perform much better when compiled to wasm when compared to the
|
|
39
|
+
// previous approach that used varargs + copying, especially on constrained (e.g. iOS) devices.
|
|
40
|
+
const points = pointsString(variable.points)
|
|
41
|
+
return [`double ${dataName}[${variable.points.length * 2}] = { ${points} };`]
|
|
42
|
+
} else if (mode === 'init-lookups') {
|
|
43
|
+
// In init mode, create the `Lookup`, passing in a pointer to the static data array declared earlier.
|
|
44
|
+
// TODO: Make use of the lookup range
|
|
45
|
+
return [` ${varLhs} = __new_lookup(${variable.points.length}, /*copy=*/false, ${dataName});`]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new Error(`Invalid code gen mode '${mode}' for lookup ${variable.modelLHS}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Return a string containing a comma separated list of [x,y] pairs from the given array of points.
|
|
54
|
+
*
|
|
55
|
+
* @param {number[][]} points The array of [x,y] tuples.
|
|
56
|
+
* @return {string} The string containing the comma separated point values.
|
|
57
|
+
*/
|
|
58
|
+
export function pointsString(points) {
|
|
59
|
+
return points.map(p => `${cdbl(p[0])}, ${cdbl(p[1])}`).join(', ')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Return a C name for the static data array associated with a lookup variable.
|
|
64
|
+
*
|
|
65
|
+
* @param {string[]} subIds The array of subscript IDs.
|
|
66
|
+
* @param {LoopIndexVars} loopIndexVars The loop index state.
|
|
67
|
+
* @return {string} The C array name.
|
|
68
|
+
*/
|
|
69
|
+
function generateLookupDataName(subIds, loopIndexVars) {
|
|
70
|
+
return subIds
|
|
71
|
+
.map(subId => {
|
|
72
|
+
if (isDimension(subId)) {
|
|
73
|
+
const i = loopIndexVars.index(subId)
|
|
74
|
+
if (isTrivialDimension(subId)) {
|
|
75
|
+
// When the dimension is trivial, we can simply emit e.g. `[i]` instead of `[_dim[i]]`
|
|
76
|
+
return `_${i}_`
|
|
77
|
+
} else {
|
|
78
|
+
return `_${subId}_${i}_`
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
return `_${sub(subId).value}_`
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.join('')
|
|
85
|
+
}
|
|
@@ -191,31 +191,36 @@ export default class EquationReader extends ModelReader {
|
|
|
191
191
|
startCell: args[2]
|
|
192
192
|
}
|
|
193
193
|
} else if (fn === '_IF_THEN_ELSE') {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
194
|
+
if (process.env.SDE_NONPUBLIC_REDUCE_VARIABLES !== '0') {
|
|
195
|
+
// Evaluate the condition expression of the `IF THEN ELSE`. If it resolves
|
|
196
|
+
// to a compile-time constant, we only need to visit one branch, which means
|
|
197
|
+
// that no references will be recorded for the other branch, therefore allowing
|
|
198
|
+
// it to be skipped in the unused reference elimination phase and during the
|
|
199
|
+
// final code generation phase.
|
|
200
|
+
const condText = ctx.expr(0).getText()
|
|
201
|
+
const exprReader = new ExprReader()
|
|
202
|
+
const condExpr = exprReader.read(condText)
|
|
203
|
+
if (condExpr.constantValue !== undefined) {
|
|
204
|
+
// Record the conditional expression and its constant value so that
|
|
205
|
+
// it can be accessed later by EquationGen. We need to record it
|
|
206
|
+
// this way because any variables referenced by the expression may
|
|
207
|
+
// be removed during the unused reference elimination phase.
|
|
208
|
+
Model.addConstantExpr(condText, condExpr.constantValue)
|
|
209
|
+
if (condExpr.constantValue !== 0) {
|
|
210
|
+
// Only visit the "if true" branch
|
|
211
|
+
this.setArgIndex(1)
|
|
212
|
+
ctx.expr(1).accept(this)
|
|
213
|
+
} else {
|
|
214
|
+
// Only visit the "if false" branch
|
|
215
|
+
this.setArgIndex(2)
|
|
216
|
+
ctx.expr(2).accept(this)
|
|
217
|
+
}
|
|
212
218
|
} else {
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
ctx.expr(2).accept(this)
|
|
219
|
+
// Visit the condition and both branches
|
|
220
|
+
super.visitExprList(ctx)
|
|
216
221
|
}
|
|
217
222
|
} else {
|
|
218
|
-
//
|
|
223
|
+
// Optimization is disabled, visit the condition and both branches
|
|
219
224
|
super.visitExprList(ctx)
|
|
220
225
|
}
|
|
221
226
|
} else {
|
package/src/model/model.js
CHANGED
|
@@ -2,7 +2,7 @@ import B from 'bufx'
|
|
|
2
2
|
import yaml from 'js-yaml'
|
|
3
3
|
import * as R from 'ramda'
|
|
4
4
|
|
|
5
|
-
import { decanonicalize, isIterable, listConcat, strlist, vlog, vsort } from '../_shared/helpers.js'
|
|
5
|
+
import { canonicalName, decanonicalize, isIterable, listConcat, strlist, vlog, vsort } from '../_shared/helpers.js'
|
|
6
6
|
import {
|
|
7
7
|
addIndex,
|
|
8
8
|
allAliases,
|
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
import { createParser } from '../parse/parser.js'
|
|
18
18
|
|
|
19
19
|
import EquationReader from './equation-reader.js'
|
|
20
|
+
import { readEquation } from './read-equations.js'
|
|
21
|
+
import { readDimensionDefs } from './read-subscripts.js'
|
|
22
|
+
import { readVariables as readVariables2 } from './read-variables.js'
|
|
23
|
+
import { reduceVariables } from './reduce-variables.js'
|
|
20
24
|
import SubscriptRangeReader from './subscript-range-reader.js'
|
|
21
25
|
import toposort from './toposort.js'
|
|
22
26
|
import VarNameReader from './var-name-reader.js'
|
|
@@ -53,7 +57,8 @@ function resetModelState() {
|
|
|
53
57
|
* Note that this function currently does not return anything and instead stores the parsed subscript
|
|
54
58
|
* definitions in the `subscript` module and the parsed/analyzed variables in this module.
|
|
55
59
|
*
|
|
56
|
-
*
|
|
60
|
+
* TODO: FIX TYPE
|
|
61
|
+
* @param {*} parsedModel The parsed model structure.
|
|
57
62
|
* @param {*} spec The parsed `spec.json` object.
|
|
58
63
|
* @param {Map<string, any>} extData The map of datasets from external `.dat` files.
|
|
59
64
|
* @param {Map<string, any>} directData The mapping of dataset name used in a `GET DIRECT DATA`
|
|
@@ -62,19 +67,38 @@ function resetModelState() {
|
|
|
62
67
|
* files for `GET DIRECT SUBSCRIPT`).
|
|
63
68
|
* @param {*} opts An optional object used by tests to stop the read process after a specific phase.
|
|
64
69
|
*/
|
|
65
|
-
function read(
|
|
70
|
+
function read(parsedModel, spec, extData, directData, modelDirname, opts) {
|
|
66
71
|
// Some arrays need to be separated into variables with individual indices to
|
|
67
72
|
// prevent eval cycles. They are manually added to the spec file.
|
|
68
73
|
let specialSeparationDims = spec.specialSeparationDims
|
|
69
74
|
|
|
70
|
-
//
|
|
71
|
-
|
|
75
|
+
// Dimensions must be defined before reading variables that use them.
|
|
76
|
+
if (parsedModel.kind === 'vensim-legacy') {
|
|
77
|
+
readSubscriptRanges(parsedModel.parseTree, modelDirname)
|
|
78
|
+
} else {
|
|
79
|
+
readDimensionDefs(parsedModel, modelDirname)
|
|
80
|
+
}
|
|
72
81
|
if (opts?.stopAfterReadSubscripts) return
|
|
73
|
-
|
|
82
|
+
resolveDimensions(spec.dimensionFamilies)
|
|
74
83
|
if (opts?.stopAfterResolveSubscripts) return
|
|
75
84
|
|
|
76
85
|
// Read variables from the model parse tree.
|
|
77
|
-
|
|
86
|
+
if (parsedModel.kind === 'vensim-legacy') {
|
|
87
|
+
// TODO: directData is actually unused in VariableReader
|
|
88
|
+
readVariables(parsedModel.parseTree, specialSeparationDims, directData)
|
|
89
|
+
} else {
|
|
90
|
+
// Read the variables
|
|
91
|
+
const vars = readVariables2(parsedModel, specialSeparationDims)
|
|
92
|
+
|
|
93
|
+
// Include a placeholder variable for the exogenous `Time` variable
|
|
94
|
+
const timeVar = new Variable(null)
|
|
95
|
+
timeVar.modelLHS = 'Time'
|
|
96
|
+
timeVar.varName = '_time'
|
|
97
|
+
vars.push(timeVar)
|
|
98
|
+
|
|
99
|
+
// Add the variables to the `Model`
|
|
100
|
+
vars.forEach(addVariable)
|
|
101
|
+
}
|
|
78
102
|
if (opts?.stopAfterReadVariables) return
|
|
79
103
|
|
|
80
104
|
if (spec) {
|
|
@@ -94,7 +118,7 @@ function read(parseTree, spec, extData, directData, modelDirname, opts) {
|
|
|
94
118
|
}
|
|
95
119
|
|
|
96
120
|
// Analyze model equations to fill in more details about variables.
|
|
97
|
-
analyze()
|
|
121
|
+
analyze(parsedModel.kind, spec?.inputVars, opts)
|
|
98
122
|
if (opts?.stopAfterAnalyze) return
|
|
99
123
|
|
|
100
124
|
// Check that all input and output vars in the spec actually exist in the model.
|
|
@@ -133,10 +157,10 @@ function readSubscriptRanges(parseTree, modelDirname) {
|
|
|
133
157
|
* @param {Object.<string, string>} dimensionFamilies The optional mapping of dimension name to family name
|
|
134
158
|
* as provided in a `spec.json` file.
|
|
135
159
|
*/
|
|
136
|
-
function
|
|
160
|
+
function resolveDimensions(dimensionFamilies) {
|
|
137
161
|
let allDims = allDimensions()
|
|
138
162
|
|
|
139
|
-
// Expand dimensions that appeared in
|
|
163
|
+
// Expand dimensions that appeared in dimension definitions into subscripts/indices.
|
|
140
164
|
// Repeat until there are only indices in dimension values.
|
|
141
165
|
let dimFoundInValue
|
|
142
166
|
do {
|
|
@@ -155,7 +179,7 @@ function resolveSubscriptRanges(dimensionFamilies) {
|
|
|
155
179
|
}
|
|
156
180
|
} while (dimFoundInValue)
|
|
157
181
|
|
|
158
|
-
// Fill in
|
|
182
|
+
// Fill in dimension aliases from their model families.
|
|
159
183
|
for (let dim of allAliases()) {
|
|
160
184
|
if (dim.value === '') {
|
|
161
185
|
let refDim = sub(dim.family)
|
|
@@ -292,27 +316,44 @@ function readVariables(tree, specialSeparationDims, directData) {
|
|
|
292
316
|
v.varName = '_time'
|
|
293
317
|
addVariable(v)
|
|
294
318
|
}
|
|
295
|
-
|
|
296
|
-
function analyze() {
|
|
319
|
+
function analyze(parsedModelKind, inputVars, opts) {
|
|
297
320
|
// Analyze the RHS of each equation in stages after all the variables are read.
|
|
298
321
|
// Find non-apply-to-all vars that are defined with more than one equation.
|
|
299
322
|
findNonAtoAVars()
|
|
323
|
+
|
|
300
324
|
// Set the refId for each variable. Only non-apply-to-all vars include subscripts in the refId.
|
|
301
325
|
setRefIds()
|
|
326
|
+
|
|
327
|
+
// If enabled, reduce expressions used in variable definitions.
|
|
328
|
+
if (parsedModelKind !== 'vensim-legacy') {
|
|
329
|
+
if (opts?.reduceVariables !== false && process.env.SDE_NONPUBLIC_REDUCE_VARIABLES !== '0') {
|
|
330
|
+
let reduceMode = opts?.reduceVariables || process.env.SDE_NONPUBLIC_REDUCE_VARIABLES || 'default'
|
|
331
|
+
reduceVariables(variables, inputVars || [], reduceMode)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (opts?.stopAfterReduceVariables === true) return
|
|
335
|
+
|
|
302
336
|
// Read the RHS to list the refIds of vars that are referenced and set the var type.
|
|
303
|
-
|
|
337
|
+
if (parsedModelKind === 'vensim-legacy') {
|
|
338
|
+
readEquations()
|
|
339
|
+
} else {
|
|
340
|
+
variables.forEach(readEquation)
|
|
341
|
+
}
|
|
304
342
|
}
|
|
305
343
|
|
|
306
344
|
function checkSpecVars(spec, extData) {
|
|
307
|
-
// Look up each var in the spec and issue and error
|
|
345
|
+
// Look up each var in the spec and issue and throw error if it does not exist.
|
|
308
346
|
|
|
309
347
|
function check(varNames, specType) {
|
|
310
348
|
if (isIterable(varNames)) {
|
|
311
349
|
for (let varName of varNames) {
|
|
350
|
+
// TODO: This code as written does not check variables that include subscripts, but
|
|
351
|
+
// we should check those as well (and make sure that subscripts or indices are not
|
|
352
|
+
// out of the valid range)
|
|
312
353
|
if (!R.contains('[', varName)) {
|
|
313
354
|
if (!varWithRefId(varName)) {
|
|
314
355
|
// Look for a variable in external data.
|
|
315
|
-
if (extData
|
|
356
|
+
if (extData?.has(varName)) {
|
|
316
357
|
// console.error(`found ${specType} ${varName} in extData`)
|
|
317
358
|
// Copy data from an external file to an equation that does a lookup.
|
|
318
359
|
let lookup = R.reduce(
|
|
@@ -323,7 +364,9 @@ function checkSpecVars(spec, extData) {
|
|
|
323
364
|
let modelEquation = `${decanonicalize(varName)} = WITH LOOKUP(Time, (${lookup}))`
|
|
324
365
|
addEquation(modelEquation)
|
|
325
366
|
} else {
|
|
326
|
-
|
|
367
|
+
throw new Error(
|
|
368
|
+
`The ${specType} variable ${varName} was declared in spec.json, but no matching variable was found in the model or external data sources`
|
|
369
|
+
)
|
|
327
370
|
}
|
|
328
371
|
}
|
|
329
372
|
}
|
|
@@ -662,7 +705,7 @@ function varWithRefId(refId) {
|
|
|
662
705
|
}
|
|
663
706
|
}
|
|
664
707
|
if (!refVar) {
|
|
665
|
-
vlog('ERROR: no var found for refId', refId)
|
|
708
|
+
// vlog('ERROR: no var found for refId', refId)
|
|
666
709
|
}
|
|
667
710
|
}
|
|
668
711
|
return refVar
|
|
@@ -752,7 +795,29 @@ function vensimName(cVarName) {
|
|
|
752
795
|
function cName(vensimVarName) {
|
|
753
796
|
// Convert a Vensim variable name to a C name.
|
|
754
797
|
// This function requires model analysis to be completed first when the variable has subscripts.
|
|
755
|
-
|
|
798
|
+
if (process.env.SDE_NONPUBLIC_USE_NEW_PARSE !== '1') {
|
|
799
|
+
// TODO: For now we use the legacy VarNameReader when the old parser is active; this
|
|
800
|
+
// code will be removed once the old parser is removed
|
|
801
|
+
return new VarNameReader().read(vensimVarName)
|
|
802
|
+
}
|
|
803
|
+
// Split the variable name from the subscripts
|
|
804
|
+
let matches = vensimVarName.match(/([^[]+)(?:\[([^\]]+)\])?/)
|
|
805
|
+
if (!matches) {
|
|
806
|
+
throw new Error(`Invalid variable name '${vensimVarName}' found when converting to C representation`)
|
|
807
|
+
}
|
|
808
|
+
let cVarName = canonicalName(matches[1])
|
|
809
|
+
if (matches[2]) {
|
|
810
|
+
// The variable name includes subscripts, so split them into individual IDs
|
|
811
|
+
let cSubIds = matches[2].split(',').map(x => canonicalName(x))
|
|
812
|
+
cSubIds = normalizeSubscripts(cSubIds)
|
|
813
|
+
// If a subscript is an index, convert it to an index number to match Vensim data exports
|
|
814
|
+
let cSubIdParts = cSubIds.map(cSubId => {
|
|
815
|
+
return isIndex(cSubId) ? `[${sub(cSubId).value}]` : `[${cSubId}]`
|
|
816
|
+
})
|
|
817
|
+
// Append the subscript parts to the base variable name to create the full reference
|
|
818
|
+
cVarName += cSubIdParts.join('')
|
|
819
|
+
}
|
|
820
|
+
return cVarName
|
|
756
821
|
}
|
|
757
822
|
function isInputVar(varName) {
|
|
758
823
|
// Return true if the given variable (in canonical form) is included in the list of
|