linny-r 1.8.1 → 1.9.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.8.1",
3
+ "version": "1.9.0",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
Binary file
package/static/index.html CHANGED
@@ -876,7 +876,7 @@ NOTE: Unit symbols are case-sensitive, so BTU ≠ Btu">
876
876
  <a href="https://creativecommons.org/licenses/by-sa/4.0"
877
877
  target="_blank">
878
878
  Creative Commons Attribution-ShareAlike (CC BY-SA) license</a>.
879
- <img src="../images/by-sa.svg" style="height:21px; margin-top:3px">
879
+ <img src="images/by-sa.svg" style="height:21px; margin-top:3px">
880
880
  </td>
881
881
  </tr>
882
882
  </table>
@@ -908,7 +908,7 @@ NOTE: Unit symbols are case-sensitive, so BTU &ne; Btu">
908
908
  <a href="https://creativecommons.org/licenses/by-sa/4.0"
909
909
  target="_blank">
910
910
  Creative Commons Attribution-ShareAlike (CC BY-SA) license</a>.
911
- <img src="../images/by-sa.svg" style="height:21px; margin-top:3px">
911
+ <img src="images/by-sa.svg" style="height:21px; margin-top:3px">
912
912
  </td>
913
913
  </tr>
914
914
  </table>
@@ -1848,6 +1848,8 @@ NOTE: * and ? will be interpreted as wildcards"
1848
1848
  title="Edit selected equation">
1849
1849
  <img id="eq-delete-btn" class="btn disab" src="images/delete.png"
1850
1850
  title="Delete selected equation">
1851
+ <img id="eq-view-btn" class="btn enab" src="images/zoom-in.png"
1852
+ title="Switch to multi-line expression display">
1851
1853
  </div>
1852
1854
  <img id="equation-outcome" class="not-selected" src="images/outcome.png"
1853
1855
  title="Click to consider/ignore selected equation as experiment outcome">
@@ -2266,6 +2268,8 @@ NOTE: * and ? will be interpreted as wildcards"
2266
2268
  title="Rename selected experiment">
2267
2269
  <img id="xp-view-btn" class="btn disab" src="images/table.png"
2268
2270
  title="View selected experiment">
2271
+ <img id="xp-order-btn" class="btn enab" src="images/sel-order.png"
2272
+ title="Specify ordering of selectors">
2269
2273
  <img id="xp-reset-btn" class="btn enab off" src="images/reset.png"
2270
2274
  title="Clear results for selected experiment">
2271
2275
  <img id="xp-delete-btn" class="btn disab" src="images/delete.png"
@@ -2429,6 +2433,24 @@ NOTE: * and ? will be interpreted as wildcards"
2429
2433
  </div>
2430
2434
  </div>
2431
2435
 
2436
+ <!-- the SELECTOR ORDER dialog permits specifying how selectors should
2437
+ be sorted. The selectors may contain a leading or trailing * as
2438
+ wildcard, and can be separated by spacrs, comma's or semicolons.
2439
+ -->
2440
+ <div id="sel-order-modal" class="modal">
2441
+ <div id="sel-order-dlg" class="inp-dlg">
2442
+ <div class="dlg-title">
2443
+ Ordering of selectors
2444
+ <img class="ok-btn" src="images/ok.png">
2445
+ <img class="cancel-btn" src="images/cancel.png">
2446
+ </div>
2447
+ <textarea id="sel-order-lines" autocomplete="off"
2448
+ placeholder="Selectors must be separated by spaces or new lines."
2449
+ autocorrect="off" autocapitalize="off" spellcheck="false">
2450
+ </textarea>
2451
+ </div>
2452
+ </div>
2453
+
2432
2454
  <!-- the PARAMETER dialog prompts for a dataset or chart name -->
2433
2455
  <div id="xp-parameter-modal" class="modal">
2434
2456
  <div id="xp-parameter-dlg" class="inp-dlg">
@@ -2506,12 +2506,26 @@ td.equation-expression {
2506
2506
  white-space: nowrap;
2507
2507
  overflow: hidden;
2508
2508
  text-overflow: ellipsis;
2509
+ font-family: monospace;
2510
+ font-size: 12px;
2509
2511
  }
2510
2512
 
2511
2513
  td.equation-expression {
2512
2514
  min-width: 60%;
2513
2515
  }
2514
2516
 
2517
+ td.equation-expression-multi {
2518
+ border-left: solid 1px Silver;
2519
+ white-space: normal;
2520
+ overflow-x: hidden;
2521
+ overflow-y: auto;
2522
+ text-overflow: ellipsis;
2523
+ font-family: monospace;
2524
+ font-size: 11px;
2525
+ line-height: 13px;
2526
+ max-height: 65px;
2527
+ }
2528
+
2515
2529
  #dataset-blackbox {
2516
2530
  position: absolute;
2517
2531
  bottom: 6px;
@@ -4006,6 +4020,17 @@ div.no-colors {
4006
4020
  width: calc(100% - 6px);
4007
4021
  }
4008
4022
 
4023
+ #sel-order-dlg {
4024
+ width: 220px;
4025
+ height: min-content;
4026
+ }
4027
+
4028
+ #sel-order-lines {
4029
+ margin: 2px;
4030
+ width: calc(100% - 4px);
4031
+ height: 220px;
4032
+ }
4033
+
4009
4034
  #xp-parameter-dlg {
4010
4035
  width: 270px;
4011
4036
  height: 45px;
@@ -11,7 +11,7 @@ warning or information messages are displayed.
11
11
  */
12
12
 
