linny-r 2.0.5 → 2.0.7

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.
@@ -10,7 +10,7 @@ the Linny-R project.
10
10
  */
11
11
 
12
12
  /*
13
- Copyright (c) 2017-2024 Delft University of Technology
13
+ Copyright (c) 2017-2025 Delft University of Technology
14
14
 
15
15
  Permission is hereby granted, free of charge, to any person obtaining a copy
16
16
  of this software and associated documentation files (the "Software"), to deal
@@ -99,6 +99,7 @@ class LinnyRModel {
99
99
  this.align_to_grid = true;
100
100
  this.with_power_flow = false;
101
101
  this.infer_cost_prices = false;
102
+ this.ignore_negative_flows = false;
102
103
  this.report_results = false;
103
104
  this.show_block_arrows = true;
104
105
  this.last_zoom_factor = 1;
@@ -2585,6 +2586,15 @@ class LinnyRModel {
2585
2586
  const l = this.links[k];
2586
2587
  xl.push(l.relative_rate, l.flow_delay);
2587
2588
  }
2589
+ for(let k in this.constraints) if(this.constraints.hasOwnProperty(k)) {
2590
+ const c = this.constraints[k];
2591
+ for(let i = 0; i < c.bound_lines.length; i++) {
2592
+ const bl = c.bound_lines[i];
2593
+ for(let j = 0; j < bl.selectors.length; j++) {
2594
+ xl.push(bl.selectors[j].expression);
2595
+ }
2596
+ }
2597
+ }
2588
2598
  return xl;
2589
2599
  }
2590
2600
 
@@ -2736,6 +2746,7 @@ class LinnyRModel {
2736
2746
  this.align_to_grid = nodeParameterValue(node, 'align-to-grid') === '1';
2737
2747
  this.with_power_flow = nodeParameterValue(node, 'power-flow') === '1';
2738
2748
  this.infer_cost_prices = nodeParameterValue(node, 'cost-prices') === '1';
2749
+ this.ignore_negative_flows = nodeParameterValue(node, 'negative-flows') === '1';
2739
2750
  this.report_results = nodeParameterValue(node, 'report-results') === '1';
2740
2751
  this.show_block_arrows = nodeParameterValue(node, 'block-arrows') === '1';
2741
2752
  // NOTE: Diagnosis option should default to TRUE unless *set* to FALSE.
@@ -3106,6 +3117,7 @@ class LinnyRModel {
3106
3117
  if(this.align_to_grid) p += ' align-to-grid="1"';
3107
3118
  if(this.with_power_flow) p += ' power-flow="1"';
3108
3119
  if(this.infer_cost_prices) p += ' cost-prices="1"';
3120
+ if(this.ignore_negative_flows) p += ' negative-flows="1"';
3109
3121
  if(this.report_results) p += ' report-results="1"';
3110
3122
  if(this.show_block_arrows) p += ' block-arrows="1"';
3111
3123
  if(this.show_notices) p += ' show-notices="1"';
@@ -3522,6 +3534,15 @@ class LinnyRModel {
3522
3534
  }
3523
3535
  this.cleanVector(l.actual_flow, p * l.relative_rate.result(0));
3524
3536
  }
3537
+ for(let k in this.constraints) if(this.constraints.hasOwnProperty(k)) {
3538
+ const c = this.constraints[k];
3539
+ for(let i = 0; i < c.bound_lines.length; i++) {
3540
+ const bl = c.bound_lines[i];
3541
+ for(let j = 0; j < bl.selectors.length; j++) {
3542
+ bl.selectors[j].expression.reset(0);
3543
+ }
3544
+ }
3545
+ }
3525
3546
  for(obj in this.datasets) if(this.datasets.hasOwnProperty(obj)) {
3526
3547
  const ds = this.datasets[obj];
3527
3548
  ds.resetExpressions();
@@ -5780,7 +5801,7 @@ class NodeBox extends ObjectWithXYWH {
5780
5801
  }
5781
5802
 
5782
5803
  resize() {
5783
- // Resizes this node; returns TRUE iff size has changed.
5804
+ // Resize this node; returns TRUE iff size has changed.
5784
5805
  // Therefore, keep track of original width and height.
5785
5806
  const
5786
5807
  ow = this.width,
@@ -5793,7 +5814,7 @@ class NodeBox extends ObjectWithXYWH {
5793
5814
  w = Math.max(w, UI.textSize(`[${this.scale_unit}]`).width);
5794
5815
  }
5795
5816
  this.frame_width = w + 7;
5796
- // Add 17 pixels height for actor name
5817
+ // Add 17 pixels height for actor name.
5797
5818
  this.height = Math.max(50, this.bbox.height + 17);
5798
5819
  if(this instanceof Process) {
5799
5820
  this.width = Math.max(90, this.frame_width + 20);
@@ -5801,11 +5822,11 @@ class NodeBox extends ObjectWithXYWH {
5801
5822
  } else if(this instanceof Cluster) {
5802
5823
  this.width = Math.max(
5803
5824
  CONFIGURATION.min_cluster_size, this.frame_width + 20);
5804
- // Clusters have a square shape
5825
+ // Clusters have a square shape.
5805
5826
  this.height = Math.max(this.width, this.height);
5806
5827
  } else {
5807
5828
  this.height += 8;
5808
- // Reserve some extra space for UB/LB if defined
5829
+ // Reserve some extra space for UB/LB if defined.
5809
5830
  if(this.lower_bound.defined || this.upper_bound.defined) {
5810
5831
  this.frame_width += 16;
5811
5832
  }
@@ -8261,20 +8282,20 @@ class Product extends Node {
8261
8282
  // By default, processes have the letter p, products the letter q.
8262
8283
  this.TEX_id = 'p';
8263
8284
  // For products, the default bounds are [0, 0], and modeler-defined bounds
8264
- // typically are equal
8285
+ // typically are equal.
8265
8286
  this.equal_bounds = true;
8266
- // In addition to LB, UB and IL, products has 1 input attribute: P
8287
+ // In addition to LB, UB and IL, products has 1 input attribute: P.
8267
8288
  this.price = new Expression(this, 'P', '');
8268
- // Products have a highest cost price, and may have a stock price (if storage)
8289
+ // Products have a highest cost price, and may have a stock price (if storage).
8269
8290
  this.highest_cost_price = [];
8270
8291
  this.stock_price = [];
8271
8292
  // Stock level changing from 0 to positive counts as "start up", while
8272
- // changing from positive to 0 counts as a "shut-down"
8273
- // NOTE: being relatively rare, start_ups and shut_downs are not vectors,
8274
- // but store the numbers of the time steps in which they occurred
8293
+ // changing from positive to 0 counts as a "shut-down".
8294
+ // NOTE: Being relatively rare, start_ups and shut_downs are not vectors,
8295
+ // but store the numbers of the time steps in which they occurred.
8275
8296
  this.start_ups = [];
8276
8297
  this.shut_downs = [];
8277
- // Modeler may set explicit properties
8298
+ // Modeler may set explicit properties.
8278
8299
  this.is_source = false;
8279
8300
  this.is_sink = false;
8280
8301
  this.is_buffer = false;
@@ -8629,13 +8650,13 @@ class Product extends Node {
8629
8650
  }
8630
8651
 
8631
8652
  get defaultAttribute() {
8632
- // Products have their level as default attribute
8653
+ // Products have their level as default attribute.
8633
8654
  return 'L';
8634
8655
  }
8635
8656
 
8636
8657
  attributeValue(a) {
8637
- // Returns the computed result for attribute `a`
8638
- // (for products, this is always a vector except IL)
8658
+ // Return the computed result for attribute `a`.
8659
+ // NOTE: For products, this is always a vector except IL.
8639
8660
  if(a === 'L') return this.level;
8640
8661
  if(a === 'CP') return this.cost_price;
8641
8662
  if(a === 'HCP') return this.highest_cost_price;
@@ -8643,7 +8664,7 @@ class Product extends Node {
8643
8664
  }
8644
8665
 
8645
8666
  attributeExpression(a) {
8646
- // Products have four expression attributes
8667
+ // Products have four expression attributes.
8647
8668
  if(a === 'LB') return this.lower_bound;
8648
8669
  if(a === 'UB') {
8649
8670
  return (this.equal_bounds ? this.lower_bound : this.upper_bound);
@@ -8727,7 +8748,7 @@ class Product extends Node {
8727
8748
  }
8728
8749
 
8729
8750
  copyPropertiesFrom(p) {
8730
- // Set properties to be identical to those of product `p`
8751
+ // Set properties to be identical to those of product `p`.
8731
8752
  this.x = p.x;
8732
8753
  this.y = p.y;
8733
8754
  this.comments = p.comments;
@@ -8744,11 +8765,11 @@ class Product extends Node {
8744
8765
  this.initial_level.text = p.initial_level.text;
8745
8766
  this.integer_level = p.integer_level;
8746
8767
  this.TEX_id = p.TEX_id;
8747
- // NOTE: do not copy the `no_links` property, nor the import/export status
8768
+ // NOTE: Do not copy the `no_links` property, nor the import/export status.
8748
8769
  }
8749
8770
 
8750
8771
  differences(p) {
8751
- // Return "dictionary" of differences, or NULL if none
8772
+ // Return "dictionary" of differences, or NULL if none.
8752
8773
  const d = differences(this, p, UI.MC.PRODUCT_PROPS);
8753
8774
  if(Object.keys(d).length > 0) return d;
8754
8775
  return null;
@@ -12678,6 +12699,10 @@ class BoundLine {
12678
12699
  VM.constraint_codes[this.type] + '] bound line #' +
12679
12700
  this.constraint.bound_lines.indexOf(this);
12680
12701
  }
12702
+
12703
+ get name() {
12704
+ return this.displayName;
12705
+ }
12681
12706
 
12682
12707
  get copy() {
12683
12708
  // Return a "clone" of this bound line.
@@ -12969,11 +12994,12 @@ class BoundLine {
12969
12994
  }
12970
12995
 
12971
12996
  get needsNoSOS() {
12972
- // Return 1 if boundline is NOT of type <= and line segments have an
12973
- // increasing slope, -1 if boundline is NOT of type >= and line segments
12997
+ // Return 1 if boundline is of type >= and line segments have an
12998
+ // increasing slope, -1 if boundline is of type <= and line segments
12974
12999
  // have a decreasing slope, and otherwise 0 (FALSE). If non-zero (TRUE),
12975
13000
  // the constraint can be implemented without the SOS2 constraint that
12976
13001
  // only two consecutive SOS variables may be non-zero.
13002
+ if(this.type === VM.EQ) return 0;
12977
13003
  if(this.type !== VM.LE) {
12978
13004
  let slope = 0,
12979
13005
  pp = this.points[0];
@@ -9,7 +9,7 @@ This JavaScript file (linny-r-utils.js) defines a variety of "helper" functions
9
9
  that are used in other Linny-R modules.
10
10
  */
