linny-r 1.1.12 → 1.1.13

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.1.12",
3
+ "version": "1.1.13",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -1471,7 +1471,7 @@ class Paper {
1471
1471
  // achieved by multiplying the "gap" being (lengths - heights)/2 by
1472
1472
  // (1 - |dy/l|). NOTE: we re-use the values of `th` and `tw`
1473
1473
  // computed in the previous block!
1474
- shift += th/2;
1474
+ shift += th / 2;
1475
1475
  s = VM.sig4Dig(luc.share_of_cost * 100) + '%';
1476
1476
  bb = this.numberSize(s, 7);
1477
1477
  const sgap = (tw + bb.width + 3 - th - bb.height) / 2;
@@ -1631,7 +1631,8 @@ class Paper {
1631
1631
  if(cp <= VM.MINUS_INFINITY || cp >= VM.PLUS_INFINITY) {
1632
1632
  s = VM.sig4Dig(cp);
1633
1633
  } else if(Math.abs(cp) <= VM.SIG_DIF_FROM_ZERO) {
1634
- s = '0';
1634
+ // DO not display CP when it is "propagated" NO_COST
1635
+ s = (cp === VM.NO_COST ? '' : '0');
1635
1636
  } else {
1636
1637
  // NOTE: use the absolute value of the flow, as cost is not affected by direction
1637
1638
  s = VM.sig4Dig(Math.abs(af) * soc * cp);
@@ -168,15 +168,6 @@ class LinnyRModel {
168
168
  return olist;
169
169
  }
170
170
 
171
- get legacyVersion() {
172
- // Return TRUE if the model as it has been loaded was not saved by
173
- // JavaScript Linny-R
174
- const
175
- vnl = this.version.split('.'),
176
- legacy = vnl[0] === '0' || (vnl[0] === '1' && vnl.length > 3);
177
- return legacy;
178
- }
179
-
180
171
  get newProcessCode() {
181
172
  // Return the next unused process code
182
173
  const n = this.next_process_number;
@@ -1994,6 +1985,15 @@ class LinnyRModel {
1994
1985
  // Initialize a model from the XML tree with `node` as root
1995
1986
  // NOTE: do NOT reset and initialize basic model properties when *including*
1996
1987
  // a module into the current model
1988
+ // NOTE: obsolete XML nodes indicate: legacy Linny-R model
1989
+ const legacy_model = (nodeParameterValue(node, 'view-options') +
1990
+ nodeParameterValue(node, 'autosave') +
1991
+ nodeParameterValue(node, 'look-ahead') +
1992
+ nodeParameterValue(node, 'save-series') +
1993
+ nodeParameterValue(node, 'show-lp') +
1994
+ nodeParameterValue(node, 'optional-slack')).length > 0;
1995
+ // Flag to set when legacy time series data are added
1996
+ this.legacy_datasets = false;
1997
1997
  if(!IO_CONTEXT) {
1998
1998
  this.reset();
1999
1999
  this.next_process_number = safeStrToInt(
@@ -2019,8 +2019,8 @@ class LinnyRModel {
2019
2019
  this.timeout_period = Math.max(0,
2020
2020
  safeStrToInt(nodeContentByTag(node, 'timeout-period')));
2021
2021
  // Legacy models have tag "optimization-period" instead of "block-length"
2022
- const bl_tag = (this.legacyVersion ?
2023
- 'optimization-period' : 'block-length');
2022
+ const bl_tag = nodeContentByTag(node, 'block-length') ||
2023
+ nodeContentByTag(node, 'optimization-period');
2024
2024
  this.block_length = Math.max(1,
2025
2025
  safeStrToInt(nodeContentByTag(node, bl_tag)));
2026
2026
  this.start_period = Math.max(1,
@@ -2177,7 +2177,7 @@ class LinnyRModel {
2177
2177
  }
2178
2178
  // Clear the default (empty) equations dataset, or it will block adding it
2179
2179
  if(!IO_CONTEXT) {
2180
- this.datasets = {};
2180
+ if(!this.legacy_datasets) this.datasets = {};
2181
2181
  this.equations_dataset = null;
2182
2182
  }
2183
2183
  // NOTE: keep track of datasets that load from URL or file
@@ -2322,7 +2322,7 @@ class LinnyRModel {
2322
2322
  // NOTE: links in legacy Linny-R models by default have 100% share-of-cost;
2323
2323
  // to minimize conversion effort, set SoC for SINGLE links OUT of processes
2324
2324
  // to 100%
2325
- if(this.legacyVersion) {
2325
+ if(legacy_model) {
2326
2326
  for(let l in this.links) if(this.links.hasOwnProperty(l)) {
2327
2327
  l = this.links[l];
2328
2328
  // NOTE: preserve non-zero SoC values, as these have been specified
@@ -2707,6 +2707,36 @@ class LinnyRModel {
2707
2707
  links = [],
2708
2708
  constraints = [],
2709
2709
  can_calculate = true;
2710
+ const
2711
+ // NOTE: define local functions as constants
2712
+ costAffectingConstraints = (p) => {
2713
+ // Returns number of relevant contraints (see below) that
2714
+ // can affect the cost price of product or process `p`
2715
+ let n = 0;
2716
+ for(let i = 0; i < constraints.length; i++) {
2717
+ const c = constraints[i];
2718
+ if((c.to_node === p && c.soc_direction === VM.SOC_X_Y) ||
2719
+ (c.from_node === p && c.soc_direction === VM.SOC_Y_X)) n++;
2720
+ }
2721
+ return n;
2722
+ },
2723
+ inputsFromProcesses = (p, t) => {
2724
+ // Returns a tuple {n, nosoc, nz} where n is the number of input links
2725
+ // from processes, nosoc the number of these that carry no cost,
2726
+ // and nz the number of links having actual flow > 0
2727
+ let tuple = {n: 0, nosoc: 0, nz: 0};
2728
+ for(let i = 0; i < p.inputs.length; i++) {
2729
+ const l = p.inputs[i];
2730
+ // NOTE: only process --> product links can carry cost
2731
+ if(l.from_node instanceof Process) {
2732
+ tuple.n++;
2733
+ if(l.share_of_cost === 0) tuple.nosoc++;
2734
+ if(l.actualFlow(t) > VM.NEAR_ZERO) tuple.nz++;
2735
+ }
2736
+ }
2737
+ return tuple;
2738
+ };
2739
+
2710
2740
  // First scan constraints X --> Y: these must have SoC > 0 and moreover
2711
2741
  // the level of both X and Y must be non-zero, or they transfer no cost
2712
2742
  for(let k in this.constraints) if(this.constraints.hasOwnProperty(k) &&
@@ -2741,12 +2771,7 @@ class LinnyRModel {
2741
2771
  break;
2742
2772
  }
2743
2773
  // Count constraints that affect CP of this process
2744
- let n = 0;
2745
- for(let i = 0; i < constraints.length; i++) {
2746
- const c = constraints[i];
2747
- if((c.to_node === p && c.soc_direction === VM.SOC_X_Y) ||
2748
- (c.from_node === p && c.soc_direction === VM.SOC_Y_X)) n++;
2749
- }
2774
+ let n = costAffectingConstraints(p);
2750
2775
  if(n || p.inputs.length) {
2751
2776
  // All inputs can affect the CP of a process
2752
2777
  p.cost_price[t] = VM.UNDEFINED;
@@ -2765,65 +2790,38 @@ class LinnyRModel {
2765
2790
  if(pr < 0) negpr -= pr * l.relative_rate.result(dt);
2766
2791
  }
2767
2792
  p.cost_price[t] = negpr;
2793
+ // Done, so not add to `processes` list
2768
2794
  }
2769
2795
  }
2770
2796
  // Then scan the products
2771
2797
  for(let k in this.products) if(this.products.hasOwnProperty(k) &&
2772
2798
  !MODEL.ignored_entities[k]) {
2773
2799
  const p = this.products[k];
2774
- let n = 0,
2775
- nc = 0,
2776
- cp = 0;
2777
- // Count number of (potential) cost-carrying product flows
2778
- for(let i = 0; i < p.inputs.length; i++) {
2779
- const l = p.inputs[i];
2780
- // NOTE: only process --> product links can carry cost
2781
- if(l.share_of_cost > 0 && l.from_node instanceof Process) n++;
2782
- }
2783
- if(p.is_buffer) {
2784
- // Stocks often introduce cycles; those having only zero-flow
2785
- // links in/out have stockprice of t-1
2800
+ let ifp = inputsFromProcesses(p, t),
2801
+ nc = costAffectingConstraints(p);
2802
+ if(p.is_buffer && !ifp.nz) {
2803
+ // Stocks for which all INput links have flow = 0 have the same
2804
+ // stock price as in t-1
2786
2805
  // NOTE: it is not good to check for zero stock, as that may be
2787
2806
  // the net result of in/outflows
2788
- let nz = 0;
2789
- for(let i = 0; i < p.inputs.length && !nz; i++) {
2790
- if(p.inputs[i].actualFlow(t) > VM.NEAR_ZERO) nz++;
2791
- }
2792
- for(let i = 0; i < p.outputs.length && !nz; i++) {
2793
- if(p.outputs[i].actualFlow(t) > VM.NEAR_ZERO) nz++;
2794
- }
2795
- if(!nz) {
2796
- n = 0;
2797
- cp = p.stockPrice(t - 1);
2798
- }
2799
- } else if(n > 1) {
2800
- // NOTE: products having no storage, and *multiple* cost-carrying
2801
- // input links that all are zero-flow have CP=0
2802
- let nz = 0;
2803
- for(let i = 0; i < p.inputs.length && !nz; i++) {
2804
- if(p.inputs[i].actualFlow(t) > VM.NEAR_ZERO) nz++;
2805
- }
2806
- if(!nz) n = 0;
2807
- }
2808
- // Add number of cost-transferring constraints
2809
- for(let i = 0; i < constraints.length; i++) {
2810
- const c = constraints[i];
2811
- if(c.to_node === p && c.soc_direction === VM.SOC_X_Y ||
2812
- (c.from_node === p && c.soc_direction === VM.SOC_Y_X)) nc++;
2813
- }
2814
- if(n + nc) {
2807
+ p.cost_price[t] = p.stockPrice(t - 1);
2808
+ p.stock_price[t] = p.cost_price[t];
2809
+ } else if(!nc && (ifp.n === ifp.nosoc || (!ifp.nz && ifp.n > ifp.nosoc + 1))) {
2810
+ // For products having only input links that carry no cost,
2811
+ // CP = 0 but coded as NO_COST so that this can propagate.
2812
+ // Furthermore, for products having no storage and *multiple*
2813
+ // cost-carrying input links that all are zero-flow, the cost
2814
+ // price cannot be inferred unambiguously => set to 0
2815
+ p.cost_price[t] = (ifp.n && ifp.n === ifp.nosoc ? VM.NO_COST : 0);
2816
+ } else {
2815
2817
  // Cost price must be calculated
2816
2818
  p.cost_price[t] = VM.UNDEFINED;
2817
- p.stock_price[t] = VM.UNDEFINED;
2818
2819
  products.push(p);
2819
- } else {
2820
- // Cost price is zero (for stocks: CP[t-1])
2821
- p.cost_price[t] = cp;
2822
- p.stock_price[t] = cp;
2823
2820
  }
2821
+ p.cost_price[t] = p.cost_price[t];
2824
2822
  }
2825
- // Finally, scan all links, and likewise retain only those for which
2826
- // the CP can not already be inferred from their FROM node
2823
+ // Finally, scan all links, and retain only those for which the CP
2824
+ // can not already be inferred from their FROM node
2827
2825
  for(let k in this.links) if(this.links.hasOwnProperty(k) &&
2828
2826
  !MODEL.ignored_entities[k]) {
2829
2827
  const
@@ -2831,15 +2829,8 @@ class LinnyRModel {
2831
2829
  ld = l.actualDelay(t),
2832
2830
  fn = l.from_node,
2833
2831
  fncp = fn.costPrice(t - ld),
2834
- tn = l.to_node,
2835
- tncp = tn.costPrice(t - ld);
2836
- if(fncp !== VM.UNDEFINED && fncp !== VM.NOT_COMPUTED) {
2837
- // Links that are output of a node having CP defined have UCP = CP
2838
- l.unit_cost_price = fncp;
2839
- } else if(tncp !== VM.UNDEFINED && tncp !== VM.NOT_COMPUTED) {
2840
- // Links that are input of a node having CP defined have UCP = CP
2841
- l.unit_cost_price = 0;
2842
- } else if(fn instanceof Product && fn.price.defined) {
2832
+ tn = l.to_node;
2833
+ if(fn instanceof Product && fn.price.defined) {
2843
2834
  // Links from products having a market price have this price
2844
2835
  // multiplied by their relative rate as unit CP
2845
2836
  l.unit_cost_price = fn.price.result(t) * l.relative_rate.result(t);
@@ -2848,6 +2839,9 @@ class LinnyRModel {
2848
2839
  // Process output links that do not carry cost and product-to-
2849
2840
  // product links have unit CP = 0
2850
2841
  l.unit_cost_price = 0;
2842
+ } else if(fncp !== VM.UNDEFINED && fncp !== VM.NOT_COMPUTED) {
2843
+ // Links that are output of a node having CP defined have UCP = CP
2844
+ l.unit_cost_price = fncp * l.relative_rate.result(t);
2851
2845
  } else {
2852
2846
  l.unit_cost_price = VM.UNDEFINED;
2853
2847
  // Do not push links related to processes having level < 0
@@ -3001,14 +2995,15 @@ class LinnyRModel {
3001
2995
  cp_sccp = VM.COMPUTING;
3002
2996
  for(let j = 0; j < p.inputs.length; j++) {
3003
2997
  const l = p.inputs[j];
3004
- if(l.from_node instanceof Process && l.share_of_cost > 0) {
2998
+ if(l.from_node instanceof Process) {
3005
2999
  cp = l.from_node.costPrice(t - l.actualDelay(t));
3006
- if(cp === VM.UNDEFINED) {
3000
+ if(cp === VM.UNDEFINED && l.share_of_cost > 0) {
3001
+ // Contibuting CP still unknown => break from FOR loop
3007
3002
  break;
3008
3003
  } else {
3009
3004
  if(cp_sccp === VM.COMPUTING) {
3010
3005
  // First CC process having a defined CP => use this CP
3011
- cp_sccp = cp;
3006
+ cp_sccp = cp * l.share_of_cost;
3012
3007
  } else {
3013
3008
  // Multiple CC processes => set CP to 0
3014
3009
  cp_sccp = 0;
@@ -3034,7 +3029,7 @@ class LinnyRModel {
3034
3029
  if(cp === VM.UNDEFINED) continue;
3035
3030
  // CP of product is 0 if no new production UNLESS it has only
3036
3031
  // one cost-carrying production input, as then its CP equals
3037
- // the CP of the producing process;
3032
+ // the CP of the producing process times the link SoC;
3038
3033
  // if new production > 0 then CP = cost / quantity
3039
3034
  if(cp_sccp !== VM.COMPUTING) {
3040
3035
  cp = (qnp > 0 ? cnp / qnp : cp_sccp);
@@ -6464,11 +6459,14 @@ class Process extends Node {
6464
6459
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
6465
6460
  this.lower_bound.text = xmlDecoded(nodeContentByTag(node, 'lower-bound'));
6466
6461
  this.upper_bound.text = xmlDecoded(nodeContentByTag(node, 'upper-bound'));
6467
- this.initial_level.text = xmlDecoded(nodeContentByTag(node, 'initial-level'));
6468
- // NOTE: until version 1.0.16, pace was stored as a node parameter
6469
- const legacy_pace = nodeParameterValue(node, 'pace');
6470
- this.pace_expression.text = legacy_pace +
6462
+ // NOTE: legacy models have no initial level field => default to 0
6463
+ const ilt = xmlDecoded(nodeContentByTag(node, 'initial-level'));
6464
+ this.initial_level.text = ilt || '0';
6465
+ // NOTE: until version 1.0.16, pace was stored as a node parameter;
6466
+ const pace_text = nodeParameterValue(node, 'pace') +
6471
6467
  xmlDecoded(nodeContentByTag(node, 'pace'));
6468
+ // NOTE: legacy models have no pace field => default to 1
6469
+ this.pace_expression.text = pace_text || '1';
6472
6470
  // NOTE: immediately evaluate pace expression as integer
6473
6471
  this.pace = Math.max(1, Math.floor(this.pace_expression.result(1)));
6474
6472
  this.x = safeStrToInt(nodeContentByTag(node, 'x-coord'));
@@ -6874,15 +6872,44 @@ class Product extends Node {
6874
6872
  this.integer_level = nodeParameterValue(node, 'integer-level') === '1';
6875
6873
  this.no_slack = nodeParameterValue(node, 'no-slack') === '1';
6876
6874
  // legacy models have tag "hidden" instead of "no-links"
6877
- const no_links_tag = (MODEL.legacyVersion ? 'hidden' : 'no-links');
6878
- this.no_links = nodeParameterValue(node, no_links_tag) === '1';
6875
+ this.no_links = (nodeParameterValue(node, 'no-links') ||
6876
+ nodeParameterValue(node, 'hidden')) === '1';
6879
6877
  this.scale_unit = MODEL.addScaleUnit(
6880
6878
  xmlDecoded(nodeContentByTag(node, 'unit')));
6881
6879
  // legacy models have tag "profit" instead of "price"
6882
- const price_tag = (MODEL.legacyVersion ? 'profit' : 'price');
6883
- this.price.text = xmlDecoded(nodeContentByTag(node, price_tag));
6880
+ let pp = nodeContentByTag(node, 'price');
6881
+ if(!pp) pp = nodeContentByTag(node, 'profit');
6882
+ this.price.text = xmlDecoded(pp);
6884
6883
  this.lower_bound.text = xmlDecoded(nodeContentByTag(node, 'lower-bound'));
6885
6884
  this.upper_bound.text = xmlDecoded(nodeContentByTag(node, 'upper-bound'));
6885
+ // legacy models can have LB and UB hexadecimal data strings
6886
+ const
6887
+ lb_data = nodeContentByTag(node, 'lower-bound-data'),
6888
+ ub_data = nodeContentByTag(node, 'upper-bound-data'),
6889
+ same = lb_data === ub_data;
6890
+ if(lb_data) {
6891
+ const
6892
+ dsn = this.displayName + (same ? '' : ' LOWER') + ' BOUND DATA',
6893
+ ds = MODEL.addDataset(dsn);
6894
+ ds.default_value = parseFloat(this.lower_bound.text);
6895
+ ds.data = stringToFloatArray(lb_data);
6896
+ ds.computeVector();
6897
+ ds.computeStatistics();
6898
+ this.lower_bound.text = `[${dsn}]`;
6899
+ if(same) this.equal_bounds = true;
6900
+ MODEL.legacy_datasets = true;
6901
+ }
6902
+ if(ub_data && !same) {
6903
+ const
6904
+ dsn = this.displayName + ' UPPER BOUND DATA',
6905
+ ds = MODEL.addDataset(dsn);
6906
+ ds.default_value = parseFloat(this.upper_bound.text);
6907
+ ds.data = stringToFloatArray(ub_data);
6908
+ ds.computeVector();
6909
+ ds.computeStatistics();
6910
+ this.upper_bound.text = `[${dsn}]`;
6911
+ MODEL.legacy_datasets = true;
6912
+ }
6886
6913
  this.initial_level.text = xmlDecoded(
6887
6914
  nodeContentByTag(node, 'initial-level'));
6888
6915
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
@@ -7190,13 +7217,14 @@ class Link {
7190
7217
  this.is_feedback = nodeParameterValue(node, 'is-feedback') === '1';
7191
7218
  this.relative_rate.text = xmlDecoded(
7192
7219
  nodeContentByTag(node, 'relative-rate'));
7193
- this.flow_delay.text = xmlDecoded(nodeContentByTag(node, 'delay'));
7220
+ // NOTE: legacy models have no flow delay field => default to 0
7221
+ const fd_text = xmlDecoded(nodeContentByTag(node, 'delay'));
7222
+ this.flow_delay.text = fd_text || '0';
7194
7223
  this.share_of_cost = safeStrToFloat(
7195
7224
  nodeContentByTag(node, 'share-of-cost'), 0);
7196
- if(MODEL.legacyVersion) {
7225
+ if(!fd_text) {
7197
7226
  // NOTE: default share-of-cost for links in legacy Linny-R was 100%;
7198
7227
  // this is dysfunctional in JS Linny-R => set to 0 if equal to 1
7199
- this.flow_delay.text = '0';
7200
7228
  if(this.share_of_cost == 1) this.share_of_cost = 0;
7201
7229
  }
7202
7230
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
@@ -554,6 +554,32 @@ function nameToLines(name, actor_name = '') {
554
554
  return lines.join('\n');
555
555
  }
556
556
 
557
+ //
558
+ // Linny-R legacy model conversion functions
559
+ //
560
+
561
+ function hexToFloat(s) {
562
+ const n = parseInt('0x' + s, 16);
563
+ if(isNaN(n)) return 0;
564
+ const
565
+ sign = (n >> 31 ? -1 : 1),
566
+ exp = Math.pow(2, ((n >> 23) & 0xFF) - 127);
567
+ return sign * (n & 0x7fffff | 0x800000) * 1.0 / Math.pow(2, 23) * exp;
568
+ }
569
+
570
+ function stringToFloatArray(s) {
571
+ let i = 8,
572
+ a = [];
573
+ while(i <= s.length) {
574
+ const
575
+ h = s.substr(i - 8, 8),
576
+ r = h.substr(6, 2) + h.substr(4, 2) + h.substr(2, 2) + h.substr(0, 2);
577
+ a.push(hexToFloat(r));
578
+ i += 8;
579
+ }
580
+ return a;
581
+ }
582
+
557
583
  //
558
584
  // Encryption-related functions
559
585
  //
@@ -1407,6 +1407,10 @@ class VirtualMachine {
1407
1407
  // NOTE: below the "near zero" limit, a number is considered zero
1408
1408
  // (this is to timely detect division-by-zero errors)
1409
1409
  this.NEAR_ZERO = 1e-10;
1410
+ // Use a specific constant smaller than near-zero to denote "no cost"
1411
+ // to differentiate "no cost" form cost prices that really are 0
1412
+ this.NO_COST = 0.987654321e-10;
1413
+
1410
1414
  // NOTE: allow for an accuracy margin: stocks may differ 0.1% from their
1411
1415
  // target without displaying them in red or blue to signal shortage or surplus
1412
1416
  this.SIG_DIF_LIMIT = 0.001;
@@ -1714,6 +1718,7 @@ class VirtualMachine {
1714
1718
  if(n >= this.NOT_COMPUTED) return [true, '\u2297']; // Circled X
1715
1719
  if(n >= this.UNDEFINED) return [true, '\u2047']; // Double question mark ??
1716
1720
  if(n >= this.PLUS_INFINITY) return [true, '\u221E'];
1721
+ if(n === this.NO_COST) return [true, '\u00A2']; // c-slash (cent symbol)
1717
1722
  return [false, n];
1718
1723
  }
1719
1724