diff --git a/models/extdata/extdata.dat b/models/extdata/extdata.dat index db0b8522..0f2ff74d 100644 --- a/models/extdata/extdata.dat +++ b/models/extdata/extdata.dat @@ -10,6 +10,42 @@ A Totals 8 218.571 9 240 10 270 +B Selection[B1] +0 0 +1 0 +2 0 +3 0 +4 0 +5 0 +6 0 +7 0 +8 0 +9 0 +10 0 +B Selection[B2] +0 0 +1 0 +2 0 +3 0 +4 0 +5 0 +6 0 +7 0 +8 0 +9 0 +10 0 +B Selection[B3] +0 1 +1 1 +2 1 +3 1 +4 1 +5 1 +6 1 +7 1 +8 1 +9 1 +10 1 B1 Totals 0 10 1 30 @@ -22,6 +58,36 @@ B1 Totals 8 135.714 9 150 10 170 +C Selection[C1] +0 1 +1 1 +2 1 +3 1 +4 1 +5 1 +6 1 +7 1 +8 1 +9 1 +10 1 +C Selection[C2] +0 0 +1 0 +2 0 +3 0 +4 0 +5 0 +6 0 +7 0 +8 0 +9 0 +10 0 +Chosen B +0 3 +Chosen C +0 1 +Chosen E +0 2 D Totals 0 11000 1 11200 @@ -34,6 +100,30 @@ D Totals 8 11571.4 9 11600 10 11800 +E Selection[E1] +0 0 +1 0 +2 0 +3 0 +4 0 +5 0 +6 0 +7 0 +8 0 +9 0 +10 0 +E Selection[E2] +0 1 +1 1 +2 1 +3 1 +4 1 +5 1 +6 1 +7 1 +8 1 +9 1 +10 1 E1 Values 0 7000 1 7100 @@ -88,6 +178,118 @@ Simple Totals 10 5500 TIME STEP 0 1 +Total EBC +0 2000 +1 2100 +2 2200 +3 2300 +4 2400 +5 2500 +6 2600 +7 2700 +8 2800 +9 2900 +10 3000 +Total EBC for Selected BC[E1] +0 1400 +1 1500 +2 1600 +3 1700 +4 1800 +5 1900 +6 2000 +7 2100 +8 2200 +9 2300 +10 2400 +Total EBC for Selected BC[E2] +0 2000 +1 2100 +2 2200 +3 2300 +4 2400 +5 2500 +6 2600 +7 2700 +8 2800 +9 2900 +10 3000 +Total EBC for Selected C[E1,B1] +0 1000 +1 1100 +2 1200 +3 1300 +4 1400 +5 1500 +6 1600 +7 1700 +8 1800 +9 1900 +10 2000 +Total EBC for Selected C[E1,B2] +0 1200 +1 1300 +2 1400 +3 1500 +4 1600 +5 1700 +6 1800 +7 1900 +8 2000 +9 2100 +10 2200 +Total EBC for Selected C[E1,B3] +0 1400 +1 1500 +2 1600 +3 1700 +4 1800 +5 1900 +6 2000 +7 2100 +8 2200 +9 2300 +10 2400 +Total EBC for Selected C[E2,B1] +0 1600 +1 1700 +2 1800 +3 1900 +4 2000 +5 2100 +6 2200 +7 2300 +8 2400 +9 2500 +10 2600 +Total EBC for Selected C[E2,B2] +0 1800 +1 1900 +2 2000 +3 2100 +4 2200 +5 2300 +6 2400 +7 2500 +8 2600 +9 2700 +10 2800 +Total EBC for Selected C[E2,B3] +0 2000 +1 2100 +2 2200 +3 2300 +4 2400 +5 2500 +6 2600 +7 2700 +8 2800 +9 2900 +10 3000 +VSERRATLEASTONE +0 1 +VSSUM +0 0 A Values[A1] 0 0 1 10 @@ -166,6 +368,42 @@ E Values[E2] 2 8200 9 8300 10 8400 +EBC Values[E1,B1,C1] +0 1000 +10 2000 +EBC Values[E1,B1,C2] +0 1100 +10 2100 +EBC Values[E1,B2,C1] +0 1200 +10 2200 +EBC Values[E1,B2,C2] +0 1300 +10 2300 +EBC Values[E1,B3,C1] +0 1400 +10 2400 +EBC Values[E1,B3,C2] +0 1500 +10 2500 +EBC Values[E2,B1,C1] +0 1600 +10 2600 +EBC Values[E2,B1,C2] +0 1700 +10 2700 +EBC Values[E2,B2,C1] +0 1800 +10 2800 +EBC Values[E2,B2,C2] +0 1900 +10 2900 +EBC Values[E2,B3,C1] +0 2000 +10 3000 +EBC Values[E2,B3,C2] +0 2100 +10 3100 Simple 1 0 1000 1 2000 diff --git a/models/extdata/extdata.mdl b/models/extdata/extdata.mdl index 0811da98..159ece34 100644 --- a/models/extdata/extdata.mdl +++ b/models/extdata/extdata.mdl @@ -36,6 +36,9 @@ E Values[E1] E Values[E2] ~~| +EBC Values[DimE,DimB,DimC] + ~~| + Simple Totals = Simple 1 + Simple 2 ~~| @@ -60,6 +63,63 @@ E2 Values = E Values[E2] ~~| +Chosen E = 2 + ~~| + +Chosen B = 3 + ~~| + +Chosen C = 1 + ~~| + +E Selection[DimE] = + IF THEN ELSE ( DimE = Chosen E , 1 , 0 ) + ~~| + +B Selection[DimB] = + IF THEN ELSE ( DimB = Chosen B , 1 , 0 ) + ~~| + +C Selection[DimC] = + IF THEN ELSE ( DimC = Chosen C , 1 , 0 ) + ~~| + +Total EBC for Selected C[DimE,DimB] = + VECTOR SELECT ( + C Selection[DimC!] , + EBC Values[DimE,DimB,DimC!] , + 0, + VSSUM, + VSERRATLEASTONE + ) + ~~| + +Total EBC for Selected BC[DimE] = + VECTOR SELECT ( + B Selection[DimB!] , + Total EBC for Selected C[DimE,DimB!] , + 0, + VSSUM, + VSERRATLEASTONE + ) + ~~| + +Total EBC = + VECTOR SELECT ( + E Selection[DimE!] , + Total EBC for Selected BC[DimE!] , + 0, + VSSUM, + VSERRATLEASTONE + ) + ~~| + +VSERRATLEASTONE = 1 + ~~| + +VSSUM = 0 + ~~| + ******************************************************** .Control ********************************************************~ diff --git a/models/extdata/extdata_data.dat b/models/extdata/extdata_data.dat index d9f2ba6b..e3ee5c92 100644 --- a/models/extdata/extdata_data.dat +++ b/models/extdata/extdata_data.dat @@ -88,3 +88,39 @@ E Values[E2] 2 8200 9 8300 10 8400 +EBC Values[E1,B1,C1] +0 1000 +10 2000 +EBC Values[E1,B1,C2] +0 1100 +10 2100 +EBC Values[E1,B2,C1] +0 1200 +10 2200 +EBC Values[E1,B2,C2] +0 1300 +10 2300 +EBC Values[E1,B3,C1] +0 1400 +10 2400 +EBC Values[E1,B3,C2] +0 1500 +10 2500 +EBC Values[E2,B1,C1] +0 1600 +10 2600 +EBC Values[E2,B1,C2] +0 1700 +10 2700 +EBC Values[E2,B2,C1] +0 1800 +10 2800 +EBC Values[E2,B2,C2] +0 1900 +10 2900 +EBC Values[E2,B3,C1] +0 2000 +10 3000 +EBC Values[E2,B3,C2] +0 2100 +10 3100 diff --git a/src/EquationGen.js b/src/EquationGen.js index 6e0d004d..b79dc3c9 100644 --- a/src/EquationGen.js +++ b/src/EquationGen.js @@ -18,6 +18,7 @@ const { } = require('./Subscript') const { canonicalName, + cartesianProductOf, cdbl, cFunctionName, isArrayFunction, @@ -26,6 +27,7 @@ const { isTrendFunction, listConcat, newTmpVarName, + permutationsOf, strToConst, vlog } = require('./Helpers') @@ -374,81 +376,64 @@ module.exports = class EquationGen extends ModelReader { // - 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) { + if (this.var.subscripts.length === 1 && !isDimension(this.var.subscripts[0])) { + // There is exactly one subscript 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 { - throw new Error(`ERROR: Data variable ${this.var.varName} missing dimensions for ${subscript}`) - } - } else { - // 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])] - } + 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 { - throw new Error(`ERROR: Data variable ${this.var.varName} missing dimensions for ${subscript}`) + if (!R.all(s => isDimension(s), this.var.subscripts)) { + // 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`) + } + + // At this point, we know that we have one or more dimensions; compute all combinations + // of the dimensions that we will iterate over + const result = [] + const allDims = R.map(s => sub(s).value, this.var.subscripts) + const dimTuples = cartesianProductOf(allDims) + for (const dims of dimTuples) { + // 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 possible permutations. + const dimNamePermutations = permutationsOf(dims) + let nameInDat, data + for (const dimNames of dimNamePermutations) { + nameInDat = `${this.var.varName}[${dimNames.join(',')}]` + data = this.extData.get(nameInDat) + if (data) { + break } - } else { - // There are two subscripts - throw new Error(`ERROR: Data variable ${this.var.varName} has 2 subscripts; not yet handled`) } - } + if (!data) { + // We currently treat this as a warning, not an error, since there can sometimes be + // datasets that are a sparse matrix, i.e., data is not defined for certain dimensions. + // For these cases, the lookup will not be initialized (the Lookup pointer will remain + // NULL, and any calls to `LOOKUP` will return `:NA:`. + console.error(`WARNING: Data for ${nameInDat} not found in external data sources`) + continue + } - throw new Error(`ERROR: Data variable ${this.var.varName} has > 2 subscripts; not yet handled`) + const subscriptIndexes = R.map(dim => sub(dim).value, dims) + const varSubscripts = R.map(index => `[${index}]`, subscriptIndexes).join('') + const lhs = `${this.var.varName}${varSubscripts}` + const lookup = newLookup(nameInDat, lhs, data, subscriptIndexes) + if (lookup) { + result.push(lookup) + } + } + return result } generateDirectDataLookup(sheet, timeRowOrCol, startCell, indexNum) { @@ -692,6 +677,20 @@ module.exports = class EquationGen extends ModelReader { } } visitVar(ctx) { + // Helper function that emits a lookup call if the variable is a data variable, + // otherwise emits a normal variable. + const emitVar = () => { + let v = Model.varWithName(this.currentVarName()) + if (v && v.varType === 'data') { + this.emit(`_LOOKUP(${this.currentVarName()}`) + super.visitVar(ctx) + this.emit(', _time)') + } else { + this.emit(this.currentVarName()) + super.visitVar(ctx) + } + } + // Push the var name on the stack and then emit it. let id = ctx.Id().getText() let varName = canonicalName(id) @@ -713,10 +712,12 @@ module.exports = class EquationGen extends ModelReader { let argIndex = this.argIndexForFunctionName('_VECTOR_SELECT') if (argIndex === 0) { this.vsSelectionArray = this.currentVarName() + super.visitVar(ctx) } else if (argIndex === 1) { - this.emit(this.currentVarName()) + emitVar() + } else { + super.visitVar(ctx) } - super.visitVar(ctx) } else if (this.currentFunctionName() === '_VECTOR_ELM_MAP') { if (this.argIndexForFunctionName('_VECTOR_ELM_MAP') === 1) { this.vemOffset = `(size_t)${this.currentVarName()}` @@ -733,15 +734,7 @@ module.exports = class EquationGen extends ModelReader { this.emit(this.currentVarName()) super.visitVar(ctx) } else { - let v = Model.varWithName(this.currentVarName()) - if (v && v.varType === 'data') { - this.emit(`_LOOKUP(${this.currentVarName()}`) - super.visitVar(ctx) - this.emit(', _time)') - } else { - this.emit(this.currentVarName()) - super.visitVar(ctx) - } + emitVar() } this.varNames.pop() } diff --git a/src/Helpers.js b/src/Helpers.js index 3e1719e3..82d18ca3 100644 --- a/src/Helpers.js +++ b/src/Helpers.js @@ -379,6 +379,45 @@ let matchRegexCaptures = (str, regex) => { return [] } } + +/** + * Return the cartesian product of the given array of arrays. + * + * For example, if we have an array that lists out two dimensions: + * [ ['a1','a2'], ['b1','b2','b3'] ] + * this function will return all the combinations, e.g.: + * [ ['a1', 'b1'], ['a1', 'b2'], ['a1', 'b3'], ['a2', 'b1'], ... ] + * + * This can be used in place of nested for loops and has the benefit of working + * for multi-dimensional inputs. + */ +const cartesianProductOf = arr => { + // Implementation based on: https://stackoverflow.com/a/36234242 + return arr.reduce((a, b) => { + return a + .map(x => b.map(y => x.concat([y]))) + .reduce((v, w) => v.concat(w), []) + }, [[]]) +} + +/** + * Return all possible permutations of the given array elements. + * + * For example, if we have an array of numbers: + * [1,2,3] + * this function will return all the permutations, e.g.: + * [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ] + */ +const permutationsOf = (elems, subperms = [[]]) => { + // Implementation based on: https://gist.github.com/CrossEye/f7c2f77f7db7a94af209 + return R.isEmpty(elems) ? + subperms : + R.addIndex(R.chain)((elem, idx) => permutationsOf( + R.remove(idx, 1, elems), + R.map(R.append(elem), subperms) + ), elems) +} + // // Debugging helpers // @@ -403,6 +442,7 @@ module.exports = { buildDir, canonicalName, canonicalVensimName, + cartesianProductOf, cdbl, cFunctionName, decanonicalize, @@ -430,6 +470,7 @@ module.exports = { newLookupVarName, newTmpVarName, outputDir, + permutationsOf, printArray, readDat, readXlsx,