From 035ab5cb88a7e1ff79423cccacde3b827ce8d0dd Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Sat, 12 Sep 2020 23:33:12 -0700 Subject: [PATCH] feat: add support for external data variables that use subscript or dimension (#41) Fixes #32 --- models/extdata/data.dat | 90 +++++++++++++++++++ models/extdata/extdata.dat | 180 +++++++++++++++++++++++++++++++++++++ models/extdata/extdata.mdl | 88 ++++++++++++++++++ models/extdata/spec.json | 6 ++ src/CodeGen.js | 1 + src/EquationGen.js | 158 +++++++++++++++++++++++++------- 6 files changed, 492 insertions(+), 31 deletions(-) create mode 100644 models/extdata/data.dat create mode 100644 models/extdata/extdata.dat create mode 100644 models/extdata/extdata.mdl create mode 100644 models/extdata/spec.json diff --git a/models/extdata/data.dat b/models/extdata/data.dat new file mode 100644 index 00000000..d9f2ba6b --- /dev/null +++ b/models/extdata/data.dat @@ -0,0 +1,90 @@ +Simple 1 +0 1000 +1 2000 +2 3000 +9 4000 +10 5000 +Simple 2 +0 100 +1 200 +2 300 +9 400 +10 500 +A Values[A1] +0 0 +1 10 +2 20 +9 70 +10 80 +A Values[A2] +0 10 +1 20 +2 30 +9 80 +10 90 +A Values[A3] +0 20 +1 30 +2 40 +9 90 +10 100 +BC Values[B1,C1] +0 0 +1 10 +2 20 +9 70 +10 80 +BC Values[B1,C2] +0 10 +1 20 +2 30 +9 80 +10 90 +BC Values[B2,C1] +0 100 +1 110 +2 120 +9 170 +10 180 +BC Values[B2,C2] +0 110 +1 120 +2 130 +9 180 +10 190 +BC Values[B3,C1] +0 200 +1 210 +2 220 +9 270 +10 280 +BC Values[B3,C2] +0 210 +1 220 +2 230 +9 280 +10 290 +D Values[D1] +0 5000 +1 5100 +2 5200 +9 5300 +10 5400 +D Values[D2] +0 6000 +1 6100 +2 6200 +9 6300 +10 6400 +E Values[E1] +0 7000 +1 7100 +2 7200 +9 7300 +10 7400 +E Values[E2] +0 8000 +1 8100 +2 8200 +9 8300 +10 8400 diff --git a/models/extdata/extdata.dat b/models/extdata/extdata.dat new file mode 100644 index 00000000..db0b8522 --- /dev/null +++ b/models/extdata/extdata.dat @@ -0,0 +1,180 @@ +A Totals +0 30 +1 60 +2 90 +3 111.429 +4 132.857 +5 154.286 +6 175.714 +7 197.143 +8 218.571 +9 240 +10 270 +B1 Totals +0 10 +1 30 +2 50 +3 64.2857 +4 78.5714 +5 92.8571 +6 107.143 +7 121.429 +8 135.714 +9 150 +10 170 +D Totals +0 11000 +1 11200 +2 11400 +3 11428.6 +4 11457.1 +5 11485.7 +6 11514.3 +7 11542.9 +8 11571.4 +9 11600 +10 11800 +E1 Values +0 7000 +1 7100 +2 7200 +3 7214.29 +4 7228.57 +5 7242.86 +6 7257.14 +7 7271.43 +8 7285.71 +9 7300 +10 7400 +E2 Values +0 8000 +1 8100 +2 8200 +3 8214.29 +4 8228.57 +5 8242.86 +6 8257.14 +7 8271.43 +8 8285.71 +9 8300 +10 8400 +FINAL TIME +0 10 +INITIAL TIME +0 0 +SAVEPER +0 1 +1 1 +2 1 +3 1 +4 1 +5 1 +6 1 +7 1 +8 1 +9 1 +10 1 +Simple Totals +0 1100 +1 2200 +2 3300 +3 3457.14 +4 3614.29 +5 3771.43 +6 3928.57 +7 4085.71 +8 4242.86 +9 4400 +10 5500 +TIME STEP +0 1 +A Values[A1] +0 0 +1 10 +2 20 +9 70 +10 80 +A Values[A2] +0 10 +1 20 +2 30 +9 80 +10 90 +A Values[A3] +0 20 +1 30 +2 40 +9 90 +10 100 +BC Values[B1,C1] +0 0 +1 10 +2 20 +9 70 +10 80 +BC Values[B1,C2] +0 10 +1 20 +2 30 +9 80 +10 90 +BC Values[B2,C1] +0 100 +1 110 +2 120 +9 170 +10 180 +BC Values[B2,C2] +0 110 +1 120 +2 130 +9 180 +10 190 +BC Values[B3,C1] +0 200 +1 210 +2 220 +9 270 +10 280 +BC Values[B3,C2] +0 210 +1 220 +2 230 +9 280 +10 290 +D Values[D1] +0 5000 +1 5100 +2 5200 +9 5300 +10 5400 +D Values[D2] +0 6000 +1 6100 +2 6200 +9 6300 +10 6400 +E Values[E1] +0 7000 +1 7100 +2 7200 +9 7300 +10 7400 +E Values[E2] +0 8000 +1 8100 +2 8200 +9 8300 +10 8400 +Simple 1 +0 1000 +1 2000 +2 3000 +9 4000 +10 5000 +Simple 2 +0 100 +1 200 +2 300 +9 400 +10 500 diff --git a/models/extdata/extdata.mdl b/models/extdata/extdata.mdl new file mode 100644 index 00000000..0811da98 --- /dev/null +++ b/models/extdata/extdata.mdl @@ -0,0 +1,88 @@ +{UTF-8} + +DimA: A1, A2, A3 + ~~| + +DimB: B1, B2, B3 + ~~| + +DimC: C1, C2 + ~~| + +DimD: D1, D2 -> DimC + ~~| + +DimE: E1, E2 + ~~| + +Simple 1 + ~~| + +Simple 2 + ~~| + +A Values[DimA] + ~~| + +BC Values[DimB,DimC] + ~~| + +D Values[DimD] + ~~| + +E Values[E1] + ~~| + +E Values[E2] + ~~| + +Simple Totals = + Simple 1 + Simple 2 + ~~| + +A Totals = + SUM( A Values[DimA!] ) + ~~| + +B1 Totals = + SUM( BC Values[B1,DimC!] ) + ~~| + +D Totals = + SUM( D Values[DimD!] ) + ~~| + +E1 Values = + E Values[E1] + ~~| + +E2 Values = + E Values[E2] + ~~| + +******************************************************** + .Control +********************************************************~ + Simulation Control Parameters + | + +FINAL TIME = 10 + ~ Month + ~ The final time for the simulation. + | + +INITIAL TIME = 0 + ~ Month + ~ The initial time for the simulation. + | + +SAVEPER = + TIME STEP + ~ Month [0,?] + ~ The frequency with which output is stored. + | + +TIME STEP = 1 + ~ Month [0,?] + ~ The time step for the simulation. + | diff --git a/models/extdata/spec.json b/models/extdata/spec.json new file mode 100644 index 00000000..2833a2f0 --- /dev/null +++ b/models/extdata/spec.json @@ -0,0 +1,6 @@ +{ + "name": "extdata", + "externalDatfiles": [ + "data.dat" + ] +} diff --git a/src/CodeGen.js b/src/CodeGen.js index 1f5cb8f5..aec9b89c 100644 --- a/src/CodeGen.js +++ b/src/CodeGen.js @@ -62,6 +62,7 @@ ${dimensionMappingsSection()} // Lookup data arrays ${section(Model.lookupVars())} +${section(Model.dataVars())} ` } diff --git a/src/EquationGen.js b/src/EquationGen.js index ede03b54..496e8a1d 100644 --- a/src/EquationGen.js +++ b/src/EquationGen.js @@ -82,7 +82,11 @@ module.exports = class EquationGen extends ModelReader { generate() { // Generate code for the variable in either init or eval mode. if (this.var.isData()) { - return this.generateData() + if (this.var.directDataArgs) { + return this.generateDirectDataInit() + } else { + return this.generateExternalDataInit() + } } if (this.var.isLookup()) { return this.generateLookup() @@ -314,47 +318,139 @@ module.exports = class EquationGen extends ModelReader { return [`double ${dataName}[${this.var.points.length * 2}] = { ${data} };`] } } - generateData() { + + generateDirectDataInit() { + // If direct data exists for this variable, copy it from the workbook into one or more lookups. let result = [] if (this.initMode) { - // If direct data exists for this variable, copy it from the workbook into one or more lookups. - if (this.var.directDataArgs) { - let { tag, sheetName, timeRowOrCol, startCell } = this.var.directDataArgs - let workbook = this.directData.get(tag) - if (workbook) { - let sheet = workbook.Sheets[sheetName] - if (sheet) { - let indexNum = 0 - if (!R.isEmpty(this.var.subscripts)) { - // Generate a lookup for a separated index in the variable's dimension. - // TODO allow the index to be in either position of a 2D subscript - let ind = sub(this.var.subscripts[0]) - indexNum = ind.value - } - result.push(this.generateDirectDataLookup(sheet, timeRowOrCol, startCell, indexNum)) - } else { - console.error(`direct data worksheet ${sheetName} tagged ${tag} not found`) + let { tag, sheetName, timeRowOrCol, startCell } = this.var.directDataArgs + let workbook = this.directData.get(tag) + if (workbook) { + let sheet = workbook.Sheets[sheetName] + if (sheet) { + let indexNum = 0 + if (!R.isEmpty(this.var.subscripts)) { + // Generate a lookup for a separated index in the variable's dimension. + // TODO allow the index to be in either position of a 2D subscript + let ind = sub(this.var.subscripts[0]) + indexNum = ind.value + } + result.push(this.generateDirectDataLookup(sheet, timeRowOrCol, startCell, indexNum)) + } else { + throw new Error(`ERROR: Direct data worksheet ${sheetName} tagged ${tag} not found`) + } + } else { + throw new Error(`ERROR: Direct data workbook tagged ${tag} not found`) + } + } + 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 + // ("decl" mode), then initialize each `Lookup` using that data in the second pass ("init" mode). + const initMode = this.initMode + + const newLookup = (name, lhs, data, subscriptIndexes) => { + if (!data) { + throw new Error(`ERROR: Data for ${name} not found in external data sources`) + } + + const dataName = this.var.varName + '_data_' + R.map(i => `_${i}_`, subscriptIndexes).join('') + if (initMode) { + // In init mode, create the `Lookup`, passing in a pointer to the static data array declared in decl mode. + return ` ${lhs} = __new_lookup(${data.size}, /*copy=*/false, ${dataName});` + } else { + // In decl mode, declare a static data array that will be used to create the associated `Lookup` + // at init time. See `generateLookup` for more details. + const points = R.reduce((a, p) => listConcat(a, `${cdbl(p[0])}, ${cdbl(p[1])}`, true), '', Array.from(data.entries())) + return `double ${dataName}[${data.size * 2}] = { ${points} };` + } + } + + // There are three common cases that we handle: + // - variable has no subscripts (C variable _thing = _thing from dat file) + // - variable has subscript(s) (C variable with index _thing[0] = _thing[_subscript] from dat file) + // - variable has dimension(s) (C variable in for loop, _thing[i] = _thing[_subscript_i] from dat file) + + // TODO: Generalize the code below to make it work for any number of subscripts/dimensions + + if (!this.var.subscripts || this.var.subscripts.length === 0) { + // No subscripts + const data = this.extData.get(this.var.varName) + return [newLookup(this.var.varName, this.lhs, data, [])] + } + + if (this.var.subscripts.length === 1) { + const subscript = this.var.subscripts[0] + if (isDimension(subscript)) { + // There is exactly one dimension + const dims = sub(subscript).value + if (dims && dims.length > 0) { + const result = [] + for (const dim of dims) { + const nameInDat = `${this.var.varName}[${dim}]` + const dimIndex = sub(dim).value + const lhs = `${this.var.varName}[${dimIndex}]` + const data = this.extData.get(nameInDat) + result.push(newLookup(nameInDat, lhs, data, [dimIndex])) } + return result } else { - console.error(`direct data workbook tagged ${tag} not found`) + throw new Error(`ERROR: Data variable ${this.var.varName} missing dimensions for ${subscript}`) } } else { - // If there is external data for this variable, copy it from an external file to a lookup. - let data = this.extData.get(this.var.varName) - if (data) { - let args = R.reduce( - (a, p) => listConcat(a, `${cdbl(p[0])}, ${cdbl(p[1])}`, true), - '', - Array.from(data.entries()) - ) - result = [` ${this.lhs} = __new_lookup(${data.size}, /*copy=*/true, (double[]){ ${args} });`] + // There is exactly one subscript + const nameInDat = `${this.var.varName}[${subscript}]` + const data = this.extData.get(nameInDat) + const subIndex = sub(subscript).value + return [newLookup(nameInDat, this.lhs, data, [subIndex])] + } + } + + if (this.var.subscripts.length === 2) { + const subscriptOuter = this.var.subscripts[0] + const subscriptInner = this.var.subscripts[1] + if (isDimension(subscriptOuter) && isDimension(subscriptInner)) { + // There are two dimensions + const dimsOuter = sub(subscriptOuter).value + const dimsInner = sub(subscriptInner).value + if (dimsOuter && dimsOuter.length > 0 && dimsInner && dimsInner.length > 0) { + const result = [] + for (const dimOuter of dimsOuter) { + for (const dimInner of dimsInner) { + // Note: It appears that the dat file can have the subscripts in a different order + // than what SDE uses when declaring the C array. If we don't find data for one + // order, we try the other. + let nameInDat = `${this.var.varName}[${dimInner},${dimOuter}]` + let data = this.extData.get(nameInDat) + if (!data) { + nameInDat = `${this.var.varName}[${dimOuter},${dimInner}]` + data = this.extData.get(nameInDat) + } + const dimIndexOuter = sub(dimOuter).value + const dimIndexInner = sub(dimInner).value + const lhs = `${this.var.varName}[${dimIndexOuter}][${dimIndexInner}]` + const lookup = newLookup(nameInDat, lhs, data, [dimIndexOuter, dimIndexInner]) + if (lookup) { + result.push(lookup) + } + } + } + return result } else { - console.error(`data variable ${this.var.varName} not found in external data sources`) + throw new Error(`ERROR: Data variable ${this.var.varName} missing dimensions for ${subscript}`) } + } else { + // There are two subscripts + throw new Error(`ERROR: Data variable ${this.var.varName} has 2 subscripts; not yet handled`) } } - return result + + throw new Error(`ERROR: Data variable ${this.var.varName} has > 2 subscripts; not yet handled`) } + generateDirectDataLookup(sheet, timeRowOrCol, startCell, indexNum) { // Read a row or column of data as (time, value) pairs from the worksheet. let dataCol, dataRow, dataCell, timeCol, timeRow, timeCell, nextCell