11
11
  /*
12
- Copyright (c) 2017-2024 Delft University of Technology
12
+ Copyright (c) 2017-2025 Delft University of Technology
13
13
 
14
14
  Permission is hereby granted, free of charge, to any person obtaining a copy
15
15
  of this software and associated documentation files (the "Software"), to deal
@@ -74,16 +74,16 @@ function safeStrToFloat(str, val=0) {
74
74
  }
75
75
 
76
76
  function safeStrToInt(str, val=0) {
77
- // Returns numeric value of integer string, IGNORING decimals after
77
+ // Return numeric value of integer string, IGNORING decimals after
78
78
  // point or comma.
79
- // NOTE: returns default value `val` if `str` is empty, null or undefined
79
+ // NOTE: Return default value `val` if `str` is empty, null or undefined.
80
80
  const n = (str ? parseInt(str) : val);
81
81
  return (isNaN(n) ? val : n);
82
82
  }
83
83
 
84
84
  function rangeToList(str, max=0) {
85
- // Parses ranges "n-m/i" into a list of integers
86
- // Returns FALSE if range is not valid according to the convention below
85
+ // Parse ranges "n-m/i" into a list of integers
86
+ // Return FALSE if range is not valid according to the convention below
87
87
  // The part "/i" is optional and denotes the increment; by default, i = 1.
88
88
  // The returned list will contain all integers starting at n and up to
89
89
  // at most (!) m, with increments of i, so [n, n+i, n+2i, ...]
@@ -112,6 +112,33 @@ function rangeToList(str, max=0) {
112
112
  return list;
113
113
  }
114
114
 
115
+ function listToRange(list) {
116
+ // Return a string that represents the given list of integers as a series
117
+ // of subranges, e.g., [0,1,2,3,5,6,9,11] results in "0-3, 5-6, 9, 11".
118
+ const
119
+ n = list.length,
120
+ subs = [];
121
+ if(!n) return '';
122
+ let i = 0,
123
+ from = list[0],
124
+ to = from;
125
+ while(i < n) {
126
+ i++;
127
+ if(list[i] === to + 1) {
128
+ to++;
129
+ } else {
130
+ if(from === to) {
131
+ subs.push(from);
132
+ } else {
133
+ subs.push(`${from}-${to}`);
134
+ }
135
+ from = list[i];
136
+ to = from;
137
+ }
138
+ }
139
+ return subs.join(', ');
140
+ }
141
+
115
142
  function dateToString(d) {
116
143
  // Returns date-time `d` in UTC format, accounting for time zone
117
144
  const offset = d.getTimezoneOffset();
@@ -1098,6 +1125,7 @@ if(NODE) module.exports = {
1098
1125
  safeStrToFloat: safeStrToFloat,
1099
1126
  safeStrToInt: safeStrToInt,
1100
1127
  rangeToList: rangeToList,
1128
+ listToRange: listToRange,
1101
1129
  dateToString: dateToString,
1102
1130
  msecToTime: msecToTime,
1103
1131
  compactClockTime: compactClockTime,
@@ -12,7 +12,7 @@ executed by the VM, construct the Simplex tableau that can be sent to the
12
12
  MILP solver.
13
13
  */
