From bd3b3e8163bc95dc6957442277311e31b5c4bef9 Mon Sep 17 00:00:00 2001 From: Todd Fincannon Date: Mon, 27 Sep 2021 13:52:47 -0700 Subject: [PATCH] feat: implement DELAY FIXED function (#108) Fixes #29 --- .prettierignore | 1 + models/delayfixed/delayfixed.dat | 530 +++++++++++++++++++++++++++++ models/delayfixed/delayfixed.mdl | 41 +++ models/delayfixed2/delayfixed2.dat | 66 ++++ models/delayfixed2/delayfixed2.mdl | 35 ++ src/CodeGen.js | 10 +- src/EquationGen.js | 24 +- src/EquationReader.js | 13 +- src/Helpers.js | 6 + src/ModelReader.js | 6 +- src/Variable.js | 7 + src/c/vensim.c | 35 +- src/c/vensim.h | 13 + 13 files changed, 779 insertions(+), 8 deletions(-) create mode 100644 models/delayfixed/delayfixed.dat create mode 100644 models/delayfixed/delayfixed.mdl create mode 100644 models/delayfixed2/delayfixed2.dat create mode 100644 models/delayfixed2/delayfixed2.mdl diff --git a/.prettierignore b/.prettierignore index e90d48e4..32d13ceb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ src/web +.vscode diff --git a/models/delayfixed/delayfixed.dat b/models/delayfixed/delayfixed.dat new file mode 100644 index 00000000..b96279af --- /dev/null +++ b/models/delayfixed/delayfixed.dat @@ -0,0 +1,530 @@ +FINAL TIME +0 50 +INITIAL TIME +0 0 +input[A1] +0 0 +1 10 +2 20 +3 30 +4 40 +5 50 +6 60 +7 70 +8 80 +9 90 +10 100 +11 110 +12 120 +13 130 +14 140 +15 150 +16 160 +17 170 +18 180 +19 190 +20 200 +21 210 +22 220 +23 230 +24 240 +25 250 +26 260 +27 270 +28 280 +29 290 +30 300 +31 310 +32 320 +33 330 +34 340 +35 350 +36 360 +37 370 +38 380 +39 390 +40 400 +41 410 +42 420 +43 430 +44 440 +45 450 +46 460 +47 470 +48 480 +49 490 +50 500 +input[A2] +0 0 +1 20 +2 40 +3 60 +4 80 +5 100 +6 120 +7 140 +8 160 +9 180 +10 200 +11 220 +12 240 +13 260 +14 280 +15 300 +16 320 +17 340 +18 360 +19 380 +20 400 +21 420 +22 440 +23 460 +24 480 +25 500 +26 520 +27 540 +28 560 +29 580 +30 600 +31 620 +32 640 +33 660 +34 680 +35 700 +36 720 +37 740 +38 760 +39 780 +40 800 +41 820 +42 840 +43 860 +44 880 +45 900 +46 920 +47 940 +48 960 +49 980 +50 1000 +input[A3] +0 0 +1 30 +2 60 +3 90 +4 120 +5 150 +6 180 +7 210 +8 240 +9 270 +10 300 +11 330 +12 360 +13 390 +14 420 +15 450 +16 480 +17 510 +18 540 +19 570 +20 600 +21 630 +22 660 +23 690 +24 720 +25 750 +26 780 +27 810 +28 840 +29 870 +30 900 +31 930 +32 960 +33 990 +34 1020 +35 1050 +36 1080 +37 1110 +38 1140 +39 1170 +40 1200 +41 1230 +42 1260 +43 1290 +44 1320 +45 1350 +46 1380 +47 1410 +48 1440 +49 1470 +50 1500 +output[A1] +0 0 +1 0 +2 10 +3 20 +4 30 +5 40 +6 50 +7 60 +8 70 +9 80 +10 90 +11 100 +12 110 +13 120 +14 130 +15 140 +16 150 +17 160 +18 170 +19 180 +20 190 +21 200 +22 210 +23 220 +24 230 +25 240 +26 250 +27 260 +28 270 +29 280 +30 290 +31 300 +32 310 +33 320 +34 330 +35 340 +36 350 +37 360 +38 370 +39 380 +40 390 +41 400 +42 410 +43 420 +44 430 +45 440 +46 450 +47 460 +48 470 +49 480 +50 490 +output[A2] +0 0 +1 0 +2 20 +3 40 +4 60 +5 80 +6 100 +7 120 +8 140 +9 160 +10 180 +11 200 +12 220 +13 240 +14 260 +15 280 +16 300 +17 320 +18 340 +19 360 +20 380 +21 400 +22 420 +23 440 +24 460 +25 480 +26 500 +27 520 +28 540 +29 560 +30 580 +31 600 +32 620 +33 640 +34 660 +35 680 +36 700 +37 720 +38 740 +39 760 +40 780 +41 800 +42 820 +43 840 +44 860 +45 880 +46 900 +47 920 +48 940 +49 960 +50 980 +output[A3] +0 0 +1 0 +2 30 +3 60 +4 90 +5 120 +6 150 +7 180 +8 210 +9 240 +10 270 +11 300 +12 330 +13 360 +14 390 +15 420 +16 450 +17 480 +18 510 +19 540 +20 570 +21 600 +22 630 +23 660 +24 690 +25 720 +26 750 +27 780 +28 810 +29 840 +30 870 +31 900 +32 930 +33 960 +34 990 +35 1020 +36 1050 +37 1080 +38 1110 +39 1140 +40 1170 +41 1200 +42 1230 +43 1260 +44 1290 +45 1320 +46 1350 +47 1380 +48 1410 +49 1440 +50 1470 +receiving +0 0 +1 0 +2 0 +3 0 +4 0 +5 0 +6 0 +7 0 +8 0 +9 0 +10 0 +11 0 +12 0 +13 0 +14 0 +15 0 +16 0 +17 0 +18 0 +19 0 +20 0 +21 0 +22 0 +23 0 +24 0 +25 0 +26 0 +27 0 +28 0 +29 0 +30 1 +31 1 +32 1 +33 1 +34 1 +35 1 +36 1 +37 1 +38 1 +39 1 +40 0 +41 0 +42 0 +43 0 +44 0 +45 0 +46 0 +47 0 +48 0 +49 0 +50 0 +reference shipping rate +0 1 +SAVEPER +0 1 +1 1 +2 1 +3 1 +4 1 +5 1 +6 1 +7 1 +8 1 +9 1 +10 1 +11 1 +12 1 +13 1 +14 1 +15 1 +16 1 +17 1 +18 1 +19 1 +20 1 +21 1 +22 1 +23 1 +24 1 +25 1 +26 1 +27 1 +28 1 +29 1 +30 1 +31 1 +32 1 +33 1 +34 1 +35 1 +36 1 +37 1 +38 1 +39 1 +40 1 +41 1 +42 1 +43 1 +44 1 +45 1 +46 1 +47 1 +48 1 +49 1 +50 1 +shipments in transit +0 0 +1 0 +2 0 +3 0 +4 0 +5 0 +6 0 +7 0 +8 0 +9 0 +10 0 +11 1 +12 2 +13 3 +14 4 +15 5 +16 6 +17 7 +18 8 +19 9 +20 10 +21 10 +22 10 +23 10 +24 10 +25 10 +26 10 +27 10 +28 10 +29 10 +30 10 +31 9 +32 8 +33 7 +34 6 +35 5 +36 4 +37 3 +38 2 +39 1 +40 0 +41 0 +42 0 +43 0 +44 0 +45 0 +46 0 +47 0 +48 0 +49 0 +50 0 +shipping +0 0 +1 0 +2 0 +3 0 +4 0 +5 0 +6 0 +7 0 +8 0 +9 0 +10 1 +11 1 +12 1 +13 1 +14 1 +15 1 +16 1 +17 1 +18 1 +19 1 +20 0 +21 0 +22 0 +23 0 +24 0 +25 0 +26 0 +27 0 +28 0 +29 0 +30 0 +31 0 +32 0 +33 0 +34 0 +35 0 +36 0 +37 0 +38 0 +39 0 +40 0 +41 0 +42 0 +43 0 +44 0 +45 0 +46 0 +47 0 +48 0 +49 0 +50 0 +shipping time +0 20 +TIME STEP +0 1 diff --git a/models/delayfixed/delayfixed.mdl b/models/delayfixed/delayfixed.mdl new file mode 100644 index 00000000..45d0702a --- /dev/null +++ b/models/delayfixed/delayfixed.mdl @@ -0,0 +1,41 @@ +{UTF-8} +receiving = DELAY FIXED(shipping, shipping time, shipping) ~~| +shipping = STEP(reference shipping rate, 10) - STEP(reference shipping rate, 20) ~~| +shipping time = 20 ~~| +reference shipping rate = 1 ~~| +shipments in transit = INTEG(shipping - receiving, shipping * shipping time) ~~| + +DimA: A1, A2, A3 ~~| +input[A1] = 10 * TIME ~~| +input[A2] = 20 * TIME ~~| +input[A3] = 30 * TIME ~~| +output[DimA] = DELAY FIXED(input[DimA], 1, 0) ~~| + +INITIAL TIME = 0 ~~| +FINAL TIME = 50 ~~| +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!@ +9:delayfixed +15:0,0,0,0,0,0 +19:100,0 +27:2, +34:0, +5:FINAL TIME +35:Date +36:YYYY-MM-DD +37:2000 +38:1 +39:1 +40:2 +41:0 +42:1 +24:0 +25:0 +26:0 diff --git a/models/delayfixed2/delayfixed2.dat b/models/delayfixed2/delayfixed2.dat new file mode 100644 index 00000000..a0f75bdc --- /dev/null +++ b/models/delayfixed2/delayfixed2.dat @@ -0,0 +1,66 @@ +FINAL TIME +10 20 +INITIAL TIME +10 10 +input1 +10 110 +11 120 +12 130 +13 140 +14 150 +15 160 +16 170 +17 180 +18 190 +19 200 +20 210 +input2 +10 110 +11 120 +12 130 +13 140 +14 150 +15 160 +16 170 +17 180 +18 190 +19 200 +20 210 +output1 +10 0 +11 110 +12 120 +13 130 +14 140 +15 150 +16 160 +17 170 +18 180 +19 190 +20 200 +output2 +10 0 +11 0 +12 0 +13 0 +14 0 +15 110 +16 120 +17 130 +18 140 +19 150 +20 160 +SAVEPER +10 1 +11 1 +12 1 +13 1 +14 1 +15 1 +16 1 +17 1 +18 1 +19 1 +20 1 +TIME STEP +10 1 diff --git a/models/delayfixed2/delayfixed2.mdl b/models/delayfixed2/delayfixed2.mdl new file mode 100644 index 00000000..2a85c9e9 --- /dev/null +++ b/models/delayfixed2/delayfixed2.mdl @@ -0,0 +1,35 @@ +{UTF-8} +input1 = 10 * TIME + 10 ~~| +output1 = DELAY FIXED(input1, 1, 0) ~~| + +input2 = 10 * TIME + 10 ~~| +output2 = DELAY FIXED(input2, 5, 0) ~~| + +INITIAL TIME = 10 ~~| +FINAL TIME = 20 ~~| +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!@ +9:delayfixed2 +15:0,0,0,0,0,0 +19:100,0 +27:2, +34:0, +5:FINAL TIME +35:Date +36:YYYY-MM-DD +37:2000 +38:1 +39:1 +40:2 +41:0 +42:1 +24:0 +25:0 +26:0 diff --git a/src/CodeGen.js b/src/CodeGen.js index d63c86be..0c0a6a08 100644 --- a/src/CodeGen.js +++ b/src/CodeGen.js @@ -197,12 +197,20 @@ ${postStep} // function declSection() { // Emit a declaration for each variable in the model. + let fixedDelayDecls = '' let decl = v => { // Build a C array declaration for the variable v. // This uses the subscript family for each dimension, which may overallocate // if the subscript is a subdimension. let varType = v.isLookup() || v.isData() ? 'Lookup* ' : 'double ' let families = subscriptFamilies(v.subscripts) + if (v.isFixedDelay()) { + // Add the associated FixedDelay var decl. + fixedDelayDecls += `\nFixedDelay* ${v.fixedDelayVarName}${R.map( + family => `[${sub(family).size}]`, + families + ).join('')};` + } return varType + v.varName + R.map(family => `[${sub(family).size}]`, families).join('') } // Non-apply-to-all variables are declared multiple times, but coalesce using uniq. @@ -212,7 +220,7 @@ ${postStep} asort, lines ) - return decls(Model.allVars()) + return decls(Model.allVars()) + fixedDelayDecls } function internalVarsSection() { // Declare internal variables to run the model. diff --git a/src/EquationGen.js b/src/EquationGen.js index 455fd51c..9f0453ab 100644 --- a/src/EquationGen.js +++ b/src/EquationGen.js @@ -754,23 +754,41 @@ export default class EquationGen extends ModelReader { let exprs = ctx.expr() let fn = this.currentFunctionName() // Split level functions into init and eval expressions. - if (fn === '_INTEG' || fn === '_SAMPLE_IF_TRUE' || fn === '_ACTIVE_INITIAL') { + if (fn === '_INTEG' || fn === '_SAMPLE_IF_TRUE' || fn === '_ACTIVE_INITIAL' || fn === '_DELAY_FIXED') { if (this.mode.startsWith('init')) { // Get the index of the argument holding the initial value. let i = 0 if (fn === '_INTEG' || fn === '_ACTIVE_INITIAL') { i = 1 - } else if (fn === '_SAMPLE_IF_TRUE') { + } else if (fn === '_SAMPLE_IF_TRUE' || fn === '_DELAY_FIXED') { i = 2 } this.setArgIndex(i) exprs[i].accept(this) + // For DELAY FIXED, also initialize the support struct out of band, as it is not a Vensim var. + if (fn === '_DELAY_FIXED') { + this.emit( + `;\n ${this.var.fixedDelayVarName}${this.lhsSubscriptGen(this.var.subscripts)} = __new_fixed_delay(` + ) + this.setArgIndex(1) + exprs[1].accept(this) + this.emit(', ') + this.setArgIndex(2) + exprs[2].accept(this) + this.emit(')') + } } else { // We are in eval mode, not init mode. - // For ACTIVE INITIAL, emit the first arg without a function call. if (fn === '_ACTIVE_INITIAL') { + // For ACTIVE INITIAL, emit the first arg without a function call. this.setArgIndex(0) exprs[0].accept(this) + } else if (fn === '_DELAY_FIXED') { + // For DELAY FIXED, emit the first arg followed by the FixedDelay support var. + this.setArgIndex(0) + exprs[0].accept(this) + this.emit(', ') + this.emit(`${this.var.fixedDelayVarName}${this.lhsSubscriptGen(this.var.subscripts)}`) } else { // Emit the variable LHS as the first arg at eval time, giving the current value for the level. this.emit(this.lhs) diff --git a/src/EquationReader.js b/src/EquationReader.js index c1816a00..10f6dca6 100644 --- a/src/EquationReader.js +++ b/src/EquationReader.js @@ -25,7 +25,8 @@ import { matchRegex, newAuxVarName, newLevelVarName, - newLookupVarName + newLookupVarName, + newFixedDelayVarName } from './Helpers.js' // Set this true to get a list of functions used in the model. This may include lookups. @@ -88,9 +89,13 @@ export default class EquationReader extends ModelReader { if (PRINT_FUNCTION_NAMES) { console.error(fn) } - if (fn === '_INTEG') { + if (fn === '_INTEG' || fn === '_DELAY_FIXED') { this.var.varType = 'level' this.var.hasInitValue = true + if (fn === '_DELAY_FIXED') { + this.var.varSubtype = 'fixedDelay' + this.var.fixedDelayVarName = canonicalName(newFixedDelayVarName()) + } } else if (fn === '_INITIAL') { this.var.varType = 'initial' this.var.hasInitValue = true @@ -236,6 +241,10 @@ export default class EquationReader extends ModelReader { // with the generated level var. } else if (this.argIndexForFunctionName('_INTEG') === 1) { this.addReferencesToList(this.var.initReferences) + } else if (this.argIndexForFunctionName('_DELAY_FIXED') === 1) { + this.addReferencesToList(this.var.initReferences) + } else if (this.argIndexForFunctionName('_DELAY_FIXED') === 2) { + this.addReferencesToList(this.var.initReferences) } else if (this.argIndexForFunctionName('_ACTIVE_INITIAL') === 1) { this.addReferencesToList(this.var.initReferences) } else if (this.argIndexForFunctionName('_SAMPLE_IF_TRUE') === 2) { diff --git a/src/Helpers.js b/src/Helpers.js index 12a6c826..5e586ee5 100644 --- a/src/Helpers.js +++ b/src/Helpers.js @@ -16,6 +16,8 @@ export const PRINT_VLOG_TRACE = false let nextTmpVarSeq = 1 // next sequence number for generated lookup variable names let nextLookupVarSeq = 1 +// next sequence number for generated fixed delay variable names +let nextFixedDelayVarSeq = 1 // next sequence number for generated level variable names let nextLevelVarSeq = 1 // next sequence number for generated aux variable names @@ -74,6 +76,10 @@ export let newLookupVarName = () => { // Return a unique lookup arg variable name return `_lookup${nextLookupVarSeq++}` } +export let newFixedDelayVarName = () => { + // Return a unique fixed delay variable name + return `_fixed_delay${nextFixedDelayVarSeq++}` +} export let newLevelVarName = (basename = null, levelNumber = 0) => { // Return a unique level variable name. let levelName = basename || nextLevelVarSeq++ diff --git a/src/ModelReader.js b/src/ModelReader.js index cc12f516..7b8970e6 100644 --- a/src/ModelReader.js +++ b/src/ModelReader.js @@ -3,20 +3,24 @@ import { ModelVisitor } from 'antlr4-vensim' export default class ModelReader extends ModelVisitor { constructor() { super() - // stack of function names and argument indices + // stack of function names and argument indices encountered on the RHS this.callStack = [] } currentFunctionName() { + // Return the name of the current function on top of the call stack. let n = this.callStack.length return n > 0 ? this.callStack[n - 1].fn : '' } setArgIndex(argIndex) { + // Set the argument index in the current function call on top of the call stack. + // This may be set in the exprList visitor and picked up in the var visitor to facilitate special argument handling. let n = this.callStack.length if (n > 0) { this.callStack[n - 1].argIndex = argIndex } } argIndexForFunctionName(name) { + // Search the call stack for the function name. Return the current argument index or undefined if not found. let argIndex for (let i = this.callStack.length - 1; i >= 0; i--) { if (this.callStack[i].fn === name) { diff --git a/src/Variable.js b/src/Variable.js index db7b35d2..879a719d 100644 --- a/src/Variable.js +++ b/src/Variable.js @@ -24,6 +24,8 @@ export default class Variable { this.refId = '' // The default varType is aux, but may be overridden later. this.varType = 'aux' + // The variable subtype accommodates special handling needed by some Vensim functions. + this.varSubtype = '' // A variable may reference other variable names at eval time. this.references = [] // Levels and certain other variables have an initial value that may reference other variable names. @@ -41,6 +43,8 @@ export default class Variable { // DELAY3* calls are expanded into new level vars and substituted during code generation. this.delayVarRefId = '' this.delayTimeVarName = '' + // DELAY FIXED calls generate a FixedDelay support var. + this.fixedDelayVarName = '' // Variables generated by special expansions are not included in output. this.includeInOutput = true } @@ -92,6 +96,9 @@ export default class Variable { isLevel() { return this.varType === 'level' } + isFixedDelay() { + return this.varSubtype === 'fixedDelay' + } isInitial() { return this.varType === 'initial' } diff --git a/src/c/vensim.c b/src/c/vensim.c index a05f0d5b..b8cee120 100644 --- a/src/c/vensim.c +++ b/src/c/vensim.c @@ -10,7 +10,6 @@ double _epsilon = 1e-6; // See the Vensim Reference Manual for descriptions of the functions. // http://www.vensim.com/documentation/index.html?22300.htm // - double _PULSE(double start, double width) { double time_plus = _time + _time_step / 2.0; if (width == 0.0) { @@ -404,3 +403,37 @@ double* _ALLOCATE_AVAILABLE( // Return a pointer to the allocations array the caller passed with the results filled in. return allocations; } + +// +// DELAY FIXED +// +FixedDelay* __new_fixed_delay(double delay_time, double initial_value) { + // Construct a FixedDelay struct with a ring buffer for the delay line. + // We don't know the size until runtime, so it must be dynamically allocated. + // The delay time is quantized to an integral number of time steps. + // The FixedDelay should be constructed at init time to latch the delay time and initial value. + FixedDelay* fixed_delay = malloc(sizeof(FixedDelay)); + fixed_delay->n = (size_t)ceil(delay_time / _time_step); + fixed_delay->data = malloc(sizeof(double) * fixed_delay->n); + fixed_delay->data_index = 0; + fixed_delay->initial_value = initial_value; + return fixed_delay; +} +double _DELAY_FIXED(double input, FixedDelay* fixed_delay) { + // Cache input values in a ring buffer for the number of time steps equal to the delay time. + // Return the init value until the time reaches the delay time. + double result = 0.0; + // Require the buffer size to be positive to protect from buffer overflows. + if (fixed_delay->n > 0) { + fixed_delay->data[fixed_delay->data_index] = input; + // Because DELAY FIXED is a level, get the value one time step ahead in the buffer. + fixed_delay->data_index = (fixed_delay->data_index + 1) % fixed_delay->n; + // Start pulling from the ring buffer when the next time step will reach the delay time. + if (_time < _initial_time + (fixed_delay->n - 1) * _time_step - 1e-6) { + result = fixed_delay->initial_value; + } else { + result = fixed_delay->data[fixed_delay->data_index]; + } + } + return result; +} diff --git a/src/c/vensim.h b/src/c/vensim.h index 2ded646e..a4616b13 100644 --- a/src/c/vensim.h +++ b/src/c/vensim.h @@ -71,6 +71,19 @@ double __get_data_between_times(double *data, size_t n, double input, LookupMode #define _GET_DATA_MODE_TO_LOOKUP_MODE(mode) ((mode) >= 1) ? Forward : (((mode) <= -1) ? Backward : Interpolate) #define _GET_DATA_BETWEEN_TIMES(lookup, x, mode) __get_data_between_times((lookup)->data, (lookup)->n, x, _GET_DATA_MODE_TO_LOOKUP_MODE(mode)) +// +// DELAY FIXED +// +typedef struct { + double* data; + size_t n; + size_t data_index; + double initial_value; +} FixedDelay; + +double _DELAY_FIXED(double input, FixedDelay* fixed_delay); +FixedDelay* __new_fixed_delay(double delay_time, double initial_value); + #ifdef __cplusplus } #endif