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 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 (June 2024)
40
- this is 22.2.0.
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: v22.2.0.
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-06-11 22:55:17] Static file: /index.html
304
- [2024-06-11 22:55:17] Static file: /scripts/iro.min.js
305
- [2024-06-11 22:55:17] Static file: /images/open.png
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "2.0.6",
3
+ "version": "2.0.7",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -10,7 +10,7 @@ file that implements the graphical user interface for Linny-R.
10
10
  */
11
11
 
12
12
  /*
13
- Copyright (c) 2017-2024 Delft University of Technology
13
+ Copyright (c) 2017-2025 Delft University of Technology
14
14
 
15
15
  Permission is hereby granted, free of charge, to any person obtaining a copy
16
16
  of this software and associated documentation files (the "Software"), to deal
@@ -822,7 +822,7 @@ div.checked.not-same-not-changed {
822
822
  }
823
823
 
824
824
  #restore-dlg {
825
- width: 440px;
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: model settings will not be changed, but nevertheless restored
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: start with base case run, hence no active parameter yet
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 an SA run
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: use a "dummy experiment object" to ensure proper XML saving and
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: use aci because run #0 is the base case w/o active parameter
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
- // and ALL expressions are reset as well
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: clear chart only when done (charts do not update during experiment)
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: only consider data of base scenario
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: the properties below are relevant only for the GUI
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
- p = pts[this.selected_point],
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
- // Updates the edited constraint, or adds a new constraint to the model
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-2024 Delft University of Technology
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: share of cost is input as a percentage, but stored as a floating
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
- // Redraw the arrow shape that represents the edited link
4334
- this.paper.drawArrow(this.on_arrow);
4335
- // Redraw the FROM node if link has become (or no longer is) "first commit"
4336
- if(redraw) this.drawObject(this.on_arrow.from_node);
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-2024 Delft University of Technology
15
+ Copyright (c) 2017-2025 Delft University of Technology
16
16
 
17
17
  Permission is hereby granted, free of charge, to any person obtaining a copy
18
18
  of this software and associated documentation files (the "Software"), to deal
@@ -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(1, 16).replace('T', ' '),
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-2024 Delft University of Technology
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-2024 Delft University of Technology
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: product positions must be updated before links are drawn
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: also ensure that notes will update their fields
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
- // Draws an arrow from FROM nodebox to TO nodebox
994
- // NOTE: first erase previously drawn arrow
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
- // determine which product is involved
1031
+ // Determine which product is involved.
1031
1032
  prod = (lnk.from_node instanceof Product ? lnk.from_node : lnk.to_node);
1032
- // NOTE: clusters "know" their input/output products
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: for processes, products cannot be BOTH input and output
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-2024 Delft University of Technology
13
+ Copyright (c) 2017-2025 Delft University of Technology
14
14
 
15
15
  Permission is hereby granted, free of charge, to any person obtaining a copy
16
16
  of this software and associated documentation files (the "Software"), to deal
@@ -5801,7 +5801,7 @@ class NodeBox extends ObjectWithXYWH {
5801
5801
  }
5802
5802
 
5803
5803
  resize() {
5804
- // Resizes this node; returns TRUE iff size has changed.
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: being relatively rare, start_ups and shut_downs are not vectors,
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
- // Returns the computed result for attribute `a`
8659
- // (for products, this is always a vector except IL)
8658
+ // Return the computed result for attribute `a`.
8659
+ // NOTE: For products, this is always a vector except IL.
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: do not copy the `no_links` property, nor the import/export status
8768
+ // NOTE: Do not copy the `no_links` property, nor the import/export status.
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-2024 Delft University of Technology
12
+ Copyright (c) 2017-2025 Delft University of Technology
13
13
 
14
14
  Permission is hereby granted, free of charge, to any person obtaining a copy
15
15
  of this software and associated documentation files (the "Software"), to deal
@@ -74,16 +74,16 @@ function safeStrToFloat(str, val=0) {
74
74
  }
75
75
 
76
76
  function safeStrToInt(str, val=0) {
77
- // Returns numeric value of integer string, IGNORING decimals after
77
+ // Return numeric value of integer string, IGNORING decimals after
78
78
  // point or comma.
79
- // NOTE: returns default value `val` if `str` is empty, null or undefined
79
+ // NOTE: Return default value `val` if `str` is empty, null or undefined.
80
80
  const n = (str ? parseInt(str) : val);
81
81
  return (isNaN(n) ? val : n);
82
82
  }
83
83
 
84
84
  function rangeToList(str, max=0) {
85
- // Parses ranges "n-m/i" into a list of integers
86
- // Returns FALSE if range is not valid according to the convention below
85
+ // Parse ranges "n-m/i" into a list of integers
86
+ // Return FALSE if range is not valid according to the convention below
87
87
  // The part "/i" is optional and denotes the increment; by default, i = 1.
88
88
  // The returned list will contain all integers starting at n and up to
89
89
  // at most (!) m, with increments of i, so [n, n+i, n+2i, ...]
@@ -112,6 +112,33 @@ function rangeToList(str, max=0) {
112
112
  return list;
113
113
  }
114
114
 
115
+ function listToRange(list) {
116
+ // Return a string that represents the given list of integers as a series
117
+ // of subranges, e.g., [0,1,2,3,5,6,9,11] results in "0-3, 5-6, 9, 11".
118
+ const
119
+ n = list.length,
120
+ subs = [];
121
+ if(!n) return '';
122
+ let i = 0,
123
+ from = list[0],
124
+ to = from;
125
+ while(i < n) {
126
+ i++;
127
+ if(list[i] === to + 1) {
128
+ to++;
129
+ } else {
130
+ if(from === to) {
131
+ subs.push(from);
132
+ } else {
133
+ subs.push(`${from}-${to}`);
134
+ }
135
+ from = list[i];
136
+ to = from;
137
+ }
138
+ }
139
+ return subs.join(', ');
140
+ }
141
+
115
142
  function dateToString(d) {
116
143
  // Returns date-time `d` in UTC format, accounting for time zone
117
144
  const offset = d.getTimezoneOffset();
@@ -1098,6 +1125,7 @@ if(NODE) module.exports = {
1098
1125
  safeStrToFloat: safeStrToFloat,
1099
1126
  safeStrToInt: safeStrToInt,
1100
1127
  rangeToList: rangeToList,
1128
+ listToRange: listToRange,
1101
1129
  dateToString: dateToString,
1102
1130
  msecToTime: msecToTime,
1103
1131
  compactClockTime: compactClockTime,
@@ -12,7 +12,7 @@ executed by the VM, construct the Simplex tableau that can be sent to the
12
12
  MILP solver.
13
13
  */