14
14
  /*
15
- Copyright (c) 2017-2024 Delft University of Technology
15
+ Copyright (c) 2017-2025 Delft University of Technology
16
16
 
17
17
  Permission is hereby granted, free of charge, to any person obtaining a copy
18
18
  of this software and associated documentation files (the "Software"), to deal
@@ -444,17 +444,21 @@ class Expression {
444
444
  // expression).
445
445
  if(t < 0 || this.isStatic) t = 0;
446
446
  if(t >= v.length) return VM.UNDEFINED;
447
- // NOTE: When VM is setting up a tableau, values computed for the
448
- // look-ahead period must be recomputed.
449
- if(v[t] === VM.NOT_COMPUTED || v[t] === VM.COMPUTING ||
447
+ // NOTES:
448
+ // (1) When VM is setting up a tableau, values computed for the
449
+ // look-ahead period must be recomputed.
450
+ // (2) Always recompute value for sensitivity analysis parameter, as
451
+ // otherwise the vector value will be scaled cumulatively.
452
+ const sap = (this === MODEL.active_sensitivity_parameter);
453
+ if(sap || v[t] === VM.NOT_COMPUTED || v[t] === VM.COMPUTING ||
450
454
  (!this.isStatic && VM.inLookAhead(t))) {
451
455
  v[t] = VM.NOT_COMPUTED;
452
456
  this.compute(t, number);
453
457
  }
454
- // NOTE: when this expression is the "active" parameter for sensitivity
455
- // analysis, the result is multiplied by 1 + delta %
456
- if(this === MODEL.active_sensitivity_parameter) {
457
- // NOTE: do NOT scale exceptional values
458
+ // NOTE: When this expression is the "active" parameter for sensitivity
459
+ // analysis, the result is multiplied by 1 + delta %.
460
+ if(sap) {
461
+ // NOTE: Do NOT scale exceptional values.
458
462
  if(v[t] > VM.MINUS_INFINITY && v[t] < VM.PLUS_INFINITY) {
459
463
  v[t] *= (1 + MODEL.sensitivity_delta * 0.01);
460
464
  }
@@ -1849,14 +1853,14 @@ class ExpressionParser {
1849
1853
  if(this.then_stack.length < 1) {
1850
1854
  this.error = 'Unexpected :';
1851
1855
  } else {
1852
- // Similar to above: when a : operator is "coded", the ELSE part
1856
+ // Similar to above: When a : operator is "coded", the ELSE part
1853
1857
  // has been coded, so the end of the code array is the target for
1854
- // the most recently added JUMP
1858
+ // the most recently added JUMP.
1855
1859
  this.code[this.then_stack.pop()][1] = this.code.length;
1856
1860
  }
1857
1861
  } else {
1858
1862
  // All other operations require VM instructions that operate on the
1859
- // expression stack
1863
+ // expression stack.
1860
1864
  this.code.push([op, null]);
1861
1865
  if(op === VMI_concat) {
1862
1866
  this.concatenating = true;
@@ -1864,8 +1868,8 @@ class ExpressionParser {
1864
1868
  const randcode = RANDOM_CODES.indexOf(op) >= 0;
1865
1869
  if(REDUCING_CODES.indexOf(op) >= 0) {
1866
1870
  if(randcode && !this.concatenating) {
1867
- // NOTE: probability distributions MUST have a parameter list but
1868
- // MIN and MAX will also accept a single argument
1871
+ // NOTE: Probability distributions MUST have a parameter list but
1872
+ // MIN and MAX will also accept a single argument.
1869
1873
  console.log('OPERATOR:', op);
1870
1874
  this.error = 'Missing parameter list';
1871
1875
  }
@@ -2120,6 +2124,8 @@ class VirtualMachine {
2120
2124
  this.numeric_issue = '';
2121
2125
  // Warnings are stored in a list to permit browsing through them.
2122
2126
  this.issue_list = [];
2127
+ // Bound issues (UB < LB) are recorded to permit compact warnings.
2128
+ this.bound_issues = {};
2123
2129
  // The call stack tracks evaluation of "nested" expression variables.
2124
2130
  this.call_stack = [];
2125
2131
  this.block_count = 0;
@@ -2468,6 +2474,8 @@ class VirtualMachine {
2468
2474
  // block).
2469
2475
  this.error_count = 0;
2470
2476
  this.block_issues = 0;
2477
+ // Clear bound issue dictionary.
2478
+ this.bound_issues = {};
2471
2479
  // Clear issue list with warnings and hide issue panel.
2472
2480
  this.issue_list.length = 0;
2473
2481
  this.issue_index = -1;
@@ -2581,6 +2589,7 @@ class VirtualMachine {
2581
2589
  // Return number `n` formatted so as to show 2-3 significant digits
2582
2590
  // NOTE: as `n` should be a number, a warning sign will typically
2583
2591
  // indicate a bug in the software.
2592
+ if(typeof n === 'string') n = parseFloat(n);
2584
2593
  if(n === undefined || isNaN(n)) return '\u26A0'; // Warning sign
2585
2594
  const sv = this.specialValue(n);
2586
2595
  // If `n` has a special value, return its representation.
@@ -2608,6 +2617,7 @@ class VirtualMachine {
2608
2617
  // Return number `n` formatted so as to show 4-5 significant digits.
2609
2618
  // NOTE: As `n` should be a number, a warning sign will typically
2610
2619
  // indicate a bug in the software.
2620
+ if(typeof n === 'string') n = parseFloat(n);
2611
2621
  if(n === undefined || isNaN(n)) return '\u26A0';
2612
2622
  const sv = this.specialValue(n);
2613
2623
  // If `n` has a special value, return its representation.
@@ -3204,8 +3214,8 @@ class VirtualMachine {
3204
3214
 
3205
3215
  setBoundConstraints(p) {
3206
3216
  // Set LB and UB constraints for product `p`.
3207
- // NOTE: This method affects the VM coefficient vector, so save it
3208
- // (if needed) before calling this method.
3217
+ // NOTE: This method affects the VM coefficient vector, so this vector
3218
+ // should be saved (using a VM instruction) if it is needed later.
3209
3219
  const
3210
3220
  vi = p.level_var_index,
3211
3221
  lesvi = p.stock_LE_slack_var_index,
@@ -4338,7 +4348,7 @@ class VirtualMachine {
4338
4348
  ' will compromise computation of its binary variables';
4339
4349
  UI.warn(msg);
4340
4350
  this.logMessage(this.block_count,
4341
- 'WARNING: ' + msg.replace(/<\/?strong>/g, '"'));
4351
+ this.WARNING + msg.replace(/<\/?strong>/g, '"'));
4342
4352
  }
4343
4353
  }
4344
4354
  if(hub !== ub) {
@@ -4620,8 +4630,8 @@ class VirtualMachine {
4620
4630
  high_rate) + 1);
4621
4631
  if(this.slack_penalty > VM.MAX_SLACK_PENALTY) {
4622
4632
  this.slack_penalty = VM.MAX_SLACK_PENALTY;
4623
- this.logMessage(this.block_count,
4624
- 'WARNING: Max. slack penalty reached; try to scale down your model coefficients');
4633
+ this.logMessage(this.block_count, this.WARNING +
4634
+ 'Max. slack penalty reached; try to scale down your model coefficients');
4625
4635
  }
4626
4636
  const m = Math.max(
4627
4637
  Math.abs(this.low_coefficient), Math.abs(this.high_coefficient));
@@ -4763,14 +4773,16 @@ class VirtualMachine {
4763
4773
  a.cash_out[b] = this.checkForInfinity(
4764
4774
  x[a.cash_out_var_index + j] * this.cash_scalar);
4765
4775
  a.cash_flow[b] = a.cash_in[b] - a.cash_out[b];
4766
- // Count occurrences of a negative cash flow (threshold -0.5 cent).
4767
- if(b <= this.nr_of_time_steps && a.cash_in[b] < -0.005) {
4768
- this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
4769
- a.displayName + ' cash IN = ' + a.cash_in[b].toPrecision(2));
4770
- }
4771
- if(b <= this.nr_of_time_steps && a.cash_out[b] < -0.005) {
4772
- this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
4773
- a.displayName + ' cash OUT = ' + a.cash_out[b].toPrecision(2));
4776
+ if(!MODEL.ignore_negative_flows) {
4777
+ // Count occurrences of a negative cash flow (threshold -0.5 cent).
4778
+ if(b <= this.nr_of_time_steps && a.cash_in[b] < -0.005) {
4779
+ this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
4780
+ a.displayName + ' cash IN = ' + a.cash_in[b].toPrecision(2));
4781
+ }
4782
+ if(b <= this.nr_of_time_steps && a.cash_out[b] < -0.005) {
4783
+ this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
4784
+ a.displayName + ' cash OUT = ' + a.cash_out[b].toPrecision(2));
4785
+ }
4774
4786
  }
4775
4787
  // Advance column offset in tableau by the # cols per time step.
4776
4788
  j += this.cols;
@@ -4785,8 +4797,7 @@ class VirtualMachine {
4785
4797
  p = MODEL.processes[o],
4786
4798
  has_OO = (p.on_off_var_index >= 0),
4787
4799
  has_SU = (p.start_up_var_index >= 0),
4788
- has_SD = (p.shut_down_var_index >= 0),
4789
- grid = p.grid;
4800
+ has_SD = (p.shut_down_var_index >= 0);
4790
4801
  // Clear all start-ups and shut-downs at t >= bb.
4791
4802
  if(has_SU) p.resetStartUps(bb);
4792
4803
  if(has_SD) p.resetShutDowns(bb);
@@ -5193,11 +5204,11 @@ class VirtualMachine {
5193
5204
  MODEL.products_with_negative_delays = {};
5194
5205
  b = bb;
5195
5206
  for(let i = 0; i < cbl; i++) {
5196
- // NOTE: Issues with cost price calculation beyond simulation
5197
- // period need not be reported.
5198
5207
  if(b <= this.nr_of_time_steps && !MODEL.calculateCostPrices(b)) {
5199
- this.logMessage(block, `${this.WARNING}(t=${b}) ` +
5200
- 'Invalid cost prices due to negative flow(s)');
5208
+ // NOTE: Issues with cost price calculation beyond simulation
5209
+ // period need not be reported unless model is set to ignore this.
5210
+ if(!MODEL.ignore_negative_flows) this.logMessage(block,
5211
+ `${this.WARNING}(t=${b}) Invalid cost prices due to negative flow(s)`);
5201
5212
  }
5202
5213
  // Move on to the next time step of the block.
5203
5214
  b++;
@@ -5695,7 +5706,6 @@ class VirtualMachine {
5695
5706
  return ` +${c} ${v}`; // Prefix coefficient with +
5696
5707
  // NOTE: This may return +0 X001.
5697
5708
  };
5698
-
5699
5709
  this.numeric_issue = '';
5700
5710
  // First add the objective (always MAXimize).
5701
5711
  if(cplex) {
@@ -6159,7 +6169,9 @@ class VirtualMachine {
6159
6169
  }
6160
6170
 
6161
6171
  stopSolving() {
6172
+ // Wrap-up after solving is completed or aborted.
6162
6173
  this.stopTimer();
6174
+ // Stop rotating the Linny-R icon, and update buttons.
6163
6175
  UI.stopSolving();
6164
6176
  }
6165
6177
 
@@ -6311,7 +6323,7 @@ Solver status = ${json.status}`);
6311
6323
  }
6312
6324
  // If negative delays require "fixating" variables for some number
6313
6325
  // of time steps, this must be logged in the monitor.
6314
- const keys = Object.keys(this.variables_to_fixate);
6326
+ let keys = Object.keys(this.variables_to_fixate);
6315
6327
  if(keys.length) {
6316
6328
  const msg = ['NOTE: Due to negative link delays, levels for ' +
6317
6329
  pluralS(keys.length, 'variable') + ' are pre-set:'];
@@ -6340,6 +6352,27 @@ Solver status = ${json.status}`);
6340
6352
  }
6341
6353
  this.logMessage(this.block_count, msg.join('\n'));
6342
6354
  }
6355
+ // Convert bound issues to warnings in the Monitor.
6356
+ keys = Object.keys(this.bound_issues).sort();
6357
+ const n = keys.length;
6358
+ if(n) {
6359
+ let vlist = '',
6360
+ first = 1e20;
6361
+ for(let i = 0; i < n; i++) {
6362
+ const
6363
+ k = keys[i],
6364
+ bit = this.bound_issues[k];
6365
+ vlist += `\n - ${k} (t=${listToRange(bit)})`;
6366
+ first = Math.min(first, bit[0]);
6367
+ }
6368
+ const msg = `Lower bound exceeds upper bound for ${n} processes`;
6369
+ this.logMessage(this.block_count,
6370
+ `${this.WARNING}(t=${first}) ${msg}:${vlist}`);
6371
+ UI.warn(msg + ' - check Monitor for details');
6372
+ // Clear bound issue dictionary, so next block starts anew.
6373
+ this.bound_issues = {};
6374
+ }
6375
+ // Create the input file for the solver.
6343
6376
  this.logMessage(this.block_count,
6344
6377
  'Creating model for block #' + this.blockWithRound);
6345
6378
  this.cbl = CONFIGURATION.progress_needle_interval * 200;
@@ -7580,6 +7613,108 @@ function VMI_ge(x) {
7580
7613
  }
7581
7614
  }
7582
7615
 
7616
+ function VMI_at(x) {
7617
+ // Pop the top number on the stack, and use its integer part as index i
7618
+ // to replace the new top element (which must be a dataset or a grouping)
7619
+ // by its i-th element.
7620
+ let d = x.pop();
7621
+ if(d !== false) {
7622
+ if(DEBUGGING) console.log('AT (' + d.join(', ') + ')');
7623
+ let a,
7624
+ from = false,
7625
+ to = false,
7626
+ step = 1,
7627
+ group = false,
7628
+ period = false,
7629
+ range = [],
7630
+ ok = true;
7631
+ // Check whether the first argument (d[0]) is indexable.
7632
+ if(d[0] instanceof Array) {
7633
+ a = d[0];
7634
+ group = true;
7635
+ } else if(d[0].entity instanceof Dataset) {
7636
+ a = d[0].entity.vector;
7637
+ period = d[0].periodic;
7638
+ } else {
7639
+ x.retop(VM.ARRAY_INDEX);
7640
+ return;
7641
+ }
7642
+ // Check whether the second argument (d[1]) is a number or a pair.
7643
+ if(d[1] instanceof Array) {
7644
+ if(d[1].length > 3 || typeof d[1][0] !== 'number') {
7645
+ ok = false;
7646
+ } else if(d[1].length === 3) {
7647
+ // Optional third index argument is range index increment.
7648
+ if(typeof d[1][2] === 'number') {
7649
+ step = Math.floor(d[1][2]);
7650
+ // Ignore increment if it truncates to zero.
7651
+ if(!step) step = 1;
7652
+ // Get the range end.
7653
+ if(typeof d[1][1] === 'number') {
7654
+ to = Math.floor(d[1][1]);
7655
+ } else {
7656
+ ok = false;
7657
+ }
7658
+ } else {
7659
+ ok = false;
7660
+ }
7661
+ } else if(d[1].length === 2) {
7662
+ // Optional second argument is range index end.
7663
+ if(typeof d[1][1] === 'number') {
7664
+ to = Math.floor(d[1][1]);
7665
+ } else {
7666
+ ok = false;
7667
+ }
7668
+ }
7669
+ if(ok) {
7670
+ from = Math.floor(d[1][0]);
7671
+ // Groupings are 0-based arrays but indexed as 1-based.
7672
+ if(group) {
7673
+ from--;
7674
+ to--;
7675
+ }
7676
+ // Check whether from, to and step are feasible.
7677
+ if(to !== false) {
7678
+ if(to <= from && step < 0) {
7679
+ for(let i = from; i >= to; i += step) range.push(i);
7680
+ } else if(to >= from && step > 0) {
7681
+ for(let i = from; i <= to; i += step) range.push(i);
7682
+ } else {
7683
+ ok = false;
7684
+ }
7685
+ }
7686
+ }
7687
+ }
7688
+ if(ok && !range.length && typeof d[1] === 'number') {
7689
+ range = [Math.floor(d[1]) - (group ? 1 : 0)];
7690
+ } else if(!range.length) {
7691
+ ok = false;
7692
+ }
7693
+ if(!ok) {
7694
+ x.retop(VM.ARRAY_INDEX);
7695
+ return;
7696
+ }
7697
+ const
7698
+ n = range.length,
7699
+ r = [];
7700
+ for(let i = 0; i < n; i++) {
7701
+ const index = range[i];
7702
+ if(index < 0) {
7703
+ r.push(VM.UNDEFINED);
7704
+ } else if(period) {
7705
+ r.push(a[index % a.length]);
7706
+ } else {
7707
+ r.push(a[index]);
7708
+ }
7709
+ }
7710
+ if(n === 1) {
7711
+ x.retop(r[0]);
7712
+ } else {
7713
+ x.retop(r);
7714
+ }
7715
+ }
7716
+ }
7717
+
7583
7718
  function VMI_add(x) {
7584
7719
  // Pop the top number on the stack, and add it to the new top number.
7585
7720
  const d = x.pop();
@@ -8216,6 +8351,13 @@ function VMI_set_bounds(args) {
8216
8351
  console.log(['set_bounds [', k, '] ', vbl.displayName, '[',
8217
8352
  VM.variables[vi - 1][0],'] t = ', VM.t, ' LB = ', VM.sig4Dig(l),
8218
8353
  ', UB = ', VM.sig4Dig(u), fixed].join(''), l, u, inf_val);
8354
+ } else if(u < l) {
8355
+ // Warn that "impossible" bounds would have been set...
8356
+ const vk = vbl.displayName;
8357
+ if(!VM.bound_issues[vk]) VM.bound_issues[vk] = [];
8358
+ VM.bound_issues[vk].push(VM.t);
8359
+ // ... and set LB to UB, so that lowest value is bounding.
8360
+ l = u;
8219
8361
  }
8220
8362
  // NOTE: Since the VM vectors for lower bounds and upper bounds are
8221
8363
  // initialized with default values (0 for LB, +INF for UB), the bounds
@@ -9269,7 +9411,7 @@ function VMI_add_available_capacity(link) {
9269
9411
  const
9270
9412
  // Valid symbols in expressions
9271
9413
  PARENTHESES = '()',
9272
- OPERATOR_CHARS = ';?:+-*/%=!<>^|',
9414
+ OPERATOR_CHARS = ';?:+-*/%=!<>^|@',
9273
9415
  // Opening bracket, space and single quote indicate a separation
