linny-r 1.7.1 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2044,29 +2044,29 @@ class ExpressionParser {
2044
2044
  // CLASS VirtualMachine
2045
2045
  class VirtualMachine {
2046
2046
  constructor() {
2047
- // Set user name to default as configured in file `linny-r-config.js`
2048
- // This will be an empty string for local host servers
2047
+ // Set user name to default as configured in file `linny-r-config.js`.
2048
+ // This will be an empty string for local host servers.
2049
2049
  this.solver_user = SOLVER.user_id;
2050
- // NOTE: if not empty, then authentication is needed
2050
+ // NOTE: If not empty, then authentication is needed.
2051
2051
  if(this.solver_user) {
2052
- // If URL contains ?u=, set user name to the passed parameter
2052
+ // If URL contains ?u=, set user name to the passed parameter.
2053
2053
  let url = decodeURI(window.location.href);
2054
- // NOTE: trim cache buster suffix that may have been added
2054
+ // NOTE: Trim cache buster suffix that may have been added.
2055
2055
  if(url.indexOf('?x=') > 0) url = url.split('?x=')[0].trim();
2056
2056
  if(url.indexOf('?u=') > 0) {
2057
2057
  this.solver_user = url.split('?u=')[1].trim();
2058
2058
  }
2059
2059
  }
2060
- // NOTE: if not null, the callback function is called when the VM has
2061
- // finished a run; this is used by the console version of Linny-R
2060
+ // NOTE: If not null, the callback function is called when the VM has
2061
+ // finished a run. This is used by the console version of Linny-R.
2062
2062
  this.callback = null;
2063
- // Solver limits may be set in file `linny-r-config.js` (0 => unlimited)
2063
+ // Solver limits may be set in file `linny-r-config.js` (0 => unlimited).
2064
2064
  this.max_solver_time = SOLVER.max_solver_time;
2065
2065
  this.max_blocks = SOLVER.max_nr_of_blocks;
2066
2066
  this.max_tableau_size = SOLVER.max_tableau_size;
2067
- // Standard variables: array of tuples [type, object]
2067
+ // Standard variables: array of tuples [type, object].
2068
2068
  this.variables = [];
2069
- // Indices for special types
2069
+ // Indices for special variable types.
2070
2070
  this.int_var_indices = [];
2071
2071
  this.bin_var_indices = [];
2072
2072
  this.sec_var_indices = [];
@@ -2075,35 +2075,35 @@ class VirtualMachine {
2075
2075
  this.fixed_var_indices = [];
2076
2076
  // Chunk variables: also an array of tuples [type, object], but
2077
2077
  // so far, type is always HI (highest increment); object can be
2078
- // a process or a product
2078
+ // a process or a product.
2079
2079
  this.chunk_variables = [];
2080
- // Array for VM instructions
2080
+ // Array for VM instructions.
2081
2081
  this.code = [];
2082
- // The Simplex tableau: matrix, rhs and ct will have same length
2082
+ // The Simplex tableau: matrix, rhs and ct will have same length.
2083
2083
  this.matrix = [];
2084
2084
  this.right_hand_side = [];
2085
2085
  this.constraint_types = [];
2086
- // String to hold lines of (solver-dependent) model equations
2086
+ // String to hold lines of (solver-dependent) model equations.
2087
2087
  this.lines = '';
2088
- // String specifying a numeric issue (empty if none)
2088
+ // String specifying a numeric issue (empty if none).
2089
2089
  this.numeric_issue = '';
2090
- // Warnings are stored in a list to permit browsing through them
2090
+ // Warnings are stored in a list to permit browsing through them.
2091
2091
  this.issue_list = [];
2092
- // The call stack tracks evaluation of "nested" expression variables
2092
+ // The call stack tracks evaluation of "nested" expression variables.
2093
2093
  this.call_stack = [];
2094
2094
  this.block_count = 0;
2095
- // Sequence of round numbers (set by default or as experiment parameter)
2095
+ // Sequence of round numbers (set by default or as experiment parameter).
2096
2096
  this.round_sequence = '';
2097
- // NOTE: current round is index in round sequence
2097
+ // NOTE: Current round is index in round sequence.
2098
2098
  this.current_round = 0;
2099
- // Add arrays for solver results per block
2099
+ // Add arrays for solver results per block.
2100
2100
  this.round_times = [];
2101
2101
  this.round_secs = [];
2102
2102
  this.solver_times = [];
2103
2103
  this.solver_secs = [];
2104
2104
  this.messages = [];
2105
2105
  this.equations = [];
2106
- // Default texts to display for (still) empty results
2106
+ // Default texts to display for (still) empty results.
2107
2107
  this.no_messages = '(no messages)';
2108
2108
  this.no_variables = '(no variables)';
2109
2109
  this.no_equations = '(select block in progress bar)';
@@ -2122,26 +2122,29 @@ class VirtualMachine {
2122
2122
  this.BEYOND_MINUS_INFINITY = -1e+35;
2123
2123
  this.SOLVER_PLUS_INFINITY = 1e+30;
2124
2124
  this.SOLVER_MINUS_INFINITY = -1e+30;
2125
- // NOTE: below the "near zero" limit, a number is considered zero
2126
- // (this is to timely detect division-by-zero errors)
2125
+ // NOTE: Below the "near zero" limit, a number is considered zero
2126
+ // (this is to timely detect division-by-zero errors).
2127
2127
  this.NEAR_ZERO = 1e-10;
2128
2128
  // Use a specific constant smaller than near-zero to denote "no cost"
2129
- // to differentiate "no cost" form cost prices that really are 0
2129
+ // to differentiate "no cost" form cost prices that really are 0.
2130
2130
  this.NO_COST = 0.987654321e-10;
2131
2131
 
2132
- // NOTE: allow for an accuracy margin: stocks may differ 0.1% from their
2133
- // target without displaying them in red or blue to signal shortage or surplus
2132
+ // NOTE: Allow for an accuracy margin: stocks may differ 0.1% from
2133
+ // their target without displaying them in red or blue to signal
2134
+ // shortage or surplus.
2134
2135
  this.SIG_DIF_LIMIT = 0.001;
2135
2136
  this.SIG_DIF_FROM_ZERO = 1e-6;
2136
- // On/off threshold is used to differentiate between level = 0 and still "ON"
2137
- // (will be displayed as +0)
2137
+ // ON/OFF threshold is used to differentiate between level = 0 and
2138
+ // still "ON" (will be displayed as +0).
2138
2139
  this.ON_OFF_THRESHOLD = 1.5e-4;
2139
- // Limit for upper bounds beyond which binaries cannot be computed correctly
2140
+ // Limit for upper bounds beyond which binaries cannot be computed
2141
+ // correctly. Modeler is warned when this occurs (typically when
2142
+ // ON/OFF variables are needed for a process having infinite bounds.
2140
2143
  this.MEGA_UPPER_BOUND = 1e6;
2141
- // Limit slack penalty to one order of magnitude below +INF
2144
+ // Limit slack penalty to one order of magnitude below +INF.
2142
2145
  this.MAX_SLACK_PENALTY = 0.1 * this.PLUS_INFINITY;
2143
2146
 
2144
- // VM constants for specifying the type of cash flow operation
2147
+ // VM constants for specifying the type of cash flow operation.
2145
2148
  this.CONSUME = 0;
2146
2149
  this.PRODUCE = 1;
2147
2150
  this.ONE_C = 2;
@@ -2149,16 +2152,16 @@ class VirtualMachine {
2149
2152
  this.THREE_X = 4;
2150
2153
  this.SPIN_RES = 5;
2151
2154
  this.PEAK_INC = 6;
2152
- // Array of corrsponding strings for more readable debugging information
2155
+ // Array of corrsponding strings for more readable debugging information.
2153
2156
  this.CF_CONSTANTS = ['CONSUME', 'PRODUCE', 'ONE_C', 'TWO_X',
2154
2157
  'THREE_X', 'SPIN_RES'];
2155
2158
 
2156
- // Constraint cost price transfer direction
2159
+ // Constraint cost price transfer direction.
2157
2160
  this.SOC_X_Y = 1;
2158
2161
  this.SOC_Y_X = -1;
2159
2162
 
2160
- // Link multiplier type numbers
2161
- // NOTE: do *NOT* change existing values, as this will cause legacy issues!!
2163
+ // Link multiplier type numbers.
2164
+ // NOTE: Do *NOT* change existing values, as this will cause legacy issues!
2162
2165
  this.LM_LEVEL = 0; // No symbol
2163
2166
  this.LM_THROUGHPUT = 1; // Symbol: two parallel right-pointing arrows
2164
2167
  this.LM_INCREASE = 2; // Symbol: Delta
@@ -2176,11 +2179,11 @@ class VirtualMachine {
2176
2179
  this.LM_SYMBOLS = ['', '\u21C9', '\u0394', '\u03A3', '\u03BC', '\u25B2',
2177
2180
  '+', '0', '\u2934', '\u2732', '\u25BC', '\u2A39'];
2178
2181
 
2179
- // VM max. expression stack size
2182
+ // VM max. expression stack size.
2180
2183
  this.MAX_STACK = 200;
2181
2184
 
2182
- // Base penalty of 10 is high relative to the (scaled) coefficients of the
2183
- // cash flows in the objective function (typically +/- 1)
2185
+ // Base penalty of 10 is high relative to the (scaled) coefficients of
2186
+ // the cash flows in the objective function (typically +/- 1).
2184
2187
  this.BASE_PENALTY = 10;
2185
2188
  // Peak variable penalty is added to make solver choose the *smallest*
2186
2189
  // value that is greater than or equal to X[t] for all t as "peak value".
@@ -2189,17 +2192,17 @@ class VirtualMachine {
2189
2192
  // solution (highest total cash flow).
2190
2193
  this.PEAK_VAR_PENALTY = 0.1;
2191
2194
 
2192
- // NOTE: the VM uses numbers >> +INF to denote special computation results
2195
+ // NOTE: The VM uses numbers >> +INF to denote special computation results.
2193
2196
  this.EXCEPTION = 1e+36; // to test for any exceptional value
2194
2197
  this.UNDEFINED = 1e+37; // to denote "unspecified by the user"
2195
2198
  this.NOT_COMPUTED = 1e+38; // initial value for VM variables (to distinguish from UNDEFINED)
2196
2199
  this.COMPUTING = 1e+39; // used by the VM to implement lazy evaluation
2197
2200
 
2198
2201
  // NOTES:
2199
- // (1) computation errors are signalled by NEGATIVE values << -10^35
2200
- // (2) JavaScript exponents can go up to +/- 308 (IEEE 754 standard)
2201
- // (3) when adding/modifying these values, ALSO update the VM methods for
2202
- // representing these values as human-readable strings!
2202
+ // (1) Computation errors are signalled by NEGATIVE values << -10^35.
2203
+ // (2) JavaScript exponents can go up to +/- 308 (IEEE 754 standard).
2204
+ // (3) when adding/modifying these values, ALSO update the VM methods
2205
+ // for representing these values as human-readable strings!
2203
2206
 
2204
2207
  this.ERROR = -1e+40; // Any lower value indicates a computation error
2205
2208
  this.CYCLIC = -1e+41;
@@ -2218,12 +2221,12 @@ class VirtualMachine {
2218
2221
  this.BAD_REF, this.UNDERFLOW, this.OVERFLOW, this.INVALID, this.PARAMS,
2219
2222
  this.UNKNOWN_ERROR, this.UNDEFINED, this.NOT_COMPUTED, this.COMPUTING];
2220
2223
 
2221
- // Prefix for warning messages that are logged in the monitor
2224
+ // Prefix for warning messages that are logged in the monitor.
2222
2225
  this.WARNING = '-- Warning: ';
2223
2226
 
2224
- // Solver constants indicating constraint type
2225
- // NOTE: these correspond to the codes used by LP_solve; when generating
2226
- // MPS files, other constants are used
2227
+ // Solver constants indicating constraint type.
2228
+ // NOTE: These correspond to the codes used in the LP format. When
2229
+ // generating MPS files, other constants are used.
2227
2230
  this.FR = 0;
2228
2231
  this.LE = 1;
2229
2232
  this.GE = 2;
@@ -2239,18 +2242,18 @@ class VirtualMachine {
2239
2242
  'hour': 1, 'minute': 1/60, 'second': 1/3600
2240
2243
  };
2241
2244
  // More or less standard time unit abbreviations.
2242
- // NOTE: minute is abbreviated to `m` to remain consistent with the constants
2243
- // that can be used in expressions. There, `min` already denotes the "minimum"
2244
- // operator.
2245
+ // NOTE: Minute is abbreviated to `m` to remain consistent with the
2246
+ // constants that can be used in expressions. There, `min` already
2247
+ // denotes the "minimum" operator.
2245
2248
  this.time_unit_shorthand = {
2246
2249
  'year': 'yr', 'week': 'wk', 'day': 'd',
2247
2250
  'hour': 'h', 'minute': 'm', 'second': 's'
2248
2251
  };
2249
2252
  // Number of rounds limited to 31 because JavaScript performs bitwise
2250
- // operations on 32 bit integers, and the sign bit may be troublesome
2253
+ // operations on 32 bit integers, and the sign bit may be troublesome.
2251
2254
  this.max_rounds = 31;
2252
2255
  this.round_letters = '?abcdefghijklmnopqrstuvwxyzABCDE';
2253
- // Standard 1-letter codes for Linny-R entities
2256
+ // Standard 1-letter codes for Linny-R entities.
2254
2257
  this.entity_names = {
2255
2258
  A: 'actor',
2256
2259
  B: 'constraint',
@@ -2262,7 +2265,7 @@ class VirtualMachine {
2262
2265
  Q: 'product'
2263
2266
  };
2264
2267
  this.entity_letters = 'ABCDELPQ';
2265
- // Standard attributes of Linny-R entities
2268
+ // Standard attributes of Linny-R entities.
2266
2269
  this.attribute_names = {
2267
2270
  'LB': 'lower bound',
2268
2271
  'UB': 'upper bound',
@@ -2282,18 +2285,18 @@ class VirtualMachine {
2282
2285
  'SOC': 'share of cost',
2283
2286
  'A': 'active'
2284
2287
  };
2285
- // NOTE: defaults are level (L), link flow (F), cluster cash flow (CF),
2286
- // actor cash flow (CF); dataset value (no attribute)
2287
- // NOTE: exogenous properties first, then the computed properties
2288
+ // NOTE: Defaults are level (L), link flow (F), cluster cash flow (CF),
2289
+ // actor cash flow (CF); dataset value (no attribute).
2290
+ // NOTE: Exogenous properties first, then the computed properties.
2288
2291
  this.process_attr = ['LB', 'UB', 'IL', 'LCF', 'L', 'CI', 'CO', 'CF', 'CP'];
2289
2292
  this.product_attr = ['LB', 'UB', 'IL', 'P', 'L', 'CP', 'HCP'];
2290
2293
  this.cluster_attr = ['CI', 'CO', 'CF'];
2291
2294
  this.link_attr = ['R', 'D', 'SOC', 'F'];
2292
2295
  this.constraint_attr = ['SOC', 'A'];
2293
2296
  this.actor_attr = ['W', 'CI', 'CO', 'CF'];
2294
- // Only expression attributes can be used for sensitivity analysis
2297
+ // Only expression attributes can be used for sensitivity analysis.
2295
2298
  this.expression_attr = ['LB', 'UB', 'IL', 'LCF', 'P', 'R', 'D', 'W'];
2296
- // Attributes per entity type letter
2299
+ // Attributes per entity type letter.
2297
2300
  this.attribute_codes = {
2298
2301
  A: this.actor_attr,
2299
2302
  B: this.constraint_attr,
@@ -2314,18 +2317,19 @@ class VirtualMachine {
2314
2317
  this.entity_attribute_names[el].push(ac[j]);
2315
2318
  }
2316
2319
  }
2317
- // Level-based attributes are computed only AFTER optimization
2320
+ // Level-based attributes are computed only AFTER optimization.
2318
2321
  this.level_based_attr = ['L', 'CP', 'HCP', 'CF', 'CI', 'CO', 'F', 'A'];
2319
2322
  this.object_types = ['Process', 'Product', 'Cluster', 'Link', 'Constraint',
2320
2323
  'Actor', 'Dataset', 'Equation'];
2321
2324
  this.type_attributes = [this.process_attr, this.product_attr,
2322
2325
  this.cluster_attr, this.link_attr, this.constraint_attr,
2323
2326
  this.actor_attr, [], []];
2324
- // Statistics that can be calculated for sets of variables
2327
+ // Statistics that can be calculated for sets of variables.
2325
2328
  this.statistic_operators =
2326
2329
  ['MAX', 'MEAN', 'MIN', 'N', 'SD', 'SUM', 'VAR',
2327
2330
  'MAXNZ', 'MEANNZ', 'MINNZ', 'NNZ', 'SDNZ', 'SUMNZ', 'VARNZ'];
2328
- // Statistics that can be calculated for outcomes and experiment run results
2331
+ // Statistics that can be calculated for outcomes and experiment run
2332
+ // results.
2329
2333
  this.outcome_statistics =
2330
2334
  ['LAST', 'MAX', 'MEAN', 'MIN', 'N', 'NZ', 'SD', 'SUM', 'VAR'];
2331
2335
  this.solver_names = {
@@ -2370,8 +2374,9 @@ class VirtualMachine {
2370
2374
  // (1) MODEL.runLength = simulation period + look-ahead, so that
2371
2375
  // should not be used to compute the number of blocks.
2372
2376
  // (2) For each block, a chunk (block + lookahead) is optimized.
2377
+ this.nr_of_time_steps = MODEL.end_period - MODEL.start_period + 1;
2373
2378
  this.nr_of_blocks = Math.ceil(
2374
- (MODEL.end_period - MODEL.start_period + 1) / MODEL.block_length);
2379
+ this.nr_of_time_steps / MODEL.block_length);
2375
2380
 
2376
2381
  // EXAMPLE: Simulation period of 55 time steps, block length of 10
2377
2382
  // time steps and no look-ahead => 6 chunks, and chunk length = block
@@ -3140,7 +3145,7 @@ class VirtualMachine {
3140
3145
 
3141
3146
  // FIRST: define indices for all variables (index = Simplex tableau column number)
3142
3147
 
3143
- // Each actor has a variable to compute its cash in and its cash out
3148
+ // Each actor has a variable to compute its cash in and its cash out.
3144
3149
  const actor_keys = Object.keys(MODEL.actors).sort();
3145
3150
  for(i = 0; i < actor_keys.length; i++) {
3146
3151
  const a = MODEL.actors[actor_keys[i]];
@@ -3247,9 +3252,9 @@ class VirtualMachine {
3247
3252
  // NOTE: Chunk variables of node `p` have LB = 0 and UB = UB of `p`.
3248
3253
  // This is effectuated by the VM "set bounds" instructions at run time.
3249
3254
 
3250
- // NOTE: Under normal assumptions (all processes having LB >= 0), bounds on
3251
- // actor cash flow variables need NOT be set because cash IN and cash OUT
3252
- // will then always be >= 0 (solver's default bounds).
3255
+ // NOTE: Under normal assumptions (all processes having LB >= 0), bounds
3256
+ // on actor cash flow variables need NOT be set because cash IN and
3257
+ // cash OUT will then always be >= 0 (solver's default bounds).
3253
3258
  // However, Linny-R does not prohibit negative bounds on processes, nor
3254
3259
  // negative rates on links. To be consistently permissive, cash IN and
3255
3260
  // cash OUT of all actors are both allowed to become negative.
@@ -3337,9 +3342,9 @@ class VirtualMachine {
3337
3342
 
3338
3343
  // NOTE: Each process generates cash flow proportional to its production
3339
3344
  // level if it produces and/or consumes a product having a price.
3340
- // Cash flow is negative (cash OUT) if a product is consumed AND has
3341
- // price > 0, but positive (cash IN) if a product is produced and has
3342
- // price < 0. Likewise for the other two cases.
3345
+ // Cash flow is negative (cash OUT) if a product is consumed AND
3346
+ // has price > 0, but positive (cash IN) if a product is produced
3347
+ // and has price < 0. Likewise for the other two cases.
3343
3348
  // To calculate the coefficient for the process variable, the
3344
3349
  // multiplier rates of the links in and out must be calculated (at
3345
3350
  // run time when dynamic expressions) such that they will add to the
@@ -3361,21 +3366,24 @@ class VirtualMachine {
3361
3366
  // on this vector. If expressions for process properties are
3362
3367
  // static, more efficient VM instructions are used.
3363
3368
 
3364
- // Initially assume "no cash flows for any actor to be considered"
3369
+ // Initially assume "no cash flows for any actor to be considered".
3370
+ // This flag will be set to FALSE when some actor cash flow constraint
3371
+ // has a non-zero coefficient.
3365
3372
  this.no_cash_flows = true;
3366
3373
 
3367
- // Iterate over all actors to add the cash flow computation constraints
3374
+ // Iterate over all actors to add the cash flow computation constraints.
3368
3375
  for(let ai = 0; ai < actor_keys.length; ai++) {
3369
3376
  const a = MODEL.actors[actor_keys[ai]];
3370
- this.code.push([VMI_clear_coefficients, null]);
3377
+ // NOTE: No need for VMI_clear_coefficients because the cash flow
3378
+ // coefficients operate on two special "registers" of the VM.
3371
3379
  for(i = 0; i < process_keys.length; i++) {
3372
3380
  k = process_keys[i];
3373
3381
  if(!MODEL.ignored_entities[k]) {
3374
3382
  const p = MODEL.processes[k];
3375
- // Only consider processes owned by this actor
3383
+ // Only consider processes owned by this actor.
3376
3384
  if(p.actor === a) {
3377
- // Iterate over links IN, but only consider consumed products having
3378
- // a market price
3385
+ // Iterate over links IN, but only consider consumed products
3386
+ // having a market price.
3379
3387
  for(j = 0; j < p.inputs.length; j++) {
3380
3388
  l = p.inputs[j];
3381
3389
  if(!MODEL.ignored_entities[l.identifier] &&
@@ -3383,18 +3391,17 @@ class VirtualMachine {
3383
3391
  if(l.from_node.price.isStatic && l.relative_rate.isStatic) {
3384
3392
  k = l.from_node.price.result(0) * l.relative_rate.result(0);
3385
3393
  // NOTE: VMI_update_cash_coefficient has at least 4 arguments:
3386
- // flow (CONSUME or PRODUCE), type (specifies the number and type
3387
- // of arguments), the level_var_index of the process, and the
3388
- // delay.
3394
+ // flow (CONSUME or PRODUCE), type (specifies the number and
3395
+ // type of arguments), the level_var_index of the process,
3396
+ // and the delay.
3397
+ // NOTE: Input links cannot have delay, so then delay = 0.
3389
3398
  if(Math.abs(k) > VM.NEAR_ZERO) {
3390
- // Consumption rate & price are static: pass one constant
3391
- // NOTE: input links cannot have delay, so delay = 0
3399
+ // Consumption rate & price are static: pass one constant.
3392
3400
  this.code.push([VMI_update_cash_coefficient,
3393
3401
  [VM.CONSUME, VM.ONE_C, p.level_var_index, 0, k]]);
3394
3402
  }
3395
3403
  } else {
3396
- // No further optimization: assume two dynamic expressions
3397
- // NOTE: input links cannot have delay, so delay = 0
3404
+ // No further optimization: assume two dynamic expressions.
3398
3405
  this.code.push([VMI_update_cash_coefficient,
3399
3406
  [VM.CONSUME, VM.TWO_X, p.level_var_index, 0,
3400
3407
  l.from_node.price, l.relative_rate]]);
@@ -3403,15 +3410,16 @@ class VirtualMachine {
3403
3410
  } // END of FOR ALL input links
3404
3411
 
3405
3412
  // Iterate over links OUT, but only consider produced products
3406
- // having a (non-zero) market price
3413
+ // having a (non-zero) market price.
3407
3414
  for(j = 0; j < p.outputs.length; j++) {
3408
3415
  l = p.outputs[j];
3409
3416
  const tnpx = l.to_node.price;
3410
3417
  if(!MODEL.ignored_entities[l.identifier] && tnpx.defined &&
3411
3418
  !(tnpx.isStatic && Math.abs(tnpx.result(0)) < VM.NEAR_ZERO)) {
3412
- // By default, use the process level as multiplier
3419
+ // By default, use the process level as multiplier.
3413
3420
  vi = p.level_var_index;
3414
- // For "binary data links", use the correct binary variable instead
3421
+ // For "binary data links", use the correct binary variable
3422
+ // instead of the level.
3415
3423
  if(l.multiplier === VM.LM_STARTUP) {
3416
3424
  vi = p.start_up_var_index;
3417
3425
  } else if(l.multiplier === VM.LM_FIRST_COMMIT) {
@@ -3432,57 +3440,63 @@ class VirtualMachine {
3432
3440
  // times the rate of `l`.
3433
3441
  for(k = 0; k < l.from_node.inputs.length; j++) {
3434
3442
  ll = l.from_node.inputs[k];
3435
- // NOTE: no attempt for efficiency -- assume that
3436
- // price and both rates are dynamic
3443
+ // NOTE: No attempt for efficiency -- assume that
3444
+ // price and both rates are dynamic.
3437
3445
  this.code.push([VMI_update_cash_coefficient, [
3438
3446
  VM.PRODUCE, VM.THREE_X, vi, l.flow_delay, tnpx,
3439
3447
  l.relative_rate, ll.relative_rate]]);
3440
3448
  }
3441
3449
  } else if(l.multiplier === VM.LM_SPINNING_RESERVE) {
3442
- // "spinning reserve" equals UB - level if level > 0, or 0
3443
- // The cash flow then equals ON/OFF * UB * price * rate MINUS
3444
- // level * price * rate, hence a special instruction type
3445
- // NOTE: only the ON/OFF variable determines whether there will
3446
- // be any cash flow, hence it is passed as the primary variable,
3447
- // and the process level as the secondary variable
3450
+ // "spinning reserve" equals UB - level if level > 0,
3451
+ // and otherwise 0. The cash flow then equals
3452
+ // ON/OFF * UB * price * rate MINUS level * price * rate,
3453
+ // hence a special instruction type.
3454
+ // NOTE: Only the ON/OFF variable determines whether
3455
+ // there will be any cash flow, hence it is passed as
3456
+ // the primary variable, and the process level as the
3457
+ // secondary variable.
3448
3458
  this.code.push([VMI_update_cash_coefficient, [
3449
- VM.PRODUCE, VM.SPIN_RES, p.on_off_var_index, l.flow_delay, vi,
3450
- l.from_node.upper_bound, tnpx, l.relative_rate]]);
3459
+ VM.PRODUCE, VM.SPIN_RES, p.on_off_var_index,
3460
+ l.flow_delay, vi, l.from_node.upper_bound, tnpx,
3461
+ l.relative_rate]]);
3451
3462
  } else if(l.multiplier === VM.LM_PEAK_INC) {
3452
- // NOTE: "peak increase" may be > 0 only in the first time step
3453
- // of the block being optimized, and in the first step of the
3454
- // look-ahead period (if peak rises in that period), and will
3455
- // be 0 in all other time steps; the VM instruction handles this
3463
+ // NOTE: "peak increase" may be > 0 only in the first
3464
+ // time step of the block being optimized, and in the
3465
+ // first step of the look-ahead period (if peak rises
3466
+ // in that period), and will be 0 in all other time steps.
3467
+ // The VM instruction handles this.
3456
3468
  // NOTE: Delay is always 0 for this link flow.
3457
3469
  this.code.push([VMI_update_cash_coefficient, [
3458
3470
  VM.PRODUCE, VM.PEAK_INC, p.peak_inc_var_index, 0,
3459
3471
  tnpx, l.relative_rate]]);
3460
3472
  } else if(tnpx.isStatic && l.relative_rate.isStatic) {
3461
- // If link rate and product price are static, only add the variable
3462
- // if rate*price is non-zero (and th en use the static VM instruction)
3473
+ // If link rate and product price are static, only add
3474
+ // the variable if rate*price is non-zero (and then pass
3475
+ // the constant rate*price to the VM instruction.
3463
3476
  k = tnpx.result(0) * l.relative_rate.result(0);
3464
3477
  if(Math.abs(k) > VM.NEAR_ZERO) {
3465
- // Production rate & price are static: pass one constant
3478
+ // Production rate & price are static: pass one constant.
3466
3479
  this.code.push([VMI_update_cash_coefficient,
3467
3480
  [VM.PRODUCE, VM.ONE_C, vi, l.flow_delay, k]]);
3468
3481
  // When multiplier is Delta, subtract level in previous t
3469
- // (so add 1 to flow delay, and consume, rather than produce)
3482
+ // (so add 1 to flow delay, and consume, rather than
3483
+ // produce).
3470
3484
  if(l.multiplier === VM.LM_INCREASE) {
3471
3485
  this.code.push([VMI_update_cash_coefficient,
3472
- // NOTE: 6th argument = 1 indicates "delay + 1"
3486
+ // NOTE: 6th argument = 1 indicates "delay + 1".
3473
3487
  [VM.CONSUME, VM.ONE_C, vi, l.flow_delay, k, 1]]);
3474
3488
  }
3475
3489
  }
3476
3490
  } else {
3477
- // Production rate or price are dynamic: pass two expressions
3491
+ // Production rate or price are dynamic: pass two expressions.
3478
3492
  this.code.push([VMI_update_cash_coefficient, [
3479
3493
  VM.PRODUCE, VM.TWO_X, vi, l.flow_delay,
3480
3494
  tnpx, l.relative_rate]]);
3481
- // When multiplier is Delta, consume level in previous t
3495
+ // When multiplier is Delta, consume level in previous t.
3482
3496
  if(l.multiplier === VM.LM_INCREASE) {
3483
3497
  this.code.push([VMI_update_cash_coefficient, [
3484
3498
  VM.CONSUME, VM.TWO_X, vi, l.flow_delay,
3485
- // NOTE: now 7th argument indicates "delay + 1"
3499
+ // NOTE: Now 7th argument indicates "delay + 1".
3486
3500
  tnpx, l.relative_rate, 1]]);
3487
3501
  }
3488
3502
  }
@@ -3491,33 +3505,26 @@ class VirtualMachine {
3491
3505
  } // END of FOR ALL output links
3492
3506
  } // END of IF process not ignored
3493
3507
  } // END of FOR ALL processes
3494
-
3495
- // NOTE: if the last VM instruction still is "clear coefficients",
3496
- // this means that (so far) no actor cash flows were detected
3497
- if(this.no_cash_flows) {
3498
- this.no_cash_flows =
3499
- this.code[this.code.length-1][0] === VMI_clear_coefficients;
3500
- }
3501
-
3502
- // ALWAYS add the two cash flow constraints for this actor, as both cash
3503
- // flow variables must be computed (will be 0 if no cash flows)
3504
- this.code.push(
3505
- [VMI_copy_cash_coefficients, VM.PRODUCE],
3506
- [VMI_add_const_to_coefficient, [a.cash_in_var_index, 1]],
3507
- [VMI_add_constraint, VM.EQ],
3508
- [VMI_copy_cash_coefficients, VM.CONSUME],
3509
- [VMI_add_const_to_coefficient, [a.cash_out_var_index, 1]],
3510
- [VMI_add_constraint, VM.EQ]
3511
- );
3512
-
3508
+
3509
+ // Check whether any VMI_update_cash_coefficient instructions have
3510
+ // been added. If so, the objective function will maximze weighted
3511
+ // sum of actor cash flows, otherwise minimize sum of process levels.
3512
+ this.no_cash_flows = this.no_cash_flows &&
3513
+ this.code[this.code.length - 1][0] !== VMI_update_cash_coefficient;
3514
+
3515
+ // ALWAYS add the two cash flow constraints for this actor, as both
3516
+ // cash flow variables must be computed (will be 0 if no cash flows).
3517
+ this.code.push([VMI_add_cash_constraints,
3518
+ [a.cash_in_var_index, a.cash_out_var_index]]);
3519
+
3513
3520
  } // END of FOR loop iterating over all actors
3514
3521
 
3515
- // NEXT: define the coefficients for the objective function
3522
+ // NEXT: Define the coefficients for the objective function.
3516
3523
  this.code.push([VMI_clear_coefficients, null]);
3517
3524
 
3518
- // NOTE: if, after all actors -- this includes (no actor) -- have been
3519
- // considered, no cash flows have been detected, the solver should aim for
3520
- // minimal effort, i.e., lowest weighted sum of process levels
3525
+ // NOTE: If, after all actors -- this includes (no actor) -- have been
3526
+ // considered, no cash flows have been detected, the solver should aim
3527
+ // for minimal effort, i.e., lowest weighted sum of process levels.
3521
3528
  if(this.no_cash_flows) {
3522
3529
  for(i = 0; i < process_keys.length; i++) {
3523
3530
  k = process_keys[i];
@@ -4289,8 +4296,9 @@ class VirtualMachine {
4289
4296
  // largest cash flow coefficient (in absolute value) within the
4290
4297
  // current block so that cash flows cannot easily "overrule" the
4291
4298
  // slack penalties in the objective function.
4292
- // NOTE: No scaling needed if model features no cash flows.
4293
- if(this.no_cash_flows) return;
4299
+ // NOTE: No scaling needed if model features no cash flows, or if
4300
+ // cash scalar equals 1.
4301
+ if(this.no_cash_flows || this.cash_scalar === 1) return;
4294
4302
  this.logMessage(this.block_count,
4295
4303
  'Cash flows scaled by 1/' + this.cash_scalar);
4296
4304
  // Use reciprocal as multiplier to scale the constraint coefficients.
@@ -4311,7 +4319,7 @@ class VirtualMachine {
4311
4319
  }
4312
4320
  }
4313
4321
  }
4314
-
4322
+
4315
4323
  checkForInfinity(n) {
4316
4324
  // Return floating point number `n`, or +INF or -INF if the absolute
4317
4325
  // value of `n` is relatively (!) close to the VM infinity constants
@@ -4337,8 +4345,6 @@ class VirtualMachine {
4337
4345
  let bb = (block - 1) * MODEL.block_length + 1,
4338
4346
  abl = this.chunk_length,
4339
4347
  cbl = this.actualBlockLength(block);
4340
- // For the last block, crop the actual block length so it does not
4341
- // extend beyond the simulation period (these results should be ignored).
4342
4348
  // If no results computed, preserve those already computed for the
4343
4349
  // pervious chunk as "look-ahead".
4344
4350
  if(err && block > 1 && MODEL.look_ahead > 0) {
@@ -4349,7 +4355,9 @@ class VirtualMachine {
4349
4355
  'No results from solver -- retained results of ' +
4350
4356
  pluralS(MODEL.look_ahead, 'look-ahead time step'));
4351
4357
  }
4352
- if(cbl <= 0) {
4358
+ // For the last block, crop the actual block length so it does not
4359
+ // extend beyond the simulation period (these results should be ignored).
4360
+ if(cbl < 0) {
4353
4361
  this.logMessage(block, 'Results of last optimization could be discarded');
4354
4362
  abl = 0;
4355
4363
  } else if(cbl < abl) {
@@ -4389,11 +4397,11 @@ class VirtualMachine {
4389
4397
  x[a.cash_out_var_index + j] * this.cash_scalar);
4390
4398
  a.cash_flow[b] = a.cash_in[b] - a.cash_out[b];
4391
4399
  // Count occurrences of a negative cash flow (threshold -0.5 cent).
4392
- if(a.cash_in[b] < -0.005) {
4400
+ if(b <= this.nr_of_time_steps && a.cash_in[b] < -0.005) {
4393
4401
  this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
4394
4402
  a.displayName + ' cash IN = ' + a.cash_in[b].toPrecision(2));
4395
4403
  }
4396
- if(a.cash_out[b] < -0.005) {
4404
+ if(b <= this.nr_of_time_steps && a.cash_out[b] < -0.005) {
4397
4405
  this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
4398
4406
  a.displayName + ' cash IN = ' + a.cash_out[b].toPrecision(2));
4399
4407
  }
@@ -4494,10 +4502,10 @@ class VirtualMachine {
4494
4502
  // NOTE: Only check after the last round has been evaluated.
4495
4503
  if(round === this.lastRound) {
4496
4504
  let b = bb;
4497
- // Iterate over all time steps in this block
4505
+ // Iterate over all time steps in this block.
4498
4506
  let j = -1;
4499
4507
  for(let i = 0; i < abl; i++) {
4500
- // Index `svt` iterates over types of slack variable (0 - 2)
4508
+ // Index `svt` iterates over types of slack variable (0 - 2).
4501
4509
  for(let svt = 0; svt <= 2; svt++) {
4502
4510
  const
4503
4511
  svl = this.slack_variables[svt],
@@ -4509,12 +4517,12 @@ class VirtualMachine {
4509
4517
  absl = Math.abs(slack);
4510
4518
  if(absl > VM.NEAR_ZERO) {
4511
4519
  const v = this.variables[vi - 1];
4512
- // NOTE: for constraints, add 'UB' or 'LB' to its vector for the
4513
- // time step where slack was used
4520
+ // NOTE: for constraints, add 'UB' or 'LB' to its vector for
4521
+ // the time step where slack was used.
4514
4522
  if(v[1] instanceof BoundLine) {
4515
4523
  v[1].constraint.slack_info[b] = v[0];
4516
4524
  }
4517
- if(absl > VM.SIG_DIF_FROM_ZERO) {
4525
+ if(b <= this.nr_of_time_steps && absl > VM.SIG_DIF_FROM_ZERO) {
4518
4526
  this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
4519
4527
  `${v[1].displayName} ${v[0]} slack = ${this.sig4Dig(slack)}`);
4520
4528
  if(v[1] instanceof Product) {
@@ -4560,6 +4568,7 @@ class VirtualMachine {
4560
4568
  // optimization period, hence start by calculating the offset `bb`
4561
4569
  // being the first time step of this block.
4562
4570
  // Blocks are numbered 1, 2, ...
4571
+ let latest_time_step = 0;
4563
4572
  const
4564
4573
  bb = (block - 1) * MODEL.block_length + 1,
4565
4574
  cbl = this.actualBlockLength(block);
@@ -4580,6 +4589,7 @@ class VirtualMachine {
4580
4589
  // NOTE: Flows may have a delay!
4581
4590
  ld = l.actualDelay(b);
4582
4591
  bt = b - ld;
4592
+ latest_time_step = Math.max(latest_time_step, bt);
4583
4593
  // If delay < 0 AND this results in a block time beyond the
4584
4594
  // block length, this means that the level of the FROM node
4585
4595
  // must be "fixated" in the next block.
@@ -4704,9 +4714,10 @@ class VirtualMachine {
4704
4714
  // INPUT links from priced products generate cash OUT...
4705
4715
  for(let j = 0; j < p.inputs.length; j++) {
4706
4716
  // NOTE: Input links do NOT have a delay.
4707
- const l = p.inputs[j],
4708
- af = l.actual_flow[b],
4709
- fnp = l.from_node.price;
4717
+ const
4718
+ l = p.inputs[j],
4719
+ af = l.actual_flow[b],
4720
+ fnp = l.from_node.price;
4710
4721
  if(af > VM.NEAR_ZERO && fnp.defined) {
4711
4722
  const pp = fnp.result(b);
4712
4723
  if(pp > 0 && pp < VM.PLUS_INFINITY) {
@@ -4719,14 +4730,14 @@ class VirtualMachine {
4719
4730
  }
4720
4731
  // OUTPUT links to priced products generate cash IN ...
4721
4732
  for(let j = 0; j < p.outputs.length; j++) {
4722
- // NOTE: Actual flows already consider delay!
4723
- const l = p.outputs[j],
4724
- ld = l.actualDelay(b),
4725
- af = l.actual_flow[b],
4726
- tnp = l.to_node.price;
4733
+ // NOTE: actualFlows already consider delay!
4734
+ const
4735
+ l = p.outputs[j],
4736
+ af = l.actualFlow(b),
4737
+ tnp = l.to_node.price;
4727
4738
  if(af > VM.NEAR_ZERO && tnp.defined) {
4728
- // NOTE: To get the correct price, again consider delays.
4729
- const pp = tnp.result(b - ld);
4739
+ // NOTE: Use the price at the time of the actual flow.
4740
+ const pp = tnp.result(b);
4730
4741
  if(pp > 0 && pp < VM.PLUS_INFINITY) {
4731
4742
  ci += pp * af;
4732
4743
  // ... unless the product price is negative; then cash OUT.
@@ -4755,15 +4766,128 @@ class VirtualMachine {
4755
4766
  // at a time because of delays, and also because expressions may refer
4756
4767
  // to values for earlier time steps.
4757
4768
  if(MODEL.infer_cost_prices) {
4769
+ // Keep track of products for which CP will not be computed correctly
4770
+ // due to negative delays.
4771
+ MODEL.products_with_negative_delays = {};
4758
4772
  b = bb;
4759
4773
  for(let i = 0; i < cbl; i++) {
4760
- if(!MODEL.calculateCostPrices(b)) {
4774
+ // NOTE: Issues with cost price calculation beyond simulation
4775
+ // period need not be reported.
4776
+ if(b <= this.nr_of_time_steps && !MODEL.calculateCostPrices(b)) {
4761
4777
  this.logMessage(block, `${this.WARNING}(t=${b}) ` +
4762
4778
  'Invalid cost prices due to negative flow(s)');
4763
4779
  }
4764
4780
  // Move on to the next time step of the block.
4765
4781
  b++;
4766
4782
  }
4783
+ // NOTE: Links with negative delays will not have correct cost
4784
+ // prices as these occur in the future. Having calculated (insofar
4785
+ // as possibe) cost prices of nodes, those on links with negative
4786
+ // delays can now be set to these "future" cost prices.
4787
+ let ts = Object.keys(MODEL.products_with_negative_delays);
4788
+ // Make a separate list of stocks, as these require post-processing.
4789
+ const stocks = [];
4790
+ // NOTE: Proceed backwards in time, using CP = 0 when time step
4791
+ // falls beyond block length + look-ahead.
4792
+ for(let i = ts.length - 1; i >= 0; i--) {
4793
+ const
4794
+ t = parseInt(ts[i]),
4795
+ pwnd = MODEL.products_with_negative_delays[t];
4796
+ // @@TO DO: Sort products in order of precedence to avoid that
4797
+ // when Q1 --> Q2 the CP of Q2 is computed first, and remains
4798
+ // "undefined" while the CP of Q1 can be known.
4799
+ for(let j = 0; j < pwnd.length; j++) {
4800
+ const p = pwnd[j];
4801
+ if(p.is_buffer) addDistinct(p, stocks);
4802
+ // Compute total cost price as sum of inflow * unit cost price.
4803
+ let tcp = 0,
4804
+ taf = 0;
4805
+ for(let k = 0; k < p.inputs.length; k++) {
4806
+ const
4807
+ l = p.inputs[k],
4808
+ d = l.actualDelay(t),
4809
+ td = t - d;
4810
+ // Only compute if t lies within the optimization period.
4811
+ if(td < bb + cbl) {
4812
+ const
4813
+ // NOTE: actualFlow already considers delays.
4814
+ af = l.actualFlow(t),
4815
+ // Get the cost price for the delayed time step.
4816
+ qcp = l.from_node.costPrice(td);
4817
+ if(qcp > VM.EXCEPTION) {
4818
+ tcp = VM.UNDEFINED;
4819
+ break;
4820
+ }
4821
+ if(af > VM.NEAR_ZERO) {
4822
+ tcp += af * qcp * l.share_of_cost;
4823
+ taf += af;
4824
+ }
4825
+ }
4826
+ }
4827
+ // CP beyond time horizon
4828
+ if(tcp >= VM.EXCEPTION) {
4829
+ p.cost_price[t] = VM.UNDEFINED;
4830
+ } else if(taf > 0) {
4831
+ // Product cost price is "per unit".
4832
+ p.cost_price[t] = tcp / taf;
4833
+ } else if(p.inputs.length === 1) {
4834
+ // If product has only one input, its cost price can be
4835
+ // inferred from this input even if there is no inflow.
4836
+ const
4837
+ l = p.inputs[0],
4838
+ d = l.actualDelay(t),
4839
+ rr = l.relative_rate.result[t - d];
4840
+ // When t - d is outside period, `rr` is undefined => CP = 0.
4841
+ p.cost_price[t] = (rr ? l.from_node.costPrice(t - d) *
4842
+ rr * l.share_of_cost : 0);
4843
+ } else {
4844
+ p.cost_price[t] = 0;
4845
+ }
4846
+ }
4847
+ }
4848
+ // NOTE: Stocks require special treatment due to working backwards
4849
+ // in time.
4850
+ for(let i = 0; i < stocks.length; i++) {
4851
+ const p = stocks[i];
4852
+ // Get previous stock level and stock price (prior to block start).
4853
+ let sl = p.actualLevel(bb - 1),
4854
+ psp = p.stock_price[bb - 1] || 0;
4855
+ for(let t = bb; t < bb + cbl; t++) {
4856
+ // Subtract outflows from stock level.
4857
+ for(let k = 0; k < p.outputs.length; k++) {
4858
+ const
4859
+ l = p.outputs[k],
4860
+ af = l.actualFlow(t);
4861
+ if(af > VM.NEAR_ZERO) sl -= af;
4862
+ }
4863
+ // Start with total CP = remaining old stock times old price.
4864
+ let tcp = sl * psp;
4865
+ // Add inflows to both level and total CP.
4866
+ for(let k = 0; k < p.inputs.length; k++) {
4867
+ const
4868
+ l = p.inputs[k],
4869
+ af = l.actualFlow(t),
4870
+ d = l.actualDelay(t);
4871
+ if(af > VM.NEAR_ZERO) {
4872
+ const cp = l.from_node.costPrice(t - d);
4873
+ if(cp >= VM.EXCEPTION) {
4874
+ tcp = VM.UNDEFINED;
4875
+ break;
4876
+ }
4877
+ sl += af;
4878
+ tcp += af * cp;
4879
+ }
4880
+ }
4881
+ if(tcp >= VM.EXCEPTION) {
4882
+ psp = VM.UNDEFINED;
4883
+ } else if(sl > VM.NEAR_ZERO) {
4884
+ psp = tcp / sl;
4885
+ } else {
4886
+ psp = 0;
4887
+ }
4888
+ p.stock_price[t] = psp;
4889
+ }
4890
+ }
4767
4891
  }
4768
4892
 
4769
4893
  // THEN: Reset all datasets that are outcomes or serve as "formulas".
@@ -4861,41 +4985,45 @@ class VirtualMachine {
4861
4985
  }
4862
4986
 
4863
4987
  resetTableau() {
4864
- // Clears tableau data: matrix, rhs and constraint types
4865
- // NOTE: this reset is called when initializing, and to free up
4866
- // memory after posting a block to the server
4988
+ // Clears tableau data: matrix, rhs and constraint types.
4989
+ // NOTE: This reset is called when initializing, and to free up
4990
+ // memory after posting a block to the server.
4867
4991
  this.matrix.length = 0;
4868
4992
  this.right_hand_side.length = 0;
4869
4993
  this.constraint_types.length = 0;
4870
4994
  }
4871
4995
 
4872
4996
  initializeTableau(abl) {
4873
- // `offset` is used to calculate the actual column index for variables
4997
+ // `offset` is used to calculate the actual column index for variables.
4874
4998
  this.offset = 0;
4875
- // NOTE: vectors are "sparse" (i.e., will contain many 0) and are hence not
4876
- // represented as arrays but as objects, e.g., {4:1.5, 8:0.3} to represent
4877
- // an array [0, 0, 0, 1.5, 0, 0, 0, 0.3, 0, 0, 0, ...]
4878
- // The keys can span the full chunk, so the objects represent vectors that
4879
- // have a "virtual length" of cols * abl
4999
+ // NOTE: Vectors are "sparse" (i.e., will contain many 0) and are hence
5000
+ // not represented as arrays but as objects, e.g., {4:1.5, 8:0.3} to
5001
+ // represent an array [0, 0, 0, 1.5, 0, 0, 0, 0.3, 0, 0, 0, ...].
5002
+ // The keys can span the full chunk, so the objects represent vectors
5003
+ // that have a "virtual length" of cols * abl.
4880
5004
  this.coefficients = {};
5005
+ this.rhs = 0;
5006
+ // NOTE: VM needs separate "registers" for cash IN and cash OUT
5007
+ // coefficients and RHS because the direction of the cash flow is
5008
+ // dynamic.
4881
5009
  this.cash_in_coefficients = {};
5010
+ this.cash_in_rhs = 0;
4882
5011
  this.cash_out_coefficients = {};
4883
- // NOTE: cash flow equation coefficients may be divided by a scalar to keep
4884
- // them amply below the base slack penalty; the scalar is increased by the
4885
- // VM instruction VMI_copy_cash_coefficients so that at the end of the
4886
- // block setup it equals the highest absolute coefficient in the cash flow
4887
- // constraint equations; the VM maintains a list of indices of matrix rows
4888
- // that then need to be scaled
5012
+ this.cash_out_rhs = 0;
5013
+ // NOTE: Cash flow equation coefficients may be divided by a scalar to
5014
+ // keep them amply below the base slack penalty; the scalar is increased
5015
+ // by the VM instruction VMI_add_cash_constraints so that at the end of
5016
+ // the block setup it equals the highest absolute coefficient in the
5017
+ // cash flow constraint equations. The VM maintains a list of indices
5018
+ // of matrix rows that then need to be scaled.
4889
5019
  this.cash_scalar = 1;
4890
5020
  this.cash_constraints = [];
5021
+ // Vector for the objective function coefficients.
4891
5022
  this.objective = {};
5023
+ // Vectors for the bounds on decision variables.
4892
5024
  this.lower_bounds = {};
4893
5025
  this.upper_bounds = {};
4894
- // NOTE: right-hand side of cash IN/OUT equations will always be 0 as the
4895
- // actors' cash flows are calculated as C - a1P1 - a2P2 - ... anPn = 0
4896
- this.rhs = 0;
4897
- // NOTE: the constraint coefficient matrix and the rhs and ct vectors
4898
- // have equal length (#rows); the matrix is a list of sparse vectors
5026
+ // Clear the tableau matrix and constraint type and RHS columns.
4899
5027
  this.resetTableau();
4900
5028
  // NOTE: setupBlock only works properly if setupProblem was successful
4901
5029
  // Every variable gets one column per time step => tableau is organized
@@ -4921,7 +5049,7 @@ class VirtualMachine {
4921
5049
  this.is_binary[parseInt(i) + j*this.cols] = true;
4922
5050
  }
4923
5051
  }
4924
- // Set list with indices of semi-contiuous variables
5052
+ // Set list with indices of semi-contiuous variables.
4925
5053
  this.is_semi_continuous = {};
4926
5054
  for(let i in this.sec_var_indices) if(Number(i)) {
4927
5055
  for(let j = 0; j < abl; j++) {
@@ -5978,7 +6106,7 @@ function VMI_push_block_number(x) {
5978
6106
 
5979
6107
  function VMI_push_run_length(x) {
5980
6108
  // Push the run length (excl. look-ahead!).
5981
- const n = MODEL.end_period - MODEL.start_period + 1;
6109
+ const n = VM.nr_of_time_steps;
5982
6110
  if(DEBUGGING) console.log('push run length N = ' + n);
5983
6111
  x.push(n);
5984
6112
  }
@@ -6257,7 +6385,7 @@ function relativeTimeStep(t, anchor, offset, dtm, x) {
6257
6385
  }
6258
6386
  if(anchor === 'l') {
6259
6387
  // Last: offset relative to the last index in the vector.
6260
- return MODEL.end_period - MODEL.start_period + 1 + offset;
6388
+ return VM.nr_of_time_steps + offset;
6261
6389
  }
6262
6390
  if(anchor === 's') {
6263
6391
  // Scaled: offset is scaled to time unit of run.
@@ -6645,7 +6773,7 @@ function VMI_push_run_result(x, args) {
6645
6773
  } else if(rrspec.nr !== false) {
6646
6774
  // Run number inferred from local time step of expression
6647
6775
  const
6648
- rl = MODEL.end_period - MODEL.start_period + 1,
6776
+ rl = VM.nr_of_time_steps,
6649
6777
  range = rangeToList(rrspec.nr, xp.runs.length - 1);
6650
6778
  if(range) {
6651
6779
  const
@@ -6774,7 +6902,7 @@ function VMI_push_statistic(x, args) {
6774
6902
  }
6775
6903
  // Negative time step is evaluated as t = 0 (initial value) t beyond
6776
6904
  // optimization period is evaluated as its last time step
6777
- const tmax = MODEL.end_period - MODEL.start_period + 1;
6905
+ const tmax = VM.nr_of_time_steps;
6778
6906
  t1 = Math.max(0, Math.min(tmax, t1));
6779
6907
  t2 = Math.max(0, Math.min(tmax, t2));
6780
6908
  // Trace only now that time step range has been computed
@@ -7611,13 +7739,28 @@ function VMI_set_bounds(args) {
7611
7739
  }
7612
7740
 
7613
7741
  function VMI_clear_coefficients() {
7742
+ // Clear the coefficients register and set RHS to zero.
7614
7743
  if(DEBUGGING) console.log('clear_coefficients');
7615
7744
  VM.coefficients = {};
7616
- VM.cash_in_coefficients = {};
7617
- VM.cash_out_coefficients = {};
7618
7745
  VM.rhs = 0;
7619
7746
  }
7620
7747
 
7748
+ // AUXILIARY FUNCTION (added to support debugging)
7749
+ function knownValue(vi, t) {
7750
+ // Return the value of decision variable X that has already been
7751
+ // computed while optimizing a previous block.
7752
+ const
7753
+ // `vi` is the variable index for X, so use vi-1 for the zero-based
7754
+ // list VM.variables list.
7755
+ vbl = VM.variables[vi - 1],
7756
+ // NOTE: priorValue deals with special cases for binary variables.
7757
+ pv = VM.priorValue(vbl, t);
7758
+ if(DEBUGGING) {
7759
+ console.log(`--known value: ${vbl[0]} ${vbl[1].displayName} @ ${t} = ${pv}`);
7760
+ }
7761
+ return pv;
7762
+ }
7763
+
7621
7764
  function VMI_add_const_to_coefficient(args) {
7622
7765
  // `args`: [var_index, number (, delay (, 1))]
7623
7766
  const
@@ -7633,9 +7776,7 @@ function VMI_add_const_to_coefficient(args) {
7633
7776
  d = args[2];
7634
7777
  }
7635
7778
  }
7636
- const
7637
- k = VM.offset + vi - d*VM.cols,
7638
- t = VM.t - d;
7779
+ const k = VM.offset + vi - d*VM.cols;
7639
7780
  if(DEBUGGING) {
7640
7781
  console.log(`add_const_to_coefficient [${k}]: ${VM.sig4Dig(n)}`);
7641
7782
  }
@@ -7649,16 +7790,7 @@ function VMI_add_const_to_coefficient(args) {
7649
7790
  // while solving a previous block. Since the value of X is known,
7650
7791
  // adding n to C is implemented as subtracting n*X from the right hand
7651
7792
  // side of the constraint.
7652
- // NOTE: Subtract 1 from index `vi` because VM.variables is a 0-based
7653
- // array.
7654
- const
7655
- vbl = VM.variables[vi - 1],
7656
- pv = VM.priorValue(vbl, t);
7657
- if(DEBUGGING) {
7658
- console.log(`--lookup[${k}]: ${vbl[0]} ${vbl[1].displayName} @ ${t} = ${pv}`);
7659
- }
7660
- // NOTE: Special cases for binary variables!
7661
- VM.rhs -= pv * n;
7793
+ VM.rhs -= knownValue(vi, VM.t - d) * n;
7662
7794
  } else if(k in VM.coefficients) {
7663
7795
  VM.coefficients[k] += n;
7664
7796
  } else {
@@ -7690,11 +7822,7 @@ function VMI_add_const_to_sum_coefficients(args) {
7690
7822
  if(k > VM.chunk_offset) return;
7691
7823
  if(k <= 0) {
7692
7824
  // See NOTE in VMI_add_const_to_coefficient instruction.
7693
- const vbl = VM.variables[vi - 1];
7694
- if(DEBUGGING) {
7695
- console.log('--lookup[' + k + ']: ' + vbl[0] + ' ' + vbl[1].displayName);
7696
- }
7697
- VM.rhs -= VM.priorValue(vbl, t) * n;
7825
+ VM.rhs -= knownValue(vi, t) * n;
7698
7826
  } else if(k in VM.coefficients) {
7699
7827
  VM.coefficients[k] += n;
7700
7828
  } else {
@@ -7728,11 +7856,7 @@ function VMI_add_var_to_coefficient(args) {
7728
7856
  if(k > VM.chunk_offset) return;
7729
7857
  if(k <= 0) {
7730
7858
  // See NOTE in VMI_add_const_to_coefficient instruction.
7731
- const vbl = VM.variables[vi - 1];
7732
- if(DEBUGGING) {
7733
- console.log('--lookup[' + k + ']: ' + vbl[0] + ' ' + vbl[1].displayName);
7734
- }
7735
- VM.rhs -= VM.priorValue(vbl, t) * r;
7859
+ VM.rhs -= knownValue(vi, t) * r;
7736
7860
  } else if(k in VM.coefficients) {
7737
7861
  VM.coefficients[k] += r;
7738
7862
  } else {
@@ -7766,11 +7890,7 @@ function VMI_add_var_to_weighted_sum_coefficients(args) {
7766
7890
  if(args.length > 3) r /= (d + 1);
7767
7891
  if(k <= 0) {
7768
7892
  // See NOTE in VMI_add_const_to_coefficient instruction
7769
- const vbl = VM.variables[vi - 1];
7770
- if(DEBUGGING) {
7771
- console.log('--lookup[' + k + ']: ' + vbl[0] + ' ' + vbl[1].displayName);
7772
- }
7773
- VM.rhs -= VM.priorValue(vbl, t) * r;
7893
+ VM.rhs -= knownValue(vi, t) * r;
7774
7894
  } else if(k in VM.coefficients) {
7775
7895
  VM.coefficients[k] += r;
7776
7896
  } else {
@@ -7792,9 +7912,7 @@ function VMI_subtract_const_from_coefficient(args) {
7792
7912
  // 4th argument indicates "delay + 1"
7793
7913
  if(args.length > 3) d++;
7794
7914
  }
7795
- const
7796
- k = VM.offset + vi - d*VM.cols,
7797
- t = VM.t - d;
7915
+ const k = VM.offset + vi - d*VM.cols;
7798
7916
  if(DEBUGGING) {
7799
7917
  console.log('subtract_const_from_coefficient [' + k + ']: ' + VM.sig4Dig(n));
7800
7918
  }
@@ -7802,11 +7920,7 @@ function VMI_subtract_const_from_coefficient(args) {
7802
7920
  if(k > VM.chunk_offset) return;
7803
7921
  if(k <= 0) {
7804
7922
  // See NOTE in VMI_add_const_to_coefficient instruction
7805
- const vbl = VM.variables[vi - 1];
7806
- if(DEBUGGING) {
7807
- console.log('--lookup[' + k + ']: ' + vbl[0] + ' ' + vbl[1].displayName);
7808
- }
7809
- VM.rhs += VM.priorValue(vbl, t) * n;
7923
+ VM.rhs += knownValue(vi, VM.t - d) * n;
7810
7924
  } else if(k in VM.coefficients) {
7811
7925
  VM.coefficients[k] -= n;
7812
7926
  } else {
@@ -7844,11 +7958,7 @@ function VMI_subtract_var_from_coefficient(args) {
7844
7958
  if(k > VM.chunk_offset) return;
7845
7959
  if(k <= 0) {
7846
7960
  // See NOTE in VMI_add_const_to_coefficient instruction.
7847
- const vbl = VM.variables[vi - 1];
7848
- if(DEBUGGING) {
7849
- console.log('--lookup[' + k + ']: ' + vbl[0] + ' ' + vbl[1].displayName);
7850
- }
7851
- VM.rhs += VM.priorValue(vbl, t) * r;
7961
+ VM.rhs += knownValue(vi, t) * r;
7852
7962
  } else if(k in VM.coefficients) {
7853
7963
  VM.coefficients[k] -= r;
7854
7964
  } else {
@@ -7858,9 +7968,9 @@ function VMI_subtract_var_from_coefficient(args) {
7858
7968
 
7859
7969
  function VMI_update_cash_coefficient(args) {
7860
7970
  // `args`: [flow, type, level_var_index, delay, x1, x2, ...]
7861
- // NOTE: flow is either CONSUME or PRODUCE; type can be ONE_C (one
7971
+ // NOTE: Flow is either CONSUME or PRODUCE; type can be ONE_C (one
7862
7972
  // constant parameter x1), TWO_X (two expressions x1 and x2), THREE_X
7863
- // (three expressions x1, x2 and x3) or SPIN_RES or PEAK_INC (see below)
7973
+ // (three expressions x1, x2 and x3) or SPIN_RES or PEAK_INC (see below).
7864
7974
  let d = 0;
7865
7975
  const
7866
7976
  flow = args[0],
@@ -7868,34 +7978,38 @@ function VMI_update_cash_coefficient(args) {
7868
7978
  vi = args[2],
7869
7979
  dx = args[3];
7870
7980
  if(dx instanceof Expression) {
7981
+ // When delay is an expression, `dx.object` is the delayed link.
7871
7982
  d = dx.object.actualDelay(VM.t);
7872
- // Extra argument indicates "delay + 1"
7983
+ // Extra argument indicates "delay + 1" (for delta-multipliers).
7873
7984
  if((type === VM.ONE_C && args.length === 6) ||
7874
7985
  (type === VM.TWO_X && args.length === 7)) d++;
7875
7986
  }
7987
+ // NOTE: When delay < 0, the coefficient must be set for a matrix row
7988
+ // that will be added to the tableau for some later VM.t.
7876
7989
  // `k` is the tableau column index of the variable that affects the CF
7877
7990
  let k = (type === VM.PEAK_INC ? VM.chunk_offset + vi :
7878
7991
  VM.offset + vi - d*VM.cols);
7879
- // NOTE: delay > 0 affects only which variable is to be used,
7880
- // not the expressions for rates or prices!
7881
- const t = VM.t - d;
7882
- // NOTE: This instruction is used only for objective function
7883
- // coefficients. Previously computed decision variables and variables
7884
- // beyond the tableau column range (when delay < 0) can be ignored.
7885
- if(k <= 0 || k > VM.chunk_offset) return;
7992
+ // NOTE: Variables beyond the tableau column range (when delay < 0) can
7993
+ // be ignored.
7994
+ if(k > VM.chunk_offset) return;
7886
7995
  // NOTE: Peak increase can generate cash only at the first time
7887
7996
  // step of a block (when VM.offset = 0) and at the first time step
7888
7997
  // of the look-ahead period (when VM.offset = block length).
7889
7998
  if(type === VM.PEAK_INC &&
7890
7999
  VM.offset > 0 && VM.offset !== MODEL.block_length) return;
8000
+ // NOTE: Delay affects only which variable is to be used. The level of
8001
+ // this variable is then the delayed production volume to be priced at
8002
+ // the *current* time.
7891
8003
  // First compute the result to be processed.
7892
8004
  let r = 0;
7893
8005
  if(type === VM.ONE_C) {
7894
8006
  r = args[4];
7895
8007
  } else if(type === VM.TWO_X || type === VM.PEAK_INC) {
7896
8008
  // NOTE: "peak increase" always passes two expressions.
8009
+ // The first expression determines the price.
7897
8010
  r = args[4].result(VM.t) * args[5].result(VM.t);
7898
8011
  } else if(type === VM.THREE_X) {
8012
+ // NOTE: Here, too, the first expression determines the price.
7899
8013
  r = args[4].result(VM.t) * args[5].result(VM.t) * args[6].result(VM.t);
7900
8014
  } else if(type === VM.SPIN_RES) {
7901
8015
  // "spinning reserve" equals UB - level if level > 0, or 0.
@@ -7905,15 +8019,27 @@ function VMI_update_cash_coefficient(args) {
7905
8019
  // expressions (UB, price, rate).
7906
8020
  const
7907
8021
  plvi = args[4],
7908
- // NOTE: column of second variable will be relative to same offset
8022
+ // NOTE: Column of second variable will be relative to same offset.
7909
8023
  plk = k + plvi - vi,
7910
8024
  ub = args[5].result(VM.t),
7911
8025
  price_rate = args[6].result(VM.t) * args[7].result(VM.t);
7912
8026
  r = ub * price_rate;
7913
8027
  // NOTE: The sign of r determines whether this spinning reserve will
7914
- // generate cash IN or cash OUT. The *subtracted* part hence be ADDED
7915
- // if r > 0, and SUBTRACTED if r < 0 (unlike the "primary" part r itself).
7916
- if(r > 0) {
8028
+ // generate cash IN or cash OUT. The *subtracted* part (production level
8029
+ // times price_rate) should hence be ADDED if r > 0, and SUBTRACTED
8030
+ // if r < 0 (unlike the "primary" part r itself).
8031
+ if(plk <= 0) {
8032
+ // NOTE: If `plk` falls PRIOR to the start of the block being solved,
8033
+ // update the right hand side of the cash flow constraint.
8034
+ // Assume that the known production level is be non-negative, so its
8035
+ // sign does not alter the flow direction.
8036
+ const pl = knownValue(plvi, VM.t - d);
8037
+ if(r > 0) {
8038
+ VM.cash_in_rhs -= pl * price_rate;
8039
+ } else {
8040
+ VM.cash_out_rhs += pl * price_rate;
8041
+ }
8042
+ } else if(r > 0) {
7917
8043
  if(plk in VM.cash_in_coefficients) {
7918
8044
  VM.cash_in_coefficients[plk] += price_rate;
7919
8045
  } else {
@@ -7931,16 +8057,27 @@ function VMI_update_cash_coefficient(args) {
7931
8057
  // be PRODUCE.
7932
8058
  if(flow === VM.CONSUME) r = -r;
7933
8059
  if(DEBUGGING) {
7934
- const vbl = (vi <= this.cols ? VM.variables[vi - 1] :
7935
- VM.chunk_variables[vi - this.cols]); //@@@ TO MAKE CORRECT FOR chunk vars!
7936
- console.log(['update_cash_coefficient [', k, ']: ', vbl[0], ' ',
7937
- vbl[1].displayName, ' (t = ', t, ') ', VM.CF_CONSTANTS[type], ' ',
7938
- VM.CF_CONSTANTS[flow], ' r = ', VM.sig4Dig(r)].join(''));
8060
+ const vbl = (vi <= VM.cols ? VM.variables[vi - 1] :
8061
+ VM.chunk_variables[vi - VM.cols]);
8062
+ //@@@ MESSAGE TO MAKE CORRECT FOR chunk vars!
8063
+ console.log(['update_cash_coefficient [', k, ']: ',
8064
+ vbl[1].displayName, '[' + vbl[0] + '] (t = ', VM.t, ') ',
8065
+ VM.CF_CONSTANTS[type], ' ', VM.CF_CONSTANTS[flow], ' r = ',
8066
+ VM.sig4Dig(r)].join(''));
7939
8067
  }
7940
8068
  // Use look-ahead peak increase when offset > 0.
7941
8069
  if(type === VM.PEAK_INC && VM.offset) k++;
7942
- // Then update the cash flow: cash IN if r > 0, otherwise cash OUT .
7943
- if(r > 0) {
8070
+ // Then update the cash flow: cash IN if r > 0, otherwise cash OUT.
8071
+ if(k <= 0) {
8072
+ // NOTE: If `k` falls PRIOR to the start of the block being solved,
8073
+ // subtract the known prior value of the decision variable from the
8074
+ // right hand side of the cash flow constraint.
8075
+ if(r > 0) {
8076
+ VM.cash_in_rhs += knownValue(vi, VM.t - d) * r;
8077
+ } else {
8078
+ VM.cash_out_rhs -= knownValue(vi, VM.t - d) * r;
8079
+ }
8080
+ } else if(r > 0) {
7944
8081
  if(k in VM.cash_in_coefficients) {
7945
8082
  VM.cash_in_coefficients[k] -= r;
7946
8083
  } else {
@@ -8087,27 +8224,69 @@ function VMI_add_constraint(ct) {
8087
8224
  }
8088
8225
  }
8089
8226
 
8090
- function VMI_copy_cash_coefficients(flow) {
8091
- // Overwrites the coefficients vector with the specified cash coefficients
8092
- // vector (cash IN for production and cash OUT for consumption)
8227
+ function VMI_add_cash_constraints(args) {
8228
+ // args = [cash IN variable index, cash OUT variable index]
8229
+ // Overwrites the coefficients vector with the cash coefficients
8230
+ // vector specified by the first argument (cash IN for production,
8231
+ // cash OUT for consumption). The second argument is passed only for
8232
+ // tracing purposes.
8093
8233
  if(DEBUGGING) {
8094
- console.log('copy_cash_coefficients: ' + VM.CF_CONSTANTS[flow]);
8234
+ console.log('add cash constraints for ',
8235
+ VM.variables[args[0]][1].displayName, '(t = ' + VM.t + ')');
8095
8236
  }
8096
- if(flow === VM.PRODUCE) {
8097
- VM.coefficients = Object.assign({}, VM.cash_in_coefficients);
8098
- } else {
8099
- VM.coefficients = Object.assign({}, VM.cash_out_coefficients);
8237
+ if(!VM.add_constraints_flag) {
8238
+ console.log('Constraint NOT added!');
8239
+ return;
8100
8240
  }
8101
- // NOTE: This instruction also keeps track of the highest cash flow constraint
8102
- // coefficient (to be used for scaling these constraint equations)
8103
- for(let i in VM.coefficients) if(VM.coefficients.hasOwnProperty(i)) {
8104
- VM.cash_scalar = Math.max(VM.cash_scalar, Math.abs(VM.coefficients[i]));
8241
+ // Add a constraint for cash IN.
8242
+ let row = {};
8243
+ for(let i in VM.cash_in_coefficients) if(VM.cash_in_coefficients.hasOwnProperty(i)) {
8244
+ const
8245
+ c = VM.cash_in_coefficients[i],
8246
+ ac = Math.abs(c);
8247
+ // Do not add variables having near-zero coefficients.
8248
+ if(ac > VM.NEAR_ZERO) {
8249
+ row[i] = c;
8250
+ // NOTE: This instruction also keeps track of the highest absolute
8251
+ // cash flow constraint coefficient, so it can be used for scaling
8252
+ // these constraint equations.
8253
+ VM.cash_scalar = Math.max(VM.cash_scalar, ac);
8254
+ }
8255
+ }
8256
+ // To permit such scaling, this instruction maintains a list of cash
8257
+ // constraint row indices, as these are the equations that need to be
8258
+ // scaled once the tableau is complete.
8259
+ VM.cash_constraints.push(VM.matrix.length);
8260
+ // Set coefficient for the cash IN variable to 1.
8261
+ row[VM.offset + args[0]] = 1;
8262
+ // Add the constraint to the tableau.
8263
+ VM.matrix.push(row);
8264
+ VM.right_hand_side.push(VM.cash_in_rhs);
8265
+ VM.constraint_types.push(VM.EQ);
8266
+ // Clear the cash IN coefficient register and RHS.
8267
+ VM.cash_in_coefficients = {};
8268
+ VM.cash_in_rhs = 0;
8269
+ // Now likewise add a constraint for cash OUT.
8270
+ row = {};
8271
+ for(let i in VM.cash_out_coefficients) if(VM.cash_out_coefficients.hasOwnProperty(i)) {
8272
+ const
8273
+ c = VM.cash_out_coefficients[i],
8274
+ ac = Math.abs(c);
8275
+ if(ac > VM.NEAR_ZERO) {
8276
+ row[i] = c;
8277
+ VM.cash_scalar = Math.max(VM.cash_scalar, ac);
8278
+ }
8105
8279
  }
8106
- // NOTE: To permit such scaling, this instruction creates a list of constraint
8107
- // row indices, as these are the equations that need to be scaled
8108
8280
  VM.cash_constraints.push(VM.matrix.length);
8109
- // Always set RHS to 0 as cash flow constraints are EQ 0 constraints
8110
- VM.rhs = 0;
8281
+ // Add the cash OUT variable index.
8282
+ row[VM.offset + args[1]] = 1;
8283
+ // Add the constraint to the tableau.
8284
+ VM.matrix.push(row);
8285
+ VM.right_hand_side.push(VM.cash_out_rhs);
8286
+ VM.constraint_types.push(VM.EQ);
8287
+ // Clear the cash OUT coefficients register and RHS (just to be sure).
8288
+ VM.cash_out_coefficients = {};
8289
+ VM.cash_out_rhs = 0;
8111
8290
  }
8112
8291
 
8113
8292
  function VMI_add_bound_line_constraint(args) {
@@ -8360,7 +8539,7 @@ function VMI_profitable_units(x) {
8360
8539
  mpa = d[3].attribute,
8361
8540
  pt = (d.length > 4 ? d[4] : 0), // the profit threshold (0 by default)
8362
8541
  // the time horizon (by default the length of the simulation period)
8363
- nt = (d.length > 5 ? d[5] : MODEL.end_period - MODEL.start_period + 1);
8542
+ nt = (d.length > 5 ? d[5] : VM.nr_of_time_steps);
8364
8543
  // Handle exceptional values of `uc` and `mc`.
8365
8544
  if(uc <= VM.BEYOND_MINUS_INFINITY || mc <= VM.BEYOND_MINUS_INFINITY) {
8366
8545
  x.retop(Math.min(uc, mc));
@@ -8498,7 +8677,7 @@ function VMI_highest_cumulative_consecutive_deviation(x) {
8498
8677
  // NOTE: an expression may not have been (fully) computed yet.
8499
8678
  x.compute(0);
8500
8679
  if(!x.isStatic) {
8501
- const nt = MODEL.end_period - MODEL.start_period + 1;
8680
+ const nt = VM.nr_of_time_steps;
8502
8681
  for(let t = 1; t <= nt; t++) x.result(t);
8503
8682
  }
8504
8683
  vector = x.vector;
@@ -8649,7 +8828,7 @@ function correlation_or_slope(x, c_or_s) {
8649
8828
  // NOTE: An equation may not have been (fully) computed yet.
8650
8829
  eq.compute(0, x.wildcard_vector_index);
8651
8830
  if(!eq.isStatic) {
8652
- const nt = MODEL.end_period - MODEL.start_period + 1;
8831
+ const nt = VM.nr_of_time_steps;
8653
8832
  for(let t = 1; t <= nt; t++) eq.result(t, x.wildcard_vector_index);
8654
8833
  }
8655
8834
  vector[k].v = eq.vector;