14
14
  /*
15
- Copyright (c) 2017-2024 Delft University of Technology
15
+ Copyright (c) 2017-2025 Delft University of Technology
16
16
 
17
17
  Permission is hereby granted, free of charge, to any person obtaining a copy
18
18
  of this software and associated documentation files (the "Software"), to deal
@@ -444,17 +444,21 @@ class Expression {
444
444
  // expression).
445
445
  if(t < 0 || this.isStatic) t = 0;
446
446
  if(t >= v.length) return VM.UNDEFINED;
447
- // NOTE: When VM is setting up a tableau, values computed for the
448
- // look-ahead period must be recomputed.
449
- if(v[t] === VM.NOT_COMPUTED || v[t] === VM.COMPUTING ||
447
+ // NOTES:
448
+ // (1) When VM is setting up a tableau, values computed for the
449
+ // look-ahead period must be recomputed.
450
+ // (2) Always recompute value for sensitivity analysis parameter, as
451
+ // otherwise the vector value will be scaled cumulatively.
452
+ const sap = (this === MODEL.active_sensitivity_parameter);
453
+ if(sap || v[t] === VM.NOT_COMPUTED || v[t] === VM.COMPUTING ||
450
454
  (!this.isStatic && VM.inLookAhead(t))) {
451
455
  v[t] = VM.NOT_COMPUTED;
452
456
  this.compute(t, number);
453
457
  }
454
- // NOTE: when this expression is the "active" parameter for sensitivity
455
- // analysis, the result is multiplied by 1 + delta %
456
- if(this === MODEL.active_sensitivity_parameter) {
457
- // NOTE: do NOT scale exceptional values
458
+ // NOTE: When this expression is the "active" parameter for sensitivity
459
+ // analysis, the result is multiplied by 1 + delta %.
460
+ if(sap) {
461
+ // NOTE: Do NOT scale exceptional values.
458
462
  if(v[t] > VM.MINUS_INFINITY && v[t] < VM.PLUS_INFINITY) {
459
463
  v[t] *= (1 + MODEL.sensitivity_delta * 0.01);
460
464
  }
@@ -1849,14 +1853,14 @@ class ExpressionParser {
1849
1853
  if(this.then_stack.length < 1) {
1850
1854
  this.error = 'Unexpected :';
1851
1855
  } else {
1852
- // Similar to above: when a : operator is "coded", the ELSE part
1856
+ // Similar to above: When a : operator is "coded", the ELSE part
1853
1857
  // has been coded, so the end of the code array is the target for
1854
- // the most recently added JUMP
1858
+ // the most recently added JUMP.
1855
1859
  this.code[this.then_stack.pop()][1] = this.code.length;
1856
1860
  }
1857
1861
  } else {
1858
1862
  // All other operations require VM instructions that operate on the
1859
- // expression stack
1863
+ // expression stack.
1860
1864
  this.code.push([op, null]);
1861
1865
  if(op === VMI_concat) {
1862
1866
  this.concatenating = true;
@@ -1864,8 +1868,8 @@ class ExpressionParser {
1864
1868
  const randcode = RANDOM_CODES.indexOf(op) >= 0;
1865
1869
  if(REDUCING_CODES.indexOf(op) >= 0) {
1866
1870
  if(randcode && !this.concatenating) {
1867
- // NOTE: probability distributions MUST have a parameter list but
1868
- // MIN and MAX will also accept a single argument
1871
+ // NOTE: Probability distributions MUST have a parameter list but
1872
+ // MIN and MAX will also accept a single argument.
1869
1873
  console.log('OPERATOR:', op);
1870
1874
  this.error = 'Missing parameter list';
1871
1875
  }
@@ -2120,6 +2124,8 @@ class VirtualMachine {
2120
2124
  this.numeric_issue = '';
2121
2125
  // Warnings are stored in a list to permit browsing through them.
2122
2126
  this.issue_list = [];
2127
+ // Bound issues (UB < LB) are recorded to permit compact warnings.
2128
+ this.bound_issues = {};
2123
2129
  // The call stack tracks evaluation of "nested" expression variables.
2124
2130
  this.call_stack = [];
2125
2131
  this.block_count = 0;
@@ -2468,6 +2474,8 @@ class VirtualMachine {
2468
2474
  // block).
2469
2475
  this.error_count = 0;
2470
2476
  this.block_issues = 0;
2477
+ // Clear bound issue dictionary.
2478
+ this.bound_issues = {};
2471
2479
  // Clear issue list with warnings and hide issue panel.
2472
2480
  this.issue_list.length = 0;
2473
2481
  this.issue_index = -1;
@@ -2581,6 +2589,7 @@ class VirtualMachine {
2581
2589
  // Return number `n` formatted so as to show 2-3 significant digits
2582
2590
  // NOTE: as `n` should be a number, a warning sign will typically
2583
2591
  // indicate a bug in the software.
2592
+ if(typeof n === 'string') n = parseFloat(n);
2584
2593
  if(n === undefined || isNaN(n)) return '\u26A0'; // Warning sign
2585
2594
  const sv = this.specialValue(n);
2586
2595
  // If `n` has a special value, return its representation.
@@ -2608,6 +2617,7 @@ class VirtualMachine {
2608
2617
  // Return number `n` formatted so as to show 4-5 significant digits.
2609
2618
  // NOTE: As `n` should be a number, a warning sign will typically
2610
2619
  // indicate a bug in the software.
2620
+ if(typeof n === 'string') n = parseFloat(n);
2611
2621
  if(n === undefined || isNaN(n)) return '\u26A0';
2612
2622
  const sv = this.specialValue(n);
2613
2623
  // If `n` has a special value, return its representation.
@@ -3204,8 +3214,8 @@ class VirtualMachine {
3204
3214
 
3205
3215
  setBoundConstraints(p) {
3206
3216
  // Set LB and UB constraints for product `p`.
3207
- // NOTE: This method affects the VM coefficient vector, so save it
3208
- // (if needed) before calling this method.
3217
+ // NOTE: This method affects the VM coefficient vector, so this vector
3218
+ // should be saved (using a VM instruction) if it is needed later.
3209
3219
  const
3210
3220
  vi = p.level_var_index,
3211
3221
  lesvi = p.stock_LE_slack_var_index,
@@ -4338,7 +4348,7 @@ class VirtualMachine {
4338
4348
  ' will compromise computation of its binary variables';
4339
4349
  UI.warn(msg);
4340
4350
  this.logMessage(this.block_count,
4341
- 'WARNING: ' + msg.replace(/<\/?strong>/g, '"'));
4351
+ this.WARNING + msg.replace(/<\/?strong>/g, '"'));
4342
4352
  }
4343
4353
  }
4344
4354
  if(hub !== ub) {
@@ -4620,8 +4630,8 @@ class VirtualMachine {
4620
4630
  high_rate) + 1);
4621
4631
  if(this.slack_penalty > VM.MAX_SLACK_PENALTY) {
4622
4632
  this.slack_penalty = VM.MAX_SLACK_PENALTY;
4623
- this.logMessage(this.block_count,
4624
- 'WARNING: Max. slack penalty reached; try to scale down your model coefficients');
4633
+ this.logMessage(this.block_count, this.WARNING +
4634
+ 'Max. slack penalty reached; try to scale down your model coefficients');
4625
4635
  }
4626
4636
  const m = Math.max(
4627
4637
  Math.abs(this.low_coefficient), Math.abs(this.high_coefficient));
@@ -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
- const keys = Object.keys(this.variables_to_fixate);
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);