@sdeverywhere/compile 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 +12 -0
- package/package.json +40 -0
- package/src/_shared/helpers.js +351 -0
- package/src/_shared/read-dat.js +71 -0
- package/src/_shared/subscript.js +373 -0
- package/src/generate/code-gen.js +356 -0
- package/src/generate/equation-gen.js +1188 -0
- package/src/generate/loop-index-vars.js +27 -0
- package/src/generate/model-lhs-reader.js +88 -0
- package/src/index.js +8 -0
- package/src/model/equation-reader.js +708 -0
- package/src/model/expr-reader.js +202 -0
- package/src/model/model.js +1045 -0
- package/src/model/subscript-range-reader.js +123 -0
- package/src/model/toposort.js +100 -0
- package/src/model/var-name-reader.js +40 -0
- package/src/model/variable-reader.js +172 -0
- package/src/model/variable.js +111 -0
- package/src/parse/model-reader.js +141 -0
- package/src/parse/parser.js +32 -0
- package/src/parse-and-generate.js +114 -0
- package/src/preprocess/preprocessor.js +247 -0
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
import B from 'bufx'
|
|
2
|
+
import yaml from 'js-yaml'
|
|
3
|
+
import R from 'ramda'
|
|
4
|
+
|
|
5
|
+
import { decanonicalize, isIterable, listConcat, strlist, vlog, vsort } from '../_shared/helpers.js'
|
|
6
|
+
import {
|
|
7
|
+
addIndex,
|
|
8
|
+
allAliases,
|
|
9
|
+
allDimensions,
|
|
10
|
+
indexNamesForSubscript,
|
|
11
|
+
isDimension,
|
|
12
|
+
isIndex,
|
|
13
|
+
normalizeSubscripts,
|
|
14
|
+
sub,
|
|
15
|
+
subscriptFamilies
|
|
16
|
+
} from '../_shared/subscript.js'
|
|
17
|
+
import { createParser } from '../parse/parser.js'
|
|
18
|
+
|
|
19
|
+
import EquationReader from './equation-reader.js'
|
|
20
|
+
import SubscriptRangeReader from './subscript-range-reader.js'
|
|
21
|
+
import toposort from './toposort.js'
|
|
22
|
+
import VarNameReader from './var-name-reader.js'
|
|
23
|
+
import Variable from './variable.js'
|
|
24
|
+
import VariableReader from './variable-reader.js'
|
|
25
|
+
|
|
26
|
+
let variables = []
|
|
27
|
+
let inputVars = []
|
|
28
|
+
let constantExprs = new Map()
|
|
29
|
+
|
|
30
|
+
// Also keep variables in a map (with `varName` as key) for faster lookup
|
|
31
|
+
const variablesByName = new Map()
|
|
32
|
+
|
|
33
|
+
let nonAtoANames = Object.create(null)
|
|
34
|
+
// Set true for diagnostic printing of init, aux, and level vars in sorted order.
|
|
35
|
+
const PRINT_SORTED_VARS = false
|
|
36
|
+
// Set true to print dependency graphs before they are sorted.
|
|
37
|
+
const PRINT_INIT_GRAPH = false
|
|
38
|
+
const PRINT_AUX_GRAPH = false
|
|
39
|
+
const PRINT_LEVEL_GRAPH = false
|
|
40
|
+
|
|
41
|
+
function read(parseTree, spec, extData, directData, modelDirname) {
|
|
42
|
+
// Some arrays need to be separated into variables with individual indices to
|
|
43
|
+
// prevent eval cycles. They are manually added to the spec file.
|
|
44
|
+
let specialSeparationDims = spec.specialSeparationDims
|
|
45
|
+
// Subscript ranges must be defined before reading variables that use them.
|
|
46
|
+
readSubscriptRanges(parseTree, spec.dimensionFamilies, spec.indexFamilies, modelDirname)
|
|
47
|
+
// Read variables from the model parse tree.
|
|
48
|
+
readVariables(parseTree, specialSeparationDims, directData)
|
|
49
|
+
if (spec) {
|
|
50
|
+
// If the spec file contains `input/outputVarNames` (with full Vensim variable names)
|
|
51
|
+
// convert those to C names first. Otherwise, use `input/outputNames` which are already
|
|
52
|
+
// assumed to be valid C names.
|
|
53
|
+
if (spec.inputVarNames) {
|
|
54
|
+
spec.inputVars = R.map(cName, spec.inputVarNames)
|
|
55
|
+
}
|
|
56
|
+
if (spec.outputVarNames) {
|
|
57
|
+
spec.outputVars = R.map(cName, spec.outputVarNames)
|
|
58
|
+
}
|
|
59
|
+
// Save the input vars locally so that they can be referenced by `isInputVar`.
|
|
60
|
+
if (spec.inputVars) {
|
|
61
|
+
inputVars = spec.inputVars
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Analyze model equations to fill in more details about variables.
|
|
65
|
+
analyze()
|
|
66
|
+
// Check that all input and output vars in the spec actually exist in the model.
|
|
67
|
+
checkSpecVars(spec, extData)
|
|
68
|
+
// Remove variables that are not referenced by an input or output variable.
|
|
69
|
+
removeUnusedVariables(spec)
|
|
70
|
+
// Resolve duplicate declarations by converting to one variable type.
|
|
71
|
+
resolveDuplicateDeclarations()
|
|
72
|
+
}
|
|
73
|
+
function readSubscriptRanges(tree, dimensionFamilies, indexFamilies, modelDirname) {
|
|
74
|
+
// Read subscript ranges from the model.
|
|
75
|
+
let subscriptRangeReader = new SubscriptRangeReader(modelDirname)
|
|
76
|
+
subscriptRangeReader.visitModel(tree)
|
|
77
|
+
let allDims = allDimensions()
|
|
78
|
+
// Expand dimensions that appeared in subscript range definitions into indices.
|
|
79
|
+
// Repeat until there are only indices in dimension values.
|
|
80
|
+
let dimFoundInValue
|
|
81
|
+
do {
|
|
82
|
+
dimFoundInValue = false
|
|
83
|
+
for (let dim of allDims) {
|
|
84
|
+
if (dim.value !== '') {
|
|
85
|
+
let value = R.flatten(
|
|
86
|
+
R.map(subscript => (isDimension(subscript) ? sub(subscript).value : subscript), dim.value)
|
|
87
|
+
)
|
|
88
|
+
if (!R.equals(value, dim.value)) {
|
|
89
|
+
dimFoundInValue = true
|
|
90
|
+
dim.value = value
|
|
91
|
+
dim.size = value.length
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} while (dimFoundInValue)
|
|
96
|
+
|
|
97
|
+
// Fill in subscript aliases from their model families.
|
|
98
|
+
for (let dim of allAliases()) {
|
|
99
|
+
if (dim.value === '') {
|
|
100
|
+
let refDim = sub(dim.family)
|
|
101
|
+
dim.value = refDim.value
|
|
102
|
+
dim.size = refDim.size
|
|
103
|
+
dim.modelValue = refDim.modelValue
|
|
104
|
+
allDims.push(dim)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Update the families of dimensions. At this point, all dimensions have their family
|
|
109
|
+
// provisionally set to their own dimension name.
|
|
110
|
+
let dimComparator = (dim1, dim2) => {
|
|
111
|
+
// Sort dimensions by size ascending, by name descending.
|
|
112
|
+
if (dim1.size < dim2.size) {
|
|
113
|
+
return -1
|
|
114
|
+
} else if (dim1.size > dim2.size) {
|
|
115
|
+
return 1
|
|
116
|
+
} else if (dim1.name > dim2.name) {
|
|
117
|
+
return -1
|
|
118
|
+
} else if (dim1.name < dim2.name) {
|
|
119
|
+
return 1
|
|
120
|
+
} else {
|
|
121
|
+
return 0
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (let dim of allDims) {
|
|
125
|
+
// Try looking up the family in the spec file dimension families if they exist.
|
|
126
|
+
if (dimensionFamilies && dimensionFamilies[dim.name]) {
|
|
127
|
+
dim.family = dimensionFamilies[dim.name]
|
|
128
|
+
} else {
|
|
129
|
+
// Find the dimension in this family with the largest number of values.
|
|
130
|
+
// This is the "maximal" dimension that serves as the subscript family.
|
|
131
|
+
// If two dimensions have the same maximal size, choose the one that comes
|
|
132
|
+
// first in alpha sort order, by convention.
|
|
133
|
+
// Take the first index in the dimension.
|
|
134
|
+
let index = dim.value[0]
|
|
135
|
+
let familyDims = R.sort(
|
|
136
|
+
dimComparator,
|
|
137
|
+
R.filter(thisDim => R.contains(index, thisDim.value), allDims)
|
|
138
|
+
)
|
|
139
|
+
if (familyDims.length > 0) {
|
|
140
|
+
dim.family = R.last(familyDims).name
|
|
141
|
+
} else {
|
|
142
|
+
console.error(`No family found for dimension ${dim.name}`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Define indices in order from the maximal (family) dimension.
|
|
148
|
+
// Until now, only dimensions have been defined. We wait until dimension families have been
|
|
149
|
+
// determined to define indices, so that they will belong to exactly one dimension (the family).
|
|
150
|
+
for (let dim of allDims) {
|
|
151
|
+
if (dim.family === dim.name) {
|
|
152
|
+
for (let i = 0; i < dim.value.length; i++) {
|
|
153
|
+
addIndex(dim.value[i], i, dim.family)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// When there is a subscript mapping, the mapping value pulled from the subscript range
|
|
159
|
+
// in the model is either a map-to dimension with the same cardinality as the map-from
|
|
160
|
+
// dimension, or a list of subscripts in the map-to dimension with the same cardinality
|
|
161
|
+
// as the map-from dimension. The mapping value must be transformed into a list of
|
|
162
|
+
// map-from indices in one-to-one correspondence with the map-to indices.
|
|
163
|
+
for (let fromDim of allDims) {
|
|
164
|
+
for (let toDimName in fromDim.mappings) {
|
|
165
|
+
let toDim = sub(toDimName)
|
|
166
|
+
let mappingValue = fromDim.mappings[toDimName]
|
|
167
|
+
let invertedMappingValue = []
|
|
168
|
+
if (R.isEmpty(mappingValue)) {
|
|
169
|
+
// When there is no list of map-to subscripts, list fromDim indices.
|
|
170
|
+
invertedMappingValue = fromDim.value
|
|
171
|
+
} else {
|
|
172
|
+
// The mapping value is a list of map-to subscripts.
|
|
173
|
+
// List fromDim indices in the order in which they map onto toDim indices.
|
|
174
|
+
// Indices are filled in the mapping value by map-to index number as they
|
|
175
|
+
// occur in the map-from dimension.
|
|
176
|
+
let setMappingValue = (toSubName, toIndNumber, fromIndName) => {
|
|
177
|
+
if (Number.isInteger(toIndNumber) && toIndNumber >= 0 && toIndNumber < toDim.size) {
|
|
178
|
+
invertedMappingValue[toIndNumber] = fromIndName
|
|
179
|
+
} else {
|
|
180
|
+
console.error(
|
|
181
|
+
`ERROR: map-to index "${toSubName}" not found when mapping from dimension "${fromDim.name}" index "${fromIndName}"`
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (let i = 0; i < fromDim.value.length; i++) {
|
|
186
|
+
let fromIndName = fromDim.value[i]
|
|
187
|
+
let toSubName = mappingValue[i]
|
|
188
|
+
let toSub = sub(toSubName)
|
|
189
|
+
if (isDimension(toSubName)) {
|
|
190
|
+
// Fill in indices from a dimension in the mapping value.
|
|
191
|
+
for (let toIndName of toSub.value) {
|
|
192
|
+
let toIndNumber = toDim.value.indexOf(toIndName)
|
|
193
|
+
setMappingValue(toSubName, toIndNumber, fromIndName)
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
// Fill in a single index from an index in the mapping value.
|
|
197
|
+
let toIndNumber = toDim.value.indexOf(toSub.name)
|
|
198
|
+
setMappingValue(toSubName, toIndNumber, fromIndName)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Replace toDim subscripts in the mapping value with fromDim subscripts that map to them.
|
|
203
|
+
fromDim.mappings[toDimName] = invertedMappingValue
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function readVariables(tree, specialSeparationDims, directData) {
|
|
208
|
+
// Read all variables in the model parse tree.
|
|
209
|
+
// This populates the variables table with basic information for each variable
|
|
210
|
+
// such as the var name and subscripts.
|
|
211
|
+
let variableReader = new VariableReader(specialSeparationDims, directData)
|
|
212
|
+
variableReader.visitModel(tree)
|
|
213
|
+
// Add a placeholder variable for the exogenous variable Time.
|
|
214
|
+
let v = new Variable(null)
|
|
215
|
+
v.modelLHS = 'Time'
|
|
216
|
+
v.varName = '_time'
|
|
217
|
+
addVariable(v)
|
|
218
|
+
}
|
|
219
|
+
function analyze() {
|
|
220
|
+
// Analyze the RHS of each equation in stages after all the variables are read.
|
|
221
|
+
// Find non-apply-to-all vars that are defined with more than one equation.
|
|
222
|
+
findNonAtoAVars()
|
|
223
|
+
// Set the refId for each variable. Only non-apply-to-all vars include subscripts in the refId.
|
|
224
|
+
setRefIds()
|
|
225
|
+
// Read the RHS to list the refIds of vars that are referenced and set the var type.
|
|
226
|
+
readEquations()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function checkSpecVars(spec, extData) {
|
|
230
|
+
// Look up each var in the spec and issue and error message if it does not exist.
|
|
231
|
+
|
|
232
|
+
function check(varNames, specType) {
|
|
233
|
+
if (isIterable(varNames)) {
|
|
234
|
+
for (let varName of varNames) {
|
|
235
|
+
if (!R.contains('[', varName)) {
|
|
236
|
+
if (!varWithRefId(varName)) {
|
|
237
|
+
// Look for a variable in external data.
|
|
238
|
+
if (extData.has(varName)) {
|
|
239
|
+
// console.error(`found ${specType} ${varName} in extData`)
|
|
240
|
+
// Copy data from an external file to an equation that does a lookup.
|
|
241
|
+
let lookup = R.reduce(
|
|
242
|
+
(a, p) => listConcat(a, `(${p[0]}, ${p[1]})`, true),
|
|
243
|
+
'',
|
|
244
|
+
Array.from(extData.get(varName))
|
|
245
|
+
)
|
|
246
|
+
let modelEquation = `${decanonicalize(varName)} = WITH LOOKUP(Time, (${lookup}))`
|
|
247
|
+
addEquation(modelEquation)
|
|
248
|
+
} else {
|
|
249
|
+
console.error(`${specType} variable ${varName} not found in the model or external data sources`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (spec) {
|
|
258
|
+
check(spec.inputVars, 'input')
|
|
259
|
+
check(spec.outputVars, 'output')
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function removeUnusedVariables(spec) {
|
|
264
|
+
// Remove any variables that are not referenced by an input or output variable.
|
|
265
|
+
// This ensures that only computations that are relevant to the outputs are performed.
|
|
266
|
+
|
|
267
|
+
// Only remove dead code if we have an explicit set of inputs and outputs
|
|
268
|
+
if (!spec.outputVars || spec.outputVars.length === 0 || !spec.inputVars || spec.inputVars.length === 0) {
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Keep track of all variable names that are referenced somewhere. Note that we
|
|
273
|
+
// don't attempt to track specific "ref ids" (e.g. `_some_variable[_subscript]`)
|
|
274
|
+
// but instead just track generic variable names (e.g. `_some_variable`). This
|
|
275
|
+
// ensures that we include all subscripts for a variable, which might mean we
|
|
276
|
+
// include some subscripts that aren't needed, but it is safer than trying to
|
|
277
|
+
// eliminate those and possibly omit something that is needed.
|
|
278
|
+
const referencedVarNames = []
|
|
279
|
+
|
|
280
|
+
// Add the given variable name to the list of referenced variables, if it's not
|
|
281
|
+
// already there.
|
|
282
|
+
const recordUsedVarName = varName => {
|
|
283
|
+
if (!referencedVarNames.includes(varName)) {
|
|
284
|
+
referencedVarNames.push(varName)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Add the given variable to the list of referenced variables, and do the same for
|
|
289
|
+
// some special things (i.e., lookups) that it might reference.
|
|
290
|
+
const recordUsedVariable = v => {
|
|
291
|
+
// Add the variable to the list of referenced variables
|
|
292
|
+
recordUsedVarName(v.varName)
|
|
293
|
+
|
|
294
|
+
// Include any lookup variables that are referenced by this variable
|
|
295
|
+
if (v.referencedLookupVarNames) {
|
|
296
|
+
for (const lookupVarName of v.referencedLookupVarNames) {
|
|
297
|
+
recordUsedVarName(lookupVarName)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Look through the list of function names that are referenced by this
|
|
302
|
+
// variable and see if any of them are lookups (which should be included in
|
|
303
|
+
// our list of referenced variables)
|
|
304
|
+
if (v.referencedFunctionNames) {
|
|
305
|
+
for (const fnName of v.referencedFunctionNames) {
|
|
306
|
+
// Convert the function name (e.g. `__damage_lookup`) to a lookup var name (chop off
|
|
307
|
+
// the leading underscore)
|
|
308
|
+
const lookupName = fnName.slice(1)
|
|
309
|
+
const varForFn = varWithName(lookupName)
|
|
310
|
+
if (varForFn && varForFn.isLookup()) {
|
|
311
|
+
recordUsedVarName(varForFn.varName)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Walk the reference tree rooted at the given var and record it (and anything
|
|
318
|
+
// that it references) as being "used".
|
|
319
|
+
const referencedRefIds = new Set()
|
|
320
|
+
const recordRefsOfVariable = v => {
|
|
321
|
+
// If this variable is subscripted, we need to record all subscript variants;
|
|
322
|
+
// `refIdsWithName` will return those. We also need to record all variables
|
|
323
|
+
// that are referenced by this variable, either directly (`v.references`) or
|
|
324
|
+
// in an "INITIAL" expression (`v.initReferences`). It's OK if we end up with
|
|
325
|
+
// duplicates in this list, because we will examine each reference only once.
|
|
326
|
+
let refIds = refIdsWithName(v.varName)
|
|
327
|
+
refIds = refIds.concat(v.references)
|
|
328
|
+
refIds = refIds.concat(v.initReferences)
|
|
329
|
+
for (const refId of refIds) {
|
|
330
|
+
if (!referencedRefIds.has(refId)) {
|
|
331
|
+
referencedRefIds.add(refId)
|
|
332
|
+
const refVar = varWithRefId(refId)
|
|
333
|
+
recordUsedVariable(refVar)
|
|
334
|
+
recordRefsOfVariable(refVar)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Always keep special vars used by SDE
|
|
340
|
+
recordUsedVarName('_initial_time')
|
|
341
|
+
recordUsedVarName('_final_time')
|
|
342
|
+
recordUsedVarName('_saveper')
|
|
343
|
+
recordUsedVarName('_time_step')
|
|
344
|
+
|
|
345
|
+
// Keep all input variables
|
|
346
|
+
for (const inputVarName of spec.inputVars) {
|
|
347
|
+
for (const v of varsWithName(inputVarName)) {
|
|
348
|
+
recordUsedVariable(v)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Keep all output variables and the variables they depend on
|
|
353
|
+
for (const outputVarName of spec.outputVars) {
|
|
354
|
+
// The outputVars can include a raw index, e.g. `_output_var[0]`,
|
|
355
|
+
// which isn't an actual "ref id", so we'll just derive the
|
|
356
|
+
// var name by chopping off the index part.
|
|
357
|
+
const outputVarBaseName = outputVarName.split('[')[0]
|
|
358
|
+
for (const v of varsWithName(outputVarBaseName)) {
|
|
359
|
+
recordUsedVariable(v)
|
|
360
|
+
recordRefsOfVariable(v)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Filter out unneeded variables so we're left with the minimal set of variables to emit
|
|
365
|
+
variables = R.filter(v => referencedVarNames.includes(v.varName), variables)
|
|
366
|
+
|
|
367
|
+
// Rebuild the variables-by-name map
|
|
368
|
+
variablesByName.clear()
|
|
369
|
+
for (const v of variables) {
|
|
370
|
+
let varsForName = variablesByName.get(v.varName)
|
|
371
|
+
if (!varsForName) {
|
|
372
|
+
varsForName = []
|
|
373
|
+
variablesByName.set(v.varName, varsForName)
|
|
374
|
+
}
|
|
375
|
+
varsForName.push(v)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function resolveDuplicateDeclarations() {
|
|
379
|
+
// Find subscripted const vars where some subscripts are data vars.
|
|
380
|
+
// TODO consider doing the same for lookup vars
|
|
381
|
+
// Least and greatest safe double values in C rounded to convenient consts
|
|
382
|
+
const MIN_SAFE_DBL = -1e308
|
|
383
|
+
const MAX_SAFE_DBL = 1e308
|
|
384
|
+
let data = dataVars()
|
|
385
|
+
for (let constVar of constVars()) {
|
|
386
|
+
if (data.find(d => d.varName === constVar.varName)) {
|
|
387
|
+
// Change the var type from const to data and add lookup data points.
|
|
388
|
+
// For a constant, the equivalent lookup has the same value over the entire x axis.
|
|
389
|
+
let value = parseFloat(constVar.modelFormula)
|
|
390
|
+
if (isNaN(value)) {
|
|
391
|
+
console.error(`The value for const var ${constVar.refId} converted to a lookup is NaN.`)
|
|
392
|
+
}
|
|
393
|
+
constVar.varType = 'data'
|
|
394
|
+
constVar.points = [
|
|
395
|
+
[MIN_SAFE_DBL, value],
|
|
396
|
+
[MAX_SAFE_DBL, value]
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
//
|
|
402
|
+
// Analysis helpers
|
|
403
|
+
//
|
|
404
|
+
function findNonAtoAVars() {
|
|
405
|
+
// Find variables with multiple instances with the same var name, which makes them
|
|
406
|
+
// elements in a non-apply-to-all array. This function constructs the nonAtoANames list.
|
|
407
|
+
function areSubsEqual(vars, i) {
|
|
408
|
+
// Scan the subscripts for each var at position i in normal order.
|
|
409
|
+
// Return true if the subscript is the same for all vars with that name.
|
|
410
|
+
let subscript = vars[0].subscripts[i]
|
|
411
|
+
for (let v of vars) {
|
|
412
|
+
if (v.subscripts[i] !== subscript) {
|
|
413
|
+
return false
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return true
|
|
417
|
+
}
|
|
418
|
+
R.forEach(name => {
|
|
419
|
+
let vars = varsWithName(name)
|
|
420
|
+
if (vars.length > 1) {
|
|
421
|
+
// This is a non-apply-to-all array. Construct the exansion dims array for it.
|
|
422
|
+
// The expansion dim is true at each dim position where the subscript varies.
|
|
423
|
+
let numDims = vars[0].subscripts.length
|
|
424
|
+
let expansionDims = []
|
|
425
|
+
for (let i = 0; i < numDims; i++) {
|
|
426
|
+
expansionDims[i] = !areSubsEqual(vars, i)
|
|
427
|
+
}
|
|
428
|
+
nonAtoANames[name] = expansionDims
|
|
429
|
+
}
|
|
430
|
+
}, varNames())
|
|
431
|
+
}
|
|
432
|
+
function addNonAtoAVar(varName, expansionDims) {
|
|
433
|
+
nonAtoANames[varName] = expansionDims
|
|
434
|
+
}
|
|
435
|
+
function setRefIds() {
|
|
436
|
+
// Set the refId for each var. This requires knowing which vars are non-apply-to-all.
|
|
437
|
+
R.forEach(v => {
|
|
438
|
+
v.refId = refIdForVar(v)
|
|
439
|
+
}, variables)
|
|
440
|
+
}
|
|
441
|
+
function readEquations() {
|
|
442
|
+
// Augment variables with information from their equations.
|
|
443
|
+
// This requires a refId for each var so that actual refIds can be resolved for the reference list.
|
|
444
|
+
R.forEach(v => {
|
|
445
|
+
let equationReader = new EquationReader(v)
|
|
446
|
+
equationReader.read()
|
|
447
|
+
}, variables)
|
|
448
|
+
}
|
|
449
|
+
function addEquation(modelEquation) {
|
|
450
|
+
// Add an equation in Vensim model format.
|
|
451
|
+
let parser = createParser(modelEquation)
|
|
452
|
+
let tree = parser.equation()
|
|
453
|
+
// Read the var and add it to the Model var table.
|
|
454
|
+
let variableReader = new VariableReader()
|
|
455
|
+
variableReader.visitEquation(tree)
|
|
456
|
+
let v = variableReader.var
|
|
457
|
+
// Fill in the refId.
|
|
458
|
+
v.refId = refIdForVar(v)
|
|
459
|
+
// Finish the variable by parsing the RHS.
|
|
460
|
+
let equationReader = new EquationReader(v)
|
|
461
|
+
equationReader.read()
|
|
462
|
+
}
|
|
463
|
+
//
|
|
464
|
+
// Model API
|
|
465
|
+
//
|
|
466
|
+
function addVariable(v) {
|
|
467
|
+
// Add the variable to the variables list.
|
|
468
|
+
variables.push(v)
|
|
469
|
+
|
|
470
|
+
// Add to the map of variables by name
|
|
471
|
+
let varsForName = variablesByName.get(v.varName)
|
|
472
|
+
if (!varsForName) {
|
|
473
|
+
varsForName = []
|
|
474
|
+
variablesByName.set(v.varName, varsForName)
|
|
475
|
+
}
|
|
476
|
+
varsForName.push(v)
|
|
477
|
+
}
|
|
478
|
+
function isNonAtoAName(varName) {
|
|
479
|
+
return R.has(varName, nonAtoANames)
|
|
480
|
+
}
|
|
481
|
+
function expansionFlags(varName) {
|
|
482
|
+
return nonAtoANames[varName]
|
|
483
|
+
}
|
|
484
|
+
function allVars() {
|
|
485
|
+
// Return all vars except placeholders.
|
|
486
|
+
function isNotPlaceholderVar(v) {
|
|
487
|
+
return v.varName !== '_time'
|
|
488
|
+
}
|
|
489
|
+
return R.filter(isNotPlaceholderVar, variables)
|
|
490
|
+
}
|
|
491
|
+
function constVars() {
|
|
492
|
+
return vsort(varsOfType('const'))
|
|
493
|
+
}
|
|
494
|
+
function lookupVars() {
|
|
495
|
+
return vsort(varsOfType('lookup'))
|
|
496
|
+
}
|
|
497
|
+
function dataVars() {
|
|
498
|
+
return vsort(varsOfType('data'))
|
|
499
|
+
}
|
|
500
|
+
function auxVars() {
|
|
501
|
+
// console.error('AUX VARS');
|
|
502
|
+
return sortVarsOfType('aux')
|
|
503
|
+
}
|
|
504
|
+
function levelVars() {
|
|
505
|
+
// console.error('LEVEL VARS');
|
|
506
|
+
return sortVarsOfType('level')
|
|
507
|
+
}
|
|
508
|
+
function initVars() {
|
|
509
|
+
// console.error('INIT VARS');
|
|
510
|
+
return sortInitVars()
|
|
511
|
+
}
|
|
512
|
+
function varWithRefId(refId) {
|
|
513
|
+
const findVarWithRefId = rid => {
|
|
514
|
+
// First see if we have a map key where ref id matches the var name
|
|
515
|
+
let varsForName = variablesByName.get(rid)
|
|
516
|
+
if (varsForName) {
|
|
517
|
+
const v = R.find(R.propEq('refId', rid), varsForName)
|
|
518
|
+
if (v) {
|
|
519
|
+
return v
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Failing that, chop off the subscript part of the ref id and
|
|
524
|
+
// find the variables that share that name
|
|
525
|
+
const varNamePart = rid.split('[')[0]
|
|
526
|
+
varsForName = variablesByName.get(varNamePart)
|
|
527
|
+
if (varsForName) {
|
|
528
|
+
const v = R.find(R.propEq('refId', rid), varsForName)
|
|
529
|
+
if (v) {
|
|
530
|
+
return v
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return undefined
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Find a variable from a reference id.
|
|
538
|
+
// A direct reference will find scalar vars, apply-to-all arrays, and non-apply-to-all array
|
|
539
|
+
// elements defined by individual index.
|
|
540
|
+
let refVar = findVarWithRefId(refId)
|
|
541
|
+
if (!refVar) {
|
|
542
|
+
// Look at variables with the reference's varName to find one with matching subscripts.
|
|
543
|
+
let refIdParts = splitRefId(refId)
|
|
544
|
+
let refVarName = refIdParts.varName
|
|
545
|
+
let refSubscripts = refIdParts.subscripts
|
|
546
|
+
let varRefIds = refIdsWithName(refVarName)
|
|
547
|
+
for (const varRefId of varRefIds) {
|
|
548
|
+
let { subscripts } = splitRefId(varRefId)
|
|
549
|
+
// Compare subscripts at each position in normal order. If the var name does not have subscripts,
|
|
550
|
+
// the match will succeed, since the var is an apply-to-all array that includes the refId.
|
|
551
|
+
let matches = true
|
|
552
|
+
for (let pos = 0; pos < subscripts.length; pos++) {
|
|
553
|
+
// If both subscripts are an index or dimension, they must match.
|
|
554
|
+
if (
|
|
555
|
+
(isIndex(subscripts[pos]) && isIndex(refSubscripts[pos])) ||
|
|
556
|
+
(isDimension(subscripts[pos]) && isDimension(refSubscripts[pos]))
|
|
557
|
+
) {
|
|
558
|
+
if (subscripts[pos] !== refSubscripts[pos]) {
|
|
559
|
+
matches = false
|
|
560
|
+
break
|
|
561
|
+
}
|
|
562
|
+
} else if (isDimension(subscripts[pos]) && isIndex(refSubscripts[pos])) {
|
|
563
|
+
// If the ref subscript is an index and the var subscript is a dimension,
|
|
564
|
+
// they match if the dimension includes the index.
|
|
565
|
+
if (!sub(subscripts[pos]).value.includes(refSubscripts[pos])) {
|
|
566
|
+
matches = false
|
|
567
|
+
break
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
// We should not encounter a case where the ref subscript is a dimension
|
|
571
|
+
// and the var subscript is an index.
|
|
572
|
+
matches = false
|
|
573
|
+
break
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (matches) {
|
|
577
|
+
refVar = findVarWithRefId(varRefId)
|
|
578
|
+
break
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (!refVar) {
|
|
582
|
+
vlog('ERROR: no var found for refId', refId)
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return refVar
|
|
586
|
+
}
|
|
587
|
+
function splitRefId(refId) {
|
|
588
|
+
// Split a refId into component parts with a regular expression matching var name and subscripts.
|
|
589
|
+
let re = /\w+|\[/g
|
|
590
|
+
let inSubs = false
|
|
591
|
+
let varName = ''
|
|
592
|
+
let subscripts = []
|
|
593
|
+
let m
|
|
594
|
+
while ((m = re.exec(refId))) {
|
|
595
|
+
if (m[0] === '[') {
|
|
596
|
+
inSubs = true
|
|
597
|
+
} else if (inSubs) {
|
|
598
|
+
subscripts.push(m[0])
|
|
599
|
+
} else {
|
|
600
|
+
varName = m[0]
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// Put subscripts in normal order.
|
|
604
|
+
subscripts = normalizeSubscripts(subscripts)
|
|
605
|
+
return { varName, subscripts }
|
|
606
|
+
}
|
|
607
|
+
function varWithName(varName) {
|
|
608
|
+
// Find a variable with the given name in canonical form.
|
|
609
|
+
// The function returns the first instance of a non-apply-to-all variable with the name.
|
|
610
|
+
const varsForName = variablesByName.get(varName)
|
|
611
|
+
if (varsForName && varsForName.length > 0) {
|
|
612
|
+
return varsForName[0]
|
|
613
|
+
} else {
|
|
614
|
+
return undefined
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function varsWithName(varName) {
|
|
618
|
+
// Find all variables with the given name in canonical form.
|
|
619
|
+
return variablesByName.get(varName) || []
|
|
620
|
+
}
|
|
621
|
+
function refIdsWithName(varName) {
|
|
622
|
+
// Find refIds of all variables with the given name in canonical form.
|
|
623
|
+
return varsWithName(varName).map(v => v.refId)
|
|
624
|
+
}
|
|
625
|
+
function varNames() {
|
|
626
|
+
// Return a sorted list of var names.
|
|
627
|
+
return R.uniq(Array.from(variablesByName.keys())).sort()
|
|
628
|
+
}
|
|
629
|
+
function vensimName(cVarName) {
|
|
630
|
+
// Convert a C variable name to a Vensim name.
|
|
631
|
+
let result = cVarName
|
|
632
|
+
// Get the variable name and subscripts with regexes.
|
|
633
|
+
let m = cVarName.match(/(_[A-Za-z0-9_]+)((\[\d+\])*)/)
|
|
634
|
+
if (m) {
|
|
635
|
+
let varName = m[1]
|
|
636
|
+
let indexNumbers = []
|
|
637
|
+
for (let x of m[2].matchAll(/\[(\d+)\]/g)) {
|
|
638
|
+
indexNumbers.push(x[1])
|
|
639
|
+
}
|
|
640
|
+
// Get the subscript families and look up the subscript names.
|
|
641
|
+
let subscripts = []
|
|
642
|
+
let v = varWithName(varName)
|
|
643
|
+
if (v) {
|
|
644
|
+
// Ensure that the C var name is subscripted when the var has subscripts.
|
|
645
|
+
if (R.isEmpty(v.subscripts) || !R.isEmpty(indexNumbers)) {
|
|
646
|
+
m = v.modelLHS.match(/[^[]+/)
|
|
647
|
+
if (m) {
|
|
648
|
+
result = m[0]
|
|
649
|
+
}
|
|
650
|
+
let families = subscriptFamilies(v.subscripts)
|
|
651
|
+
for (let i = 0; i < families.length; i++) {
|
|
652
|
+
let indexNames = indexNamesForSubscript(families[i])
|
|
653
|
+
let indexNumber = Number.parseInt(indexNumbers[i])
|
|
654
|
+
let indexModelName = decanonicalize(indexNames[indexNumber])
|
|
655
|
+
subscripts.push(indexModelName)
|
|
656
|
+
}
|
|
657
|
+
if (!R.isEmpty(subscripts)) {
|
|
658
|
+
result += `[${subscripts.join(',')}]`
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
console.error(`${cVarName} has no subscripts in vensimName`)
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
console.error(`no var with name ${varName} in vensimName`)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return result
|
|
668
|
+
}
|
|
669
|
+
function cName(vensimVarName) {
|
|
670
|
+
// Convert a Vensim variable name to a C name.
|
|
671
|
+
// This function requires model analysis to be completed first when the variable has subscripts.
|
|
672
|
+
return new VarNameReader().read(vensimVarName)
|
|
673
|
+
}
|
|
674
|
+
function isInputVar(varName) {
|
|
675
|
+
// Return true if the given variable (in canonical form) is included in the list of
|
|
676
|
+
// input variables in the spec file.
|
|
677
|
+
return inputVars.includes(varName)
|
|
678
|
+
}
|
|
679
|
+
function addConstantExpr(exprText, constantValue) {
|
|
680
|
+
// Record the constant value for the given expression in a map for later lookup.
|
|
681
|
+
constantExprs.set(exprText, constantValue)
|
|
682
|
+
}
|
|
683
|
+
function getConstantExprValue(exprText) {
|
|
684
|
+
// Return the constant value for the given expression if one was recorded.
|
|
685
|
+
return constantExprs.get(exprText)
|
|
686
|
+
}
|
|
687
|
+
//
|
|
688
|
+
// Helpers for getting lists of vars
|
|
689
|
+
//
|
|
690
|
+
function varsOfType(varType, vars = null) {
|
|
691
|
+
// Extract vars of the given var type.
|
|
692
|
+
if (!vars) {
|
|
693
|
+
vars = variables
|
|
694
|
+
}
|
|
695
|
+
function pass(v) {
|
|
696
|
+
return v.varType === varType && v.varName !== '_time'
|
|
697
|
+
}
|
|
698
|
+
return R.filter(pass, vars)
|
|
699
|
+
}
|
|
700
|
+
function sortVarsOfType(varType) {
|
|
701
|
+
if (PRINT_SORTED_VARS) {
|
|
702
|
+
console.error(varType.toUpperCase())
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Get vars with varType 'aux' or 'level' sorted in dependency order at eval time.
|
|
706
|
+
// Start with vars of the given varType.
|
|
707
|
+
let vars = varsOfType(varType)
|
|
708
|
+
|
|
709
|
+
// Accumulate a list of variable dependencies as var pairs.
|
|
710
|
+
let graph = R.unnest(R.map(v => refs(v), vars))
|
|
711
|
+
function refs(v) {
|
|
712
|
+
// Return a list of dependency pairs for all vars referenced by v at eval time.
|
|
713
|
+
let refs = R.map(refId => varWithRefId(refId), v.references)
|
|
714
|
+
// Only consider references having the correct var type.
|
|
715
|
+
// Remove duplicate references.
|
|
716
|
+
refs = R.uniq(R.filter(R.propEq('varType', varType), refs))
|
|
717
|
+
// Return the list of dependencies as refId pairs.
|
|
718
|
+
return R.map(ref => {
|
|
719
|
+
if (v.varType === 'level' && ref.varType === 'level') {
|
|
720
|
+
// Reverse the order of level-to-level references so that level evaluation refers
|
|
721
|
+
// to the value in the previous time step rather than the currently evaluated one.
|
|
722
|
+
return [ref.refId, v.refId]
|
|
723
|
+
} else {
|
|
724
|
+
return [v.refId, ref.refId]
|
|
725
|
+
}
|
|
726
|
+
}, refs)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Sort into an lhs dependency list.
|
|
730
|
+
if (PRINT_AUX_GRAPH) printDepsGraph(graph, 'AUX')
|
|
731
|
+
if (PRINT_LEVEL_GRAPH) printDepsGraph(graph, 'LEVEL')
|
|
732
|
+
let deps
|
|
733
|
+
try {
|
|
734
|
+
deps = toposort(graph).reverse()
|
|
735
|
+
} catch (e) {
|
|
736
|
+
console.error(e.message)
|
|
737
|
+
process.exit(1)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Turn the dependency-sorted var name list into a var list.
|
|
741
|
+
let sortedVars = varsOfType(
|
|
742
|
+
varType,
|
|
743
|
+
R.map(refId => varWithRefId(refId), deps)
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
// Add the ref ids to a set for faster lookup in the next step
|
|
747
|
+
const sortedVarRefIds = new Set()
|
|
748
|
+
for (const v of sortedVars) {
|
|
749
|
+
sortedVarRefIds.add(v.refId)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Find vars of the given varType with no dependencies, and add them to the list.
|
|
753
|
+
const nodepVars = R.filter(v => !sortedVarRefIds.has(v.refId), vars)
|
|
754
|
+
const sortedNodepVars = vsort(nodepVars)
|
|
755
|
+
sortedVars = R.concat(sortedNodepVars, sortedVars)
|
|
756
|
+
|
|
757
|
+
if (PRINT_SORTED_VARS) {
|
|
758
|
+
sortedVars.forEach(v => console.error(`${v.refId}`))
|
|
759
|
+
}
|
|
760
|
+
return sortedVars
|
|
761
|
+
}
|
|
762
|
+
function sortInitVars() {
|
|
763
|
+
if (PRINT_SORTED_VARS) {
|
|
764
|
+
console.error('INIT')
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Get dependencies at init time for vars with init values, such as levels.
|
|
768
|
+
// This will be a subgraph of all dependencies rooted in vars with init values.
|
|
769
|
+
// Therefore, we have to recurse into dependencies starting with those vars.
|
|
770
|
+
let initVars = R.filter(R.propEq('hasInitValue', true), variables)
|
|
771
|
+
// vlog('initVars.length', initVars.length);
|
|
772
|
+
|
|
773
|
+
// Copy the list so we can mutate it and have the original list later.
|
|
774
|
+
// This starts a queue of vars to examine. Referenced var will be added to the queue.
|
|
775
|
+
let vars = R.map(v => v.copy(), initVars)
|
|
776
|
+
// printVars(vars);
|
|
777
|
+
// R.forEach(v => { console.error(v.refId); console.error(v.references); }, vars);
|
|
778
|
+
|
|
779
|
+
// Keep track of which var ref ids are currently in the queue for faster lookup
|
|
780
|
+
const queueRefIds = new Set()
|
|
781
|
+
for (const v of vars) {
|
|
782
|
+
queueRefIds.add(v.refId)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Build a map of dependencies indexed by the lhs of each var.
|
|
786
|
+
const depsMap = new Map()
|
|
787
|
+
while (vars.length > 0) {
|
|
788
|
+
let v = vars.pop()
|
|
789
|
+
queueRefIds.delete(v.refId)
|
|
790
|
+
// console.error(`- ${v.refId} (${vars.length})`);
|
|
791
|
+
addDepsToMap(v)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function addDepsToMap(v) {
|
|
795
|
+
// Add dependencies of var v to the map when they are not already present.
|
|
796
|
+
// Use init references for vars such as levels that have an initial value.
|
|
797
|
+
let refIds = v.hasInitValue ? v.initReferences : v.references
|
|
798
|
+
// console.error(`${v.refId} ${refIds.length}`);
|
|
799
|
+
if (refIds.length > 0) {
|
|
800
|
+
// console.error(`${v.refId}`);
|
|
801
|
+
// Add dependencies for each referenced var.
|
|
802
|
+
depsMap.set(v.refId, refIds)
|
|
803
|
+
// console.error(`→ ${v.refId}`);
|
|
804
|
+
R.forEach(refId => {
|
|
805
|
+
// Add each dependency onto the queue if it has not already been analyzed.
|
|
806
|
+
if (!depsMap.get(refId)) {
|
|
807
|
+
// console.error(refId);
|
|
808
|
+
let refVar = varWithRefId(refId)
|
|
809
|
+
if (refVar) {
|
|
810
|
+
if (refVar.varType !== 'const' && !queueRefIds.has(refVar.refId)) {
|
|
811
|
+
vars.push(refVar)
|
|
812
|
+
queueRefIds.add(refVar.refId)
|
|
813
|
+
// console.error(`+ ${refVar.refId}`);
|
|
814
|
+
}
|
|
815
|
+
} else {
|
|
816
|
+
console.error(`no var with refId for ${refId}, referenced by ${v.refId}`)
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}, refIds)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Construct a dependency graph in the form of [var name, dependency var name] pairs.
|
|
824
|
+
// We use refIds instead of vars here because the deps are stated in refIds.
|
|
825
|
+
let graph = []
|
|
826
|
+
// vlog('depsMap', depsMap);
|
|
827
|
+
for (let refId of depsMap.keys()) {
|
|
828
|
+
R.forEach(dep => graph.push([refId, dep]), depsMap.get(refId))
|
|
829
|
+
}
|
|
830
|
+
if (PRINT_INIT_GRAPH) printDepsGraph(graph, 'INIT')
|
|
831
|
+
|
|
832
|
+
// Sort into a reference id dependency list.
|
|
833
|
+
let deps
|
|
834
|
+
try {
|
|
835
|
+
deps = toposort(graph).reverse()
|
|
836
|
+
} catch (e) {
|
|
837
|
+
console.error(e.message)
|
|
838
|
+
process.exit(1)
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Turn the reference id list into a var list.
|
|
842
|
+
let sortedVars = R.map(refId => varWithRefId(refId), deps)
|
|
843
|
+
|
|
844
|
+
// Filter out vars with constant values.
|
|
845
|
+
sortedVars = R.reject(
|
|
846
|
+
R.propSatisfies(varType => varType === 'const' || varType === 'lookup' || varType === 'data', 'varType'),
|
|
847
|
+
sortedVars
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
// Add the ref ids to a set for faster lookup in the next step
|
|
851
|
+
const sortedVarRefIds = new Set()
|
|
852
|
+
for (const v of sortedVars) {
|
|
853
|
+
sortedVarRefIds.add(v.refId)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Find vars with init values but no dependencies, and add them to the list.
|
|
857
|
+
const nodepVars = R.filter(v => !sortedVarRefIds.has(v.refId), initVars)
|
|
858
|
+
const sortedNodepVars = vsort(nodepVars)
|
|
859
|
+
sortedVars = R.concat(sortedNodepVars, sortedVars)
|
|
860
|
+
|
|
861
|
+
if (PRINT_SORTED_VARS) {
|
|
862
|
+
sortedVars.forEach(v => console.error(`${v.refId}`))
|
|
863
|
+
}
|
|
864
|
+
return sortedVars
|
|
865
|
+
}
|
|
866
|
+
//
|
|
867
|
+
// Helpers for refIds
|
|
868
|
+
//
|
|
869
|
+
function refIdForVar(v) {
|
|
870
|
+
// Start a reference id using the variable name.
|
|
871
|
+
let refId = v.varName
|
|
872
|
+
// References to apply-to-all arrays reference the entire array, so no subscripts
|
|
873
|
+
// are required in the refId.
|
|
874
|
+
if (v.hasSubscripts() && isNonAtoAName(v.varName)) {
|
|
875
|
+
// Add subscripts already sorted in normal form for references to non-apply-to-all arrays.
|
|
876
|
+
refId += `[${v.subscripts.join(',')}]`
|
|
877
|
+
}
|
|
878
|
+
return refId
|
|
879
|
+
}
|
|
880
|
+
//
|
|
881
|
+
// Helpers for model analysis
|
|
882
|
+
//
|
|
883
|
+
function printVarList() {
|
|
884
|
+
// Print full information on each var.
|
|
885
|
+
B.clearBuf()
|
|
886
|
+
let vars = R.sortBy(R.prop('refId'), variables)
|
|
887
|
+
for (const v of vars) {
|
|
888
|
+
printVar(v)
|
|
889
|
+
}
|
|
890
|
+
return B.getBuf()
|
|
891
|
+
}
|
|
892
|
+
function yamlVarList() {
|
|
893
|
+
// Print selected properties of all variable objects to a YAML string.
|
|
894
|
+
let vars = R.sortBy(
|
|
895
|
+
R.prop('refId'),
|
|
896
|
+
R.map(v => filterVar(v), variables)
|
|
897
|
+
)
|
|
898
|
+
return yaml.safeDump(vars)
|
|
899
|
+
}
|
|
900
|
+
function printVar(v) {
|
|
901
|
+
let nonAtoA = isNonAtoAName(v.varName) ? ' (non-apply-to-all)' : ''
|
|
902
|
+
B.emitLine(`${v.modelLHS}: ${v.varType}${nonAtoA}`)
|
|
903
|
+
if (!v.hasPoints()) {
|
|
904
|
+
B.emitLine(`= ${v.modelFormula}`)
|
|
905
|
+
}
|
|
906
|
+
B.emitLine(`refId(${v.refId})`)
|
|
907
|
+
if (v.hasSubscripts()) {
|
|
908
|
+
B.emitLine(`families(${strlist(subscriptFamilies(v.subscripts))})`)
|
|
909
|
+
B.emitLine(`subscripts(${strlist(v.subscripts)})`)
|
|
910
|
+
}
|
|
911
|
+
if (v.separationDims.length > 0) {
|
|
912
|
+
B.emitLine(`separationDims(${strlist(v.separationDims)})`)
|
|
913
|
+
}
|
|
914
|
+
B.emitLine(`hasInitValue(${v.hasInitValue})`)
|
|
915
|
+
if (v.references.length > 0) {
|
|
916
|
+
B.emitLine(`refs(${strlist(v.references)})`)
|
|
917
|
+
}
|
|
918
|
+
if (v.initReferences.length > 0) {
|
|
919
|
+
B.emitLine(`initRefs(${strlist(v.initReferences)})`)
|
|
920
|
+
}
|
|
921
|
+
// if (v.hasPoints()) {
|
|
922
|
+
// B.emitLine(R.map(p => `(${p[0]}, ${p[1]})`, v.points));
|
|
923
|
+
// }
|
|
924
|
+
B.emitLine('')
|
|
925
|
+
}
|
|
926
|
+
function filterVar(v) {
|
|
927
|
+
let varObj = {}
|
|
928
|
+
varObj.refId = v.refId
|
|
929
|
+
varObj.varName = v.varName
|
|
930
|
+
if (v.hasSubscripts()) {
|
|
931
|
+
varObj.subscripts = v.subscripts
|
|
932
|
+
varObj.families = subscriptFamilies(v.subscripts)
|
|
933
|
+
}
|
|
934
|
+
if (v.references.length > 0) {
|
|
935
|
+
varObj.references = v.references
|
|
936
|
+
}
|
|
937
|
+
varObj.hasInitValue = v.hasInitValue
|
|
938
|
+
if (v.initReferences.length > 0) {
|
|
939
|
+
varObj.initReferences = v.initReferences
|
|
940
|
+
}
|
|
941
|
+
varObj.varType = v.varType
|
|
942
|
+
if (v.separationDims.length > 0) {
|
|
943
|
+
varObj.separationDims = v.separationDims
|
|
944
|
+
}
|
|
945
|
+
varObj.modelLHS = v.modelLHS
|
|
946
|
+
varObj.modelFormula = v.modelFormula
|
|
947
|
+
return varObj
|
|
948
|
+
}
|
|
949
|
+
function printRefIdTest() {
|
|
950
|
+
// Verify that each variable has the correct number of instances of the var name.
|
|
951
|
+
R.forEach(v => {
|
|
952
|
+
let varName = v.varName
|
|
953
|
+
let vars = varsWithName(varName)
|
|
954
|
+
if (v.hasSubscripts()) {
|
|
955
|
+
if (isNonAtoAName(varName)) {
|
|
956
|
+
// A non-apply-to-all array has more than one instance of the var name in practice.
|
|
957
|
+
if (vars.length < 2) {
|
|
958
|
+
vlog('ERROR: only one instance of non-apply-to-all array', varName)
|
|
959
|
+
}
|
|
960
|
+
} else {
|
|
961
|
+
// An apply-to-all array should have only one instance of the var name.
|
|
962
|
+
if (vars.length > 1) {
|
|
963
|
+
vlog('ERROR: more than one instance of apply-to-all array', varName)
|
|
964
|
+
// printVars(vars)
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
// The var is a scalar and should only have one instance of the var name.
|
|
969
|
+
if (vars.length > 1) {
|
|
970
|
+
vlog('ERROR: more than one instance of scalar var', varName)
|
|
971
|
+
// printVars(vars)
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}, variables)
|
|
975
|
+
// Verify that each refId in references exists as the refId of a concrete variable.
|
|
976
|
+
R.forEach(v => {
|
|
977
|
+
R.forEach(refId => checkRefVar(refId), v.references)
|
|
978
|
+
R.forEach(refId => checkRefVar(refId), v.initReferences)
|
|
979
|
+
}, variables)
|
|
980
|
+
function checkRefVar(refId) {
|
|
981
|
+
let refVar = R.find(R.propEq('refId', refId), variables)
|
|
982
|
+
if (!refVar) {
|
|
983
|
+
vlog('ERROR: no var for refId', refId)
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function printRefGraph(varName) {
|
|
988
|
+
// Walk the reference tree rooted at varName and print it out in indented form.
|
|
989
|
+
let printRefs = (v, indent, stack) => {
|
|
990
|
+
for (let refId of v.references) {
|
|
991
|
+
// Exclude a variable here to limit the depth of the search.
|
|
992
|
+
// if (!refId.startsWith('_policy_levels')) {
|
|
993
|
+
if (!stack.includes(refId)) {
|
|
994
|
+
console.log(`${' '.repeat(indent)}${refId}`)
|
|
995
|
+
let refVar = R.find(R.propEq('refId', refId), variables)
|
|
996
|
+
printRefs(refVar, indent + 1, R.append(refId, stack))
|
|
997
|
+
}
|
|
998
|
+
// }
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
for (let v of varsWithName(varName)) {
|
|
1002
|
+
console.log(v.varName)
|
|
1003
|
+
printRefs(v, 1, [])
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function printDepsGraph(graph, varType) {
|
|
1007
|
+
// The dependency graph is an array of pairs.
|
|
1008
|
+
console.error(`${varType} GRAPH`)
|
|
1009
|
+
for (const dep of graph) {
|
|
1010
|
+
console.error(`${dep[0]} → ${dep[1]}`)
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
export default {
|
|
1014
|
+
addConstantExpr,
|
|
1015
|
+
addEquation,
|
|
1016
|
+
addNonAtoAVar,
|
|
1017
|
+
addVariable,
|
|
1018
|
+
allVars,
|
|
1019
|
+
auxVars,
|
|
1020
|
+
cName,
|
|
1021
|
+
constVars,
|
|
1022
|
+
dataVars,
|
|
1023
|
+
expansionFlags,
|
|
1024
|
+
filterVar,
|
|
1025
|
+
getConstantExprValue,
|
|
1026
|
+
initVars,
|
|
1027
|
+
isInputVar,
|
|
1028
|
+
isNonAtoAName,
|
|
1029
|
+
levelVars,
|
|
1030
|
+
lookupVars,
|
|
1031
|
+
printRefGraph,
|
|
1032
|
+
printRefIdTest,
|
|
1033
|
+
printVarList,
|
|
1034
|
+
read,
|
|
1035
|
+
refIdForVar,
|
|
1036
|
+
refIdsWithName,
|
|
1037
|
+
splitRefId,
|
|
1038
|
+
variables,
|
|
1039
|
+
varNames,
|
|
1040
|
+
varsWithName,
|
|
1041
|
+
varWithName,
|
|
1042
|
+
varWithRefId,
|
|
1043
|
+
vensimName,
|
|
1044
|
+
yamlVarList
|
|
1045
|
+
}
|