linny-r 1.8.2 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,8 @@ 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;
110
+ this.show_notices = true;
108
111
 
109
112
  // Sensitivity-related properties
110
113
  this.base_case_selectors = '';
@@ -2668,6 +2671,7 @@ class LinnyRModel {
2668
2671
  this.report_results = nodeParameterValue(node, 'report-results') === '1';
2669
2672
  this.show_block_arrows = nodeParameterValue(node, 'block-arrows') === '1';
2670
2673
  this.always_diagnose = nodeParameterValue(node, 'diagnose') === '1';
2674
+ this.show_notices = nodeParameterValue(node, 'show-notices') === '1';
2671
2675
  this.name = xmlDecoded(nodeContentByTag(node, 'name'));
2672
2676
  this.author = xmlDecoded(nodeContentByTag(node, 'author'));
2673
2677
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
@@ -2905,6 +2909,10 @@ class LinnyRModel {
2905
2909
  }
2906
2910
  }
2907
2911
  }
2912
+ // Infer selector order list by splitting text on any white space.
2913
+ this.selector_order_string = xmlDecoded(nodeContentByTag(node,
2914
+ 'selector-order'));
2915
+ this.selector_order_list = this.selector_order_string.trim().split(/\s+/);
2908
2916
  // Infer dimensions of experimental design space
2909
2917
  this.inferDimensions();
2910
2918
  // NOTE: when including a model, IGNORE sensitivity analysis, experiments
@@ -3017,6 +3025,7 @@ class LinnyRModel {
3017
3025
  if(this.report_results) p += ' report-results="1"';
3018
3026
  if(this.show_block_arrows) p += ' block-arrows="1"';
3019
3027
  if(this.always_diagnose) p += ' diagnose="1"';
3028
+ if(this.show_notices) p += ' show-notices="1"';
3020
3029
  let xml = this.xml_header + ['<model', p, '><name>', xmlEncoded(this.name),
3021
3030
  '</name><author>', xmlEncoded(this.author),
3022
3031
  '</author><notes>', xmlEncoded(this.comments),
@@ -3074,8 +3083,11 @@ class LinnyRModel {
3074
3083
  if(this.datasets.hasOwnProperty(obj)) xml += this.datasets[obj].asXML;
3075
3084
  }
3076
3085
  xml += '</datasets><charts>';
3077
- for(let i = 0; i < this.charts.length; i++) xml += this.charts[i].asXML;
3078
- xml += '</charts>';
3086
+ for(let i = 0; i < this.charts.length; i++) {
3087
+ xml += this.charts[i].asXML;
3088
+ }
3089
+ xml += '</charts><selector-order>' +
3090
+ xmlEncoded(this.selector_order_string) + '</selector-order>';
3079
3091
  // NOTE: when "black-boxing", SA and experiments are not stored
3080
3092
  if(!this.black_box) {
3081
3093
  xml += '<base-case-selectors>' +
@@ -7943,54 +7955,59 @@ class Product extends Node {
7943
7955
  }
7944
7956
 
7945
7957
  get isSinkNode() {
7946
- // Returns TRUE if this product behaves as a sink
7958
+ // Return TRUE if this product behaves as a sink.
7947
7959
  return (this.is_sink || this.allOutputsAreData) &&
7948
7960
  !(this.upper_bound.defined ||
7949
- // NOTE: UB may be set by equalling it to LB
7961
+ // NOTE: UB may be set by equalling it to LB.
7950
7962
  (this.equal_bounds && this.lower_bound.defined));
7951
7963
  }
7952
7964
 
7953
7965
  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
7966
+ // Infer the upper bound for this product from its own UB, or from
7967
+ // its ingoing links (type, rate, and UB of their from nodes).
7968
+ // This is used while compiling the VM instructions that compute the
7969
+ // ON/OFF binary variable for this product.
7970
+ // This method performs a graph traversal. If this product is part
7971
+ // of a cycle in the graph, its highest UB co-depends on its own UB,
7972
+ // which is not constrained, so then return +INF.
7973
+ // No need to check for sink nodes, as even on those nodes a max. UB
7974
+ // might be inferred from their max. inflows.
7963
7975
  if(visited.indexOf(this) >= 0) return VM.PLUS_INFINITY;
7964
7976
  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"
7977
+ // If dynamic expression, return +INF to signal "no lower UB can be inferred".
7966
7978
  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
7979
+ // If static, use its value as initial highest value.
7980
+ const max_ub = (ub.defined ? ub.result(0) : VM.PLUS_INFINITY);
7981
+ // See if the sum of its max. inflows will be lower than this value.
7970
7982
  let sum = 0;
7971
- // Preclude infinite recursion
7983
+ // Preclude infinite recursion.
7972
7984
  visited.push(this);
7973
7985
  for(let i = 0; i < this.inputs.length; i++) {
7974
7986
  const
7975
7987
  l = this.inputs[i],
7976
7988
  r = l.relative_rate,
7977
7989
  fn = l.from_node;
7978
- // Dynamic rate => inflows cannot constrain the UB any further
7990
+ // Dynamic rate => inflows cannot constrain the UB any further.
7979
7991
  if(!r.isStatic) return max_ub;
7992
+ let rate = r.result(0);
7980
7993
  if([VM.LM_STARTUP, VM.LM_POSITIVE, VM.LM_ZERO, VM.LM_FIRST_COMMIT,
7981
7994
  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));
7995
+ // For binary multipliers, the rate is the highest possible flow.
7996
+ // NOTE: Do not add negative flows, as actual flow may be 0.
7997
+ sum += Math.max(0, rate);
7985
7998
  } else {
7986
- // For other multipliers, max flow = rate * UB of the FROM node
7987
- // (for products, this will recurse; processes return their UB)
7999
+ // For other multipliers, max flow = rate * UB of the FROM node.
8000
+ // For products, this will recurse; processes return their UB.
7988
8001
  let fnub = fn.highestUpperBound(visited);
7989
8002
  // If +INF, no lower UB can be inferred => return initial maximum
7990
8003
  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);
8004
+ // Otherwise, add rate * UB to the max. total inflow.
8005
+ // NOTE: For SUM multipliers, also consider the delay.
8006
+ if(l.multiplier === VM.LM_SUM) {
8007
+ rate *= Math.max(1, l.flow_delay.result(0));
8008
+ }
8009
+ // NOTE: Do not add negative flows, as actual flow may be 0.
8010
+ sum += Math.max(0, rate * fnub);
7994
8011
  }
7995
8012
  }
7996
8013
  // 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;