9274
9416
  SEPARATOR_CHARS = PARENTHESES + OPERATOR_CHARS + "[ '",
9275
9417
  COMPOUND_OPERATORS = ['!=', '<>', '>=', '<='],
@@ -9301,13 +9443,13 @@ const
9301
9443
  VMI_weibull, VMI_npv],
9302
9444
  DYADIC_OPERATORS = [
9303
9445
  ';', '?', ':', 'or', 'and',
9304
- '=', '<>', '!=',
9305
- '>', '<', '>=', '<=', '+', '-', '*', '/',
9446
+ '=', '<>', '!=', '>', '<', '>=', '<=',
9447
+ '@', '+', '-', '*', '/',
9306
9448
  '%', '^', 'log', '|'],
9307
9449
  DYADIC_CODES = [
9308
9450
  VMI_concat, VMI_if_then, VMI_if_else, VMI_or, VMI_and,
9309
9451
  VMI_eq, VMI_ne, VMI_ne, VMI_gt, VMI_lt, VMI_ge, VMI_le,
9310
- VMI_add, VMI_sub, VMI_mul, VMI_div, VMI_mod,
9452
+ VMI_at, VMI_add, VMI_sub, VMI_mul, VMI_div, VMI_mod,
9311
9453
  VMI_power, VMI_log, VMI_replace_undefined],
