diff --git a/models/directconst/data/a.csv b/models/directconst/data/a.csv new file mode 100644 index 00000000..9b0ed36a --- /dev/null +++ b/models/directconst/data/a.csv @@ -0,0 +1,2 @@ +a, +,2050 \ No newline at end of file diff --git a/models/directconst/data/b.csv b/models/directconst/data/b.csv new file mode 100644 index 00000000..b4b3b5e2 --- /dev/null +++ b/models/directconst/data/b.csv @@ -0,0 +1,4 @@ +b, +A1,1 +A2,2 +A3,3 \ No newline at end of file diff --git a/models/directconst/data/c.csv b/models/directconst/data/c.csv new file mode 100644 index 00000000..517ee623 --- /dev/null +++ b/models/directconst/data/c.csv @@ -0,0 +1,4 @@ +c,C1,C2 +B1,1,2 +B2,3,4 +B3,5,6 \ No newline at end of file diff --git a/models/directconst/directconst.dat b/models/directconst/directconst.dat new file mode 100644 index 00000000..1f18763a --- /dev/null +++ b/models/directconst/directconst.dat @@ -0,0 +1,53 @@ +a +0 2050 +b[B1] +0 1 +b[B2] +0 2 +b[B3] +0 3 +c[B1,C1] +0 1 +c[B1,C2] +0 2 +c[B2,C1] +0 3 +c[B2,C2] +0 4 +c[B3,C1] +0 5 +c[B3,C2] +0 6 +d[D1,B1,C1] +0 1 +d[D1,B1,C2] +0 2 +d[D1,B2,C1] +0 3 +d[D1,B2,C2] +0 4 +d[D1,B3,C1] +0 5 +d[D1,B3,C2] +0 6 +e[C1,B1] +0 1 +e[C1,B2] +0 3 +e[C1,B3] +0 5 +e[C2,B1] +0 2 +e[C2,B2] +0 4 +e[C2,B3] +0 6 +FINAL TIME +0 1 +INITIAL TIME +0 0 +SAVEPER +0 1 +1 1 +TIME STEP +0 1 diff --git a/models/directconst/directconst.mdl b/models/directconst/directconst.mdl new file mode 100644 index 00000000..83bef079 --- /dev/null +++ b/models/directconst/directconst.mdl @@ -0,0 +1,84 @@ +{UTF-8} +DimB: B1, B2, B3 ~~| +DimC: C1, C2 ~~| +DimD: D1, D2 ~~| + +a = + GET DIRECT CONSTANTS( + 'data/a.csv', + ',', + 'B2' + ) ~~~:SUPPLEMENTARY| + +b[DimB] = + GET DIRECT CONSTANTS( + 'data/b.csv', + ',', + 'B2*' + ) ~~~:SUPPLEMENTARY| + +c[DimB, DimC] = + GET DIRECT CONSTANTS( + 'data/c.csv', + ',', + 'B2' + ) ~~~:SUPPLEMENTARY| + +d[D1, DimB, DimC] = + GET DIRECT CONSTANTS( + 'data/c.csv', + ',', + 'B2' + ) ~~~:SUPPLEMENTARY| + +e[DimC, DimB] = + GET DIRECT CONSTANTS( + 'data/c.csv', + ',', + 'B2*' + ) ~~~:SUPPLEMENTARY| + +******************************************************** + .Control +********************************************************~ + Simulation Control Parameters + | + +INITIAL TIME = 0 ~~| +FINAL TIME = 1 ~~| +TIME STEP = 1 ~~| +SAVEPER = TIME STEP ~~| + +\\\---/// Sketch information - do not modify anything except names +V300 Do not put anything below this section - it will be ignored +*View 1 +$0-0-0,0,|0||0-0-0|0-0-0|0-0-0|0-0-0|0-0-0|0,0,100,0 +///---\\\ +:L<%^E!@ +1: + directconst.vdfx +4:Tim +e +5:a[DimA] + +6 +:A1 +9:directconst +19:100,0 +24:0 +25:1 +26:1 +15:0,0,0,0,0,0 +27:0, +34:0, +42:0 +72:0 +3:0 95:0 +96:0 77:0 78:0 93:0 +94:0 +92:0 +91:0 +90:0 +87:0 +75: +43: diff --git a/src/CodeGen.js b/src/CodeGen.js index 44351959..d8c8ff3a 100644 --- a/src/CodeGen.js +++ b/src/CodeGen.js @@ -253,7 +253,7 @@ ${postStep} let a = R.map(indexName => sub(indexName).value, indices) return strlist(a) } - function expandedVarNames(vensimNames) { + function expandedVarNames(vensimNames = false) { // Return a list of var names for all variables except lookups and data variables. // The names are in Vensim format if vensimNames is true, otherwise they are in C format. // Expand subscripted vars into separate var names with each index. diff --git a/src/EquationGen.js b/src/EquationGen.js index 3f0b15cd..5c4bc6df 100644 --- a/src/EquationGen.js +++ b/src/EquationGen.js @@ -99,6 +99,10 @@ export default class EquationGen extends ModelReader { } // Show the model var as a comment for reference. this.comments.push(` // ${this.var.modelLHS} = ${this.var.modelFormula.replace('\n', '')}`) + // Emit direct constants individually without separating them first. + if (this.var.directConstArgs) { + return this.generateDirectConstInit() + } // Initialize array variables with dimensions in a loop for each dimension. let dimNames = dimensionNames(this.var.subscripts) // Turn each dimension name into a loop with a loop index variable. @@ -124,7 +128,12 @@ export default class EquationGen extends ModelReader { R.map(dimName => ` }`, dimNames) ) // Assemble code from each channel into final var code output. - return this.comments.concat(this.subscriptLoopOpeningCode, this.tmpVarCode, formula, this.subscriptLoopClosingCode) + return this.comments.concat( + this.subscriptLoopOpeningCode, + this.tmpVarCode, + formula, + this.subscriptLoopClosingCode + ) } // // Helpers @@ -245,7 +254,9 @@ export default class EquationGen extends ModelReader { } // See if we need to apply a mapping because the RHS dim is not found on the LHS. try { - let found = this.var.subscripts.findIndex(lhsSub => sub(lhsSub).family === sub(rhsSub).family) + let found = this.var.subscripts.findIndex( + lhsSub => sub(lhsSub).family === sub(rhsSub).family + ) if (found < 0) { // Find the mapping from the RHS subscript to a LHS subscript. for (let lhsSub of this.var.subscripts) { @@ -301,6 +312,13 @@ export default class EquationGen extends ModelReader { // Emit the tmp var subscript just after emitting the tmp var elsewhere. this.emit(`[${this.vsoTmpDimName}[${i}]]`) } + directConstSubscriptGen(subscripts) { + // Construct numeric constant variable subscripts in normal order. + let cSubscripts = subscripts.map(s => (isDimension(s) ? sub(s).value : [s])) + let indexSubscripts = cartesianProductOf(cSubscripts) + let numericSubscripts = indexSubscripts.map(idx => idx.map(s => sub(s).value)) + return numericSubscripts.map(s => s.reduce((a, v) => a.concat(`[${v}]`), '')) + } functionIsLookup() { // See if the function name in the current call is actually a lookup. // console.error(`isLookup ${this.lookupName()}`); @@ -316,12 +334,18 @@ export default class EquationGen extends ModelReader { // at init time. Using static arrays is better for code size, helps us avoid creating a copy of // the data in memory, and seems to perform much better when compiled to wasm when compared to the // previous approach that used varargs + copying, especially on constrained (e.g. iOS) devices. - let data = R.reduce((a, p) => listConcat(a, `${cdbl(p[0])}, ${cdbl(p[1])}`, true), '', this.var.points) + let data = R.reduce( + (a, p) => listConcat(a, `${cdbl(p[0])}, ${cdbl(p[1])}`, true), + '', + this.var.points + ) return [`double ${dataName}[${this.var.points.length * 2}] = { ${data} };`] } else if (this.mode === 'init-lookups') { // In init mode, create the `Lookup`, passing in a pointer to the static data array declared earlier. // TODO: Make use of the lookup range - return [` ${this.lhs} = __new_lookup(${this.var.points.length}, /*copy=*/false, ${dataName});`] + return [ + ` ${this.lhs} = __new_lookup(${this.var.points.length}, /*copy=*/false, ${dataName});` + ] } else { return [] } @@ -370,7 +394,93 @@ export default class EquationGen extends ModelReader { } return result } - + generateDirectDataLookup(getCellValue, timeRowOrCol, startCell, indexNum) { + // Read a row or column of data as (time, value) pairs from the worksheet. + // The cell(c,r) function wraps data access by column and row. + let dataCol, dataRow, dataValue, timeCol, timeRow, timeValue, nextCell + let lookupData = '' + let lookupSize = 0 + let dataAddress = XLSX.utils.decode_cell(startCell) + dataCol = dataAddress.c + dataRow = dataAddress.r + if (isNaN(parseInt(timeRowOrCol))) { + // Time values are in a column. + timeCol = XLSX.utils.decode_col(timeRowOrCol) + timeRow = dataRow + dataCol += indexNum + nextCell = () => { + dataRow++ + timeRow++ + } + } else { + // Time values are in a row. + timeCol = dataCol + timeRow = XLSX.utils.decode_row(timeRowOrCol) + dataRow += indexNum + nextCell = () => { + dataCol++ + timeCol++ + } + } + timeValue = getCellValue(timeCol, timeRow) + dataValue = getCellValue(dataCol, dataRow) + while (timeValue != null && dataValue != null) { + lookupData = listConcat(lookupData, `${timeValue}, ${dataValue}`, true) + lookupSize++ + nextCell() + dataValue = getCellValue(dataCol, dataRow) + timeValue = getCellValue(timeCol, timeRow) + } + return [ + ` ${this.lhs} = __new_lookup(${lookupSize}, /*copy=*/true, (double[]){ ${lookupData} });` + ] + } + generateDirectConstInit() { + // Map zero, one, or two dimensions on the LHS in model order to a table of numbers in a CSV file. + let result = this.comments + let { file, tab, startCell } = this.var.directConstArgs + let data = readCsv(file, tab) + if (data) { + let getCellValue = (c, r) => (data[r] != null && data[r][c] != null ? cdbl(data[r][c]) : null) + let modelLHSReader = new ModelLHSReader() + modelLHSReader.read(this.var.modelLHS) + // Get C subscripts in text form for the LHS in normal order. + let lhsSubscripts = this.directConstSubscriptGen(this.var.subscripts) + // Generate cell offsets for the data table corresponding to each LHS subscript. + let dimNames = this.var.subscripts.filter(s => isDimension(s)) + let inds = dimNames.map(dim => [...Array(sub(dim).size).keys()]) + // Add a second dimension if necessary to get row, column pairs. + if (inds.length === 1) { + inds.unshift([0]) + } + // Read values by column first when the start cell ends with an asterisk. + // Ref: https://www.vensim.com/documentation/fn_get_direct_constants.html + if (startCell.endsWith('*')) { + inds.reverse() + startCell = startCell.slice(0, -1) + } + // If there are two data dimensions and the model order differs from normal order, transpose them. + if (dimNames.length > 1) { + let modelDimNames = modelLHSReader.modelSubscripts.filter(s => isDimension(s)) + if (dimNames[0] !== modelDimNames[0]) { + inds.reverse() + } + } + // Read CSV data into an indexed variable for each cell. + let cellOffsets = cartesianProductOf(inds) + let dataAddress = XLSX.utils.decode_cell(startCell) + let startCol = dataAddress.c + let startRow = dataAddress.r + for (let i = 0; i < cellOffsets.length; i++) { + let rowOffset = cellOffsets[i][0] ? cellOffsets[i][0] : 0 + let colOffset = cellOffsets[i][1] ? cellOffsets[i][1] : 0 + let dataValue = getCellValue(startCol + colOffset, startRow + rowOffset) + let lhs = `${this.var.varName}${lhsSubscripts[i] || ''}` + result.push(` ${lhs} = ${dataValue};`) + } + } + return result + } generateExternalDataInit() { // If there is external data for this variable, copy it from an external file to a lookup. // Just like in generateLookup(), we declare static arrays to hold the data points in the first pass @@ -424,7 +534,9 @@ export default class EquationGen extends ModelReader { // We don't yet handle the case where there are more than one subscript or a mix of // subscripts and dimensions // TODO: Remove this restriction - throw new Error(`ERROR: Data variable ${this.var.varName} has >= 2 subscripts; not yet handled`) + throw new Error( + `ERROR: Data variable ${this.var.varName} has >= 2 subscripts; not yet handled` + ) } // At this point, we know that we have one or more dimensions; compute all combinations @@ -466,45 +578,6 @@ export default class EquationGen extends ModelReader { } return result } - generateDirectDataLookup(getCellValue, timeRowOrCol, startCell, indexNum) { - // Read a row or column of data as (time, value) pairs from the worksheet. - // The cell(c,r) function wraps data access by column and row. - let dataCol, dataRow, dataValue, timeCol, timeRow, timeValue, nextCell - let lookupData = '' - let lookupSize = 0 - let dataAddress = XLSX.utils.decode_cell(startCell) - dataCol = dataAddress.c - dataRow = dataAddress.r - if (isNaN(parseInt(timeRowOrCol))) { - // Time values are in a column. - timeCol = XLSX.utils.decode_col(timeRowOrCol) - timeRow = dataRow - dataCol += indexNum - nextCell = () => { - dataRow++ - timeRow++ - } - } else { - // Time values are in a row. - timeCol = dataCol - timeRow = XLSX.utils.decode_row(timeRowOrCol) - dataRow += indexNum - nextCell = () => { - dataCol++ - timeCol++ - } - } - timeValue = getCellValue(timeCol, timeRow) - dataValue = getCellValue(dataCol, dataRow) - while (timeValue != null && dataValue != null) { - lookupData = listConcat(lookupData, `${timeValue}, ${dataValue}`, true) - lookupSize++ - nextCell() - dataValue = getCellValue(dataCol, dataRow) - timeValue = getCellValue(timeCol, timeRow) - } - return [` ${this.lhs} = __new_lookup(${lookupSize}, /*copy=*/true, (double[]){ ${lookupData} });`] - } // // Visitor callbacks // diff --git a/src/EquationReader.js b/src/EquationReader.js index b102e835..caea60b5 100644 --- a/src/EquationReader.js +++ b/src/EquationReader.js @@ -96,6 +96,8 @@ export default class EquationReader extends ModelReader { this.var.hasInitValue = true } else if (fn === '_GET_DIRECT_DATA') { this.var.varType = 'data' + } else if (fn === '_GET_DIRECT_CONSTANTS') { + this.var.varType = 'const' } super.visitCall(ctx) this.callStack.pop() @@ -141,6 +143,19 @@ export default class EquationReader extends ModelReader { timeRowOrCol: args[2], startCell: args[3] } + } else if (fn === '_GET_DIRECT_CONSTANTS') { + // Extract string constant arguments into an object used in code generation. + // The file argument gives a relative pathname in the model directory. + // The tab argument gives the delimiter character. + let args = R.map( + arg => matchRegex(arg, /'(.*)'/), + R.map(expr => expr.getText(), ctx.expr()) + ) + this.var.directConstArgs = { + file: args[0], + tab: args[1], + startCell: args[2] + } } else { // Keep track of all function names referenced in this expression. Note that lookup // variables are sometimes function-like, so they will be included here. This will be diff --git a/src/ModelLHSReader.js b/src/ModelLHSReader.js index dcfb32df..eb8e835f 100644 --- a/src/ModelLHSReader.js +++ b/src/ModelLHSReader.js @@ -14,6 +14,7 @@ export default class ModelLHSReader extends ModelReader { super() this.varName = '' this.modelLHSList = [] + this.modelSubscripts = [] } read(modelLHS) { // Parse a model LHS and return the var name without subscripts. @@ -44,6 +45,7 @@ export default class ModelLHSReader extends ModelReader { // Construct the modelLHSList array with the LHS expanded into an entry for each index // in the same format as Vensim log files. let subscripts = R.map(id => id.getText(), ctx.Id()) + this.modelSubscripts = subscripts.map(s => canonicalName(s)) let modelLHSInds = dim => { // Construct the model indices for a dimension. // If the subscript range contains a dimension, expand it into index names in place. diff --git a/src/Variable.js b/src/Variable.js index 6df29739..e5fb7b6e 100644 --- a/src/Variable.js +++ b/src/Variable.js @@ -15,6 +15,8 @@ export default class Variable { this.separationDims = [] // Direct data function arguments are saved here for use in code generation. this.directDataArgs = null + // Direct constants function arguments are saved here for use in code generation. + this.directConstArgs = null // Lookup vars have lookup points and an optional range. this.range = [] this.points = []