13
13
  /*
14
- Copyright (c) 2017-2022 Delft University of Technology
14
+ Copyright (c) 2017-2024 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
@@ -1025,7 +1025,12 @@ class GUIController extends Controller {
1025
1025
  ' as preferred solver');
1026
1026
  return;
1027
1027
  }
1028
- const pd = postData({action: 'change', solver: sid});
1028
+ const pd = postData({
1029
+ action: 'change',
1030
+ solver: sid,
1031
+ user: VM.solver_user,
1032
+ token: VM.solver_token
1033
+ });
1029
1034
  fetch('solver/', pd)
1030
1035
  .then((response) => {
1031
1036
  if(!response.ok) {
@@ -54,6 +54,9 @@ class EquationManager {
54
54
  'click', () => EQUATION_MANAGER.editEquation());
55
55
  document.getElementById('eq-delete-btn').addEventListener(
56
56
  'click', () => EQUATION_MANAGER.deleteEquation());
57
+ this.view_btn = document.getElementById('eq-view-btn');
58
+ this.view_btn.addEventListener(
59
+ 'click', () => EQUATION_MANAGER.toggleMultiLineDisplay());
57
60
  this.outcome_btn = document.getElementById('equation-outcome');
58
61
  this.outcome_btn.addEventListener(
59
62
  'click', () => EQUATION_MANAGER.toggleOutcome());
@@ -78,6 +81,7 @@ class EquationManager {
78
81
  'click', () => EQUATION_MANAGER.clone_modal.hide());
79
82
 
80
83
  // Initialize the dialog properties
84
+ this.multi_line = false;
81
85
  this.reset();
82
86
  }
83
87
 
@@ -130,6 +134,19 @@ class EquationManager {
130
134
  }
131
135
  }
132
136
 
137
+ toggleMultiLineDisplay() {
138
+ // Toggle between single-line view and multi-line expressions.
139
+ this.multi_line = !this.multi_line;
140
+ if(this.multi_line) {
141
+ this.view_btn.src = 'images/zoom-out.png';
142
+ this.view_btn.title = 'Switch to single-line expression display';
143
+ } else {
144
+ this.view_btn.src = 'images/zoom-in.png';
145
+ this.view_btn.title = 'Switch to multi-line expression display';
146
+ }
147
+ this.updateDialog();
148
+ }
149
+
133
150
  updateDialog() {
134
151
  // Updates equation list, highlighting selected equation (if any)
135
152
  const
@@ -148,10 +165,11 @@ class EquationManager {
148
165
  m = ed.modifiers[UI.nameToID(msl[i])],
149
166
  wild = (m.selector.indexOf('??') >= 0),
150
167
  method = m.selector.startsWith(':'),
168
+ multi = (this.multi_line ? '-multi' : ''),
151
169
  issue = (m.expression.compile_issue ? ' compile-issue' :
152
170
  (m.expression.compute_issue ? ' compute-issue' : '')),
153
171
  clk = '" onclick="EQUATION_MANAGER.selectModifier(event, \'' +
154
- m.selector + '\'',
172
+ escapedSingleQuotes(m.selector) + '\'',
155
173
  mover = (method ? ' onmouseover="EQUATION_MANAGER.showInfo(\'' +
156
174
  m.identifier + '\', event.shiftKey);"' : '');
157
175
  if(m === sm) smid += i;
@@ -165,7 +183,7 @@ class EquationManager {
165
183
  (wild ? ' wildcard' : ''), clk, ', false);"', mover, '>',
166
184
  (m.outcome_equation ? '<span class="outcome"></span>' : ''),
167
185
  (wild ? wildcardFormat(m.selector) : m.selector),
168
- '</td><td class="equation-expression', issue,
186
+ '</td><td class="equation-expression', multi, issue,
169
187
  (issue ? '"title="' +
170
188
  safeDoubleQuotes(m.expression.compile_issue ||
171
189
  m.expression.compute_issue) : ''),
@@ -46,6 +46,9 @@ class GUIExperimentManager extends ExperimentManager {
46
46
  this.view_btn = document.getElementById('xp-view-btn');
47
47
  this.view_btn.addEventListener(
48
48
  'click', () => EXPERIMENT_MANAGER.viewerMode());
49
+ this.sel_order_btn = document.getElementById('xp-order-btn');
50
+ this.sel_order_btn.addEventListener(
51
+ 'click', () => EXPERIMENT_MANAGER.showSelectorOrder());
49
52
  this.reset_btn = document.getElementById('xp-reset-btn');
50
53
  this.reset_btn.addEventListener(
51
54
  'click', () => EXPERIMENT_MANAGER.clearRunResults());
@@ -154,6 +157,13 @@ class GUIExperimentManager extends ExperimentManager {
154
157
  this.rename_modal.cancel.addEventListener(
155
158
  'click', () => EXPERIMENT_MANAGER.rename_modal.hide());
156
159
 
160
+ this.sel_order_modal = new ModalDialog('sel-order');
161
+ this.sel_order_modal.ok.addEventListener(
162
+ 'click', () => EXPERIMENT_MANAGER.modifySelectorOrder());
163
+ this.sel_order_modal.cancel.addEventListener(
164
+ 'click', () => EXPERIMENT_MANAGER.sel_order_modal.hide());
165
+ this.sel_order_lines = this.sel_order_modal.element('lines');
166
+
157
167
  this.parameter_modal = new ModalDialog('xp-parameter');
158
168
  this.parameter_modal.ok.addEventListener(
159
169
  'click', () => EXPERIMENT_MANAGER.addParameter());
@@ -473,9 +483,23 @@ class GUIExperimentManager extends ExperimentManager {
473
483
  }
474
484
  }
475
485
  }
486
+
487
+ showSelectorOrder() {
488
+ // Show selector order modal.
489
+ this.sel_order_lines.value = MODEL.selector_order_string;
490
+ this.sel_order_modal.show();
491
+ }
476
492
 
493
+ modifySelectorOrder() {
494
+ // Save text area contents as new selector order string.
495
+ MODEL.selector_order_string = this.sel_order_lines.value.trim();
496
+ MODEL.selector_order_list = MODEL.selector_order_string.trim().split(/\s+/);
497
+ this.sel_order_modal.hide();
498
+ UI.updateControllerDialogs('DX');
499
+ }
500
+
477
501
  designMode() {
478
- // Switch to default view
502
+ // Switch to default view.
479
503
  this.viewer.style.display = 'none';
480
504
  this.design.style.display = 'block';
481
505
  }
@@ -380,7 +380,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
380
380
  }
381
381
  va.innerHTML = options.join('');
382
382
  // NOTE: Chart Manager variable dialog is 60px wider
383
- va.style.width = (prefix ? 'calc(100% - 84px)' : 'calc(100% - 142px)');
383
+ va.style.width = (prefix ? 'calc(100% - 86px)' : 'calc(100% - 142px)');
384
384
  return;
385
385
  }
386
386
  // Add "empty" as first and initial option, as it denotes "use default"
@@ -333,7 +333,12 @@ class GUIFileManager {
333
333
  return response.text();
334
334
  })
335
335
  .then((data) => {
336
- UI.postResponseOK(data);
336
+ if(!UI.postResponseOK(data) && data.indexOf('not implemented') >= 0) {
337
+ // Switch off auto-save when server does not implement it.
338
+ AUTO_SAVE.interval = 0;
339
+ AUTO_SAVE.not_implemented = true;
340
+ console.log('Auto-save disabled');
341
+ }
337
342
  bcl.remove('stay-activ');
338
343
  })
339
344
  .catch((err) => {
@@ -40,7 +40,7 @@ class Finder {
40
40
  this.dialog = UI.draggableDialog('finder');
41
41
  UI.resizableDialog('finder', 'FINDER');
42
42
  this.close_btn = document.getElementById('finder-close-btn');
43
- // Make toolbar buttons responsive
43
+ // Make toolbar buttons responsive.
44
44
  this.close_btn.addEventListener('click', (e) => UI.toggleDialog(e));
45
45
  this.filter_input = document.getElementById('finder-filter-text');
46
46
  this.filter_input.addEventListener('input', () => FINDER.changeFilter());
@@ -78,6 +78,7 @@ class Finder {
78
78
  this.filtered_types.length = 0;
79
79
  this.selected_entity = null;
80
80
  this.filter_input.value = '';
81
+ this.filter_string = '';
81
82
  this.filter_pattern = null;
82
83
  this.entity_types = VM.entity_letters;
83
84
  this.find_links = true;
@@ -105,7 +106,7 @@ class Finder {
105
106
  }
106
107
 
107
108
  enterKey() {
108
- // Open "edit properties" dialog for the selected entity
109
+ // Open "edit properties" dialog for the selected entity.
109
110
  const srl = this.entity_table.getElementsByClassName('sel-set');
110
111
  if(srl.length > 0) {
111
112
  const r = this.entity_table.rows[srl[0].rowIndex];
@@ -118,7 +119,7 @@ class Finder {
118
119
  }
119
120
 
120
121
  upDownKey(dir) {
121
- // Select row above or below the selected one (if possible)
122
+ // Select row above or below the selected one (if possible).
122
123
  const srl = this.entity_table.getElementsByClassName('sel-set');
123
124
  if(srl.length > 0) {
124
125
  const r = this.entity_table.rows[srl[0].rowIndex + dir];
@@ -139,7 +140,7 @@ class Finder {
139
140
  let imgs = '';
140
141
  this.entities.length = 0;
141
142
  this.filtered_types.length = 0;
142
- // No list unless a pattern OR a specified SUB-set of entity types
143
+ // No list unless a pattern OR a specified SUB-set of entity types.
143
144
  if(fp || et && et !== VM.entity_letters) {
144
145
  if(et.indexOf('A') >= 0) {
145
146
  imgs += '<img src="images/actor.png">';
@@ -151,7 +152,7 @@ class Finder {
151
152
  }
152
153
  }
153
154
  }
154
- // NOTE: do not list black-boxed entities
155
+ // NOTE: Do not list black-boxed entities.
155
156
  if(et.indexOf('P') >= 0) {
156
157
  imgs += '<img src="images/process.png">';
157
158
  for(let k in MODEL.processes) if(MODEL.processes.hasOwnProperty(k)) {
@@ -191,7 +192,7 @@ class Finder {
191
192
  const ds = MODEL.datasets[k];
192
193
  if(!k.startsWith(UI.BLACK_BOX) && (!fp || patternMatch(
193
194
  ds.displayName, this.filter_pattern))) {
194
- // NOTE: do not list the equations dataset
195
+ // NOTE: Do not list the equations dataset.
195
196
  if(ds !== MODEL.equations_dataset) {
196
197
  enl.push(k);
197
198
  this.entities.push(MODEL.datasets[k]);
@@ -217,11 +218,11 @@ class Finder {
217
218
  if(et.indexOf('L') >= 0) {
218
219
  imgs += '<img src="images/link.png">';
219
220
  for(let k in MODEL.links) if(MODEL.links.hasOwnProperty(k)) {
220
- // NOTE: "black-boxed" link identifiers are not prefixed => other test
221
+ // NOTE: "black-boxed" link identifiers are not prefixed => other test.
221
222
  const
222
223
  l = MODEL.links[k],
223
224
  ldn = l.displayName,
224
- // A links is "black-boxed" when BOTH nodes are "black-boxed"
225
+ // A link is "black-boxed" when BOTH nodes are "black-boxed".
225
226
  bb = ldn.split(UI.BLACK_BOX).length > 2;
226
227
  if(!bb && (!fp || patternMatch(ldn, this.filter_pattern))) {
227
228
  enl.push(k);
@@ -233,7 +234,7 @@ class Finder {
233
234
  if(et.indexOf('B') >= 0) {
234
235
  imgs += '<img src="images/constraint.png">';
235
236
  for(let k in MODEL.constraints) {
236
- // NOTE: likewise, constraint identifiers can be prefixed by %
237
+ // NOTE: Likewise, constraint identifiers can be prefixed by %.
237
238
  if(MODEL.constraints.hasOwnProperty(k)) {
238
239
  if(!k.startsWith(UI.BLACK_BOX) && (!fp || patternMatch(
239
240
  MODEL.constraints[k].displayName, this.filter_pattern))) {
@@ -244,6 +245,35 @@ class Finder {
244
245
  }
245
246
  }
246
247
  }
248
+ // Also allow search for scale unit names.
249
+ if(et.indexOf('U') >= 0) {
250
+ imgs += '<img src="images/scale.png">';
251
+ for(let k in MODEL.products) if(MODEL.products.hasOwnProperty(k)) {
252
+ if(fp && !k.startsWith(UI.BLACK_BOX) && patternMatch(
253
+ MODEL.products[k].scale_unit, this.filter_pattern)) {
254
+ enl.push(k);
255
+ this.entities.push(MODEL.products[k]);
256
+ addDistinct('Q', this.filtered_types);
257
+ }
258
+ }
259
+ }
260
+ // Also allow search for link multiplier symbols.
261
+ if(et.indexOf('M') >= 0) {
262
+ if(imgs.indexOf('/link.') < 0) imgs += '<img src="images/link.png">';
263
+ for(let k in MODEL.links) if(MODEL.links.hasOwnProperty(k)) {
264
+ // NOTE: "black-boxed" link identifiers are not prefixed => other test.
265
+ const
266
+ l = MODEL.links[k],
267
+ m = VM.LM_LETTERS.charAt(l.multiplier),
268
+ // A link is "black-boxed" when BOTH nodes are "black-boxed".
269
+ bb = l.displayName.split(UI.BLACK_BOX).length > 2;
270
+ if(fp && !bb && this.filter_string.indexOf(m) >= 0) {
271
+ enl.push(k);
272
+ this.entities.push(l);
273
+ addDistinct('L', this.filtered_types);
274
+ }
275
+ }
276
+ }
247
277
  enl.sort((a, b) => UI.compareFullNames(a, b, true));
248
278
  }
249
279
  document.getElementById('finder-entity-imgs').innerHTML = imgs;
@@ -259,7 +289,7 @@ class Finder {
259
289
  e.type.toLowerCase(), '.png">', e.displayName,
260
290
  '</td></tr>'].join(''));
261
291
  }
262
- // NOTE: reset `selected_entity` if not in the new list
292
+ // NOTE: Reset `selected_entity` if not in the new list.
263
293
  if(seid === 'etr') this.selected_entity = null;
264
294
  this.entity_table.innerHTML = el.join('');
265
295
  UI.scrollIntoView(document.getElementById(seid));
@@ -316,18 +346,18 @@ class Finder {
316
346
  let hdr = '(no entity selected)';
317
347
  if(se) {
318
348
  hdr = `<em>${se.type}:</em> <strong>${se.displayName}</strong>`;
319
- // Make occurrence list
349
+ // Make occurrence list.
320
350
  if(se instanceof Process || se instanceof Cluster) {
321
- // Processes and clusters "occur" in their parent cluster
351
+ // Processes and clusters "occur" in their parent cluster.
322
352
  if(se.cluster) occ.push(se.cluster.identifier);
323
353
  } else if(se instanceof Product) {
324
- // Products "occur" in clusters where they have a position
354
+ // Products "occur" in clusters where they have a position.
325
355
  const cl = se.productPositionClusters;
326
356
  for(let i = 0; i < cl.length; i++) {
327
357
  occ.push(cl[i].identifier);
328
358
  }
329
359
  } else if(se instanceof Actor) {
330
- // Actors "occur" in clusters where they "own" processes or clusters
360
+ // Actors "occur" in clusters where they "own" processes or clusters.
331
361
  for(let k in MODEL.processes) if(MODEL.processes.hasOwnProperty(k)) {
332
362
  const p = MODEL.processes[k];
333
363
  if(p.actor === se) occ.push(p.identifier);
@@ -337,13 +367,13 @@ class Finder {
337
367
  if(c.actor === se) occ.push(c.identifier);
338
368
  }
339
369
  } else if(se instanceof Link || se instanceof Constraint) {
340
- // Links and constraints "occur" in their "best" parent cluster
370
+ // Links and constraints "occur" in their "best" parent cluster.
341
371
  const c = MODEL.inferParentCluster(se);
342
372
  if(c) occ.push(c.identifier);
343
373
  }
344
- // NOTE: no "occurrence" of datasets or equations
374
+ // NOTE: No "occurrence" of datasets or equations.
345
375
  // @@TO DO: identify MODULES (?)
346
- // All entities can also occur as chart variables
376
+ // All entities can also occur as chart variables.
347
377
  for(let j = 0; j < MODEL.charts.length; j++) {
348
378
  const c = MODEL.charts[j];
349
379
  for(let k = 0; k < c.variables.length; k++) {
@@ -355,12 +385,12 @@ class Finder {
355
385
  }
356
386
  }
357
387
  }
358
- // Now also look for occurrences of entity references in expressions
388
+ // Now also look for occurrences of entity references in expressions.
359
389
  const
360
390
  raw = escapeRegex(se.displayName),
361
391
  re = new RegExp(
362
392
  '\\[\\s*!?' + raw.replace(/\s+/g, '\\s+') + '\\s*[\\|\\@\\]]');
363
- // Check actor weight expressions
393
+ // Check actor weight expressions.
364
394
  for(let k in MODEL.actors) if(MODEL.actors.hasOwnProperty(k)) {
365
395
  const a = MODEL.actors[k];
366
396
  if(re.test(a.weight.text)) {
@@ -368,7 +398,7 @@ class Finder {
368
398
  xol.push(a.identifier);
369
399
  }
370
400
  }
371
- // Check all process attribute expressions
401
+ // Check all process attribute expressions.
372
402
  for(let k in MODEL.processes) if(MODEL.processes.hasOwnProperty(k)) {
373
403
  const p = MODEL.processes[k];
374
404
  if(re.test(p.lower_bound.text)) {
@@ -384,7 +414,7 @@ class Finder {
384
414
  xol.push(p.identifier);
385
415
  }
386
416
  }
387
- // Check all product attribute expressions
417
+ // Check all product attribute expressions.
388
418
  for(let k in MODEL.products) if(MODEL.products.hasOwnProperty(k)) {
389
419
  const p = MODEL.products[k];
390
420
  if(re.test(p.lower_bound.text)) {
@@ -404,7 +434,7 @@ class Finder {
404
434
  xol.push(p.identifier);
405
435
  }
406
436
  }
407
- // Check all notes in clusters for their color expressions and field
437
+ // Check all notes in clusters for their color expressions and field.
408
438
  for(let k in MODEL.clusters) if(MODEL.clusters.hasOwnProperty(k)) {
409
439
  const c = MODEL.clusters[k];
410
440
  for(let i = 0; i < c.notes.length; i++) {
@@ -416,7 +446,7 @@ class Finder {
416
446
  }
417
447
  }
418
448
  }
419
- // Check all link rate expressions
449
+ // Check all link rate expressions.
420
450
  for(let k in MODEL.links) if(MODEL.links.hasOwnProperty(k)) {
421
451
  const l = MODEL.links[k];
422
452
  if(re.test(l.relative_rate.text)) {
@@ -428,7 +458,7 @@ class Finder {
428
458
  xol.push(l.identifier);
429
459
  }
430
460
  }
431
- // Check all dataset modifier expressions
461
+ // Check all dataset modifier expressions.
432
462
  for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
433
463
  const ds = MODEL.datasets[k];
434
464
  for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
@@ -451,22 +481,22 @@ class Finder {
451
481
  '</td></tr>'].join(''));
452
482
  }
453
483
  this.item_table.innerHTML = el.join('');
454
- // Clear the table row list
484
+ // Clear the table row list.
455
485
  el.length = 0;
456
- // Now fill it with entity+attribute having a matching expression
486
+ // Now fill it with entity+attribute having a matching expression.
457
487
  for(let i = 0; i < xal.length; i++) {
458
488
  const
459
489
  id = xol[i],
460
490
  e = MODEL.objectByID(id),
461
491
  attr = (e instanceof Note ? '' : xal[i]);
462
492
  let img = e.type.toLowerCase(),
463
- // NOTE: a small left-pointing triangle denotes that the right-hand
464
- // part has the left hand part as its attribute
493
+ // NOTE: A small left-pointing triangle denotes that the right-hand
494
+ // part has the left hand part as its attribute.
465
495
  cs = '',
466
496
  td = attr + '</td><td>&#x25C2;</td><td style="width:95%">' +
467
497
  e.displayName;
468
- // NOTE: equations may have LONG names while the equations dataset name
469
- // is irrelevant, hence use 3 columns (no triangle)
498
+ // NOTE: Equations may have LONG names while the equations dataset
499
+ // name is irrelevant, hence use 3 columns (no triangle).
470
500
  if(e === MODEL.equations_dataset) {
471
501
  img = 'equation';
472
502
  cs = ' colspan="3"';
@@ -484,7 +514,7 @@ class Finder {
484
514
  }
485
515
 
486
516
  drag(ev) {
487
- // Start dragging the selected entity
517
+ // Start dragging the selected entity.
488
518
  let t = ev.target;
489
519
  while(t && t.nodeName !== 'TD') t = t.parentNode;
490
520
  ev.dataTransfer.setData('text', MODEL.objectByName(t.innerText).identifier);
@@ -493,30 +523,31 @@ class Finder {
493
523
 
494
524
  changeFilter() {
495
525
  // Filter expression can start with 1+ entity letters plus `?` to
496
- // look only for the entity types denoted by these letters
526
+ // look only for the entity types denoted by these letters.
497
527
  let ft = this.filter_input.value,
498
528
  et = VM.entity_letters;
499
- if(/^(\*|[ABCDELPQ]+)\?/i.test(ft)) {
529
+ if(/^(\*|U|M|[ABCDELPQ]+)\?/i.test(ft)) {
500
530
  ft = ft.split('?');
501
- // NOTE: *? denotes "all entity types except constraints"
531
+ // NOTE: *? denotes "all entity types except constraints".
502
532
  et = (ft[0] === '*' ? 'ACDELPQ' : ft[0].toUpperCase());
503
533
  ft = ft.slice(1).join('=');
504
534
  }
535
+ this.filter_string = ft;
505
536
  this.filter_pattern = patternList(ft);
506
537
  this.entity_types = et;
507
538
  this.updateDialog();
508
539
  }
509
540
 
510
541
  showInfo(id, shift) {
511
- // Displays documentation for the entity identified by `id`
542
+ // Display documentation for the entity identified by `id`.
512
543
  const e = MODEL.objectByID(id);
513
544
  if(e) DOCUMENTATION_MANAGER.update(e, shift);
514
545
  }
515
546
 
516
547
  selectEntity(id, alt=false) {
517
- // Looks up entity, selects it in the left pane, and updates the
518
- // right pane; opens the "edit properties" modal dialog on double-click
519
- // and Alt-click if the entity is editable
548
+ // Look up entity, select it in the left pane, and update the right
549
+ // pane. Open the "edit properties" modal dialog on double-click
550
+ // and Alt-click if the entity is editable.
520
551
  const obj = MODEL.objectByID(id);
521
552
  this.selected_entity = obj;
522
553
  this.updateDialog();
@@ -551,7 +582,7 @@ class Finder {
551
582
  }
552
583
 
553
584
  reveal(id) {
554
- // Shows selected occurrence
585
+ // Show selected occurrence.
555
586
  const
556
587
  se = this.selected_entity,
557
588
  obj = (se ? MODEL.objectByID(id) : null);
@@ -559,7 +590,7 @@ class Finder {
559
590
  // If cluster, make it focal...
560
591
  if(obj instanceof Cluster) {
561
592
  UI.makeFocalCluster(obj);
562
- // ... and select the entity unless it is an actor or dataset
593
+ // ... and select the entity unless it is an actor or dataset.
563
594
  if(!(se instanceof Actor || se instanceof Dataset)) {
564
595
  MODEL.select(se);
565
596
  if(se instanceof Link || se instanceof Constraint) {
@@ -572,7 +603,7 @@ class Finder {
572
603
  } else if(obj instanceof Process || obj instanceof Note) {
573
604
  // If occurrence is a process or a note, then make its cluster focal...
574
605
  UI.makeFocalCluster(obj.cluster);
575
- // ... and select it
606
+ // ... and select it.
576
607
  MODEL.select(obj);
577
608
  UI.scrollIntoView(obj.shape.element.childNodes[0]);
578
609
  } else if(obj instanceof Product) {
@@ -586,7 +617,7 @@ class Finder {
586
617
  if(a) UI.scrollIntoView(a.shape.element.childNodes[0]);
587
618
  }
588
619
  } else if(obj instanceof Chart) {
589
- // If occurrence is a chart, select and show it in the chart manager
620
+ // If occurrence is a chart, select and show it in the chart manager.
590
621
  CHART_MANAGER.chart_index = MODEL.charts.indexOf(obj);
591
622
  if(CHART_MANAGER.chart_index >= 0) {
592
623
  if(UI.hidden('chart-dlg')) {
@@ -595,7 +626,7 @@ class Finder {
595
626
  }
596
627
  CHART_MANAGER.updateDialog();
597
628
  }
598
- // NOTE: return the object to save a second lookup by revealExpression
629
+ // NOTE: Return the object to save a second lookup by revealExpression.
599
630
  return obj;
600
631
  }
601
632
 
@@ -41,6 +41,7 @@ class ModelAutoSaver {
41
41
  this.timeout_id = 0;
42
42
  this.interval = 10; // auto-save every 10 minutes
43
43
  this.period = 24; // delete models older than 24 hours
44
+ this.not_implemented = false;
44
45
  this.model_list = [];
45
46
  // Overwite defaults if settings still in local storage of browser.
46
47
  this.getSettings();
@@ -65,6 +66,7 @@ class ModelAutoSaver {
65
66
 
66
67
  getSettings() {
67
68
  // Reads custom auto-save settings from local storage.
69
+ if(this.not_implemented) return;
68
70
  try {
69
71
  const item = window.localStorage.getItem('Linny-R-autosave');
70
72
  if(item) {
@@ -86,6 +88,7 @@ class ModelAutoSaver {
86
88
 
87
89
  setSettings() {
88
90
  // Writes custom auto-save settings to local storage.
91
+ if(this.not_implemented) return;
89
92
  try {
90
93
  window.localStorage.setItem('Linny-R-autosave',
91
94
  this.interval + '|' + this.period);
@@ -199,7 +202,7 @@ class ModelAutoSaver {
199
202
  // Close the restore auto-save model dialog.
200
203
  document.getElementById('confirm-remove-models').style.display = 'none';
201
204
  // NOTE: Cancel button or ESC will pass `cancel` as FALSE => do not save.
202
- if(!save) {
205
+ if(!save || this.not_implemented) {
203
206
  document.getElementById('restore-modal').style.display = 'none';
204
207
  return;
205
208
  }
@@ -326,6 +326,7 @@ class GUIMonitor {
326
326
  } else if(jsr.server) {
327
327
  VM.solver_token = jsr.token;
328
328
  VM.selectSolver(jsr.solver);
329
+ VM.solver_list = jsr.solver_list;
329
330
  // Remote solver may indicate user-specific solver time limit.
330
331
  let utl = '';
331
332
  if(jsr.time_limit) {
@@ -368,7 +368,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
368
368
  }
369
369
 
370
370
  addRepository(name) {
371
- // Adds repository if name is unique and valid
371
+ // Adds repository if name is unique and valid.
372
372
  let r = null,
373
373
  can_store = false;
374
374
  if(name.endsWith('+')) {
@@ -389,8 +389,8 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
389
389
  }
390
390
 
391
391
  removeRepository() {
392
- // Removes selected repository from list
393
- // NOTE: do not remove the first item (local host)
392
+ // Removes selected repository from list.
393
+ // NOTE: Do not remove the first item (local host).
394
394
  if(this.repository_index < 1) return;
395
395
  fetch('repo/', postData({
396
396
  action: 'remove',
@@ -415,7 +415,8 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
415
415
  }
416
416
 
417
417
  promptForRepository() {
418
- // Opens "Add repository" dialog
418
+ // Open "Add repository" dialog (only on local host).
419
+ if(!this.isLocalHost) return;
419
420
  this.add_modal.element('name').value = '';
420
421
  this.add_modal.element('url').value = '';
421
422
  this.add_modal.element('token').value = '';
@@ -423,7 +424,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
423
424
  }
424
425
 
425
426
  registerRepository() {
426
- // Checks whether URL defines a Linny-R repository, and if so, adds it
427
+ // Check whether URL defines a Linny-R repository, and if so, add it.
427
428
  fetch('repo/', postData({
428
429
  action: 'add',
429
430
  repo: this.add_modal.element('name').value,
@@ -341,7 +341,7 @@ module.exports = class MILPSolver {
341
341
  'write problem', s.solver_model,
342
342
  'set limit time %T%',
343
343
  'set numerics feastol %I%',
344
- // NOTE: MIP gap setting for SCIP is unclear, hence ignored.
344
+ 'set limit gap %M%',
345
345
  'optimize',
346
346
  'write solution', s.solution,
347
347
  'quit'
@@ -392,6 +392,7 @@ module.exports = class MILPSolver {
392
392
  s.args = [
393
393
  '-timeout %T%',
394
394
  '-v4',
395
+ '-ac 5e-6',
395
396
  '-e %I%',
396
397
  '-gr %M%',
397
398
  '-epsel 1e-7',
@@ -838,14 +839,13 @@ module.exports = class MILPSolver {
838
839
  }
839
840
  }
840
841
  if(solved) {
841
- // Look for line with first variable.
842
- let i = 0;
843
- while(i < output.length && !output[i].startsWith('X')) i++;
842
+ // Line 0 holds solution status, line 1 the objective value,
843
+ // and lines 2+ the variables.
844
+ result.obj = parseFloat(output[1].split(':')[1]);
844
845
  // Fill dictionary with variable name: value entries .
845
- while(i < output.length) {
846
+ for(let i = 2; i < output.length; i++) {
846
847
  const v = output[i].split(/\s+/);
847
848
  x_dict[v[0]] = parseFloat(v[1]);
848
- i++;
849
849
  }
850
850
  // Fill the solution vector, adding 0 for missing columns.
851
851
  getValuesFromDict();
@@ -873,6 +873,8 @@ module.exports = class MILPSolver {
873
873
  }
874
874
  result.messages = msgs;
875
875
  if(solved) {
876
+ // Get value of objective function
877
+ result.obj = parseFloat(output[i].split(':')[1]);
876
878
  // Look for line with first variable.
877
879
  while(i < output.length && !output[i].startsWith('X')) i++;
878
880
  // Fill dictionary with variable name: value entries.
@@ -70,6 +70,8 @@ class LinnyRModel {
70
70
  this.charts = [];
71
71
  this.experiments = [];
72
72
  this.dimensions = [];
73
+ this.selector_order_string = '';
74
+ this.selector_order_list = [];
73
75
  this.next_process_number = 0;
74
76
  this.next_product_number = 0;
75
77
  this.focal_cluster = null;
@@ -104,7 +106,7 @@ class LinnyRModel {
104
106
  this.preferred_solver = ''; // empty string denotes "use default"
105
107
  this.integer_tolerance = 5e-7; // integer feasibility tolerance
106
108
  this.MIP_gap = 1e-4; // relative MIP gap
107
- this.always_diagnose = false;
109
+ this.always_diagnose = true;
108
110
 
109
111
  // Sensitivity-related properties
110
112
  this.base_case_selectors = '';
@@ -2905,6 +2907,10 @@ class LinnyRModel {
2905
2907
  }
2906
2908
  }
2907
2909
  }
2910
+ // Infer selector order list by splitting text on any white space.
2911
+ this.selector_order_string = xmlDecoded(nodeContentByTag(node,
2912
+ 'selector-order'));
2913
+ this.selector_order_list = this.selector_order_string.trim().split(/\s+/);
2908
2914
  // Infer dimensions of experimental design space
2909
2915
  this.inferDimensions();
2910
2916
  // NOTE: when including a model, IGNORE sensitivity analysis, experiments
@@ -3074,8 +3080,11 @@ class LinnyRModel {
3074
3080
  if(this.datasets.hasOwnProperty(obj)) xml += this.datasets[obj].asXML;
3075
3081
  }
3076
3082
  xml += '</datasets><charts>';
3077
- for(let i = 0; i < this.charts.length; i++) xml += this.charts[i].asXML;
3078
- xml += '</charts>';
3083
+ for(let i = 0; i < this.charts.length; i++) {
3084
+ xml += this.charts[i].asXML;
3085
+ }
3086
+ xml += '</charts><selector-order>' +
3087
+ xmlEncoded(this.selector_order_string) + '</selector-order>';
3079
3088
  // NOTE: when "black-boxing", SA and experiments are not stored
3080
3089
  if(!this.black_box) {
3081
3090
  xml += '<base-case-selectors>' +
@@ -7943,54 +7952,59 @@ class Product extends Node {
7943
7952
  }
7944
7953
 
7945
7954
  get isSinkNode() {
7946
- // Returns TRUE if this product behaves as a sink
7955
+ // Return TRUE if this product behaves as a sink.
7947
7956
  return (this.is_sink || this.allOutputsAreData) &&
7948
7957
  !(this.upper_bound.defined ||
7949
- // NOTE: UB may be set by equalling it to LB
7958
+ // NOTE: UB may be set by equalling it to LB.
7950
7959
  (this.equal_bounds && this.lower_bound.defined));
7951
7960
  }
7952
7961
 
7953
7962
  highestUpperBound(visited) {
7954
- // Infers the upper bound for this product from its own UB, or from its
7955
- // ingoing links (type, rate, and UB of their from nodes)
7956
- // NOTE: this is used while compiling the VM instructions that compute the
7957
- // ON/OFF binary variable for this product
7958
- // NOTE: this method performs a graph traversal. If this product is part
7959
- // of a cycle in the graph, its highest UB co-depends on its own, which
7960
- // is not constrained, so return +INF
7961
- // NOTE: no need to check for sink nodes, as even on those nodes a max. UB
7962
- // might be inferred from their max. inflows
7963
+ // Infer the upper bound for this product from its own UB, or from
7964
+ // its ingoing links (type, rate, and UB of their from nodes).
7965
+ // This is used while compiling the VM instructions that compute the
7966
+ // ON/OFF binary variable for this product.
7967
+ // This method performs a graph traversal. If this product is part
7968
+ // of a cycle in the graph, its highest UB co-depends on its own UB,
7969
+ // which is not constrained, so then return +INF.
7970
+ // No need to check for sink nodes, as even on those nodes a max. UB
7971
+ // might be inferred from their max. inflows.
7963
7972
  if(visited.indexOf(this) >= 0) return VM.PLUS_INFINITY;
7964
7973
  let ub = (this.equal_bounds ? this.lower_bound : this.upper_bound);
7965
- // If an expression, return +INF to signal "no lower UB can be inferred"
7974
+ // If dynamic expression, return +INF to signal "no lower UB can be inferred".
7966
7975
  if(ub.defined && !ub.isStatic) return VM.PLUS_INFINITY;
7967
- // If static, use its value as initial highest value
7968
- const max_ub = ub.result(0);
7969
- // See if the sum of its max. inflows will be lower than this value
7976
+ // If static, use its value as initial highest value.
7977
+ const max_ub = (ub.defined ? ub.result(0) : VM.PLUS_INFINITY);
7978
+ // See if the sum of its max. inflows will be lower than this value.
7970
7979
  let sum = 0;
7971
- // Preclude infinite recursion
7980
+ // Preclude infinite recursion.
7972
7981
  visited.push(this);
7973
7982
  for(let i = 0; i < this.inputs.length; i++) {
7974
7983
  const
7975
7984
  l = this.inputs[i],
7976
7985
  r = l.relative_rate,
7977
7986
  fn = l.from_node;
7978
- // Dynamic rate => inflows cannot constrain the UB any further
7987
+ // Dynamic rate => inflows cannot constrain the UB any further.
7979
7988
  if(!r.isStatic) return max_ub;
7989
+ let rate = r.result(0);
7980
7990
  if([VM.LM_STARTUP, VM.LM_POSITIVE, VM.LM_ZERO, VM.LM_FIRST_COMMIT,
7981
7991
  VM.LM_SHUTDOWN].indexOf(l.multiplier) >= 0) {
7982
- // For binary multipliers, the rate is the highest possible flow
7983
- // NOTE: do not add negative flows, as actual flow may be 0
7984
- sum += Math.max(0, r.result(0));
7992
+ // For binary multipliers, the rate is the highest possible flow.
7993
+ // NOTE: Do not add negative flows, as actual flow may be 0.
7994
+ sum += Math.max(0, rate);
7985
7995
  } else {
7986
- // For other multipliers, max flow = rate * UB of the FROM node
7987
- // (for products, this will recurse; processes return their UB)
7996
+ // For other multipliers, max flow = rate * UB of the FROM node.
7997
+ // For products, this will recurse; processes return their UB.
7988
7998
  let fnub = fn.highestUpperBound(visited);
7989
7999
  // If +INF, no lower UB can be inferred => return initial maximum
7990
8000
  if(fnub >= VM.PLUS_INFINITY) return max_ub;
7991
- // Otherwise, add rate * UB to the max. total inflow
7992
- // NOTE: do not add negative flows, as actual flow may be 0
7993
- sum += Math.max(0, r.result(0) * fnub);
8001
+ // Otherwise, add rate * UB to the max. total inflow.
8002
+ // NOTE: For SUM multipliers, also consider the delay.
8003
+ if(l.multiplier === VM.LM_SUM) {
8004
+ rate *= Math.max(1, l.flow_delay.result(0));
8005
+ }
8006
+ // NOTE: Do not add negative flows, as actual flow may be 0.
8007
+ sum += Math.max(0, rate * fnub);
7994
8008
  }
7995
8009
  }
7996
8010
  // Return the sum of max. inflows as the lowest max. UB, or the initial
@@ -492,7 +492,7 @@ function matchingNumber(m, s) {
492
492
  // (where asterisks match 0 or more characters, and question marks 1
493
493
  // character) and the matching parts jointly denote an integer.
494
494
  // NOTE: A "+" must be escaped, "*" and "?" must become groups.
495
- let raw = s.replaceAll('+', '\+')
495
+ let raw = s.replaceAll('+', '\\+')
496
496
  .replace(/\*/g, '(.*)').replace(/\?/g, '(.)'),