9312
9454
 
9313
9455
  // Compiler checks for random codes as they make an expression dynamic
@@ -9315,7 +9457,7 @@ const
9315
9457
  VMI_triangular, VMI_weibull],
9316
9458
 
9317
9459
  // Compiler checks for reducing codes to unset its "concatenating" flag
9318
- REDUCING_CODES = [VMI_min, VMI_max, VMI_binomial, VMI_normal,
9460
+ REDUCING_CODES = [VMI_at, VMI_min, VMI_max, VMI_binomial, VMI_normal,
9319
9461
  VMI_triangular, VMI_weibull, VMI_npv],
9320
9462
 
9321
9463
  // Custom operators may make an expression level-based
@@ -9323,7 +9465,7 @@ const
9323
9465
 
9324
9466
  OPERATORS = DYADIC_OPERATORS.concat(MONADIC_OPERATORS),
9325
9467
  OPERATOR_CODES = DYADIC_CODES.concat(MONADIC_CODES),
9326
- PRIORITIES = [1, 2, 2, 3, 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 7, 7, 7, 8, 8, 10,
9468
+ PRIORITIES = [1, 2, 2, 3, 4, 5, 5, 5, 5, 5, 5, 5, 5.5, 6, 6, 7, 7, 7, 8, 8, 10,
9327
9469
  9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9],
9328
9470
  ACTUAL_SYMBOLS = CONSTANT_SYMBOLS.concat(OPERATORS),
9329
9471
  SYMBOL_CODES = CONSTANT_CODES.concat(OPERATOR_CODES);