Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement GET DIRECT CONSTANTS for CSV #86

Merged
merged 12 commits into from
Jul 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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