Skip to content

Commit

Permalink
feat: implement GET DIRECT CONSTANTS for CSV (#86)
Browse files Browse the repository at this point in the history
Fixes #83 

Co-authored-by: Chris Campbell <[email protected]>
  • Loading branch information
ToddFincannon and chrispcampbell authored Jul 26, 2021
1 parent b40e738 commit beedd4f
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 46 deletions.
2 changes: 2 additions & 0 deletions models/directconst/data/a.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
a,
,2050
4 changes: 4 additions & 0 deletions models/directconst/data/b.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
b,
A1,1
A2,2
A3,3
4 changes: 4 additions & 0 deletions models/directconst/data/c.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
c,C1,C2
B1,1,2
B2,3,4
B3,5,6
53 changes: 53 additions & 0 deletions models/directconst/directconst.dat
Original file line number Diff line number Diff line change
@@ -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
84 changes: 84 additions & 0 deletions models/directconst/directconst.mdl
Original file line number Diff line number Diff line change
@@ -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:
2 changes: 1 addition & 1 deletion src/CodeGen.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
165 changes: 120 additions & 45 deletions src/EquationGen.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,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.
Expand All @@ -127,7 +131,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
Expand Down Expand Up @@ -248,7 +257,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) {
Expand Down Expand Up @@ -304,6 +315,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()}`);
Expand All @@ -319,12 +337,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 []
}
Expand Down Expand Up @@ -374,7 +398,94 @@ 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 csvPathname = path.resolve(this.modelDirname, file)
let data = readCsv(csvPathname, 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
Expand Down Expand Up @@ -428,7 +539,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
Expand Down Expand Up @@ -470,45 +583,7 @@ 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
//
Expand Down
Loading

0 comments on commit beedd4f

Please sign in to comment.