linny-r 1.7.0 → 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
@@ -2390,8 +2395,13 @@ class VirtualMachine {
2390
2395
  this.issue_list.length = 0;
2391
2396
  this.issue_index = -1;
2392
2397
  UI.updateIssuePanel();
2393
- // NOTE: Special tracking of potential solver license errors.
2398
+ // Special tracking of potential solver license errors.
2394
2399
  this.license_expired = 0;
2400
+ // Variables that will be decided by the solver again in the next
2401
+ // block must be "fixated" when, due to a negative link delay, they
2402
+ // would have consequences for the previous block (and these will be
2403
+ // ignored).
2404
+ this.variables_to_fixate = {};
2395
2405
  // Reset solver result arrays.
2396
2406
  this.round_times.length = 0;
2397
2407
  this.solver_times.length = 0;
@@ -2951,7 +2961,6 @@ class VirtualMachine {
2951
2961
  return obj.la_peak_inc[c];
2952
2962
  }
2953
2963
  const prior_level = obj.actualLevel(t);
2954
- //console.log('HERE obj prilev t', obj.displayName, prior_level, t, obj.level);
2955
2964
  if(type === 'OO') return prior_level > 0 ? 1 : 0;
2956
2965
  if(type === 'IZ') return prior_level === 0 ? 1 : 0;
2957
2966
  // Start-up at time t entails that t is in the list of start-up
@@ -3136,7 +3145,7 @@ class VirtualMachine {
3136
3145
 
3137
3146
  // FIRST: define indices for all variables (index = Simplex tableau column number)
3138
3147
 
3139
- // 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.
3140
3149
  const actor_keys = Object.keys(MODEL.actors).sort();
3141
3150
  for(i = 0; i < actor_keys.length; i++) {
3142
3151
  const a = MODEL.actors[actor_keys[i]];
@@ -3243,9 +3252,9 @@ class VirtualMachine {
3243
3252
  // NOTE: Chunk variables of node `p` have LB = 0 and UB = UB of `p`.
3244
3253
  // This is effectuated by the VM "set bounds" instructions at run time.
3245
3254
 
3246
- // NOTE: Under normal assumptions (all processes having LB >= 0), bounds on
3247
- // actor cash flow variables need NOT be set because cash IN and cash OUT
3248
- // 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).
3249
3258
  // However, Linny-R does not prohibit negative bounds on processes, nor
3250
3259
  // negative rates on links. To be consistently permissive, cash IN and
3251
3260
  // cash OUT of all actors are both allowed to become negative.
@@ -3271,18 +3280,18 @@ class VirtualMachine {
3271
3280
  if(!MODEL.ignored_entities[k]) {
3272
3281
  p = MODEL.processes[k];
3273
3282
  lbx = p.lower_bound;
3274
- // NOTE: if UB = LB, set UB to LB only if LB is defined,
3283
+ // NOTE: If UB = LB, set UB to LB only if LB is defined,
3275
3284
  // because LB expressions default to -INF while UB expressions
3276
- // default to + INF
3285
+ // default to +INF.
3277
3286
  ubx = (p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
3278
3287
  if(lbx.isStatic) lbx = lbx.result(0);
3279
3288
  if(ubx.isStatic) ubx = ubx.result(0);
3280
- // NOTE: pass TRUE as fourth parameter to indicate that +INF
3289
+ // NOTE: Pass TRUE as fourth parameter to indicate that +INF
3281
3290
  // and -INF can be coded as the infinity values used by the
3282
3291
  // solver, rather than the Linny-R values used to detect
3283
- // unbounded problems
3292
+ // unbounded problems.
3284
3293
  this.code.push([VMI_set_bounds, [p.level_var_index, lbx, ubx, true]]);
3285
- // Add level variable index to "fixed" list for specified rounds
3294
+ // Add level variable index to "fixed" list for specified rounds.
3286
3295
  const rf = p.actor.round_flags;
3287
3296
  if(rf != 0) {
3288
3297
  // Note: 32-bit integer `b` is used for bit-wise AND
@@ -3333,9 +3342,9 @@ class VirtualMachine {
3333
3342
 
3334
3343
  // NOTE: Each process generates cash flow proportional to its production
3335
3344
  // level if it produces and/or consumes a product having a price.
3336
- // Cash flow is negative (cash OUT) if a product is consumed AND has
3337
- // price > 0, but positive (cash IN) if a product is produced and has
3338
- // 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.
3339
3348
  // To calculate the coefficient for the process variable, the
3340
3349
  // multiplier rates of the links in and out must be calculated (at
3341
3350
  // run time when dynamic expressions) such that they will add to the
@@ -3357,21 +3366,24 @@ class VirtualMachine {
3357
3366
  // on this vector. If expressions for process properties are
3358
3367
  // static, more efficient VM instructions are used.
3359
3368
 
3360
- // 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.
3361
3372
  this.no_cash_flows = true;
3362
3373
 
3363
- // Iterate over all actors to add the cash flow computation constraints
3374
+ // Iterate over all actors to add the cash flow computation constraints.
3364
3375
  for(let ai = 0; ai < actor_keys.length; ai++) {
3365
3376
  const a = MODEL.actors[actor_keys[ai]];
3366
- 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.
3367
3379
  for(i = 0; i < process_keys.length; i++) {
3368
3380
  k = process_keys[i];
3369
3381
  if(!MODEL.ignored_entities[k]) {
3370
3382
  const p = MODEL.processes[k];
3371
- // Only consider processes owned by this actor
3383
+ // Only consider processes owned by this actor.
3372
3384
  if(p.actor === a) {
3373
- // Iterate over links IN, but only consider consumed products having
3374
- // a market price
3385
+ // Iterate over links IN, but only consider consumed products
3386
+ // having a market price.
3375
3387
  for(j = 0; j < p.inputs.length; j++) {
3376
3388
  l = p.inputs[j];
3377
3389
  if(!MODEL.ignored_entities[l.identifier] &&
@@ -3379,18 +3391,17 @@ class VirtualMachine {
3379
3391
  if(l.from_node.price.isStatic && l.relative_rate.isStatic) {
3380
3392
  k = l.from_node.price.result(0) * l.relative_rate.result(0);
3381
3393
  // NOTE: VMI_update_cash_coefficient has at least 4 arguments:
3382
- // flow (CONSUME or PRODUCE), type (specifies the number and type
3383
- // of arguments), the level_var_index of the process, and the
3384
- // 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.
3385
3398
  if(Math.abs(k) > VM.NEAR_ZERO) {
3386
- // Consumption rate & price are static: pass one constant
3387
- // NOTE: input links cannot have delay, so delay = 0
3399
+ // Consumption rate & price are static: pass one constant.
3388
3400
  this.code.push([VMI_update_cash_coefficient,
3389
3401
  [VM.CONSUME, VM.ONE_C, p.level_var_index, 0, k]]);
3390
3402
  }
3391
3403
  } else {
3392
- // No further optimization: assume two dynamic expressions
3393
- // NOTE: input links cannot have delay, so delay = 0
3404
+ // No further optimization: assume two dynamic expressions.
3394
3405
  this.code.push([VMI_update_cash_coefficient,
3395
3406
  [VM.CONSUME, VM.TWO_X, p.level_var_index, 0,
3396
3407
  l.from_node.price, l.relative_rate]]);
@@ -3399,15 +3410,16 @@ class VirtualMachine {
3399
3410
  } // END of FOR ALL input links
3400
3411
 
3401
3412
  // Iterate over links OUT, but only consider produced products
3402
- // having a (non-zero) market price
3413
+ // having a (non-zero) market price.
3403
3414
  for(j = 0; j < p.outputs.length; j++) {
3404
3415
  l = p.outputs[j];
3405
3416
  const tnpx = l.to_node.price;
3406
3417
  if(!MODEL.ignored_entities[l.identifier] && tnpx.defined &&
3407
3418
  !(tnpx.isStatic && Math.abs(tnpx.result(0)) < VM.NEAR_ZERO)) {
3408
- // By default, use the process level as multiplier
3419
+ // By default, use the process level as multiplier.
3409
3420
  vi = p.level_var_index;
3410
- // 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.
3411
3423
  if(l.multiplier === VM.LM_STARTUP) {
3412
3424
  vi = p.start_up_var_index;
3413
3425
  } else if(l.multiplier === VM.LM_FIRST_COMMIT) {
@@ -3428,57 +3440,63 @@ class VirtualMachine {
3428
3440
  // times the rate of `l`.
3429
3441
  for(k = 0; k < l.from_node.inputs.length; j++) {
3430
3442
  ll = l.from_node.inputs[k];
3431
- // NOTE: no attempt for efficiency -- assume that
3432
- // price and both rates are dynamic
3443
+ // NOTE: No attempt for efficiency -- assume that
3444
+ // price and both rates are dynamic.
3433
3445
  this.code.push([VMI_update_cash_coefficient, [
3434
3446
  VM.PRODUCE, VM.THREE_X, vi, l.flow_delay, tnpx,
3435
3447
  l.relative_rate, ll.relative_rate]]);
3436
3448
  }
3437
3449
  } else if(l.multiplier === VM.LM_SPINNING_RESERVE) {
3438
- // "spinning reserve" equals UB - level if level > 0, or 0
3439
- // The cash flow then equals ON/OFF * UB * price * rate MINUS
3440
- // level * price * rate, hence a special instruction type
3441
- // NOTE: only the ON/OFF variable determines whether there will
3442
- // be any cash flow, hence it is passed as the primary variable,
3443
- // 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.
3444
3458
  this.code.push([VMI_update_cash_coefficient, [
3445
- VM.PRODUCE, VM.SPIN_RES, p.on_off_var_index, l.flow_delay, vi,
3446
- 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]]);
3447
3462
  } else if(l.multiplier === VM.LM_PEAK_INC) {
3448
- // NOTE: "peak increase" may be > 0 only in the first time step
3449
- // of the block being optimized, and in the first step of the
3450
- // look-ahead period (if peak rises in that period), and will
3451
- // 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.
3452
3468
  // NOTE: Delay is always 0 for this link flow.
3453
3469
  this.code.push([VMI_update_cash_coefficient, [
3454
3470
  VM.PRODUCE, VM.PEAK_INC, p.peak_inc_var_index, 0,
3455
3471
  tnpx, l.relative_rate]]);
3456
3472
  } else if(tnpx.isStatic && l.relative_rate.isStatic) {
3457
- // If link rate and product price are static, only add the variable
3458
- // 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.
3459
3476
  k = tnpx.result(0) * l.relative_rate.result(0);
3460
3477
  if(Math.abs(k) > VM.NEAR_ZERO) {
3461
- // Production rate & price are static: pass one constant
3478
+ // Production rate & price are static: pass one constant.
3462
3479
  this.code.push([VMI_update_cash_coefficient,
3463
3480
  [VM.PRODUCE, VM.ONE_C, vi, l.flow_delay, k]]);
3464
3481
  // When multiplier is Delta, subtract level in previous t
3465
- // (so add 1 to flow delay, and consume, rather than produce)
3482
+ // (so add 1 to flow delay, and consume, rather than
3483
+ // produce).
3466
3484
  if(l.multiplier === VM.LM_INCREASE) {
3467
3485
  this.code.push([VMI_update_cash_coefficient,
3468
- // NOTE: 6th argument = 1 indicates "delay + 1"
3486
+ // NOTE: 6th argument = 1 indicates "delay + 1".
3469
3487
  [VM.CONSUME, VM.ONE_C, vi, l.flow_delay, k, 1]]);
3470
3488
  }
3471
3489
  }
3472
3490
  } else {
3473
- // Production rate or price are dynamic: pass two expressions
3491
+ // Production rate or price are dynamic: pass two expressions.
3474
3492
  this.code.push([VMI_update_cash_coefficient, [
3475
3493
  VM.PRODUCE, VM.TWO_X, vi, l.flow_delay,
3476
3494
  tnpx, l.relative_rate]]);
3477
- // When multiplier is Delta, consume level in previous t
3495
+ // When multiplier is Delta, consume level in previous t.
3478
3496
  if(l.multiplier === VM.LM_INCREASE) {
3479
3497
  this.code.push([VMI_update_cash_coefficient, [
3480
3498
  VM.CONSUME, VM.TWO_X, vi, l.flow_delay,
3481
- // NOTE: now 7th argument indicates "delay + 1"
3499
+ // NOTE: Now 7th argument indicates "delay + 1".
3482
3500
  tnpx, l.relative_rate, 1]]);
3483
3501
  }
3484
3502
  }
@@ -3487,33 +3505,26 @@ class VirtualMachine {
3487
3505
  } // END of FOR ALL output links
3488
3506
  } // END of IF process not ignored
3489
3507
  } // END of FOR ALL processes
3490
-
3491
- // NOTE: if the last VM instruction still is "clear coefficients",
3492
- // this means that (so far) no actor cash flows were detected
3493
- if(this.no_cash_flows) {
3494
- this.no_cash_flows =
3495
- this.code[this.code.length-1][0] === VMI_clear_coefficients;
3496
- }
3497
-
3498
- // ALWAYS add the two cash flow constraints for this actor, as both cash
3499
- // flow variables must be computed (will be 0 if no cash flows)
3500
- this.code.push(
3501
- [VMI_copy_cash_coefficients, VM.PRODUCE],
3502
- [VMI_add_const_to_coefficient, [a.cash_in_var_index, 1]],
3503
- [VMI_add_constraint, VM.EQ],
3504
- [VMI_copy_cash_coefficients, VM.CONSUME],
3505
- [VMI_add_const_to_coefficient, [a.cash_out_var_index, 1]],
3506
- [VMI_add_constraint, VM.EQ]
3507
- );
3508
-
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
+
3509
3520
  } // END of FOR loop iterating over all actors
3510
3521
 
3511
- // NEXT: define the coefficients for the objective function
3522
+ // NEXT: Define the coefficients for the objective function.
3512
3523
  this.code.push([VMI_clear_coefficients, null]);
3513
3524
 
3514
- // NOTE: if, after all actors -- this includes (no actor) -- have been
3515
- // considered, no cash flows have been detected, the solver should aim for
3516
- // 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.
3517
3528
  if(this.no_cash_flows) {
3518
3529
  for(i = 0; i < process_keys.length; i++) {
3519
3530
  k = process_keys[i];
@@ -4285,8 +4296,9 @@ class VirtualMachine {
4285
4296
  // largest cash flow coefficient (in absolute value) within the
4286
4297
  // current block so that cash flows cannot easily "overrule" the
4287
4298
  // slack penalties in the objective function.
4288
- // NOTE: No scaling needed if model features no cash flows.
4289
- 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;
4290
4302
  this.logMessage(this.block_count,
4291
4303
  'Cash flows scaled by 1/' + this.cash_scalar);
4292
4304
  // Use reciprocal as multiplier to scale the constraint coefficients.
@@ -4307,7 +4319,7 @@ class VirtualMachine {
4307
4319
  }
4308
4320
  }
4309
4321
  }
4310
-
4322
+
4311
4323
  checkForInfinity(n) {
4312
4324
  // Return floating point number `n`, or +INF or -INF if the absolute
4313
4325
  // value of `n` is relatively (!) close to the VM infinity constants
@@ -4333,8 +4345,6 @@ class VirtualMachine {
4333
4345
  let bb = (block - 1) * MODEL.block_length + 1,
4334
4346
  abl = this.chunk_length,
4335
4347
  cbl = this.actualBlockLength(block);
4336
- // For the last block, crop the actual block length so it does not
4337
- // extend beyond the simulation period (these results should be ignored).
4338
4348
  // If no results computed, preserve those already computed for the
4339
4349
  // pervious chunk as "look-ahead".
4340
4350
  if(err && block > 1 && MODEL.look_ahead > 0) {
@@ -4345,7 +4355,9 @@ class VirtualMachine {
4345
4355
  'No results from solver -- retained results of ' +
4346
4356
  pluralS(MODEL.look_ahead, 'look-ahead time step'));
4347
4357
  }
4348
- 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) {
4349
4361
  this.logMessage(block, 'Results of last optimization could be discarded');
4350
4362
  abl = 0;
4351
4363
  } else if(cbl < abl) {
@@ -4365,41 +4377,41 @@ class VirtualMachine {
4365
4377
  ncv_msg + ' is not a multiple of # columns', this.cols);
4366
4378
  // Assume no warnings or errors.
4367
4379
  this.error_count = 0;
4368
- // Set cash flows for all actors
4369
- // NOTE: all cash IN and cash OUT values should normally be non-negative,
4380
+ // Set cash flows for all actors.
4381
+ // NOTE: All cash IN and cash OUT values should normally be non-negative,
4370
4382
  // but since Linny-R permits negative lower bounds on processes, and also
4371
4383
  // negative link rates, cash flows may become negative. If that occurs,
4372
4384
  // the modeler should be warned.
4373
4385
  for(let o in MODEL.actors) if(MODEL.actors.hasOwnProperty(o)) {
4374
4386
  const a = MODEL.actors[o];
4375
- // NOTE: `b` is the index to be used for the vectors
4387
+ // NOTE: `b` is the index to be used for the vectors.
4376
4388
  let b = bb;
4377
- // Iterate over all time steps in this block
4378
- // NOTE: -1 because indices start at 1, but list is zero-based
4389
+ // Iterate over all time steps in this block.
4390
+ // NOTE: -1 because indices start at 1, but list is zero-based.
4379
4391
  let j = -1;
4380
4392
  for(let i = 0; i < abl; i++) {
4381
- // NOTE: cash coefficients computed by the solver must be scaled back
4393
+ // NOTE: Cash coefficients computed by the solver must be scaled back.
4382
4394
  a.cash_in[b] = this.checkForInfinity(
4383
4395
  x[a.cash_in_var_index + j] * this.cash_scalar);
4384
4396
  a.cash_out[b] = this.checkForInfinity(
4385
4397
  x[a.cash_out_var_index + j] * this.cash_scalar);
4386
4398
  a.cash_flow[b] = a.cash_in[b] - a.cash_out[b];
4387
- // Count occurrences of a negative cash flow (threshold -0.5 cent)
4388
- if(a.cash_in[b] < -0.005) {
4399
+ // Count occurrences of a negative cash flow (threshold -0.5 cent).
4400
+ if(b <= this.nr_of_time_steps && a.cash_in[b] < -0.005) {
4389
4401
  this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
4390
4402
  a.displayName + ' cash IN = ' + a.cash_in[b].toPrecision(2));
4391
4403
  }
4392
- if(a.cash_out[b] < -0.005) {
4404
+ if(b <= this.nr_of_time_steps && a.cash_out[b] < -0.005) {
4393
4405
  this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
4394
4406
  a.displayName + ' cash IN = ' + a.cash_out[b].toPrecision(2));
4395
4407
  }
4396
- // Advance column offset in tableau by the # cols per time step
4408
+ // Advance column offset in tableau by the # cols per time step.
4397
4409
  j += this.cols;
4398
- // Advance to the next time step in this block
4410
+ // Advance to the next time step in this block.
4399
4411
  b++;
4400
4412
  }
4401
4413
  }
4402
- // Set production levels and start-up moments for all processes
4414
+ // Set production levels and start-up moments for all processes.
4403
4415
  for(let o in MODEL.processes) if(MODEL.processes.hasOwnProperty(o) &&
4404
4416
  !MODEL.ignored_entities[o]) {
4405
4417
  const
@@ -4490,10 +4502,10 @@ class VirtualMachine {
4490
4502
  // NOTE: Only check after the last round has been evaluated.
4491
4503
  if(round === this.lastRound) {
4492
4504
  let b = bb;
4493
- // Iterate over all time steps in this block
4505
+ // Iterate over all time steps in this block.
4494
4506
  let j = -1;
4495
4507
  for(let i = 0; i < abl; i++) {
4496
- // Index `svt` iterates over types of slack variable (0 - 2)
4508
+ // Index `svt` iterates over types of slack variable (0 - 2).
4497
4509
  for(let svt = 0; svt <= 2; svt++) {
4498
4510
  const
4499
4511
  svl = this.slack_variables[svt],
@@ -4505,12 +4517,12 @@ class VirtualMachine {
4505
4517
  absl = Math.abs(slack);
4506
4518
  if(absl > VM.NEAR_ZERO) {
4507
4519
  const v = this.variables[vi - 1];
4508
- // NOTE: for constraints, add 'UB' or 'LB' to its vector for the
4509
- // 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.
4510
4522
  if(v[1] instanceof BoundLine) {
4511
4523
  v[1].constraint.slack_info[b] = v[0];
4512
4524
  }
4513
- if(absl > VM.SIG_DIF_FROM_ZERO) {
4525
+ if(b <= this.nr_of_time_steps && absl > VM.SIG_DIF_FROM_ZERO) {
4514
4526
  this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
4515
4527
  `${v[1].displayName} ${v[0]} slack = ${this.sig4Dig(slack)}`);
4516
4528
  if(v[1] instanceof Product) {
@@ -4556,10 +4568,13 @@ class VirtualMachine {
4556
4568
  // optimization period, hence start by calculating the offset `bb`
4557
4569
  // being the first time step of this block.
4558
4570
  // Blocks are numbered 1, 2, ...
4571
+ let latest_time_step = 0;
4559
4572
  const
4560
4573
  bb = (block - 1) * MODEL.block_length + 1,
4561
4574
  cbl = this.actualBlockLength(block);
4562
4575
 
4576
+ // Start with an empty list of variables to "fixate" in the next block.
4577
+ this.variables_to_fixate = {};
4563
4578
  // FIRST: Calculate the actual flows on links.
4564
4579
  let b, bt, p, pl, ld, ci;
4565
4580
  for(let l in MODEL.links) if(MODEL.links.hasOwnProperty(l) &&
@@ -4574,6 +4589,23 @@ class VirtualMachine {
4574
4589
  // NOTE: Flows may have a delay!
4575
4590
  ld = l.actualDelay(b);
4576
4591
  bt = b - ld;
4592
+ latest_time_step = Math.max(latest_time_step, bt);
4593
+ // If delay < 0 AND this results in a block time beyond the
4594
+ // block length, this means that the level of the FROM node
4595
+ // must be "fixated" in the next block.
4596
+ const nbt = bt - bb - MODEL.block_length + 1;
4597
+ // NOTE: `nbt` (next block time) cannot be beyond the look-ahead
4598
+ // period, as for those time steps the levels are still undefined,
4599
+ // NOR can it be later than the duration of the (negative) delay
4600
+ // on the link.
4601
+ if(ld < 0 && nbt > 0 && nbt <= MODEL.look_ahead && nbt <= -ld) {
4602
+ this.addNodeToFixate(l.from_node, nbt,
4603
+ // NOTE: Use the level at time `bt` (i.e., in the future)
4604
+ // because that is the optimal level computed for this chunk
4605
+ // (in its look-ahead period) that should be maintained in
4606
+ // the next block.
4607
+ l.from_node.nonZeroLevel(bt));
4608
+ }
4577
4609
  // NOTE: Block index may fall beyond actual chunk length.
4578
4610
  ci = i - ld;
4579
4611
  // NOTE: Use non-zero level here to ignore non-zero values that
@@ -4682,9 +4714,10 @@ class VirtualMachine {
4682
4714
  // INPUT links from priced products generate cash OUT...
4683
4715
  for(let j = 0; j < p.inputs.length; j++) {
4684
4716
  // NOTE: Input links do NOT have a delay.
4685
- const l = p.inputs[j],
4686
- af = l.actual_flow[b],
4687
- fnp = l.from_node.price;
4717
+ const
4718
+ l = p.inputs[j],
4719
+ af = l.actual_flow[b],
4720
+ fnp = l.from_node.price;
4688
4721
  if(af > VM.NEAR_ZERO && fnp.defined) {
4689
4722
  const pp = fnp.result(b);
4690
4723
  if(pp > 0 && pp < VM.PLUS_INFINITY) {
@@ -4697,14 +4730,14 @@ class VirtualMachine {
4697
4730
  }
4698
4731
  // OUTPUT links to priced products generate cash IN ...
4699
4732
  for(let j = 0; j < p.outputs.length; j++) {
4700
- // NOTE: Actual flows already consider delay!
4701
- const l = p.outputs[j],
4702
- ld = l.actualDelay(b),
4703
- af = l.actual_flow[b],
4704
- 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;
4705
4738
  if(af > VM.NEAR_ZERO && tnp.defined) {
4706
- // NOTE: To get the correct price, again consider delays.
4707
- const pp = tnp.result(b - ld);
4739
+ // NOTE: Use the price at the time of the actual flow.
4740
+ const pp = tnp.result(b);
4708
4741
  if(pp > 0 && pp < VM.PLUS_INFINITY) {
4709
4742
  ci += pp * af;
4710
4743
  // ... unless the product price is negative; then cash OUT.
@@ -4733,15 +4766,128 @@ class VirtualMachine {
4733
4766
  // at a time because of delays, and also because expressions may refer
4734
4767
  // to values for earlier time steps.
4735
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 = {};
4736
4772
  b = bb;
4737
4773
  for(let i = 0; i < cbl; i++) {
4738
- 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)) {
4739
4777
  this.logMessage(block, `${this.WARNING}(t=${b}) ` +
4740
4778
  'Invalid cost prices due to negative flow(s)');
4741
4779
  }
4742
4780
  // Move on to the next time step of the block.
4743
4781
  b++;
4744
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
+ }
4745
4891
  }
4746
4892
 
4747
4893
  // THEN: Reset all datasets that are outcomes or serve as "formulas".
@@ -4839,41 +4985,45 @@ class VirtualMachine {
4839
4985
  }
4840
4986
 
4841
4987
  resetTableau() {
4842
- // Clears tableau data: matrix, rhs and constraint types
4843
- // NOTE: this reset is called when initializing, and to free up
4844
- // 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.
4845
4991
  this.matrix.length = 0;
4846
4992
  this.right_hand_side.length = 0;
4847
4993
  this.constraint_types.length = 0;
4848
4994
  }
4849
4995
 
4850
4996
  initializeTableau(abl) {
4851
- // `offset` is used to calculate the actual column index for variables
4997
+ // `offset` is used to calculate the actual column index for variables.
4852
4998
  this.offset = 0;
4853
- // NOTE: vectors are "sparse" (i.e., will contain many 0) and are hence not
4854
- // represented as arrays but as objects, e.g., {4:1.5, 8:0.3} to represent
4855
- // an array [0, 0, 0, 1.5, 0, 0, 0, 0.3, 0, 0, 0, ...]
4856
- // The keys can span the full chunk, so the objects represent vectors that
4857
- // 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.
4858
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.
4859
5009
  this.cash_in_coefficients = {};
5010
+ this.cash_in_rhs = 0;
4860
5011
  this.cash_out_coefficients = {};
4861
- // NOTE: cash flow equation coefficients may be divided by a scalar to keep
4862
- // them amply below the base slack penalty; the scalar is increased by the
4863
- // VM instruction VMI_copy_cash_coefficients so that at the end of the
4864
- // block setup it equals the highest absolute coefficient in the cash flow
4865
- // constraint equations; the VM maintains a list of indices of matrix rows
4866
- // 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.
4867
5019
  this.cash_scalar = 1;
4868
5020
  this.cash_constraints = [];
5021
+ // Vector for the objective function coefficients.
4869
5022
  this.objective = {};
5023
+ // Vectors for the bounds on decision variables.
4870
5024
  this.lower_bounds = {};
4871
5025
  this.upper_bounds = {};
4872
- // NOTE: right-hand side of cash IN/OUT equations will always be 0 as the
4873
- // actors' cash flows are calculated as C - a1P1 - a2P2 - ... anPn = 0
4874
- this.rhs = 0;
4875
- // NOTE: the constraint coefficient matrix and the rhs and ct vectors
4876
- // have equal length (#rows); the matrix is a list of sparse vectors
5026
+ // Clear the tableau matrix and constraint type and RHS columns.
4877
5027
  this.resetTableau();
4878
5028
  // NOTE: setupBlock only works properly if setupProblem was successful
4879
5029
  // Every variable gets one column per time step => tableau is organized
@@ -4899,7 +5049,7 @@ class VirtualMachine {
4899
5049
  this.is_binary[parseInt(i) + j*this.cols] = true;
4900
5050
  }
4901
5051
  }
4902
- // Set list with indices of semi-contiuous variables
5052
+ // Set list with indices of semi-contiuous variables.
4903
5053
  this.is_semi_continuous = {};
4904
5054
  for(let i in this.sec_var_indices) if(Number(i)) {
4905
5055
  for(let j = 0; j < abl; j++) {
@@ -5028,7 +5178,7 @@ class VirtualMachine {
5028
5178
  let rem = (MODEL.runLength - MODEL.look_ahead) % MODEL.block_length;
5029
5179
  // If this remainder equals 0, the last block is a full chunk.
5030
5180
  if(rem === 0) return this.chunk_length;
5031
- // Otherwise, the last block if remainder + look-ahead time steps.
5181
+ // Otherwise, the last block has remainder + look-ahead time steps.
5032
5182
  return rem + MODEL.look_ahead;
5033
5183
  }
5034
5184
 
@@ -5058,6 +5208,33 @@ class VirtualMachine {
5058
5208
  return this.chunk_length * this.cols + this.chunk_variables.length;
5059
5209
  }
5060
5210
 
5211
+ addNodeToFixate(n, bt, level) {
5212
+ // Record that level of node `n` must be fixated for block time `bt`
5213
+ // in the next block by setting it to the specified level.
5214
+ const
5215
+ vi = n.level_var_index,
5216
+ oo = n.on_off_var_index,
5217
+ iz = n.is_zero_var_index;
5218
+ if(!this.variables_to_fixate.hasOwnProperty(vi)) {
5219
+ this.variables_to_fixate[vi] = {};
5220
+ }
5221
+ this.variables_to_fixate[vi][bt] = level;
5222
+ if(oo >= 0) {
5223
+ if(!this.variables_to_fixate.hasOwnProperty(oo)) {
5224
+ this.variables_to_fixate[oo] = {};
5225
+ }
5226
+ this.variables_to_fixate[oo][bt] = (
5227
+ Math.abs(level) < VM.NEAR_ZERO ? 0 : 1);
5228
+ }
5229
+ if(iz >= 0) {
5230
+ if(!this.variables_to_fixate.hasOwnProperty(iz)) {
5231
+ this.variables_to_fixate[iz] = {};
5232
+ }
5233
+ this.variables_to_fixate[iz][bt] = (
5234
+ Math.abs(level) < VM.NEAR_ZERO ? 1 : 0);
5235
+ }
5236
+ }
5237
+
5061
5238
  writeLpFormat(cplex=false) {
5062
5239
  // NOTE: Up to version 1.5.6, actual block length of last block used
5063
5240
  // to be shorter than the chunk length so as not to go beyond the
@@ -5701,6 +5878,37 @@ Solver status = ${json.status}`);
5701
5878
  this.logMessage(this.block_count, 'Zero columns -- nothing to solve');
5702
5879
  return;
5703
5880
  }
5881
+ // If negative delays require "fixating" variables for some number
5882
+ // of time steps, this must be logged in the monitor.
5883
+ const keys = Object.keys(this.variables_to_fixate);
5884
+ if(keys.length) {
5885
+ const msg = ['NOTE: Due to negative link delays, levels for ' +
5886
+ pluralS(keys.length, 'variable') + ' are pre-set:'];
5887
+ for(let i = 0; i < keys.length; i++) {
5888
+ const
5889
+ vi = parseInt(keys[i]),
5890
+ // NOTE: Subtract 1 because variable list is zero-based.
5891
+ vbl = this.variables[vi - 1],
5892
+ fv = this.variables_to_fixate[vi],
5893
+ fvk = Object.keys(fv),
5894
+ fvl = [];
5895
+ // Add constraints that fixate the levels directly to the tableau.
5896
+ for(let i = 0; i < fvk.length; i++) {
5897
+ const
5898
+ bt = fvk[i],
5899
+ pl = fv[bt],
5900
+ k = (bt - 1) * VM.cols + vi,
5901
+ row = {};
5902
+ row[k] = 1;
5903
+ VM.matrix.push(row);
5904
+ VM.right_hand_side.push(pl);
5905
+ VM.constraint_types.push(VM.EQ);
5906
+ fvl.push(pl + ' for bt=' + bt);
5907
+ }
5908
+ msg.push(`- ${vbl[1].displayName} [${vbl[0]}]: ${fvl.join(', ')}`);
5909
+ }
5910
+ this.logMessage(this.block_count, msg.join('\n'));
5911
+ }
5704
5912
  this.logMessage(this.block_count,
5705
5913
  'Creating model for block #' + this.blockWithRound);
5706
5914
  this.cbl = CONFIGURATION.progress_needle_interval * 200;
@@ -5898,7 +6106,7 @@ function VMI_push_block_number(x) {
5898
6106
 
5899
6107
  function VMI_push_run_length(x) {
5900
6108
  // Push the run length (excl. look-ahead!).
5901
- const n = MODEL.end_period - MODEL.start_period + 1;
6109
+ const n = VM.nr_of_time_steps;
5902
6110
  if(DEBUGGING) console.log('push run length N = ' + n);
5903
6111
  x.push(n);
5904
6112
  }
@@ -6177,7 +6385,7 @@ function relativeTimeStep(t, anchor, offset, dtm, x) {
6177
6385
  }
6178
6386
  if(anchor === 'l') {
6179
6387
  // Last: offset relative to the last index in the vector.
6180
- return MODEL.end_period - MODEL.start_period + 1 + offset;
6388
+ return VM.nr_of_time_steps + offset;
6181
6389
  }
6182
6390
  if(anchor === 's') {
6183
6391
  // Scaled: offset is scaled to time unit of run.
@@ -6565,7 +6773,7 @@ function VMI_push_run_result(x, args) {
6565
6773
  } else if(rrspec.nr !== false) {
6566
6774
  // Run number inferred from local time step of expression
6567
6775
  const
6568
- rl = MODEL.end_period - MODEL.start_period + 1,
6776
+ rl = VM.nr_of_time_steps,
6569
6777
  range = rangeToList(rrspec.nr, xp.runs.length - 1);
6570
6778
  if(range) {
6571
6779
  const
@@ -6694,7 +6902,7 @@ function VMI_push_statistic(x, args) {
6694
6902
  }
6695
6903
  // Negative time step is evaluated as t = 0 (initial value) t beyond
6696
6904
  // optimization period is evaluated as its last time step
6697
- const tmax = MODEL.end_period - MODEL.start_period + 1;
6905
+ const tmax = VM.nr_of_time_steps;
6698
6906
  t1 = Math.max(0, Math.min(tmax, t1));
6699
6907
  t2 = Math.max(0, Math.min(tmax, t2));
6700
6908
  // Trace only now that time step range has been computed
@@ -7467,19 +7675,19 @@ function VMI_set_bounds(args) {
7467
7675
  k = VM.offset + vi,
7468
7676
  r = VM.round_letters.indexOf(VM.round_sequence[VM.current_round]),
7469
7677
  // Optional fourth parameter indicates whether the solver's
7470
- // infinity values should be used
7678
+ // infinity values should be used.
7471
7679
  solver_inf = args.length > 3 && args[3],
7472
7680
  inf_val = (solver_inf ? VM.SOLVER_PLUS_INFINITY : VM.PLUS_INFINITY);
7473
7681
  let l,
7474
7682
  u,
7475
7683
  fixed = (vi in VM.fixed_var_indices[r - 1]);
7476
7684
  if(fixed) {
7477
- // Set both bounds equal to the level set in the previous round, or to 0
7478
- // if this is the first round
7685
+ // Set both bounds equal to the level set in the previous round,
7686
+ // or to 0 if this is the first round.
7479
7687
  if(VM.current_round) {
7480
7688
  l = vbl.actualLevel(VM.t);
7481
- // QUICK PATCH! should resolve that small non-zero process levels
7482
- // computed in prior round make problem infeasible
7689
+ // QUICK PATCH! Should resolve that small non-zero process levels
7690
+ // computed in prior round make problem infeasible.
7483
7691
  if(l < 0.0005) l = 0;
7484
7692
  } else {
7485
7693
  l = 0;
@@ -7487,7 +7695,7 @@ function VMI_set_bounds(args) {
7487
7695
  u = l;
7488
7696
  fixed = ' (FIXED ' + vbl.displayName + ')';
7489
7697
  } else {
7490
- // Set bounds as specified by the two arguments
7698
+ // Set bounds as specified by the two arguments.
7491
7699
  l = args[1];
7492
7700
  if(l instanceof Expression) l = l.result(VM.t);
7493
7701
  if(l === VM.UNDEFINED) l = 0;
@@ -7500,22 +7708,22 @@ function VMI_set_bounds(args) {
7500
7708
  }
7501
7709
  fixed = '';
7502
7710
  }
7503
- // NOTE: to see in the console whether fixing across rounds works, insert
7504
- // "fixed !== '' || " before DEBUGGING below
7711
+ // NOTE: To see in the console whether fixing across rounds works, insert
7712
+ // "fixed !== '' || " before DEBUGGING below.
7505
7713
  if(DEBUGGING) {
7506
7714
  console.log(['set_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
7507
7715
  ' LB = ', VM.sig4Dig(l), ', UB = ', VM.sig4Dig(u), fixed].join(''));
7508
7716
  }
7509
- // NOTE: since the VM vectors for lower bounds and upper bounds are
7717
+ // NOTE: Since the VM vectors for lower bounds and upper bounds are
7510
7718
  // initialized with default values (0 for LB, +INF for UB), there is
7511
- // no need to set them
7719
+ // no need to set them.
7512
7720
  if(l !== 0 || u < inf_val) {
7513
7721
  VM.lower_bounds[k] = l;
7514
7722
  VM.upper_bounds[k] = u;
7515
7723
  // If associated node is FROM-node of a "peak increase" link, then
7516
7724
  // the "peak increase" variables of this node must have the highest
7517
7725
  // UB of the node (for all t in this block, hence MAX) MINUS their
7518
- // peak level in previous block
7726
+ // peak level in previous block.
7519
7727
  if(vbl.peak_inc_var_index >= 0) {
7520
7728
  u = Math.max(0, u - vbl.b_peak[VM.block_count - 1]);
7521
7729
  const
@@ -7531,13 +7739,28 @@ function VMI_set_bounds(args) {
7531
7739
  }
7532
7740
 
7533
7741
  function VMI_clear_coefficients() {
7742
+ // Clear the coefficients register and set RHS to zero.
7534
7743
  if(DEBUGGING) console.log('clear_coefficients');
7535
7744
  VM.coefficients = {};
7536
- VM.cash_in_coefficients = {};
7537
- VM.cash_out_coefficients = {};
7538
7745
  VM.rhs = 0;
7539
7746
  }
7540
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
+
7541
7764
  function VMI_add_const_to_coefficient(args) {
7542
7765
  // `args`: [var_index, number (, delay (, 1))]
7543
7766
  const
@@ -7553,15 +7776,13 @@ function VMI_add_const_to_coefficient(args) {
7553
7776
  d = args[2];
7554
7777
  }
7555
7778
  }
7556
- const
7557
- k = VM.offset + vi - d*VM.cols,
7558
- t = VM.t - d;
7779
+ const k = VM.offset + vi - d*VM.cols;
7559
7780
  if(DEBUGGING) {
7560
7781
  console.log(`add_const_to_coefficient [${k}]: ${VM.sig4Dig(n)}`);
7561
7782
  }
7562
7783
  // A negative delay may result in a variable index beyond the tableau
7563
- // column range. Such "future variables" should be ignored.
7564
- if(d < 0 && k > VM.chunk_offset) return;
7784
+ // column range. Such "future variables" should always be ignored.
7785
+ if(k > VM.chunk_offset) return;
7565
7786
  if(k <= 0) {
7566
7787
  // NOTE: If `k` falls PRIOR to the start of the block being solved,
7567
7788
  // this means that the value of the decision variable X for which the
@@ -7569,16 +7790,7 @@ function VMI_add_const_to_coefficient(args) {
7569
7790
  // while solving a previous block. Since the value of X is known,
7570
7791
  // adding n to C is implemented as subtracting n*X from the right hand
7571
7792
  // side of the constraint.
7572
- // NOTE: Subtract 1 from index `vi` because VM.variables is a 0-based
7573
- // array.
7574
- const
7575
- vbl = VM.variables[vi - 1],
7576
- pv = VM.priorValue(vbl, t);
7577
- if(DEBUGGING) {
7578
- console.log(`--lookup[${k}]: ${vbl[0]} ${vbl[1].displayName} @ ${t} = ${pv}`);
7579
- }
7580
- // NOTE: special cases for binary variables!
7581
- VM.rhs -= pv * n;
7793
+ VM.rhs -= knownValue(vi, VM.t - d) * n;
7582
7794
  } else if(k in VM.coefficients) {
7583
7795
  VM.coefficients[k] += n;
7584
7796
  } else {
@@ -7587,7 +7799,7 @@ function VMI_add_const_to_coefficient(args) {
7587
7799
  }
7588
7800
 
7589
7801
  function VMI_add_const_to_sum_coefficients(args) {
7590
- // NOTE: used to implement data links with SUM multiplier
7802
+ // NOTE: Used to implement data links with SUM multiplier.
7591
7803
  // `args`: [var_index, number, delay (, 1)]
7592
7804
  const vi = args[0];
7593
7805
  let d = args[2].object.actualDelay(VM.t),
@@ -7606,16 +7818,11 @@ function VMI_add_const_to_sum_coefficients(args) {
7606
7818
  d = -d;
7607
7819
  }
7608
7820
  for(let i = 0; i <= d; i++) {
7609
- // A negative delay may result in a variable index beyond the tableau
7610
- // column range. Such "future variables" should be ignored.
7821
+ // Variables beyond the chunk length should be ignored.
7611
7822
  if(k > VM.chunk_offset) return;
7612
7823
  if(k <= 0) {
7613
- // See NOTE in VMI_add_const_to_coefficient instruction
7614
- const vbl = VM.variables[vi - 1];
7615
- if(DEBUGGING) {
7616
- console.log('--lookup[' + k + ']: ' + vbl[0] + ' ' + vbl[1].displayName);
7617
- }
7618
- VM.rhs -= VM.priorValue(vbl, t) * n;
7824
+ // See NOTE in VMI_add_const_to_coefficient instruction.
7825
+ VM.rhs -= knownValue(vi, t) * n;
7619
7826
  } else if(k in VM.coefficients) {
7620
7827
  VM.coefficients[k] += n;
7621
7828
  } else {
@@ -7632,29 +7839,24 @@ function VMI_add_var_to_coefficient(args) {
7632
7839
  let d = 0;
7633
7840
  if(args.length > 2 && args[2] instanceof Expression) {
7634
7841
  d = args[2].object.actualDelay(VM.t);
7635
- // 4th argument = 1 indicates "delay + 1"
7842
+ // 4th argument = 1 indicates "delay + 1".
7636
7843
  if(args.length > 3 && args[3]) d++;
7637
7844
  }
7638
7845
  const
7639
7846
  k = VM.offset + vi - d*VM.cols,
7640
7847
  t = VM.t - d;
7641
7848
  let r = args[1].result(t);
7642
- // Optional 5th parameter is a constant multiplier
7849
+ // Optional 5th parameter is a constant multiplier.
7643
7850
  if(args.length > 4) r *= args[4];
7644
7851
  if(DEBUGGING) {
7645
7852
  console.log('add_var_to_coefficient [' + k + ']: ' +
7646
7853
  args[1].variableName + ' (t = ' + t + ')');
7647
7854
  }
7648
- // A negative delay may result in a variable index beyond the tableau
7649
- // column range. Such "future variables" should be ignored.
7855
+ // Ignore "future variables".
7650
7856
  if(k > VM.chunk_offset) return;
7651
7857
  if(k <= 0) {
7652
- // See NOTE in VMI_add_const_to_coefficient instruction
7653
- const vbl = VM.variables[vi - 1];
7654
- if(DEBUGGING) {
7655
- console.log('--lookup[' + k + ']: ' + vbl[0] + ' ' + vbl[1].displayName);
7656
- }
7657
- VM.rhs -= VM.priorValue(vbl, t) * r;
7858
+ // See NOTE in VMI_add_const_to_coefficient instruction.
7859
+ VM.rhs -= knownValue(vi, t) * r;
7658
7860
  } else if(k in VM.coefficients) {
7659
7861
  VM.coefficients[k] += r;
7660
7862
  } else {
@@ -7682,18 +7884,13 @@ function VMI_add_var_to_weighted_sum_coefficients(args) {
7682
7884
  d = -d;
7683
7885
  }
7684
7886
  for(let i = 0; i <= d; i++) {
7685
- // A negative delay may result in a variable index beyond the tableau
7686
- // column range. Such "future variables" should be ignored.
7887
+ // Ignore "future variables".
7687
7888
  if(k > VM.chunk_offset) return;
7688
7889
  let r = v.result(t);
7689
7890
  if(args.length > 3) r /= (d + 1);
7690
7891
  if(k <= 0) {
7691
7892
  // See NOTE in VMI_add_const_to_coefficient instruction
7692
- const vbl = VM.variables[vi - 1];
7693
- if(DEBUGGING) {
7694
- console.log('--lookup[' + k + ']: ' + vbl[0] + ' ' + vbl[1].displayName);
7695
- }
7696
- VM.rhs -= VM.priorValue(vbl, t) * r;
7893
+ VM.rhs -= knownValue(vi, t) * r;
7697
7894
  } else if(k in VM.coefficients) {
7698
7895
  VM.coefficients[k] += r;
7699
7896
  } else {
@@ -7715,22 +7912,15 @@ function VMI_subtract_const_from_coefficient(args) {
7715
7912
  // 4th argument indicates "delay + 1"
7716
7913
  if(args.length > 3) d++;
7717
7914
  }
7718
- const
7719
- k = VM.offset + vi - d*VM.cols,
7720
- t = VM.t - d;
7915
+ const k = VM.offset + vi - d*VM.cols;
7721
7916
  if(DEBUGGING) {
7722
7917
  console.log('subtract_const_from_coefficient [' + k + ']: ' + VM.sig4Dig(n));
7723
7918
  }
7724
- // A negative delay may result in a variable index beyond the tableau
7725
- // column range. Such "future variables" should be ignored.
7919
+ // Ignore "future variables".
7726
7920
  if(k > VM.chunk_offset) return;
7727
7921
  if(k <= 0) {
7728
7922
  // See NOTE in VMI_add_const_to_coefficient instruction
7729
- const vbl = VM.variables[vi - 1];
7730
- if(DEBUGGING) {
7731
- console.log('--lookup[' + k + ']: ' + vbl[0] + ' ' + vbl[1].displayName);
7732
- }
7733
- VM.rhs += VM.priorValue(vbl, t) * n;
7923
+ VM.rhs += knownValue(vi, VM.t - d) * n;
7734
7924
  } else if(k in VM.coefficients) {
7735
7925
  VM.coefficients[k] -= n;
7736
7926
  } else {
@@ -7764,16 +7954,11 @@ function VMI_subtract_var_from_coefficient(args) {
7764
7954
  console.log('subtract_var_from_coefficient [' + k + ']: ' +
7765
7955
  args[1].variableName + ' (t = ' + t + ')');
7766
7956
  }
7767
- // A negative delay may result in a variable index beyond the tableau
7768
- // column range. Such "future variables" should be ignored.
7957
+ // Ignore "future variables".
7769
7958
  if(k > VM.chunk_offset) return;
7770
7959
  if(k <= 0) {
7771
- // See NOTE in VMI_add_const_to_coefficient instruction
7772
- const vbl = VM.variables[vi - 1];
7773
- if(DEBUGGING) {
7774
- console.log('--lookup[' + k + ']: ' + vbl[0] + ' ' + vbl[1].displayName);
7775
- }
7776
- VM.rhs += VM.priorValue(vbl, t) * r;
7960
+ // See NOTE in VMI_add_const_to_coefficient instruction.
7961
+ VM.rhs += knownValue(vi, t) * r;
7777
7962
  } else if(k in VM.coefficients) {
7778
7963
  VM.coefficients[k] -= r;
7779
7964
  } else {
@@ -7783,9 +7968,9 @@ function VMI_subtract_var_from_coefficient(args) {
7783
7968
 
7784
7969
  function VMI_update_cash_coefficient(args) {
7785
7970
  // `args`: [flow, type, level_var_index, delay, x1, x2, ...]
7786
- // 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
7787
7972
  // constant parameter x1), TWO_X (two expressions x1 and x2), THREE_X
7788
- // (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).
7789
7974
  let d = 0;
7790
7975
  const
7791
7976
  flow = args[0],
@@ -7793,52 +7978,68 @@ function VMI_update_cash_coefficient(args) {
7793
7978
  vi = args[2],
7794
7979
  dx = args[3];
7795
7980
  if(dx instanceof Expression) {
7981
+ // When delay is an expression, `dx.object` is the delayed link.
7796
7982
  d = dx.object.actualDelay(VM.t);
7797
- // Extra argument indicates "delay + 1"
7983
+ // Extra argument indicates "delay + 1" (for delta-multipliers).
7798
7984
  if((type === VM.ONE_C && args.length === 6) ||
7799
7985
  (type === VM.TWO_X && args.length === 7)) d++;
7800
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.
7801
7989
  // `k` is the tableau column index of the variable that affects the CF
7802
7990
  let k = (type === VM.PEAK_INC ? VM.chunk_offset + vi :
7803
7991
  VM.offset + vi - d*VM.cols);
7804
- // NOTE: delay > 0 affects only which variable is to be used,
7805
- // not the expressions for rates or prices!
7806
- const t = VM.t - d;
7807
- // NOTE: this instruction is used only for objective function
7808
- // coefficients; previously computed decision variables and variables
7809
- // beyond the tableau column range (when delay < 0) can be ignored.
7810
- if(k <= 0 || k > VM.chunk_offset) return;
7811
- // NOTE: peak increase can generate cash only at the first time
7992
+ // NOTE: Variables beyond the tableau column range (when delay < 0) can
7993
+ // be ignored.
7994
+ if(k > VM.chunk_offset) return;
7995
+ // NOTE: Peak increase can generate cash only at the first time
7812
7996
  // step of a block (when VM.offset = 0) and at the first time step
7813
- // of the look-ahead period (when VM.offset = block length)
7997
+ // of the look-ahead period (when VM.offset = block length).
7814
7998
  if(type === VM.PEAK_INC &&
7815
7999
  VM.offset > 0 && VM.offset !== MODEL.block_length) return;
7816
- // First compute the result to be processed
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.
8003
+ // First compute the result to be processed.
7817
8004
  let r = 0;
7818
8005
  if(type === VM.ONE_C) {
7819
8006
  r = args[4];
7820
8007
  } else if(type === VM.TWO_X || type === VM.PEAK_INC) {
7821
- // NOTE: "peak increase" always passes two expressions
8008
+ // NOTE: "peak increase" always passes two expressions.
8009
+ // The first expression determines the price.
7822
8010
  r = args[4].result(VM.t) * args[5].result(VM.t);
7823
8011
  } else if(type === VM.THREE_X) {
8012
+ // NOTE: Here, too, the first expression determines the price.
7824
8013
  r = args[4].result(VM.t) * args[5].result(VM.t) * args[6].result(VM.t);
7825
8014
  } else if(type === VM.SPIN_RES) {
7826
- // "spinning reserve" equals UB - level if level > 0, or 0
8015
+ // "spinning reserve" equals UB - level if level > 0, or 0.
7827
8016
  // The cash flow then equals ON/OFF*UB*price*rate - level*price*rate.
7828
8017
  // The ON/OFF variable index is passed as third argument, hence `plvi`
7829
8018
  // (process level variable index) as first extra parameter, plus three
7830
- // expressions (UB, price, rate)
8019
+ // expressions (UB, price, rate).
7831
8020
  const
7832
8021
  plvi = args[4],
7833
- // NOTE: column of second variable will be relative to same offset
8022
+ // NOTE: Column of second variable will be relative to same offset.
7834
8023
  plk = k + plvi - vi,
7835
8024
  ub = args[5].result(VM.t),
7836
8025
  price_rate = args[6].result(VM.t) * args[7].result(VM.t);
7837
8026
  r = ub * price_rate;
7838
- // NOTE: the sign of r determines whether this spinning reserve will
7839
- // generate cash IN or cash OUT; the *subtracted* part hence be ADDED
7840
- // if r > 0, and SUBTRACTED if r < 0 (unlike the "primary" part r itself)
7841
- if(r > 0) {
8027
+ // NOTE: The sign of r determines whether this spinning reserve will
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) {
7842
8043
  if(plk in VM.cash_in_coefficients) {
7843
8044
  VM.cash_in_coefficients[plk] += price_rate;
7844
8045
  } else {
@@ -7852,27 +8053,38 @@ function VMI_update_cash_coefficient(args) {
7852
8053
  }
7853
8054
  }
7854
8055
  }
7855
- // NOTE: for spinning reserve and highest increment, flow will always
7856
- // be PRODUCE
8056
+ // NOTE: For spinning reserve and highest increment, flow will always
8057
+ // be PRODUCE.
7857
8058
  if(flow === VM.CONSUME) r = -r;
7858
8059
  if(DEBUGGING) {
7859
- const vbl = (vi <= this.cols ? VM.variables[vi - 1] :
7860
- VM.chunk_variables[vi - this.cols]); //@@@ TO MAKE CORRECT FOR chunk vars!
7861
- console.log(['update_cash_coefficient [', k, ']: ', vbl[0], ' ',
7862
- vbl[1].displayName, ' (t = ', t, ') ', VM.CF_CONSTANTS[type], ' ',
7863
- VM.CF_CONSTANTS[flow], ' r = ', VM.sig4Dig(r)].join(''));
7864
- }
7865
- // Use look-ahead peak increase when offset > 0
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(''));
8067
+ }
8068
+ // Use look-ahead peak increase when offset > 0.
7866
8069
  if(type === VM.PEAK_INC && VM.offset) k++;
7867
- // Then update the cash flow: cash IN if r > 0, otherwise cash OUT
7868
- 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) {
7869
8081
  if(k in VM.cash_in_coefficients) {
7870
8082
  VM.cash_in_coefficients[k] -= r;
7871
8083
  } else {
7872
8084
  VM.cash_in_coefficients[k] = -r;
7873
8085
  }
7874
8086
  } else if(r < 0) {
7875
- // NOTE: Test for r < 0 because no action is needed if r = 0
8087
+ // NOTE: Test for r < 0 because no action is needed if r = 0.
7876
8088
  if(k in VM.cash_out_coefficients) {
7877
8089
  VM.cash_out_coefficients[k] += r;
7878
8090
  } else {
@@ -7882,7 +8094,7 @@ function VMI_update_cash_coefficient(args) {
7882
8094
  }
7883
8095
 
7884
8096
  function VMI_add_throughput_to_coefficient(args) {
7885
- // Special instruction to deal with throughput calculation
8097
+ // Special instruction to deal with throughput calculation.
7886
8098
  // Function: to add the contribution of variable X to the level of
7887
8099
  // variable Z when Z depends (a.o.) on the throughput of variable Y, i.e.,
7888
8100
  // X --(r2,d2)--> Y --(r1,d1)--> Z
@@ -7892,8 +8104,9 @@ function VMI_add_throughput_to_coefficient(args) {
7892
8104
  vi = args[0],
7893
8105
  d1 = args[2].object.actualDelay(VM.t),
7894
8106
  d2 = (args[4] ? args[4].object.actualDelay(VM.t) : 0),
7895
- k = VM.offset + vi - (d1 + d2)*VM.cols,
7896
- t = VM.t - d1 - d2,
8107
+ dsum = d1 + d2,
8108
+ k = VM.offset + vi - dsum * VM.cols,
8109
+ t = VM.t - dsum,
7897
8110
  // Compute the value to be added to the coefficient
7898
8111
  v = args[1].result(VM.t) * args[3].result(VM.t - d1);
7899
8112
  if(DEBUGGING) {
@@ -7901,8 +8114,7 @@ function VMI_add_throughput_to_coefficient(args) {
7901
8114
  args[1].variableName + ' * ' + args[3].variableName +
7902
8115
  ' (t = ' + VM.t + ')');
7903
8116
  }
7904
- // A negative delay may result in a variable index beyond the tableau
7905
- // column range. Such "future variables" should be ignored.
8117
+ // Ignore "future variables".
7906
8118
  if(k > VM.chunk_offset) return;
7907
8119
  if(k <= 0) {
7908
8120
  const vbl = VM.variables[vi - 1];
@@ -8012,27 +8224,69 @@ function VMI_add_constraint(ct) {
8012
8224
  }
8013
8225
  }
8014
8226
 
8015
- function VMI_copy_cash_coefficients(flow) {
8016
- // Overwrites the coefficients vector with the specified cash coefficients
8017
- // 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.
8018
8233
  if(DEBUGGING) {
8019
- 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 + ')');
8020
8236
  }
8021
- if(flow === VM.PRODUCE) {
8022
- VM.coefficients = Object.assign({}, VM.cash_in_coefficients);
8023
- } else {
8024
- VM.coefficients = Object.assign({}, VM.cash_out_coefficients);
8237
+ if(!VM.add_constraints_flag) {
8238
+ console.log('Constraint NOT added!');
8239
+ return;
8025
8240
  }
8026
- // NOTE: This instruction also keeps track of the highest cash flow constraint
8027
- // coefficient (to be used for scaling these constraint equations)
8028
- for(let i in VM.coefficients) if(VM.coefficients.hasOwnProperty(i)) {
8029
- 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
+ }
8030
8279
  }
8031
- // NOTE: To permit such scaling, this instruction creates a list of constraint
8032
- // row indices, as these are the equations that need to be scaled
8033
8280
  VM.cash_constraints.push(VM.matrix.length);
8034
- // Always set RHS to 0 as cash flow constraints are EQ 0 constraints
8035
- 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;
8036
8290
  }
8037
8291
 
8038
8292
  function VMI_add_bound_line_constraint(args) {
@@ -8285,7 +8539,7 @@ function VMI_profitable_units(x) {
8285
8539
  mpa = d[3].attribute,
8286
8540
  pt = (d.length > 4 ? d[4] : 0), // the profit threshold (0 by default)
8287
8541
  // the time horizon (by default the length of the simulation period)
8288
- 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);
8289
8543
  // Handle exceptional values of `uc` and `mc`.
8290
8544
  if(uc <= VM.BEYOND_MINUS_INFINITY || mc <= VM.BEYOND_MINUS_INFINITY) {
8291
8545
  x.retop(Math.min(uc, mc));
@@ -8423,7 +8677,7 @@ function VMI_highest_cumulative_consecutive_deviation(x) {
8423
8677
  // NOTE: an expression may not have been (fully) computed yet.
8424
8678
  x.compute(0);
8425
8679
  if(!x.isStatic) {
8426
- const nt = MODEL.end_period - MODEL.start_period + 1;
8680
+ const nt = VM.nr_of_time_steps;
8427
8681
  for(let t = 1; t <= nt; t++) x.result(t);
8428
8682
  }
8429
8683
  vector = x.vector;
@@ -8574,7 +8828,7 @@ function correlation_or_slope(x, c_or_s) {
8574
8828
  // NOTE: An equation may not have been (fully) computed yet.
8575
8829
  eq.compute(0, x.wildcard_vector_index);
8576
8830
  if(!eq.isStatic) {
8577
- const nt = MODEL.end_period - MODEL.start_period + 1;
8831
+ const nt = VM.nr_of_time_steps;
8578
8832
  for(let t = 1; t <= nt; t++) eq.result(t, x.wildcard_vector_index);
8579
8833
  }
8580
8834
  vector[k].v = eq.vector;
@@ -8601,7 +8855,7 @@ function correlation_or_slope(x, c_or_s) {
8601
8855
  x.retop(x.cache[cache_key]);
8602
8856
  return;
8603
8857
  }
8604
- if(true||DEBUGGING) {
8858
+ if(DEBUGGING) {
8605
8859
  console.log(`-- ${vmi}(${vector.x.name}, ${vector.y.name})`);
8606
8860
  }
8607
8861
  // NOTE: Vectors should have equal length.