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.
- package/README.md +6 -6
- package/package.json +32 -32
- package/server.js +27 -19
- package/static/images/icon.svg +23 -23
- package/static/index.html +6 -0
- package/static/linny-r.css +2 -2
- package/static/scripts/iro.min.js +7 -7
- package/static/scripts/linny-r-ctrl.js +40 -39
- package/static/scripts/linny-r-gui-constraint-editor.js +7 -3
- package/static/scripts/linny-r-gui-controller.js +19 -10
- package/static/scripts/linny-r-gui-finder.js +13 -0
- package/static/scripts/linny-r-gui-model-autosaver.js +2 -2
- package/static/scripts/linny-r-gui-monitor.js +1 -1
- package/static/scripts/linny-r-gui-paper.js +29 -28
- package/static/scripts/linny-r-model.js +47 -21
- package/static/scripts/linny-r-utils.js +33 -5
- package/static/scripts/linny-r-vm.js +182 -40
@@ -10,7 +10,7 @@ the Linny-R project.
|
|
10
10
|
*/
|
11
11
|
|
12
12
|
/*
|
13
|
-
Copyright (c) 2017-
|
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
|
-
//
|
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:
|
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
|
-
//
|
8638
|
-
//
|
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:
|
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
|
12973
|
-
// increasing slope, -1 if boundline is
|
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-
|
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
|
-
//
|
77
|
+
// Return numeric value of integer string, IGNORING decimals after
|
78
78
|
// point or comma.
|
79
|
-
// NOTE:
|
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
|
-
//
|
86
|
-
//
|
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-
|
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
|
-
//
|
448
|
-
//
|
449
|
-
|
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:
|
455
|
-
// analysis, the result is multiplied by 1 + delta
|
456
|
-
if(
|
457
|
-
// NOTE:
|
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:
|
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:
|
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
|
3208
|
-
// (
|
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
|
-
|
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
|
-
|
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
|
-
|
4767
|
-
|
4768
|
-
|
4769
|
-
|
4770
|
-
|
4771
|
-
|
4772
|
-
|
4773
|
-
|
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
|
-
|
5200
|
-
|
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
|
-
|
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);
|