linny-r 1.7.4 → 1.8.1
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 +10 -10
- package/package.json +1 -1
- package/static/index.html +23 -16
- package/static/linny-r.css +11 -1
- package/static/scripts/linny-r-ctrl.js +2 -0
- package/static/scripts/linny-r-gui-controller.js +177 -123
- package/static/scripts/linny-r-gui-documentation-manager.js +1 -1
- package/static/scripts/linny-r-gui-expression-editor.js +10 -10
- package/static/scripts/linny-r-gui-paper.js +13 -6
- package/static/scripts/linny-r-milp.js +55 -17
- package/static/scripts/linny-r-model.js +7 -1
- package/static/scripts/linny-r-vm.js +102 -37
@@ -283,7 +283,7 @@ class DocumentationManager {
|
|
283
283
|
if(list.indexOf(this.entity) >= 0) {
|
284
284
|
this.stopEditing();
|
285
285
|
this.entity = null;
|
286
|
-
this.title.innerHTML = '
|
286
|
+
this.title.innerHTML = 'Information and documentation';
|
287
287
|
this.viewer.innerHTML = this.about_linny_r;
|
288
288
|
}
|
289
289
|
}
|
@@ -74,16 +74,16 @@ Attributes, however, are case sensitive!">[Actor X|CF]</code> for cash flow.
|
|
74
74
|
<code title="Number of last round in the sequence (1=a, 2=b, etc.)">lr</code>,
|
75
75
|
<code title="Number of rounds in the sequence">nr</code>,
|
76
76
|
<code title="Number of current experiment run (starts at 0)">x</code>,
|
77
|
-
<code title="Number of runs in the experiment">nx</code>,
|
78
|
-
<span title="Index variables of iterator dimensions
|
77
|
+
<code title="Number of runs in the current experiment">nx</code>,
|
78
|
+
<span title="Index variables of iterator dimensions">
|
79
79
|
<code>i</code>, <code>j</code>, <code>k</code>,
|
80
80
|
</span>
|
81
|
-
<code title="Number of time steps in 1 year
|
82
|
-
<code title="Number of time steps in 1 week
|
83
|
-
<code title="Number of time steps in 1 day
|
84
|
-
<code title="Number of time steps in 1 hour
|
85
|
-
<code title="Number of time steps in 1 minute
|
86
|
-
<code title="Number of time steps in 1 second
|
81
|
+
<code title="Number of time steps in 1 year">yr</code>,
|
82
|
+
<code title="Number of time steps in 1 week">wk</code>,
|
83
|
+
<code title="Number of time steps in 1 day">d</code>,
|
84
|
+
<code title="Number of time steps in 1 hour">h</code>,
|
85
|
+
<code title="Number of time steps in 1 minute">m</code>,
|
86
|
+
<code title="Number of time steps in 1 second">s</code>,
|
87
87
|
<code title="A random number from the uniform distribution U(0, 1)">random</code>),
|
88
88
|
constants (<code title="Mathematical constant π = ${Math.PI}">pi</code>,
|
89
89
|
<code title="Logical constant true = 1
|
@@ -178,8 +178,8 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
178
178
|
}
|
179
179
|
|
180
180
|
editExpression(event) {
|
181
|
-
// Infer which entity property expression is to edited from the
|
182
|
-
// that was clicked, and then opens the dialog.
|
181
|
+
// Infer which entity property expression is to be edited from the
|
182
|
+
// button that was clicked, and then opens the dialog.
|
183
183
|
const
|
184
184
|
btn = event.target,
|
185
185
|
ids = btn.id.split('-'), // 3-tuple [entity type, attribute, 'x']
|
@@ -300,9 +300,9 @@ class Paper {
|
|
300
300
|
at_process_ub_arrow: '#f0b0e8',
|
301
301
|
// NOTE: special color when level at negative lower bound
|
302
302
|
at_process_neg_lb: '#800050',
|
303
|
-
// Process with unbound level
|
304
|
-
|
305
|
-
|
303
|
+
// Process with unbound level: +INF marine-blue, -INF maroon-red
|
304
|
+
plus_infinite_level: '#1000a0',
|
305
|
+
minus_infinite_level: '#a00010',
|
306
306
|
// Process state change symbols are displayed in red
|
307
307
|
switch_on_off: '#b00000',
|
308
308
|
// Compound arrows with non-zero actual flow are displayed in red-purple
|
@@ -1979,9 +1979,16 @@ class Paper {
|
|
1979
1979
|
if(MODEL.solved && !ignored) {
|
1980
1980
|
if(l === VM.PLUS_INFINITY) {
|
1981
1981
|
// Infinite level => unbounded solution
|
1982
|
-
stroke_color = this.palette.
|
1983
|
-
fill_color = this.palette.
|
1984
|
-
lrect_color = this.palette.
|
1982
|
+
stroke_color = this.palette.plus_infinite_level;
|
1983
|
+
fill_color = this.palette.above_upper_bound;
|
1984
|
+
lrect_color = this.palette.plus_infinite_level;
|
1985
|
+
font_color = 'white';
|
1986
|
+
stroke_width = 2;
|
1987
|
+
} else if(l === VM.MINUS_INFINITY) {
|
1988
|
+
// Infinite level => unbounded solution
|
1989
|
+
stroke_color = this.palette.minus_infinite_level;
|
1990
|
+
fill_color = this.palette.below_lower_bound;
|
1991
|
+
lrect_color = this.palette.minus_infinite_level;
|
1985
1992
|
font_color = 'white';
|
1986
1993
|
stroke_width = 2;
|
1987
1994
|
} else if(l > ub - VM.SIG_DIF_FROM_ZERO ||
|
@@ -612,15 +612,21 @@ module.exports = class MILPSolver {
|
|
612
612
|
json = fs.readFileSync(s.solution, 'utf8').trim(),
|
613
613
|
sol = JSON.parse(json);
|
614
614
|
result.seconds = sol.SolutionInfo.Runtime;
|
615
|
+
let status = sol.SolutionInfo.Status;
|
615
616
|
// NOTE: Status = 2 indicates success!
|
616
|
-
if(
|
617
|
-
|
618
|
-
|
617
|
+
if(status !== 2) {
|
618
|
+
let msg = s.statusMessage(status);
|
619
|
+
if(msg) {
|
620
|
+
// If solver exited with known status code, report message.
|
621
|
+
result.status = status;
|
622
|
+
result.solution = s.usableSolution(status);
|
623
|
+
result.error = msg;
|
624
|
+
}
|
619
625
|
if(!result.error) result.error = 'Unknown solver error';
|
620
626
|
console.log(`Solver status: ${result.status} - ${result.error}`);
|
621
627
|
}
|
622
628
|
// Objective value.
|
623
|
-
result.obj = sol.SolutionInfo.ObjVal;
|
629
|
+
result.obj = sol.SolutionInfo.ObjVal || 0;
|
624
630
|
// Values of solution vector.
|
625
631
|
if(sol.Vars) {
|
626
632
|
// Fill dictionary with variable name: value entries.
|
@@ -676,21 +682,29 @@ module.exports = class MILPSolver {
|
|
676
682
|
if(result.status.indexOf('OPTIMAL') >= 0) {
|
677
683
|
result.status = 0;
|
678
684
|
result.error = '';
|
685
|
+
} else if(result.status.indexOf('DUAL_INFEASIBLE') >= 0) {
|
686
|
+
result.error = 'Problem is unbounded';
|
687
|
+
solved = false;
|
688
|
+
} else if(result.status.indexOf('INFEASIBLE') >= 0) {
|
689
|
+
result.error = 'Problem is infeasible';
|
690
|
+
solved = false;
|
679
691
|
}
|
680
|
-
|
681
|
-
i
|
682
|
-
|
683
|
-
// Fill dictionary with variable name: value entries.
|
684
|
-
while(i < output.length) {
|
685
|
-
const m = output[i].match(/^\d+\s+X(\d+)\s+SB\s+([^\s]+)\s+/);
|
686
|
-
if(m !== null) {
|
687
|
-
const vn = 'X' + m[1].padStart(7, '0');
|
688
|
-
x_dict[vn] = parseFloat(m[2]);
|
692
|
+
if(solved) {
|
693
|
+
while(i < output.length && output[i].indexOf('VARIABLES') < 0) {
|
694
|
+
i++;
|
689
695
|
}
|
690
|
-
|
696
|
+
// Fill dictionary with variable name: value entries.
|
697
|
+
while(i < output.length) {
|
698
|
+
const m = output[i].match(/^\d+\s+X(\d+)\s+\w\w\s+([^\s]+)\s+/);
|
699
|
+
if(m !== null) {
|
700
|
+
const vn = 'X' + m[1].padStart(7, '0');
|
701
|
+
x_dict[vn] = parseFloat(m[2]);
|
702
|
+
}
|
703
|
+
i++;
|
704
|
+
}
|
705
|
+
// Fill the solution vector, adding 0 for missing columns.
|
706
|
+
getValuesFromDict();
|
691
707
|
}
|
692
|
-
// Fill the solution vector, adding 0 for missing columns.
|
693
|
-
getValuesFromDict();
|
694
708
|
} else {
|
695
709
|
console.log('No solution found');
|
696
710
|
}
|
@@ -698,6 +712,14 @@ module.exports = class MILPSolver {
|
|
698
712
|
result.seconds = 0;
|
699
713
|
const
|
700
714
|
no_license = (log.indexOf('No license found') >= 0),
|
715
|
+
// NOTE: Omit first letter U, I and P as they may be either in
|
716
|
+
// upper case or lower case.
|
717
|
+
unbounded = (log.indexOf('nbounded') >= 0),
|
718
|
+
infeasible = (log.indexOf('nfeasible') >= 0),
|
719
|
+
primal_unbounded = (log.indexOf('rimal unbounded') >= 0),
|
720
|
+
err = log.match(/CPLEX Error\s+(\d+):\s+(.+)\./),
|
721
|
+
err_nr = (err && err.length > 1 ? parseInt(err[1]) : 0),
|
722
|
+
err_msg = (err_nr ? err[2] : ''),
|
701
723
|
// NOTE: Solver reports time with 1/100 secs precision.
|
702
724
|
mst = log.match(/Solution time \=\s+(\d+\.\d+) sec/);
|
703
725
|
if(mst && mst.length > 1) result.seconds = parseFloat(mst[1]);
|
@@ -712,6 +734,15 @@ module.exports = class MILPSolver {
|
|
712
734
|
// Non-zero solver exit code indicates serious trouble.
|
713
735
|
result.error = 'CPLEX solver terminated with error';
|
714
736
|
result.status = -13;
|
737
|
+
} else if(err_nr) {
|
738
|
+
result.status = err_nr;
|
739
|
+
if(infeasible && !primal_unbounded) {
|
740
|
+
result.error = 'Problem is infeasible';
|
741
|
+
} else if(unbounded) {
|
742
|
+
result.error = 'Problem is unbounded';
|
743
|
+
} else {
|
744
|
+
result.error = err_msg;
|
745
|
+
}
|
715
746
|
} else {
|
716
747
|
try {
|
717
748
|
output = fs.readFileSync(s.solution, 'utf8').trim();
|
@@ -792,7 +823,14 @@ module.exports = class MILPSolver {
|
|
792
823
|
}
|
793
824
|
}
|
794
825
|
if(result.status) {
|
795
|
-
|
826
|
+
let msg = s.statusMessage(result.status);
|
827
|
+
if(msg) {
|
828
|
+
// If solver exited with known status code, report message.
|
829
|
+
result.solution = s.usableSolution(result.status);
|
830
|
+
result.error = msg;
|
831
|
+
}
|
832
|
+
if(!result.error) result.error = 'Unknown solver error';
|
833
|
+
console.log(`Solver status: ${result.status} - ${result.error}`);
|
796
834
|
}
|
797
835
|
} else if (m.startsWith('Solving Time')) {
|
798
836
|
result.seconds = parseFloat(m.split(':')[1]);
|
@@ -104,6 +104,7 @@ class LinnyRModel {
|
|
104
104
|
this.preferred_solver = ''; // empty string denotes "use default"
|
105
105
|
this.integer_tolerance = 5e-7; // integer feasibility tolerance
|
106
106
|
this.MIP_gap = 1e-4; // relative MIP gap
|
107
|
+
this.always_diagnose = false;
|
107
108
|
|
108
109
|
// Sensitivity-related properties
|
109
110
|
this.base_case_selectors = '';
|
@@ -2666,6 +2667,7 @@ class LinnyRModel {
|
|
2666
2667
|
this.infer_cost_prices = nodeParameterValue(node, 'cost-prices') === '1';
|
2667
2668
|
this.report_results = nodeParameterValue(node, 'report-results') === '1';
|
2668
2669
|
this.show_block_arrows = nodeParameterValue(node, 'block-arrows') === '1';
|
2670
|
+
this.always_diagnose = nodeParameterValue(node, 'diagnose') === '1';
|
2669
2671
|
this.name = xmlDecoded(nodeContentByTag(node, 'name'));
|
2670
2672
|
this.author = xmlDecoded(nodeContentByTag(node, 'author'));
|
2671
2673
|
this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
|
@@ -3014,6 +3016,7 @@ class LinnyRModel {
|
|
3014
3016
|
if(this.infer_cost_prices) p += ' cost-prices="1"';
|
3015
3017
|
if(this.report_results) p += ' report-results="1"';
|
3016
3018
|
if(this.show_block_arrows) p += ' block-arrows="1"';
|
3019
|
+
if(this.always_diagnose) p += ' diagnose="1"';
|
3017
3020
|
let xml = this.xml_header + ['<model', p, '><name>', xmlEncoded(this.name),
|
3018
3021
|
'</name><author>', xmlEncoded(this.author),
|
3019
3022
|
'</author><notes>', xmlEncoded(this.comments),
|
@@ -6302,7 +6305,9 @@ class Cluster extends NodeBox {
|
|
6302
6305
|
}
|
6303
6306
|
|
6304
6307
|
usesSlack(t, p, slack_type) {
|
6305
|
-
// Adds slack-using product `p` to slack info for this cluster
|
6308
|
+
// Adds slack-using product `p` to slack info for this cluster.
|
6309
|
+
// NOTE: When diagnosing an unbounded problem, `p` can also be a
|
6310
|
+
// process with an infinite level.
|
6306
6311
|
let s;
|
6307
6312
|
if(t in this.slack_info) {
|
6308
6313
|
s = this.slack_info[t];
|
@@ -6311,6 +6316,7 @@ class Cluster extends NodeBox {
|
|
6311
6316
|
this.slack_info[t] = s;
|
6312
6317
|
}
|
6313
6318
|
addDistinct(p, s[slack_type]);
|
6319
|
+
// NOTE: Recursive call to let the slack use info "bubble up".
|
6314
6320
|
if(this.cluster) this.cluster.usesSlack(t, p, slack_type);
|
6315
6321
|
}
|
6316
6322
|
|
@@ -2078,6 +2078,15 @@ class VirtualMachine {
|
|
2078
2078
|
// so far, type is always HI (highest increment); object can be
|
2079
2079
|
// a process or a product.
|
2080
2080
|
this.chunk_variables = [];
|
2081
|
+
// NOTE: As of version 1.8.0, diagnosis is performed only when the
|
2082
|
+
// modeler Alt-clicks the "run" button or clicks the link in the
|
2083
|
+
// infoline warning that is displayed when the solver reports that a
|
2084
|
+
// block poses a problem that is infeasible (too tight constraints)
|
2085
|
+
// or unbounded (no upper limit on some processes). Diagnosis is
|
2086
|
+
// implemented by adding slack and setting finite bounds on processes
|
2087
|
+
// and then make a second attempt to solve the block.
|
2088
|
+
this.diagnose = false;
|
2089
|
+
this.prompt_to_diagnose = false;
|
2081
2090
|
// Array for VM instructions.
|
2082
2091
|
this.code = [];
|
2083
2092
|
// The Simplex tableau: matrix, rhs and ct will have same length.
|
@@ -2112,17 +2121,23 @@ class VirtualMachine {
|
|
2112
2121
|
// Floating-point constants used in calculations
|
2113
2122
|
// Meaningful solver results are assumed to lie wihin reasonable bounds.
|
2114
2123
|
// Extreme absolute values (10^25 and above) are used to signal particular
|
2115
|
-
// outcomes. This 10^25 limit is used because the
|
2116
|
-
// LP_solve considers a problem to be unbounded if
|
2117
|
-
// reach +INF (1e+30) or -INF (-1e+30), and a solution
|
2118
|
-
// extreme values get too close to +/-INF. The higher
|
2119
|
-
// chosen arbitrarily.
|
2124
|
+
// outcomes. This 10^25 limit is used because the original MILP solver
|
2125
|
+
// used by Linny-R (LP_solve) considers a problem to be unbounded if
|
2126
|
+
// decision variables reach +INF (1e+30) or -INF (-1e+30), and a solution
|
2127
|
+
// inaccurate if extreme values get too close to +/-INF. The higher
|
2128
|
+
// values have been chosen arbitrarily.
|
2120
2129
|
this.PLUS_INFINITY = 1e+25;
|
2121
2130
|
this.MINUS_INFINITY = -1e+25;
|
2122
2131
|
this.BEYOND_PLUS_INFINITY = 1e+35;
|
2123
2132
|
this.BEYOND_MINUS_INFINITY = -1e+35;
|
2133
|
+
// The 1e+30 value is recognized by all supported solvers as "infinity",
|
2134
|
+
// and hence can be used to indicate that a variable has no upper bound.
|
2124
2135
|
this.SOLVER_PLUS_INFINITY = 1e+30;
|
2125
2136
|
this.SOLVER_MINUS_INFINITY = -1e+30;
|
2137
|
+
// As of version 1.8.0, Linny-R imposes no +INF bounds on processes
|
2138
|
+
// unless diagnosing an unbounded problem. For such diagnosis, the
|
2139
|
+
// (relatively) low value 9.99999999e+9 is used.
|
2140
|
+
this.DIAGNOSIS_UPPER_BOUND = 9.99999999e+9;
|
2126
2141
|
// NOTE: Below the "near zero" limit, a number is considered zero
|
2127
2142
|
// (this is to timely detect division-by-zero errors).
|
2128
2143
|
this.NEAR_ZERO = 1e-10;
|
@@ -2345,17 +2360,6 @@ class VirtualMachine {
|
|
2345
2360
|
selectSolver(id) {
|
2346
2361
|
if(id in this.solver_names) {
|
2347
2362
|
this.solver_id = id;
|
2348
|
-
/*
|
2349
|
-
if(id === 'mosek') {
|
2350
|
-
this.PLUS_INFINITY = 1e+6;
|
2351
|
-
this.MINUS_INFINITY = -1e+6;
|
2352
|
-
this.MAX_SLACK_PENALTY = 1e+6;
|
2353
|
-
} else {
|
2354
|
-
this.PLUS_INFINITY = 1e+25;
|
2355
|
-
this.PLUS_INFINITY = -1e+25;
|
2356
|
-
this.MAX_SLACK_PENALTY = 1e+24;
|
2357
|
-
}
|
2358
|
-
*/
|
2359
2363
|
} else {
|
2360
2364
|
UI.alert(`Invalid solver ID "${id}"`);
|
2361
2365
|
}
|
@@ -2996,7 +3000,7 @@ class VirtualMachine {
|
|
2996
3000
|
// to respect certain constraints. This may result in infeasible
|
2997
3001
|
// MILP problems. The solver will report this, but provide no
|
2998
3002
|
// clue as to which constraints may be critical.
|
2999
|
-
if(p instanceof Product && !p.no_slack) {
|
3003
|
+
if(p instanceof Product && this.diagnose && !p.no_slack) {
|
3000
3004
|
p.stock_LE_slack_var_index = this.addVariable('LE', p);
|
3001
3005
|
p.stock_GE_slack_var_index = this.addVariable('GE', p);
|
3002
3006
|
}
|
@@ -3125,7 +3129,7 @@ class VirtualMachine {
|
|
3125
3129
|
);
|
3126
3130
|
// ... or if P is neither source nor sink.
|
3127
3131
|
} else if(p.equal_bounds && notsrc && notsnk) {
|
3128
|
-
if(!p.no_slack) {
|
3132
|
+
if(this.diagnose && !p.no_slack) {
|
3129
3133
|
// NOTE: For EQ, both slack variables should be used, having
|
3130
3134
|
// respectively -1 and +1 as coefficients.
|
3131
3135
|
this.code.push(
|
@@ -3141,7 +3145,7 @@ class VirtualMachine {
|
|
3141
3145
|
} else {
|
3142
3146
|
// Add lower bound (GE) constraint unless product is a source node.
|
3143
3147
|
if(notsrc) {
|
3144
|
-
if(!p.no_slack) {
|
3148
|
+
if(this.diagnose && !p.no_slack) {
|
3145
3149
|
// Add the GE slack index with coefficient +1 (so it can
|
3146
3150
|
// INcrease the left-hand side of the equation)
|
3147
3151
|
this.code.push([VMI_add_const_to_coefficient, [gesvi, 1]]);
|
@@ -3154,7 +3158,7 @@ class VirtualMachine {
|
|
3154
3158
|
}
|
3155
3159
|
// Add upper bound (LE) constraint unless product is a sink node
|
3156
3160
|
if(notsnk) {
|
3157
|
-
if(!p.no_slack) {
|
3161
|
+
if(this.diagnose && !p.no_slack) {
|
3158
3162
|
// Add the stock LE index with coefficient -1 (so it can
|
3159
3163
|
// DEcrease the LHS).
|
3160
3164
|
this.code.push([VMI_add_const_to_coefficient, [lesvi, -1]]);
|
@@ -3189,6 +3193,14 @@ class VirtualMachine {
|
|
3189
3193
|
this.fixed_var_indices.push([]);
|
3190
3194
|
}
|
3191
3195
|
|
3196
|
+
// Log if run is performed in "diagnosis" mode.
|
3197
|
+
if(this.diagnose) {
|
3198
|
+
this.logMessage(this.block_count, 'DIAGNOSTIC RUN' +
|
3199
|
+
(MODEL.always_diagnose ? ' (default -- see model settings)': '') +
|
3200
|
+
'\n- slack variables on products and constraints' +
|
3201
|
+
'\n- finite bounds on all processes');
|
3202
|
+
}
|
3203
|
+
|
3192
3204
|
// Just in case: re-determine which entities can be ignored.
|
3193
3205
|
MODEL.inferIgnoredEntities();
|
3194
3206
|
const n = Object.keys(MODEL.ignored_entities).length;
|
@@ -3254,7 +3266,7 @@ class VirtualMachine {
|
|
3254
3266
|
// solver does not support special ordered sets).
|
3255
3267
|
// NOTE: `addVariable` will add as many as there are points!
|
3256
3268
|
bl.first_sos_var_index = this.addVariable('W1', bl);
|
3257
|
-
if(!c.no_slack) {
|
3269
|
+
if(this.diagnose && !c.no_slack) {
|
3258
3270
|
// Define the slack variable(s) for bound line constraints.
|
3259
3271
|
// NOTE: Category [2] means: highest slack penalty.
|
3260
3272
|
if(bl.type !== VM.GE) {
|
@@ -3375,7 +3387,7 @@ class VirtualMachine {
|
|
3375
3387
|
p = MODEL.products[k];
|
3376
3388
|
// Get index of variable that is constrained by LB and UB.
|
3377
3389
|
vi = p.level_var_index;
|
3378
|
-
if(p.no_slack) {
|
3390
|
+
if(p.no_slack || !this.diagnose) {
|
3379
3391
|
// If no slack, the bound constraints can be set on the
|
3380
3392
|
// variables themselves.
|
3381
3393
|
lbx = p.lower_bound;
|
@@ -3658,7 +3670,7 @@ class VirtualMachine {
|
|
3658
3670
|
k = product_keys[i];
|
3659
3671
|
if(!MODEL.ignored_entities[k]) {
|
3660
3672
|
p = MODEL.products[k];
|
3661
|
-
if(p.level_var_index >= 0 && !p.no_slack) {
|
3673
|
+
if(p.level_var_index >= 0 && !p.no_slack && this.diagnose) {
|
3662
3674
|
hb = p.hasBounds;
|
3663
3675
|
pen = (p.is_data ? 2 :
|
3664
3676
|
// NOTE: Lowest penalty also for IMPLIED sources and sinks.
|
@@ -4605,7 +4617,8 @@ class VirtualMachine {
|
|
4605
4617
|
// Compute the peak from the peak increase.
|
4606
4618
|
p.b_peak[block] = p.b_peak[block - 1] + p.b_peak_inc[block];
|
4607
4619
|
}
|
4608
|
-
// Add warning to messages if slack has been used
|
4620
|
+
// Add warning to messages if slack has been used, or some process
|
4621
|
+
// level is "infinite" while diagnosing an unbounded problem.
|
4609
4622
|
// NOTE: Only check after the last round has been evaluated.
|
4610
4623
|
if(round === this.lastRound) {
|
4611
4624
|
let b = bb;
|
@@ -4646,6 +4659,27 @@ class VirtualMachine {
|
|
4646
4659
|
}
|
4647
4660
|
}
|
4648
4661
|
}
|
4662
|
+
if(this.diagnose) {
|
4663
|
+
// Iterate over all processes, and set the "slack use" flag
|
4664
|
+
// for their cluster so that these clusters will be highlighted.
|
4665
|
+
for(let o in MODEL.processes) if(MODEL.processes.hasOwnProperty(o) &&
|
4666
|
+
!MODEL.ignored_entities[o]) {
|
4667
|
+
const
|
4668
|
+
p = MODEL.processes[o],
|
4669
|
+
l = p.level[b];
|
4670
|
+
if(l >= VM.PLUS_INFINITY) {
|
4671
|
+
this.logMessage(block,
|
4672
|
+
`${this.WARNING}(t=${b}${round}) ${p.displayName} has level +INF`);
|
4673
|
+
// NOTE: +INF is signalled in blue, just like use of LE slack.
|
4674
|
+
p.cluster.usesSlack(b, p, 'LE');
|
4675
|
+
} else if(l <= VM.MINUS_INFINITY) {
|
4676
|
+
this.logMessage(block,
|
4677
|
+
`${this.WARNING}(t=${b}${round}) ${p.displayName} has level -INF`);
|
4678
|
+
// NOTE: -INF is signalled in red, just like use of GE slack.
|
4679
|
+
p.cluster.usesSlack(b, p, 'GE');
|
4680
|
+
}
|
4681
|
+
}
|
4682
|
+
}
|
4649
4683
|
j += this.cols;
|
4650
4684
|
b++;
|
4651
4685
|
}
|
@@ -5878,6 +5912,9 @@ Solver status = ${json.status}`);
|
|
5878
5912
|
}
|
5879
5913
|
this.logMessage(bnr, errmsg);
|
5880
5914
|
UI.alert(errmsg);
|
5915
|
+
if(errmsg.indexOf('nfeasible') >= 0 || errmsg.indexOf('nbounded') >= 0) {
|
5916
|
+
this.prompt_to_diagnose = true;
|
5917
|
+
}
|
5881
5918
|
}
|
5882
5919
|
this.logMessage(bnr, msg);
|
5883
5920
|
this.equations[bnr - 1] = json.model;
|
@@ -5936,7 +5973,12 @@ Solver status = ${json.status}`);
|
|
5936
5973
|
RECEIVER.report();
|
5937
5974
|
}
|
5938
5975
|
// Warn modeler if any issues occurred.
|
5939
|
-
if(this.
|
5976
|
+
if(this.prompt_to_diagnose && !this.diagnose) {
|
5977
|
+
UI.warn('Model is infeasible or unbounded -- ' +
|
5978
|
+
'<strong>Alt</strong>-click on the <em>Run</em> button ' +
|
5979
|
+
'<img id="solve-btn" class="sgray" src="images/solve.png">' +
|
5980
|
+
' for diagnosis');
|
5981
|
+
} else if(this.block_issues) {
|
5940
5982
|
let msg = 'Issues occurred in ' +
|
5941
5983
|
pluralS(this.block_issues, 'block') +
|
5942
5984
|
' -- details can be viewed in the monitor';
|
@@ -6091,7 +6133,7 @@ Solver status = ${json.status}`);
|
|
6091
6133
|
this.solveBlocks();
|
6092
6134
|
}
|
6093
6135
|
|
6094
|
-
solveModel() {
|
6136
|
+
solveModel(diagnose=false) {
|
6095
6137
|
// Start the sequence of data loading, model translation, solving
|
6096
6138
|
// consecutive blocks, and finally calculating dependent variables.
|
6097
6139
|
// NOTE: Do this only if the model defines a MILP problem, i.e.,
|
@@ -6101,6 +6143,22 @@ Solver status = ${json.status}`);
|
|
6101
6143
|
UI.notify('Nothing to solve');
|
6102
6144
|
return;
|
6103
6145
|
}
|
6146
|
+
// Diagnosis (by adding slack variables and finite bounds on processes)
|
6147
|
+
// is activated when Alt-clicking the "run" button, or by clicking the
|
6148
|
+
// "clicke HERE to diagnose" link on the infoline.
|
6149
|
+
this.diagnose = diagnose || MODEL.always_diagnose;
|
6150
|
+
if(this.diagnose) {
|
6151
|
+
this.PLUS_INFINITY = this.DIAGNOSIS_UPPER_BOUND;
|
6152
|
+
this.MINUS_INFINITY = -this.DIAGNOSIS_UPPER_BOUND;
|
6153
|
+
console.log('DIAGNOSIS ON');
|
6154
|
+
} else {
|
6155
|
+
this.PLUS_INFINITY = 1e+25;
|
6156
|
+
this.MINUS_INFINITY = -1e+25;
|
6157
|
+
console.log('DIAGNOSIS OFF');
|
6158
|
+
}
|
6159
|
+
// The "propt to diagnose" flag is set when some block posed an
|
6160
|
+
// infeasible or unbounded problem.
|
6161
|
+
this.prompt_to_diagnose = false;
|
6104
6162
|
const n = MODEL.loading_datasets.length;
|
6105
6163
|
if(n > 0) {
|
6106
6164
|
// Still within reasonable time? (3 seconds per dataset)
|
@@ -7784,10 +7842,11 @@ function VMI_set_bounds(args) {
|
|
7784
7842
|
vbl = VM.variables[vi - 1][1],
|
7785
7843
|
k = VM.offset + vi,
|
7786
7844
|
r = VM.round_letters.indexOf(VM.round_sequence[VM.current_round]),
|
7787
|
-
//
|
7788
|
-
//
|
7789
|
-
|
7790
|
-
inf_val = (
|
7845
|
+
// When diagnosing an unbounded problem, use low value for INFINITY,
|
7846
|
+
// but the optional fourth parameter indicates whether the solver's
|
7847
|
+
// infinity values should override the diagnosis INFINITY.
|
7848
|
+
inf_val = (VM.diagnose && !(args.length > 3 && args[3]) ?
|
7849
|
+
VM.DIAGNOSIS_UPPER_BOUND : VM.SOLVER_PLUS_INFINITY);
|
7791
7850
|
let l,
|
7792
7851
|
u,
|
7793
7852
|
fixed = (vi in VM.fixed_var_indices[r - 1]);
|
@@ -7812,17 +7871,15 @@ function VMI_set_bounds(args) {
|
|
7812
7871
|
u = args[2];
|
7813
7872
|
if(u instanceof Expression) u = u.result(VM.t);
|
7814
7873
|
u = Math.min(u, VM.PLUS_INFINITY);
|
7815
|
-
if(
|
7816
|
-
|
7817
|
-
if(u === VM.PLUS_INFINITY) u = inf_val;
|
7818
|
-
}
|
7874
|
+
if(l === VM.MINUS_INFINITY) l = -inf_val;
|
7875
|
+
if(u === VM.PLUS_INFINITY) u = inf_val;
|
7819
7876
|
fixed = '';
|
7820
7877
|
}
|
7821
7878
|
// NOTE: To see in the console whether fixing across rounds works, insert
|
7822
7879
|
// "fixed !== '' || " before DEBUGGING below.
|
7823
7880
|
if(DEBUGGING) {
|
7824
7881
|
console.log(['set_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
|
7825
|
-
' LB = ', VM.sig4Dig(l), ', UB = ', VM.sig4Dig(u), fixed].join(''));
|
7882
|
+
' LB = ', VM.sig4Dig(l), ', UB = ', VM.sig4Dig(u), fixed, l, u, inf_val].join(''));
|
7826
7883
|
}
|
7827
7884
|
// NOTE: Since the VM vectors for lower bounds and upper bounds are
|
7828
7885
|
// initialized with default values (0 for LB, +INF for UB), there is
|
@@ -8327,7 +8384,15 @@ function VMI_add_constraint(ct) {
|
|
8327
8384
|
}
|
8328
8385
|
}
|
8329
8386
|
VM.matrix.push(row);
|
8330
|
-
VM.
|
8387
|
+
let rhs = VM.rhs;
|
8388
|
+
if(rhs >= VM.PLUS_INFINITY) {
|
8389
|
+
rhs = (VM.diagnose ? VM.DIAGNOSIS_UPPER_BOUND :
|
8390
|
+
VM.SOLVER_PLUS_INFINITY);
|
8391
|
+
} else if(rhs <= VM.MINUS_INFINITY) {
|
8392
|
+
rhs = (VM.diagnose ? -VM.DIAGNOSIS_UPPER_BOUND :
|
8393
|
+
VM.SOLVER_MINUS_INFINITY);
|
8394
|
+
}
|
8395
|
+
VM.right_hand_side.push(rhs);
|
8331
8396
|
VM.constraint_types.push(ct);
|
8332
8397
|
} else if(DEBUGGING) {
|
8333
8398
|
console.log('Constraint NOT added!');
|
@@ -8480,7 +8545,7 @@ function VMI_add_bound_line_constraint(args) {
|
|
8480
8545
|
for(let i = 0; i < w.length; i++) {
|
8481
8546
|
VM.coefficients[w[i]] = -y[i];
|
8482
8547
|
}
|
8483
|
-
if(!bl.constraint.no_slack) {
|
8548
|
+
if(VM.diagnose && !bl.constraint.no_slack) {
|
8484
8549
|
// Add coefficients for slack variables unless omitted.
|
8485
8550
|
if(bl.type != VM.LE) VM.coefficients[VM.offset + bl.GE_slack_var_index] = 1;
|
8486
8551
|
if(bl.type != VM.GE) VM.coefficients[VM.offset + bl.LE_slack_var_index] = -1;
|