linny-r 2.0.6 → 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 +1 -1
- package/static/linny-r.css +2 -2
- 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 +17 -10
- 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 +23 -19
- package/static/scripts/linny-r-utils.js +33 -5
- package/static/scripts/linny-r-vm.js +167 -26
package/README.md
CHANGED
@@ -36,8 +36,8 @@ Linny-R is developed as a JavaScript package, and requires that **Node.js**
|
|
36
36
|
is installed on your computer. This software can be downloaded from
|
37
37
|
<a href="https://nodejs.org" target="_blank">https://nodejs.org</a>.
|
38
38
|
Make sure that you choose the correct installer for your computer.
|
39
|
-
Linny-R is developed using the _current_ release. Presently (
|
40
|
-
this is
|
39
|
+
Linny-R is developed using the _current_ release. Presently (December 2024)
|
40
|
+
this is 23.3.0.
|
41
41
|
|
42
42
|
Run the installer and accept the default settings.
|
43
43
|
There is <u>**no**</u> need to install the optional _Tools for Native Modules_.
|
@@ -48,7 +48,7 @@ Verify the installation by typing:
|
|
48
48
|
|
49
49
|
``node --version``
|
50
50
|
|
51
|
-
The response should be the version number of Node.js, for example:
|
51
|
+
The response should be the version number of Node.js, for example: v23.3.0.
|
52
52
|
|
53
53
|
## Installing Linny-R
|
54
54
|
It is advisable to install Linny-R in a directory on your computer, **not**
|
@@ -300,9 +300,9 @@ The Linny-R GUI should show in your browser window, while in the CLI you
|
|
300
300
|
should see a long series of server log messages like:
|
301
301
|
|
302
302
|
<pre>
|
303
|
-
[2024-
|
304
|
-
[2024-
|
305
|
-
[2024-
|
303
|
+
[2024-12-01 22:55:17] Static file: /index.html
|
304
|
+
[2024-12-01 22:55:17] Static file: /scripts/iro.min.js
|
305
|
+
[2024-12-01 22:55:17] Static file: /images/open.png
|
306
306
|
... etc.
|
307
307
|
</pre>
|
308
308
|
|
package/package.json
CHANGED
package/static/linny-r.css
CHANGED
@@ -10,7 +10,7 @@ file that implements the graphical user interface for Linny-R.
|
|
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
|
@@ -822,7 +822,7 @@ div.checked.not-same-not-changed {
|
|
822
822
|
}
|
823
823
|
|
824
824
|
#restore-dlg {
|
825
|
-
width:
|
825
|
+
width: 450px;
|
826
826
|
max-height: 219px;
|
827
827
|
}
|
828
828
|
|
@@ -875,9 +875,9 @@ class ChartManager {
|
|
875
875
|
// CLASS SensitivityAnalysis provides the sensitivity analysis functionality
|
876
876
|
class SensitivityAnalysis {
|
877
877
|
constructor() {
|
878
|
-
// Initialize main dialog properties
|
878
|
+
// Initialize main dialog properties.
|
879
879
|
this.reset();
|
880
|
-
// Sensitivity analysis creates & disposes an experiment and a chart
|
880
|
+
// Sensitivity analysis creates & disposes an experiment and a chart.
|
881
881
|
this.experiment_title = '___SENSITIVITY_ANALYSIS___';
|
882
882
|
this.chart_title = '___SENSITIVITY_ANALYSIS_CHART___';
|
883
883
|
}
|
@@ -912,7 +912,7 @@ class SensitivityAnalysis {
|
|
912
912
|
// a dummy chart is created that includes all these outcomes as *chart*
|
913
913
|
// variables.
|
914
914
|
if(!this.experiment) {
|
915
|
-
// Clear results from previous analysis
|
915
|
+
// Clear results from previous analysis.
|
916
916
|
this.clearResults();
|
917
917
|
this.parameters = [];
|
918
918
|
for(let i = 0; i < MODEL.sensitivity_parameters.length; i++) {
|
@@ -924,7 +924,7 @@ class SensitivityAnalysis {
|
|
924
924
|
if(oax) {
|
925
925
|
this.parameters.push(oax);
|
926
926
|
} else if(vn.length === 1 && obj instanceof Dataset) {
|
927
|
-
// Dataset without selector => push the dataset vector
|
927
|
+
// Dataset without selector => push the dataset vector.
|
928
928
|
this.parameters.push(obj.vector);
|
929
929
|
} else {
|
930
930
|
UI.alert(`Parameter ${p} is not a dataset or expression`);
|
@@ -938,23 +938,24 @@ class SensitivityAnalysis {
|
|
938
938
|
this.experiment = new Experiment(this.experiment_title);
|
939
939
|
this.experiment.charts = [this.chart];
|
940
940
|
this.experiment.inferVariables();
|
941
|
-
// This experiment always uses the same combination: the base selectors
|
941
|
+
// This experiment always uses the same combination: the base selectors.
|
942
942
|
const bs = MODEL.base_case_selectors.split(' ');
|
943
943
|
this.experiment.combinations = [];
|
944
|
-
// Add this combination N+1 times for N parameters
|
944
|
+
// Add this combination N+1 times for N parameters.
|
945
945
|
for(let i = 0; i <= this.parameters.length; i++) {
|
946
946
|
this.experiment.combinations.push(bs);
|
947
947
|
}
|
948
|
-
// NOTE:
|
948
|
+
// NOTE: Model settings will not be changed, but will be restored after
|
949
|
+
// each run => store the original settings.
|
949
950
|
this.experiment.original_model_settings = MODEL.settingsString;
|
950
951
|
this.experiment.original_round_sequence = MODEL.round_sequence;
|
951
952
|
}
|
952
|
-
// Change the button (GUI only -- console will return FALSE)
|
953
|
+
// Change the button (GUI only -- console will return FALSE).
|
953
954
|
const paused = this.resumeButtons();
|
954
955
|
if(!paused) {
|
955
956
|
this.experiment.time_started = new Date().getTime();
|
956
957
|
this.experiment.active_combination_index = 0;
|
957
|
-
// NOTE:
|
958
|
+
// NOTE: Start with base case run, hence no active parameter yet.
|
958
959
|
MODEL.running_experiment = this.experiment;
|
959
960
|
}
|
960
961
|
// Let the experiment manager do the work!!
|
@@ -962,31 +963,31 @@ class SensitivityAnalysis {
|
|
962
963
|
}
|
963
964
|
|
964
965
|
processRestOfRun() {
|
965
|
-
// This method is called by the experiment manager after
|
966
|
+
// This method is called by the experiment manager after a SA run.
|
966
967
|
const x = MODEL.running_experiment;
|
967
968
|
if(!x) return;
|
968
|
-
// Double-check that indeed the SA experiment is running
|
969
|
+
// Double-check that indeed the SA experiment is running.
|
969
970
|
if(x !== this.experiment) {
|
970
971
|
UI.alert('ERROR: Expected SA experiment run, but got ' + x.title);
|
971
972
|
return;
|
972
973
|
}
|
973
974
|
const aci = x.active_combination_index;
|
974
|
-
// Always add solver messages
|
975
|
+
// Always add solver messages.
|
975
976
|
x.runs[aci].addMessages();
|
976
|
-
// NOTE:
|
977
|
-
// loading , as the actual experiment is not stored
|
977
|
+
// NOTE: Use a "dummy experiment object" to ensure proper XML saving and
|
978
|
+
// loading , as the actual experiment is not stored.
|
978
979
|
x.runs.experiment = {title: SENSITIVITY_ANALYSIS.experiment_title};
|
979
|
-
// Add run to the sensitivity analysis
|
980
|
+
// Add run to the sensitivity analysis.
|
980
981
|
MODEL.sensitivity_runs.push(x.runs[aci]);
|
981
982
|
this.showProgress('Run #' + aci);
|
982
|
-
// See if more runs should be done
|
983
|
+
// See if more runs should be done.
|
983
984
|
const n = x.combinations.length;
|
984
985
|
if(!VM.halted && aci < n - 1) {
|
985
986
|
if(this.must_pause) {
|
986
987
|
this.pausedButtons(aci);
|
987
988
|
UI.setMessage('');
|
988
989
|
} else {
|
989
|
-
// NOTE:
|
990
|
+
// NOTE: Use aci because run #0 is the base case w/o active parameter.
|
990
991
|
MODEL.active_sensitivity_parameter = this.parameters[aci];
|
991
992
|
x.active_combination_index++;
|
992
993
|
setTimeout(() => EXPERIMENT_MANAGER.runModel(), 5);
|
@@ -1001,31 +1002,31 @@ class SensitivityAnalysis {
|
|
1001
1002
|
} else {
|
1002
1003
|
this.showCheckmark(msecToTime(x.time_stopped - x.time_started));
|
1003
1004
|
}
|
1004
|
-
// No more runs => perform wrap-up
|
1005
|
-
// Restore original model settings
|
1005
|
+
// No more runs => perform wrap-up.
|
1006
|
+
// (1) Restore original model settings.
|
1006
1007
|
MODEL.running_experiment = null;
|
1007
1008
|
MODEL.active_sensitivity_parameter = null;
|
1008
1009
|
MODEL.parseSettings(x.original_model_settings);
|
1009
1010
|
MODEL.round_sequence = x.original_round_sequence;
|
1010
|
-
// Reset the Virtual Machine so t=0 at the status line,
|
1011
|
-
//
|
1011
|
+
// (2) Reset the Virtual Machine so t=0 at the status line, and ALL
|
1012
|
+
// expressions are reset as well.
|
1012
1013
|
VM.reset();
|
1013
|
-
// Free the SA experiment and SA chart
|
1014
|
+
// Free the SA experiment and SA chart.
|
1014
1015
|
this.experiment = null;
|
1015
1016
|
this.chart = null;
|
1016
|
-
// Reset buttons (GUI only)
|
1017
|
+
// Reset buttons (GUI only).
|
1017
1018
|
this.readyButtons();
|
1018
1019
|
}
|
1019
1020
|
this.updateDialog();
|
1020
|
-
// Reset the model, as results of last run will be showing still
|
1021
|
+
// Reset the model, as results of last run will be showing still.
|
1021
1022
|
UI.resetModel();
|
1022
1023
|
CHART_MANAGER.resetChartVectors();
|
1023
|
-
// NOTE:
|
1024
|
+
// NOTE: Clear chart only when done (charts do not update during experiment).
|
1024
1025
|
if(!MODEL.running_experiment) CHART_MANAGER.updateDialog();
|
1025
1026
|
}
|
1026
1027
|
|
1027
1028
|
stop() {
|
1028
|
-
// Interrupt solver but retain data on server (and no resume)
|
1029
|
+
// Interrupt solver but retain data on server (and no resume).
|
1029
1030
|
VM.halt();
|
1030
1031
|
this.readyButtons();
|
1031
1032
|
this.showProgress('');
|
@@ -1033,13 +1034,13 @@ class SensitivityAnalysis {
|
|
1033
1034
|
}
|
1034
1035
|
|
1035
1036
|
clearResults() {
|
1036
|
-
// Clear results and reset control buttons
|
1037
|
+
// Clear results, and reset control buttons.
|
1037
1038
|
MODEL.sensitivity_runs.length = 0;
|
1038
1039
|
this.selected_run = -1;
|
1039
1040
|
}
|
1040
1041
|
|
1041
1042
|
computeData(sas) {
|
1042
|
-
// Compute data value or status for statistic `sas
|
1043
|
+
// Compute data value or status for statistic `sas`.
|
1043
1044
|
this.perc = {};
|
1044
1045
|
this.shade = {};
|
1045
1046
|
this.data = {};
|
@@ -1047,12 +1048,12 @@ class SensitivityAnalysis {
|
|
1047
1048
|
ol = MODEL.sensitivity_outcomes.length,
|
1048
1049
|
rl = MODEL.sensitivity_runs.length;
|
1049
1050
|
if(ol === 0) return;
|
1050
|
-
// Always find highest relative change
|
1051
|
+
// Always find highest relative change.
|
1051
1052
|
let max_dif = 0;
|
1052
1053
|
for(let i = 0; i < ol; i++) {
|
1053
1054
|
this.data[i] = [];
|
1054
1055
|
for(let j = 0; j < rl; j++) {
|
1055
|
-
// Get the selected statistic for each run to get an array of numbers
|
1056
|
+
// Get the selected statistic for each run to get an array of numbers.
|
1056
1057
|
const rr = MODEL.sensitivity_runs[j].results[i];
|
1057
1058
|
if(!rr) {
|
1058
1059
|
this.data[i].push(VM.UNDEFINED);
|
@@ -1076,7 +1077,7 @@ class SensitivityAnalysis {
|
|
1076
1077
|
this.data[i].push(rr.last);
|
1077
1078
|
}
|
1078
1079
|
}
|
1079
|
-
// Compute relative change
|
1080
|
+
// Compute the relative change.
|
1080
1081
|
let bsv = this.data[i][0];
|
1081
1082
|
if(Math.abs(bsv) < VM.NEAR_ZERO) bsv = 0;
|
1082
1083
|
this.perc[i] = [];
|
@@ -1097,26 +1098,26 @@ class SensitivityAnalysis {
|
|
1097
1098
|
for(let j = 1; j < this.data[i].length; j++) this.perc[i].push('-');
|
1098
1099
|
}
|
1099
1100
|
}
|
1100
|
-
// Now use max_dif to compute shades
|
1101
|
+
// Now use max_dif to compute shades.
|
1101
1102
|
for(let i = 0; i < ol; i++) {
|
1102
1103
|
this.shade[i] = [];
|
1103
|
-
// Color scale range is -max ... +max (0 in center => white)
|
1104
|
+
// Color scale range is -max ... +max (0 in center => white).
|
1104
1105
|
for(let j = 0; j < this.perc[i].length; j++) {
|
1105
1106
|
const p = this.perc[i][j];
|
1106
1107
|
this.shade[i].push(p === VM.UNDEFINED || max_dif < VM.NEAR_ZERO ?
|
1107
1108
|
0.5 : (p / max_dif + 1) / 2);
|
1108
1109
|
}
|
1109
|
-
// Convert to sig4Dig
|
1110
|
+
// Convert to sig4Dig.
|
1110
1111
|
for(let j = 0; j < this.data[i].length; j++) {
|
1111
1112
|
this.data[i][j] = VM.sig4Dig(this.data[i][j]);
|
1112
1113
|
}
|
1113
|
-
// Format data such that they all have same number of decimals
|
1114
|
+
// Format data such that they all have same number of decimals.
|
1114
1115
|
if(this.relative_scale && this.perc[i][0] !== '-') {
|
1115
1116
|
for(let j = 0; j < this.perc[i].length; j++) {
|
1116
1117
|
this.perc[i][j] = VM.sig4Dig(this.perc[i][j]);
|
1117
1118
|
}
|
1118
1119
|
uniformDecimals(this.perc[i]);
|
1119
|
-
// NOTE:
|
1120
|
+
// NOTE: Only consider data of base scenario.
|
1120
1121
|
this.data[i][0] = VM.sig4Dig(this.data[i][0]);
|
1121
1122
|
} else {
|
1122
1123
|
uniformDecimals(this.data[i]);
|
@@ -1125,11 +1126,11 @@ class SensitivityAnalysis {
|
|
1125
1126
|
}
|
1126
1127
|
|
1127
1128
|
resumeButtons() {
|
1128
|
-
// Console experiments cannot be paused, and hence not resumed
|
1129
|
+
// Console experiments cannot be paused, and hence not resumed.
|
1129
1130
|
return false;
|
1130
1131
|
}
|
1131
1132
|
|
1132
|
-
// Dummy methods: actions that are meaningful only for the graphical UI
|
1133
|
+
// Dummy methods: actions that are meaningful only for the graphical UI.
|
1133
1134
|
updateDialog() {}
|
1134
1135
|
showCheckmark() {}
|
1135
1136
|
showProgress() {}
|
@@ -1143,7 +1144,7 @@ class SensitivityAnalysis {
|
|
1143
1144
|
// Class ExperimentManager controls the collection of experiments of the model
|
1144
1145
|
class ExperimentManager {
|
1145
1146
|
constructor() {
|
1146
|
-
// NOTE:
|
1147
|
+
// NOTE: The properties below are relevant only for the GUI.
|
1147
1148
|
this.experiment_table = null;
|
1148
1149
|
this.focal_table = null;
|
1149
1150
|
}
|
@@ -283,7 +283,8 @@ class ConstraintEditor {
|
|
283
283
|
i = this.selected_point,
|
284
284
|
pts = this.selected.points,
|
285
285
|
li = pts.length - 1,
|
286
|
-
|
286
|
+
// NOTE: Use a copy of the selected point, or it will not be updated.
|
287
|
+
p = pts[this.selected_point].slice(),
|
287
288
|
minx = (i === 0 ? 0 : (i === li ? 100 : pts[i - 1][0])),
|
288
289
|
maxx = (i === 0 ? 0 : (i === li ? 100 : pts[i + 1][0]));
|
289
290
|
let cx = false,
|
@@ -317,6 +318,9 @@ class ConstraintEditor {
|
|
317
318
|
p[1] = Math.round(3 * p[1]) / 3;
|
318
319
|
}
|
319
320
|
}
|
321
|
+
this.dragged_point = this.selected_point;
|
322
|
+
this.movePoint(p[0], p[1]);
|
323
|
+
this.dragged_point = -1;
|
320
324
|
this.draw();
|
321
325
|
this.updateEquation();
|
322
326
|
}
|
@@ -670,7 +674,7 @@ class ConstraintEditor {
|
|
670
674
|
}
|
671
675
|
|
672
676
|
deleteSelector() {
|
673
|
-
// Delete modifier from selected dataset
|
677
|
+
// Delete modifier from selected dataset.
|
674
678
|
if(!this.selected) return;
|
675
679
|
const
|
676
680
|
bl = this.selected,
|
@@ -1156,7 +1160,7 @@ class ConstraintEditor {
|
|
1156
1160
|
}
|
1157
1161
|
|
1158
1162
|
updateConstraint() {
|
1159
|
-
//
|
1163
|
+
// Update the edited constraint, or add a new constraint to the model.
|
1160
1164
|
// TO DO: prepare for undo
|
1161
1165
|
if(this.edited_constraint === null) {
|
1162
1166
|
this.edited_constraint = MODEL.addConstraint(this.from_node, this.to_node);
|
@@ -13,7 +13,7 @@ handler functions.
|
|
13
13
|
*/
|
14
14
|
|
15
15
|
/*
|
16
|
-
Copyright (c) 2017-
|
16
|
+
Copyright (c) 2017-2025 Delft University of Technology
|
17
17
|
|
18
18
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
19
19
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -3673,7 +3673,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3673
3673
|
UI.notify('To diagnose unbounded problems, values beyond 1e+10 ' +
|
3674
3674
|
'are considered as infinite (\u221E)');
|
3675
3675
|
}
|
3676
|
-
// Some changes may necessitate redrawing the diagram
|
3676
|
+
// Some changes may necessitate redrawing the diagram.
|
3677
3677
|
let cb = UI.boxChecked('settings-align-to-grid'),
|
3678
3678
|
redraw = !model.align_to_grid && cb;
|
3679
3679
|
model.align_to_grid = cb;
|
@@ -3688,7 +3688,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3688
3688
|
cb = UI.boxChecked('settings-block-arrows');
|
3689
3689
|
redraw = redraw || cb !== model.show_block_arrows;
|
3690
3690
|
model.show_block_arrows = cb;
|
3691
|
-
// Changes affecting run length (hence vector lengths) require a model reset
|
3691
|
+
// Changes affecting run length (hence vector lengths) require a model reset.
|
3692
3692
|
let reset = false;
|
3693
3693
|
reset = reset || (ts != model.time_scale);
|
3694
3694
|
model.time_scale = ts;
|
@@ -4283,7 +4283,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
4283
4283
|
const
|
4284
4284
|
md = this.modals.link,
|
4285
4285
|
l = this.edited_object;
|
4286
|
-
// Check whether all input fields are valid
|
4286
|
+
// Check whether all input fields are valid.
|
4287
4287
|
if(!this.updateExpressionInput('link-R', 'rate', l.relative_rate)) {
|
4288
4288
|
return false;
|
4289
4289
|
}
|
@@ -4318,8 +4318,10 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
4318
4318
|
`</strong> will cause issues for ${VM.LM_SYMBOLS[m]} link`);
|
4319
4319
|
}
|
4320
4320
|
}
|
4321
|
-
// NOTE:
|
4322
|
-
// point value between 0 and 1
|
4321
|
+
// NOTE: Share of cost is input as a percentage, but stored as a floating
|
4322
|
+
// point value between 0 and 1.
|
4323
|
+
// If SoC is changed, *all* output links must be redrawn.
|
4324
|
+
const soc_change = (l.share_of_cost !== soc / 100);
|
4323
4325
|
l.share_of_cost = soc / 100;
|
4324
4326
|
if(md.group.length > 1) {
|
4325
4327
|
// NOTE: Special care must be taken to not set special multipliers
|
@@ -4330,10 +4332,15 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
4330
4332
|
MODEL.focal_cluster.clearAllProcesses();
|
4331
4333
|
UI.drawDiagram(MODEL);
|
4332
4334
|
} else {
|
4333
|
-
|
4334
|
-
|
4335
|
-
|
4336
|
-
|
4335
|
+
if(soc_change) {
|
4336
|
+
// Redraw process with its links so that all SoC labels are updated.
|
4337
|
+
this.on_arrow.from_node.drawWithLinks();
|
4338
|
+
} else {
|
4339
|
+
// Only redraw the arrow shape that represents the edited link.
|
4340
|
+
this.paper.drawArrow(this.on_arrow);
|
4341
|
+
// Redraw the FROM node if link has become (or no longer is) "first commit".
|
4342
|
+
if(redraw) this.drawObject(this.on_arrow.from_node);
|
4343
|
+
}
|
4337
4344
|
}
|
4338
4345
|
md.hide();
|
4339
4346
|
}
|
@@ -12,7 +12,7 @@ dialogs, the main drawing canvas, and event handler functions.
|
|
12
12
|
*/
|
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
|
@@ -165,7 +165,7 @@ class ModelAutoSaver {
|
|
165
165
|
html += ['<tr class="dataset" style="color: gray" ',
|
166
166
|
'onclick="FILE_MANAGER.loadAutoSavedModel(\'',
|
167
167
|
m.name,'\');"><td class="restore-name">', m.name, '</td><td>',
|
168
|
-
m.date.substring(
|
168
|
+
m.date.substring(0, 16).replace('T', ' '),
|
169
169
|
'</td><td style="text-align: right">',
|
170
170
|
bytes[0], '</td><td>', bytes[1], '</td><td style="width:15px">',
|
171
171
|
'<img class="del-asm-btn" src="images/delete.png" ',
|
@@ -11,7 +11,7 @@ for the Linny-R Monitor dialog.
|
|
11
11
|
*/
|
12
12
|
|
13
13
|
/*
|
14
|
-
Copyright (c) 2017-
|
14
|
+
Copyright (c) 2017-2025 Delft University of Technology
|
15
15
|
|
16
16
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
17
17
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -11,7 +11,7 @@ functionality for the Linny-R model editor.
|
|
11
11
|
*/
|
12
12
|
|
13
13
|
/*
|
14
|
-
Copyright (c) 2017-
|
14
|
+
Copyright (c) 2017-2025 Delft University of Technology
|
15
15
|
|
16
16
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
17
17
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -909,16 +909,17 @@ class Paper {
|
|
909
909
|
}
|
910
910
|
|
911
911
|
//
|
912
|
-
// Diagram-drawing method draws the diagram for the focal cluster
|
912
|
+
// Diagram-drawing method draws the diagram for the focal cluster.
|
913
913
|
//
|
914
914
|
|
915
915
|
drawModel(mdl) {
|
916
|
-
// Draw the diagram for the focal cluster
|
916
|
+
// Draw the diagram for the focal cluster.
|
917
917
|
this.clear();
|
918
|
-
// Prepare to draw all elements in the focal cluster
|
918
|
+
// Prepare to draw all elements in the focal cluster.
|
919
919
|
const fc = mdl.focal_cluster;
|
920
920
|
fc.categorizeEntities();
|
921
|
-
// NOTE:
|
921
|
+
// NOTE: Product positions must be updated before links are drawn, so
|
922
|
+
// that links arrows will be drawn over their shapes.
|
922
923
|
fc.positionProducts();
|
923
924
|
for(let i = 0; i < fc.processes.length; i++) {
|
924
925
|
fc.processes[i].clearHiddenIO();
|
@@ -926,10 +927,10 @@ class Paper {
|
|
926
927
|
for(let i = 0; i < fc.sub_clusters.length; i++) {
|
927
928
|
fc.sub_clusters[i].clearHiddenIO();
|
928
929
|
}
|
929
|
-
// NOTE:
|
930
|
+
// NOTE: Also ensure that notes will update their fields.
|
930
931
|
fc.resetNoteFields();
|
931
932
|
// Draw link arrows and constraints first, as all other entities are
|
932
|
-
// slightly transparent so they cannot completely hide these lines
|
933
|
+
// slightly transparent so they cannot completely hide these lines.
|
933
934
|
for(let i = 0; i < fc.arrows.length; i++) {
|
934
935
|
this.drawArrow(fc.arrows[i]);
|
935
936
|
}
|
@@ -945,25 +946,25 @@ class Paper {
|
|
945
946
|
for(let i = 0; i < fc.sub_clusters.length; i++) {
|
946
947
|
this.drawCluster(fc.sub_clusters[i]);
|
947
948
|
}
|
948
|
-
// Draw notes last, as they are semi-transparent (and can be quite small)
|
949
|
+
// Draw notes last, as they are semi-transparent (and can be quite small).
|
949
950
|
for(let i = 0; i < fc.notes.length; i++) {
|
950
951
|
this.drawNote(fc.notes[i]);
|
951
952
|
}
|
952
|
-
// Resize paper if necessary
|
953
|
+
// Resize paper if necessary.
|
953
954
|
this.extend();
|
954
|
-
// Display model name in browser
|
955
|
+
// Display model name in browser.
|
955
956
|
document.title = mdl.name || 'Linny-R';
|
956
957
|
}
|
957
958
|
|
958
959
|
drawSelection(mdl, dx=0, dy=0) {
|
959
960
|
// NOTE: Clear this global, as Bezier curves move from under the cursor
|
960
|
-
// without a mouseout event
|
961
|
+
// without a mouseout event.
|
961
962
|
this.constraint_under_cursor = null;
|
962
|
-
// Draw the selected entities and associated links, and also constraints
|
963
|
+
// Draw the selected entities and associated links, and also constraints.
|
963
964
|
for(let i = 0; i < mdl.selection.length; i++) {
|
964
965
|
const obj = mdl.selection[i];
|
965
966
|
// Links and constraints are drawn separately, so do not draw those
|
966
|
-
// contained in the selection
|
967
|
+
// contained in the selection.
|
967
968
|
if(!(obj instanceof Link || obj instanceof Constraint)) {
|
968
969
|
if(obj instanceof Note) obj.parsed = false;
|
969
970
|
UI.drawObject(obj, dx, dy);
|
@@ -972,12 +973,12 @@ class Paper {
|
|
972
973
|
if(mdl.selection_related_arrows.length === 0) {
|
973
974
|
mdl.selection_related_arrows = mdl.focal_cluster.selectedArrows();
|
974
975
|
}
|
975
|
-
// Only draw the arrows that relate to the selection
|
976
|
+
// Only draw the arrows that relate to the selection.
|
976
977
|
for(let i = 0; i < mdl.selection_related_arrows.length; i++) {
|
977
978
|
this.drawArrow(mdl.selection_related_arrows[i]);
|
978
979
|
}
|
979
980
|
// As they typically are few, simply redraw all constraints that relate to
|
980
|
-
// the focal cluster
|
981
|
+
// the focal cluster.
|
981
982
|
for(let i = 0; i < mdl.focal_cluster.related_constraints.length; i++) {
|
982
983
|
this.drawConstraint(mdl.focal_cluster.related_constraints[i]);
|
983
984
|
}
|
@@ -990,23 +991,23 @@ class Paper {
|
|
990
991
|
//
|
991
992
|
|
992
993
|
drawArrow(arrw, dx=0, dy=0) {
|
993
|
-
//
|
994
|
-
// NOTE:
|
994
|
+
// Draw an arrow from FROM nodebox to TO nodebox.
|
995
|
+
// NOTE: First erase previously drawn arrow.
|
995
996
|
arrw.shape.clear();
|
996
997
|
arrw.hidden_nodes.length = 0;
|
997
|
-
// Use local variables so as not to change any "real" attribute values
|
998
|
+
// Use local variables so as not to change any "real" attribute values.
|
998
999
|
let cnb, proc, prod, fnx, fny, fnw, fnh, tnx, tny, tnw, tnh,
|
999
1000
|
cp, rr, aa, bb, dd, nn, af, l, s, w, tw, th, bpx, bpy, epx, epy,
|
1000
1001
|
sda, stroke_color, stroke_width, arrow_start, arrow_end,
|
1001
1002
|
font_color, font_weight, luc = null, grid = null;
|
1002
|
-
// Get the main arrow attributes
|
1003
|
+
// Get the main arrow attributes.
|
1003
1004
|
const
|
1004
1005
|
from_nb = arrw.from_node,
|
1005
1006
|
to_nb = arrw.to_node;
|
1006
|
-
// Use "let" because `ignored` may also be set later on (for single link)
|
1007
|
+
// Use "let" because `ignored` may also be set later on (for single link).
|
1007
1008
|
let ignored = (from_nb && MODEL.ignored_entities[from_nb.identifier]) ||
|
1008
1009
|
(to_nb && MODEL.ignored_entities[to_nb.identifier]);
|
1009
|
-
// First check if this is a block arrow (ONE node being null)
|
1010
|
+
// First check if this is a block arrow (ONE node being null).
|
1010
1011
|
if(!from_nb) {
|
1011
1012
|
cnb = to_nb;
|
1012
1013
|
} else if(!to_nb) {
|
@@ -1014,22 +1015,22 @@ class Paper {
|
|
1014
1015
|
} else {
|
1015
1016
|
cnb = null;
|
1016
1017
|
}
|
1017
|
-
// If not NULL `cnb` is the cluster or node box (product or process) having
|
1018
|
+
// If not NULL, `cnb` is the cluster or node box (product or process) having
|
1018
1019
|
// links to entities outside the focal cluster. Such links are summarized
|
1019
1020
|
// by "block arrows": on the left edge of the box to indicate inflows,
|
1020
1021
|
// on the right edge to indicate outflows, and two-headed on the top edge
|
1021
1022
|
// to indicate two-way flows. When the cursor is moved over a block arrow,
|
1022
1023
|
// the Documentation dialog will display the list of associated nodes
|
1023
|
-
// (with their actual flows if non-zero)
|
1024
|
+
// (with their actual flows if non-zero).
|
1024
1025
|
if(cnb) {
|
1025
|
-
// Distinguish between input, output and io products
|
1026
|
+
// Distinguish between input, output and io products.
|
1026
1027
|
let ip = [], op = [], iop = [];
|
1027
1028
|
if(cnb instanceof Cluster) {
|
1028
1029
|
for(let i = 0; i < arrw.links.length; i++) {
|
1029
1030
|
const lnk = arrw.links[i];
|
1030
|
-
//
|
1031
|
+
// Determine which product is involved.
|
1031
1032
|
prod = (lnk.from_node instanceof Product ? lnk.from_node : lnk.to_node);
|
1032
|
-
// NOTE:
|
1033
|
+
// NOTE: Clusters "know" their input/output products.
|
1033
1034
|
if(cnb.io_products.indexOf(prod) >= 0) {
|
1034
1035
|
addDistinct(prod, iop);
|
1035
1036
|
} else if(cnb.consumed_products.indexOf(prod) >= 0) {
|
@@ -1039,7 +1040,7 @@ class Paper {
|
|
1039
1040
|
}
|
1040
1041
|
}
|
1041
1042
|
} else {
|
1042
|
-
// cnb is process or product => knows its inputs and outputs
|
1043
|
+
// `cnb` is process or product => knows its inputs and outputs.
|
1043
1044
|
for(let i = 0; i < arrw.links.length; i++) {
|
1044
1045
|
const lnk = arrw.links[i];
|
1045
1046
|
if(lnk.from_node === cnb) {
|
@@ -1047,7 +1048,7 @@ class Paper {
|
|
1047
1048
|
} else {
|
1048
1049
|
addDistinct(lnk.from_node, ip);
|
1049
1050
|
}
|
1050
|
-
// NOTE:
|
1051
|
+
// NOTE: For processes, products cannot be BOTH input and output.
|
1051
1052
|
}
|
1052
1053
|
}
|
1053
1054
|
cnb.hidden_inputs = ip;
|
@@ -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
|
@@ -5801,7 +5801,7 @@ class NodeBox extends ObjectWithXYWH {
|
|
5801
5801
|
}
|
5802
5802
|
|
5803
5803
|
resize() {
|
5804
|
-
//
|
5804
|
+
// Resize this node; returns TRUE iff size has changed.
|
5805
5805
|
// Therefore, keep track of original width and height.
|
5806
5806
|
const
|
5807
5807
|
ow = this.width,
|
@@ -5814,7 +5814,7 @@ class NodeBox extends ObjectWithXYWH {
|
|
5814
5814
|
w = Math.max(w, UI.textSize(`[${this.scale_unit}]`).width);
|
5815
5815
|
}
|
5816
5816
|
this.frame_width = w + 7;
|
5817
|
-
// Add 17 pixels height for actor name
|
5817
|
+
// Add 17 pixels height for actor name.
|
5818
5818
|
this.height = Math.max(50, this.bbox.height + 17);
|
5819
5819
|
if(this instanceof Process) {
|
5820
5820
|
this.width = Math.max(90, this.frame_width + 20);
|
@@ -5822,11 +5822,11 @@ class NodeBox extends ObjectWithXYWH {
|
|
5822
5822
|
} else if(this instanceof Cluster) {
|
5823
5823
|
this.width = Math.max(
|
5824
5824
|
CONFIGURATION.min_cluster_size, this.frame_width + 20);
|
5825
|
-
// Clusters have a square shape
|
5825
|
+
// Clusters have a square shape.
|
5826
5826
|
this.height = Math.max(this.width, this.height);
|
5827
5827
|
} else {
|
5828
5828
|
this.height += 8;
|
5829
|
-
// Reserve some extra space for UB/LB if defined
|
5829
|
+
// Reserve some extra space for UB/LB if defined.
|
5830
5830
|
if(this.lower_bound.defined || this.upper_bound.defined) {
|
5831
5831
|
this.frame_width += 16;
|
5832
5832
|
}
|
@@ -8282,20 +8282,20 @@ class Product extends Node {
|
|
8282
8282
|
// By default, processes have the letter p, products the letter q.
|
8283
8283
|
this.TEX_id = 'p';
|
8284
8284
|
// For products, the default bounds are [0, 0], and modeler-defined bounds
|
8285
|
-
// typically are equal
|
8285
|
+
// typically are equal.
|
8286
8286
|
this.equal_bounds = true;
|
8287
|
-
// 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.
|
8288
8288
|
this.price = new Expression(this, 'P', '');
|
8289
|
-
// 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).
|
8290
8290
|
this.highest_cost_price = [];
|
8291
8291
|
this.stock_price = [];
|
8292
8292
|
// Stock level changing from 0 to positive counts as "start up", while
|
8293
|
-
// changing from positive to 0 counts as a "shut-down"
|
8294
|
-
// NOTE:
|
8295
|
-
// 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.
|
8296
8296
|
this.start_ups = [];
|
8297
8297
|
this.shut_downs = [];
|
8298
|
-
// Modeler may set explicit properties
|
8298
|
+
// Modeler may set explicit properties.
|
8299
8299
|
this.is_source = false;
|
8300
8300
|
this.is_sink = false;
|
8301
8301
|
this.is_buffer = false;
|
@@ -8650,13 +8650,13 @@ class Product extends Node {
|
|
8650
8650
|
}
|
8651
8651
|
|
8652
8652
|
get defaultAttribute() {
|
8653
|
-
// Products have their level as default attribute
|
8653
|
+
// Products have their level as default attribute.
|
8654
8654
|
return 'L';
|
8655
8655
|
}
|
8656
8656
|
|
8657
8657
|
attributeValue(a) {
|
8658
|
-
//
|
8659
|
-
//
|
8658
|
+
// Return the computed result for attribute `a`.
|
8659
|
+
// NOTE: For products, this is always a vector except IL.
|
8660
8660
|
if(a === 'L') return this.level;
|
8661
8661
|
if(a === 'CP') return this.cost_price;
|
8662
8662
|
if(a === 'HCP') return this.highest_cost_price;
|
@@ -8664,7 +8664,7 @@ class Product extends Node {
|
|
8664
8664
|
}
|
8665
8665
|
|
8666
8666
|
attributeExpression(a) {
|
8667
|
-
// Products have four expression attributes
|
8667
|
+
// Products have four expression attributes.
|
8668
8668
|
if(a === 'LB') return this.lower_bound;
|
8669
8669
|
if(a === 'UB') {
|
8670
8670
|
return (this.equal_bounds ? this.lower_bound : this.upper_bound);
|
@@ -8748,7 +8748,7 @@ class Product extends Node {
|
|
8748
8748
|
}
|
8749
8749
|
|
8750
8750
|
copyPropertiesFrom(p) {
|
8751
|
-
// Set properties to be identical to those of product `p
|
8751
|
+
// Set properties to be identical to those of product `p`.
|
8752
8752
|
this.x = p.x;
|
8753
8753
|
this.y = p.y;
|
8754
8754
|
this.comments = p.comments;
|
@@ -8765,11 +8765,11 @@ class Product extends Node {
|
|
8765
8765
|
this.initial_level.text = p.initial_level.text;
|
8766
8766
|
this.integer_level = p.integer_level;
|
8767
8767
|
this.TEX_id = p.TEX_id;
|
8768
|
-
// NOTE:
|
8768
|
+
// NOTE: Do not copy the `no_links` property, nor the import/export status.
|
8769
8769
|
}
|
8770
8770
|
|
8771
8771
|
differences(p) {
|
8772
|
-
// Return "dictionary" of differences, or NULL if none
|
8772
|
+
// Return "dictionary" of differences, or NULL if none.
|
8773
8773
|
const d = differences(this, p, UI.MC.PRODUCT_PROPS);
|
8774
8774
|
if(Object.keys(d).length > 0) return d;
|
8775
8775
|
return null;
|
@@ -12699,6 +12699,10 @@ class BoundLine {
|
|
12699
12699
|
VM.constraint_codes[this.type] + '] bound line #' +
|
12700
12700
|
this.constraint.bound_lines.indexOf(this);
|
12701
12701
|
}
|
12702
|
+
|
12703
|
+
get name() {
|
12704
|
+
return this.displayName;
|
12705
|
+
}
|
12702
12706
|
|
12703
12707
|
get copy() {
|
12704
12708
|
// Return a "clone" of this bound line.
|
@@ -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));
|
@@ -5696,7 +5706,6 @@ class VirtualMachine {
|
|
5696
5706
|
return ` +${c} ${v}`; // Prefix coefficient with +
|
5697
5707
|
// NOTE: This may return +0 X001.
|
5698
5708
|
};
|
5699
|
-
|
5700
5709
|
this.numeric_issue = '';
|
5701
5710
|
// First add the objective (always MAXimize).
|
5702
5711
|
if(cplex) {
|
@@ -6160,7 +6169,9 @@ class VirtualMachine {
|
|
6160
6169
|
}
|
6161
6170
|
|
6162
6171
|
stopSolving() {
|
6172
|
+
// Wrap-up after solving is completed or aborted.
|
6163
6173
|
this.stopTimer();
|
6174
|
+
// Stop rotating the Linny-R icon, and update buttons.
|
6164
6175
|
UI.stopSolving();
|
6165
6176
|
}
|
6166
6177
|
|
@@ -6312,7 +6323,7 @@ Solver status = ${json.status}`);
|
|
6312
6323
|
}
|
6313
6324
|
// If negative delays require "fixating" variables for some number
|
6314
6325
|
// of time steps, this must be logged in the monitor.
|
6315
|
-
|
6326
|
+
let keys = Object.keys(this.variables_to_fixate);
|
6316
6327
|
if(keys.length) {
|
6317
6328
|
const msg = ['NOTE: Due to negative link delays, levels for ' +
|
6318
6329
|
pluralS(keys.length, 'variable') + ' are pre-set:'];
|
@@ -6341,6 +6352,27 @@ Solver status = ${json.status}`);
|
|
6341
6352
|
}
|
6342
6353
|
this.logMessage(this.block_count, msg.join('\n'));
|
6343
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.
|
6344
6376
|
this.logMessage(this.block_count,
|
6345
6377
|
'Creating model for block #' + this.blockWithRound);
|
6346
6378
|
this.cbl = CONFIGURATION.progress_needle_interval * 200;
|
@@ -7581,6 +7613,108 @@ function VMI_ge(x) {
|
|
7581
7613
|
}
|
7582
7614
|
}
|
7583
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
|
+
|
7584
7718
|
function VMI_add(x) {
|
7585
7719
|
// Pop the top number on the stack, and add it to the new top number.
|
7586
7720
|
const d = x.pop();
|
@@ -8217,6 +8351,13 @@ function VMI_set_bounds(args) {
|
|
8217
8351
|
console.log(['set_bounds [', k, '] ', vbl.displayName, '[',
|
8218
8352
|
VM.variables[vi - 1][0],'] t = ', VM.t, ' LB = ', VM.sig4Dig(l),
|
8219
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;
|
8220
8361
|
}
|
8221
8362
|
// NOTE: Since the VM vectors for lower bounds and upper bounds are
|
8222
8363
|
// initialized with default values (0 for LB, +INF for UB), the bounds
|
@@ -9270,7 +9411,7 @@ function VMI_add_available_capacity(link) {
|
|
9270
9411
|
const
|
9271
9412
|
// Valid symbols in expressions
|
9272
9413
|
PARENTHESES = '()',
|
9273
|
-
OPERATOR_CHARS = '
|
9414
|
+
OPERATOR_CHARS = ';?:+-*/%=!<>^|@',
|
9274
9415
|
// Opening bracket, space and single quote indicate a separation
|
9275
9416
|
SEPARATOR_CHARS = PARENTHESES + OPERATOR_CHARS + "[ '",
|
9276
9417
|
COMPOUND_OPERATORS = ['!=', '<>', '>=', '<='],
|
@@ -9302,13 +9443,13 @@ const
|
|
9302
9443
|
VMI_weibull, VMI_npv],
|
9303
9444
|
DYADIC_OPERATORS = [
|
9304
9445
|
';', '?', ':', 'or', 'and',
|
9305
|
-
'=', '<>', '!=',
|
9306
|
-
'
|
9446
|
+
'=', '<>', '!=', '>', '<', '>=', '<=',
|
9447
|
+
'@', '+', '-', '*', '/',
|
9307
9448
|
'%', '^', 'log', '|'],
|
9308
9449
|
DYADIC_CODES = [
|
9309
9450
|
VMI_concat, VMI_if_then, VMI_if_else, VMI_or, VMI_and,
|
9310
9451
|
VMI_eq, VMI_ne, VMI_ne, VMI_gt, VMI_lt, VMI_ge, VMI_le,
|
9311
|
-
VMI_add, VMI_sub, VMI_mul, VMI_div, VMI_mod,
|
9452
|
+
VMI_at, VMI_add, VMI_sub, VMI_mul, VMI_div, VMI_mod,
|
9312
9453
|
VMI_power, VMI_log, VMI_replace_undefined],
|
9313
9454
|
|
9314
9455
|
// Compiler checks for random codes as they make an expression dynamic
|
@@ -9316,7 +9457,7 @@ const
|
|
9316
9457
|
VMI_triangular, VMI_weibull],
|
9317
9458
|
|
9318
9459
|
// Compiler checks for reducing codes to unset its "concatenating" flag
|
9319
|
-
REDUCING_CODES = [VMI_min, VMI_max, VMI_binomial, VMI_normal,
|
9460
|
+
REDUCING_CODES = [VMI_at, VMI_min, VMI_max, VMI_binomial, VMI_normal,
|
9320
9461
|
VMI_triangular, VMI_weibull, VMI_npv],
|
9321
9462
|
|
9322
9463
|
// Custom operators may make an expression level-based
|
@@ -9324,7 +9465,7 @@ const
|
|
9324
9465
|
|
9325
9466
|
OPERATORS = DYADIC_OPERATORS.concat(MONADIC_OPERATORS),
|
9326
9467
|
OPERATOR_CODES = DYADIC_CODES.concat(MONADIC_CODES),
|
9327
|
-
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,
|
9328
9469
|
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9],
|
9329
9470
|
ACTUAL_SYMBOLS = CONSTANT_SYMBOLS.concat(OPERATORS),
|
9330
9471
|
SYMBOL_CODES = CONSTANT_CODES.concat(OPERATOR_CODES);
|