497
497
  match = m.match(new RegExp(`^${raw}$`)),
498
498
  n = '';
@@ -534,9 +534,21 @@ function compareSelectors(s1, s2) {
534
534
  // Dataset selectors comparison is case-insensitive, and puts wildcards
535
535
  // last, where * comes later than ?, and leading colons come AFTER
536
536
  // regular selector names.
537
- // NOTE: Without wildcards, strings that are identical except for the
538
- // digits they *end* on are sorted on this "end number" (so abc12 > abc2).
539
- // NOTE: This also applies to percentages ("end number"+ %).
537
+ // NOTES:
538
+ // (1) Without wildcards, strings that are identical except for
539
+ // the digits they *end* on are sorted on this "end number"
540
+ // (so abc12 > abc2).
541
+ // (2) This also applies to percentages ("end number"+ %).
542
+ // (3) As of version 1.9.0, the order of selectors can be (partially)
543
+ // specified by the modeler.
544
+ if(MODEL) {
545
+ const
546
+ i1 = MODEL.selector_order_list.indexOf(s1),
547
+ i2 = MODEL.selector_order_list.indexOf(s2);
548
+ // BOTH selectors must be in the list, or regular sorting order is
549
+ // applied.
550
+ if(i1 >= 0 && i2 >= 0) return i1 - i2;
551
+ }
540
552
  if(s1 === s2) return 0;
541
553
  if(s1 === '*') return 1;
542
554
  if(s2 === '*') return -1;
@@ -580,22 +592,36 @@ function compareSelectors(s1, s2) {
580
592
  // by as many spaces (ASCII 32) and add a '!' (ASCII 33). This will then
581
593
  // "sorts things out".
582
594
  let n = s_1.length,
583
- i = n - 1;
595
+ i = n - 1,
596
+ plusmin = false;
584
597
  while(i >= 0 && s_1[i] === '-') i--;
585
- // If trailing minuses, replace by as many spaces and add an exclamation point
598
+ // If trailing minuses, replace by as many spaces and add an exclamation
599
+ // point.
586
600
  if(i < n - 1) {
587
- s_1 = s_1.substring(0, i);
601
+ s_1 = s_1.substring(0, i + 1);
602
+ // NOTE: Consider X- as lower than just X.
603
+ if(s_1 === s_2) return -1;
588
604
  while(s_1.length < n) s_1 += ' ';
589
605
  s_1 += '!';
606
+ plusmin = true;
590
607
  }
591
- // Do the same for the second "normalized" selector
608
+ // Do the same for the second "normalized" selector.
592
609
  n = s_2.length;
593
610
  i = n - 1;
594
611
  while(i >= 0 && s_2[i] === '-') i--;
595
612
  if(i < n - 1) {
596
- s_2 = s_2.substring(0, i);
613
+ s_2 = s_2.substring(0, i + 1);
614
+ // NOTE: Consider X as higher than X-.
615
+ if(s_2 === s_1) return 1;
597
616
  while(s_2.length < n) s_2 += ' ';
598
617
  s_2 += '!';
618
+ plusmin = true;
619
+ }
620
+ if(plusmin) {
621
+ // As X0 typically denotes "base case", replace a trailing zero by
622
+ // ")" to ensure that X- < X0 < X+.
623
+ s_1.replace(/([^0])0$/, '$1)');
624
+ s_2.replace(/([^0])0$/, '$1)');
599
625
  }
600
626
  // Now compare the two "normalized" selectors
601
627
  if(s_1 < s_2) return -1;
@@ -317,6 +317,7 @@ class Expression {
317
317
  if((typeof number !== 'number' ||
318
318
  (this.isStatic && !this.isWildcardExpression)) &&
319
319
  !this.isMethod) return this.vector;
320
+ //console.log('HERE choosing wcnr', number, this);
320
321
  // Method expressions are not "numbered" but differentiate by the
321
322
  // entity to which they are applied. Their "vector number" is then
322
323
  // inferred by looking up this entity in a method object list.
@@ -331,7 +332,9 @@ class Expression {
331
332
  }
332
333
  // Use the vector for the wildcard number (create it if necessary).
333
334
  if(!this.wildcard_vectors.hasOwnProperty(number)) {
335
+ //console.log('HERE adding wc vector', number, this);
334
336
  this.wildcard_vectors[number] = [];
337
+ //console.log('HERE adding wc vector', number, this.wildcard_vectors);
335
338
  if(this.isStatic) {
336
339
  this.wildcard_vectors[number][0] = VM.NOT_COMPUTED;
337
340
  } else {
@@ -2136,8 +2139,8 @@ class VirtualMachine {
2136
2139
  this.SOLVER_MINUS_INFINITY = -1e+30;
2137
2140
  // As of version 1.8.0, Linny-R imposes no +INF bounds on processes
2138
2141
  // unless diagnosing an unbounded problem. For such diagnosis, the
2139
- // (relatively) low value 9.99999999e+9 is used.
2140
- this.DIAGNOSIS_UPPER_BOUND = 9.99999999e+9;
2142
+ // (relatively) low value 9.999999999e+9 is used.
2143
+ this.DIAGNOSIS_UPPER_BOUND = 9.999999999e+9;
2141
2144
  // NOTE: Below the "near zero" limit, a number is considered zero
2142
2145
  // (this is to timely detect division-by-zero errors).
2143
2146
  this.NEAR_ZERO = 1e-10;
@@ -2149,7 +2152,8 @@ class VirtualMachine {
2149
2152
  // their target without displaying them in red or blue to signal
2150
2153
  // shortage or surplus.
2151
2154
  this.SIG_DIF_LIMIT = 0.001;
2152
- this.SIG_DIF_FROM_ZERO = 1e-6;
2155
+ // Numbers near zero are displayed as +0 or -0.
2156
+ this.SIG_DIF_FROM_ZERO = 5e-5;
2153
2157
  // ON/OFF threshold is used to differentiate between level = 0 and
2154
2158
  // still "ON" (will be displayed as +0).
2155
2159
  this.ON_OFF_THRESHOLD = 1.5e-4;
@@ -2194,6 +2198,7 @@ class VirtualMachine {
2194
2198
  this.LM_NEEDING_ON_OFF = [5, 6, 7, 8, 9, 10];
2195
2199
  this.LM_SYMBOLS = ['', '\u21C9', '\u0394', '\u03A3', '\u03BC', '\u25B2',
2196
2200
  '+', '0', '\u2934', '\u2732', '\u25BC', '\u2A39'];
2201
+ this.LM_LETTERS = ' TDSMU+0RFDP';
2197
2202
 
2198
2203
  // VM max. expression stack size.
2199
2204
  this.MAX_STACK = 200;
@@ -2537,7 +2542,7 @@ class VirtualMachine {
2537
2542
  if(sv[0]) return sv[1];
2538
2543
  const a = Math.abs(n);
2539
2544
  // Signal small differences from true 0 by leading + or - sign.
2540
- if(n !== 0 && a < 0.0005) return n > 0 ? '+0' : '-0';
2545
+ if(n !== 0 && a <= this.ON_OFF_THRESHOLD) return n > 0 ? '+0' : '-0';
2541
2546
  if(a >= 999999.5) return n.toPrecision(2);
2542
2547
  if(Math.abs(a-Math.round(a)) < 0.05) return Math.round(n);
2543
2548
  if(a < 1) return Math.round(n*100) / 100;
@@ -2556,7 +2561,7 @@ class VirtualMachine {
2556
2561
  if(sv[0]) return sv[1];
2557
2562
  const a = Math.abs(n);
2558
2563
  // Signal small differences from true 0 by a leading + or - sign.
2559
- if(n !== 0 && a < 0.0005) return n > 0 ? '+0' : '-0';
2564
+ if(n !== 0 && a <= this.ON_OFF_THRESHOLD) return n > 0 ? '+0' : '-0';
2560
2565
  if(a >= 9999995) return n.toPrecision(4);
2561
2566
  if(Math.abs(a-Math.round(a)) < 0.0005) return Math.round(n);
2562
2567
  if(a < 1) return Math.round(n*10000) / 10000;
@@ -2922,19 +2927,20 @@ class VirtualMachine {
2922
2927
  for(let i = 2; i <= n; i++) {
2923
2928
  this.variables.push(['W' + i, obj]);
2924
2929
  }
2925
- // NOTE: Some solvers do not support SOS. To ensure that only 2
2926
- // adjacent w[i]-variables are non-zero (they range from 0 to 1),
2927
- // as many binary variables b[i] must be defined, and additional
2928
- // constraints must be added (see function VMI_add_boundline).
2929
- // NOTE: These additional variables and constraints are not needed
2930
- // when a bound line defines a convex feasible area.
2931
- const sos_with_bin = this.noSupportForSOS && !obj.needsNoSOS;
2932
- this.sos_var_indices.push([index, n, sos_with_bin]);
2933
- if(sos_with_bin) {
2934
- for(let i = 1; i <= n; i++) {
2935
- const bi = this.variables.push(['b' + i, obj]);
2936
- this.bin_var_indices[bi] = true;
2937
- }
2930
+ // NOTE: SOS constraints are not needed when a bound line defines
2931
+ // a convex feasible area.
2932
+ if(!obj.needsNoSOS) {
2933
+ this.sos_var_indices.push([index, n]);
2934
+ // NOTE: Some solvers do not support SOS. To ensure that only 2
2935
+ // adjacent w[i]-variables are non-zero (they range from 0 to 1),
2936
+ // as many binary variables b[i] must be defined, and additional
2937
+ // constraints must be added (see VMI_add_bound_line_constraint).
2938
+ if(this.noSupportForSOS) {
2939
+ for(let i = 1; i <= n; i++) {
2940
+ const bi = this.variables.push(['b' + i, obj]);
2941
+ this.bin_var_indices[bi] = true;
2942
+ }
2943
+ }
2938
2944
  }
2939
2945
  }
2940
2946
  return index;
@@ -4044,7 +4050,7 @@ class VirtualMachine {
4044
4050
  these variables can take on higher values. The modeler must ensure
4045
4051
  that there is a cost associated with the actual flow, not a revenue.
4046
4052
  */
4047
- // NOTE: as of 20 June 2021, binary attributes of products are also computed
4053
+ // NOTE: As of 20 June 2021, binary attributes of products are also computed.
4048
4054
  const pp_nodes = [];
4049
4055
  for(i = 0; i < process_keys.length; i++) {
4050
4056
  k = process_keys[i];
@@ -4093,8 +4099,8 @@ class VirtualMachine {
4093
4099
  [VMI_add_const_to_coefficient, [p.level_var_index, 1]]
4094
4100
  );
4095
4101
  if(ubx.isStatic) {
4096
- // If UB is very high (typically: undefined, so +INF), try to infer
4097
- // a lower value for UB to use for the ON/OFF binary
4102
+ // If UB is very high (typically: undefined, so +INF), try to
4103
+ // infer a lower value for UB to use for the ON/OFF binary.
4098
4104
  let ub = ubx.result(0),
4099
4105
  hub = ub;
4100
4106
  if(ub > VM.MEGA_UPPER_BOUND) {
@@ -4642,7 +4648,7 @@ class VirtualMachine {
4642
4648
  if(v[1] instanceof BoundLine) {
4643
4649
  v[1].constraint.slack_info[b] = v[0];
4644
4650
  }
4645
- if(b <= this.nr_of_time_steps && absl > VM.SIG_DIF_FROM_ZERO) {
4651
+ if(b <= this.nr_of_time_steps && absl > VM.ON_OFF_THRESHOLD) {
4646
4652
  this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
4647
4653
  `${v[1].displayName} ${v[0]} slack = ${this.sig4Dig(slack)}`);
4648
4654
  if(v[1] instanceof Product) {
@@ -4652,9 +4658,9 @@ class VirtualMachine {
4652
4658
  }
4653
4659
  }
4654
4660
  } else if(CONFIGURATION.slight_slack_notices) {
4655
- this.logMessage(block, '-- Notice: (t=' + b + round + ') ' +
4661
+ this.logMessage(block, '---- Notice: (t=' + b + round + ') ' +
4656
4662
  v[1].displayName + ' ' + v[0] + ' slack = ' +
4657
- slack.toPrecision(2));
4663
+ slack.toPrecision(1));
4658
4664
  }
4659
4665
  }
4660
4666
  }
@@ -5410,7 +5416,7 @@ class VirtualMachine {
5410
5416
  this.numeric_issue = '';
5411
5417
  // First add the objective (always MAXimize).
5412
5418
  if(cplex) {
5413
- this.lines = 'Maximize\n';
5419
+ this.lines = `\\${this.solver_id}\nMaximize\n`;
5414
5420
  } else {
5415
5421
  this.lines = '/* Objective function */\nmax:\n';
5416
5422
  }
@@ -5461,6 +5467,12 @@ class VirtualMachine {
5461
5467
  }
5462
5468
  }
5463
5469
  c = this.right_hand_side[r];
5470
+ // NOTE: When previous block was infeasible or unbounded (no solution),
5471
+ // expressions for RHS may not evaluate as a number.
5472
+ if(Number.isNaN(c)) {
5473
+ this.setNumericIssue(c, r, 'constraint RHS');
5474
+ c = 0;
5475
+ }
5464
5476
  this.lines += line + ' ' +
5465
5477
  this.constraint_symbols[this.constraint_types[r]] + ' ' + c + EOL;
5466
5478
  line = '';
@@ -5507,8 +5519,14 @@ class VirtualMachine {
5507
5519
  }
5508
5520
  } else {
5509
5521
  // Bounds can be specified on a single line: lb <= X001 <= ub.
5510
- if(lb !== null && lb !== 0) line = lb + ' <= ' + line;
5511
- if(ub !== null) line += ' <= ' + ub;
5522
+ // NOTE: LP_solve has Infinity value 1e+25. Use this literal
5523
+ // because VM.PLUS_INFINITY may be set to *diagnostic* value.
5524
+ if(lb !== null && lb !== 0 && lb > -1e+25) {
5525
+ line = lb + ' <= ' + line;
5526
+ }
5527
+ if(ub !== null && ub < 1e+25) line += ' <= ' + ub;
5528
+ // NOTE: Do not add line if both bounds are infinite.
5529
+ if(line.indexOf('<=') < 0) line = '';
5512
5530
  }
5513
5531
  }
5514
5532
  if(line) this.lines += line + EOL;
@@ -5568,19 +5586,17 @@ class VirtualMachine {
5568
5586
  // NOTE: Add SOS section only if the solver supports SOS.
5569
5587
  if(this.sos_var_indices.length > 0 && !this.noSupportForSOS) {
5570
5588
  this.lines += 'SOS\n';
5571
- let sos = 0;
5572
5589
  const v_set = [];
5573
5590
  for(let j = 0; j < abl; j++) {
5574
5591
  for(let i = 0; i < this.sos_var_indices.length; i++) {
5592
+ const svi = this.sos_var_indices[i];
5575
5593
  v_set.length = 0;
5576
- let vi = this.sos_var_indices[i][0] + j * this.cols;
5577
- const n = this.sos_var_indices[i][1];
5578
- for(let j = 1; j <= n; j++) {
5594
+ let vi = svi[0] + j * this.cols;
5595
+ for(let j = 1; j <= svi[1]; j++) {
5579
5596
  v_set.push(`${vbl(vi)}:${j}`);
5580
5597
  vi++;
5581
5598
  }
5582
- this.lines += ` s${sos}: S2:: ${v_set.join(' ')}\n`;
5583
- sos++;
5599
+ this.lines += ` s${i}: S2:: ${v_set.join(' ')}\n`;
5584
5600
  }
5585
5601
  }
5586
5602
  }
@@ -5605,18 +5621,16 @@ class VirtualMachine {
5605
5621
  // LP_solve supports SOS, so add the SOS section if needed.
5606
5622
  if(this.sos_var_indices.length > 0) {
5607
5623
  this.lines += 'sos\n';
5608
- let sos = 1;
5609
5624
  for(let j = 0; j < abl; j++) {
5610
5625
  for(let i = 0; i < this.sos_var_indices.length; i++) {
5626
+ const svi = this.sos_var_indices[i];
5611
5627
  v_set.length = 0;
5612
- let vi = this.sos_var_indices[i][0] + j * this.cols;
5613
- const n = this.sos_var_indices[i][1];
5614
- for(let j = 1; j <= n; j++) {
5628
+ let vi = svi[0] + j * this.cols;
5629
+ for(let j = 1; j <= svi[1]; j++) {
5615
5630
  v_set.push(vbl(vi));
5616
5631
  vi++;
5617
5632
  }
5618
5633
  this.lines += `SOS${sos}: ${v_set.join(',')} <= 2;\n`;
5619
- sos++;
5620
5634
  }
5621
5635
  }
5622
5636
  }
@@ -5825,19 +5839,18 @@ class VirtualMachine {
5825
5839
  if(this.sos_var_indices.length > 0) {
5826
5840
  this.lines += 'SOS\n';
5827
5841
  const abl = this.actualBlockLength(this.block_count);
5828
- let sos = 1;
5829
5842
  for(let j = 0; j < abl; j++) {
5830
5843
  for(let i = 0; i < this.sos_var_indices.length; i++) {
5831
- this.lines += ' S2 sos' + sos + '\n';
5832
- let vi = this.sos_var_indices[i][0] + j * this.cols;
5833
- const n = this.sos_var_indices[i][1];
5834
- for(let j = 1; j <= n; j++) {
5835
- const s = ' X' + vi.toString().padStart(this.decimals, '0') +
5844
+ const svi = this.sos_var_indices[i];
5845
+ this.lines += ` S2 sos${i + 1}\n`;
5846
+ let vi = svi[0] + j * this.cols;
5847
+ for(let j = 1; j <= svi[1]; j++) {
5848
+ const s = ' X' +
5849
+ vi.toString().padStart(this.decimals, '0') +
5836
5850
  ' ';
5837
5851
  this.lines += s.substring(0, 15) + j + '\n';
5838
5852
  vi++;
5839
5853
  }
5840
- sos++;
5841
5854
  }
5842
5855
  }
5843
5856
  }
@@ -6145,7 +6158,7 @@ Solver status = ${json.status}`);
6145
6158
  }
6146
6159
  // Diagnosis (by adding slack variables and finite bounds on processes)
6147
6160
  // is activated when Alt-clicking the "run" button, or by clicking the
6148
- // "clicke HERE to diagnose" link on the infoline.
6161
+ // "clicke *here* to diagnose" link on the infoline.
6149
6162
  this.diagnose = diagnose || MODEL.always_diagnose;
6150
6163
  if(this.diagnose) {
6151
6164
  this.PLUS_INFINITY = this.DIAGNOSIS_UPPER_BOUND;
@@ -8329,10 +8342,11 @@ function VMI_set_const_rhs(c) {
8329
8342
  }
8330
8343
 
8331
8344
  function VMI_set_var_rhs(x) {
8345
+ VM.rhs = x.result(VM.t);
8332
8346
  if(DEBUGGING) {
8333
- console.log('set_var_rhs: ' + x.variableName + ' (t = ' + VM.t + ')');
8347
+ console.log(`set_var_rhs: ${x.variableName} (t = ${VM.t}) = ` +
8348
+ VM.sig4Dig(VM.rhs));
8334
8349
  }
8335
- VM.rhs = x.result(VM.t);
8336
8350
  }
8337
8351
 
8338
8352
  function VMI_add_const_to_rhs(c) {