linny-r 1.1.22 → 1.2.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.
@@ -54,7 +54,8 @@ class LinnyRModel {
54
54
  this.currency_unit = CONFIGURATION.default_currency_unit;
55
55
  this.default_unit = CONFIGURATION.default_scale_unit;
56
56
  this.decimal_comma = CONFIGURATION.decimal_comma;
57
- this.scale_units = [this.default_unit];
57
+ // NOTE: Default scale unit list comprises only the primitive base unit
58
+ this.scale_units = {'1': new ScaleUnit('1', '1', '1')};
58
59
  this.actors = {};
59
60
  this.products = {};
60
61
  this.processes = {};
@@ -735,8 +736,9 @@ class LinnyRModel {
735
736
  }
736
737
  // Then infer dimensions from the datasets (which have been changed)
737
738
  this.inferDimensions();
738
- // Find dimension that has been removed or (only reduced)
739
- let removed = null, reduced = null;
739
+ // Find dimension that has been removed (or only reduced)
740
+ let removed = null,
741
+ reduced = null;
740
742
  if(od.length > this.dimensions.length) {
741
743
  // Dimension removed => find out which one
742
744
  let rd = null;
@@ -781,7 +783,14 @@ class LinnyRModel {
781
783
  UI.updateControllerDialogs('X');
782
784
  }
783
785
  }
784
-
786
+
787
+ renameSelectorInExperiments(olds, news) {
788
+ // Replace all occurrences of `olds` in dimension strings by `news`
789
+ for(let i = 0; i < this.experiments.length; i++) {
790
+ this.experiments[i].renameSelectorInDimensions(olds, news);
791
+ }
792
+ }
793
+
785
794
  ignoreClusterInThisRun(c) {
786
795
  // Returns TRUE iff an experiment is running and cluster `c` is in the
787
796
  // clusters-to-ignore list and its selectors in this list overlap with the
@@ -823,16 +832,183 @@ class LinnyRModel {
823
832
  return this.actors[id];
824
833
  }
825
834
 
826
- addScaleUnit(name) {
835
+ addScaleUnit(name, scalar='1', base_unit='1') {
836
+ // Add a scale unit to the model, and return its symbol
837
+ // (1) To permit things like 1 kWh = 3.6 MJ, and 1 GJ = 1000 MJ,
838
+ // scale units have a multiplier and a base unit; by default,
839
+ // multiplier = 1 and base unit = '1' to denote "atomic unit"
840
+ // (2) Linny-R remains agnostic about physics, SI standards etc.
841
+ // so modelers can do anything they like
842
+ // (3) Linny-R may in the future be extended with a unit consistency
843
+ // check
827
844
  name = UI.cleanName(name);
828
- if(name === '' || name === ' ') return this.default_unit;
829
- // scale units (case sensitive!) are assumed to be universal
830
- if(this.scale_units.indexOf(name) === -1) {
831
- this.scale_units.push(name);
845
+ // NOTE: empty string denotes default unit, so test this first to
846
+ // avoid a warning
847
+ if(!name) return this.default_unit;
848
+ // NOTE: do not replace or modify an existing scale unit
849
+ if(!this.scale_units.hasOwnProperty(name)) {
850
+ this.scale_units[name] = new ScaleUnit(name, scalar, base_unit);
851
+ UI.updateScaleUnitList();
832
852
  }
833
853
  return name;
834
854
  }
835
855
 
856
+ addPreconfiguredScaleUnits() {
857
+ // Add scale units defined in file `config.js` (by default: none)
858
+ for(let i = 0; i < CONFIGURATION.scale_units.length; i++) {
859
+ const su = CONFIGURATION.scale_units[i];
860
+ this.addScaleUnit(...su);
861
+ }
862
+ }
863
+
864
+ cleanUpScaleUnits() {
865
+ // Remove all scale units that are not used and have base unit '1'
866
+ const suiu = {};
867
+ // Collect all non-empty product units
868
+ for(let p in this.products) if(this.products.hasOwnProperty(p)) {
869
+ const su = this.products[p].scale_unit;
870
+ if(su) suiu[su] = true;
871
+ }
872
+ // Likewise collect all non-empty dataset units
873
+ for(let ds in this.datasets) if(this.datasets.hasOwnProperty(ds)) {
874
+ const su = this.datasets[ds].scale_unit;
875
+ if(su) suiu[su] = true;
876
+ }
877
+ // Also collect base units and units having base unit other than '1'
878
+ for(let su in this.scale_units) if(this.scale_units.hasOwnProperty(su)) {
879
+ const u = this.scale_units[su];
880
+ suiu[u.base_unit] = true;
881
+ if(u.base_unit !== '1') suiu[u.name] = true;
882
+ }
883
+ // Now all scale units NOT in `suiu` can be removed
884
+ for(let su in this.scale_units) if(this.scale_units.hasOwnProperty(su)) {
885
+ if(!suiu.hasOwnProperty(su)) {
886
+ delete this.scale_units[su];
887
+ }
888
+ }
889
+ }
890
+
891
+ renameScaleUnit(oldu, newu) {
892
+ let nr = 0;
893
+ // Update the default product unit
894
+ if(MODEL.default_unit === oldu) {
895
+ MODEL.default_unit = newu;
896
+ nr++;
897
+ }
898
+ // Rename product scale units
899
+ for(let p in MODEL.products) if(MODEL.products.hasOwnProperty(p)) {
900
+ if(MODEL.products[p].scale_unit === oldu) {
901
+ MODEL.products[p].scale_unit = newu;
902
+ nr++;
903
+ }
904
+ }
905
+ // Rename product and dataset units
906
+ for(let ds in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(ds)) {
907
+ if(MODEL.datasets[ds].scale_unit === oldu) {
908
+ MODEL.datasets[ds].scale_unit = newu;
909
+ nr++;
910
+ }
911
+ }
912
+ // Also rename conversion units in note fields
913
+ for(let k in MODEL.clusters) if(MODEL.clusters.hasOwnProperty(k)) {
914
+ const c = MODEL.clusters[k];
915
+ for(let i = 0; i < c.notes.length; i++) {
916
+ const
917
+ n = c.notes[i],
918
+ tags = n.contents.match(/\[\[[^\]]+\]\]/g);
919
+ if(tags) {
920
+ for(let i = 0; i < tags.length; i++) {
921
+ const
922
+ ot = tags[i],
923
+ parts = ot.split('->'),
924
+ last = parts.pop().trim();
925
+ if(last === oldu + ']]') {
926
+ const nt = parts.join('->') + `->${newu}]]`;
927
+ n.contents = n.contents.replace(ot, nt);
928
+ }
929
+ }
930
+ n.parsed = false;
931
+ }
932
+ }
933
+ }
934
+ // Also rename scale units in expressions (quoted if needed)
935
+ oldu = UI.nameAsConstantString(oldu);
936
+ newu = UI.nameAsConstantString(newu);
937
+ const ax = MODEL.allExpressions;
938
+ if(oldu.startsWith("'")) {
939
+ // Simple case: replace quoted old unit by new
940
+ for(let i = 0; i < ax.length; i++) {
941
+ let parts = ax[i].text.split(oldu);
942
+ nr += parts.length - 1;
943
+ ax[i].text = parts.join(newu);
944
+ }
945
+ } else {
946
+ // Old unit is not enclosed in quotes; then care must be taken
947
+ // not to replace partial matches, e.g., kton => ktonne when 'ton'
948
+ // is renamed to 'tonne'; solution is to ensure that old unit must
949
+ // have a separator character on both sides, or it is not replaced
950
+ const
951
+ sep = SEPARATOR_CHARS + "]'",
952
+ esep = escapeRegex(sep),
953
+ eou = escapeRegex(oldu),
954
+ raw = `\[[^\[]*\]|(^|\s|[${esep}])(${eou})($|\s|[${esep}])`;
955
+ const
956
+ // NOTE: this will match anything within brackets, and the unit
957
+ re = new RegExp(raw, 'g');
958
+ // Iterate over all expressions
959
+ for(let i = 0; i < ax.length; i++) {
960
+ let ot = ax[i].text,
961
+ nt = '',
962
+ m = re.exec(ot);
963
+ while (m !== null) {
964
+ // NOTE: A match with the unit name will have 3 groups, and the
965
+ // middle one (so element with index 2) should then be equal to
966
+ // the unit name; other matches will be bracketed text that
967
+ // should be ignored
968
+ if(m.length > 2 && m[2] === oldu) {
969
+ // NOTE: lastIndex points right after the complete match,
970
+ // and this match m[0] can have separator characters on both
971
+ // sides which should not be removed
972
+ const
973
+ parts = m[0].split(oldu),
974
+ left = parts[0].split('').pop(),
975
+ right = (parts[1] ? parts[1].split('')[0] : '');
976
+ // NOTE: if one separator is a single quote, the the other
977
+ // must also be a single quote (to avoid 'ton/m3' to be
978
+ // renamed when 'ton' is renamed)
979
+ if(!((left === "'" || right === "'") && left !== right) &&
980
+ sep.indexOf(left) >= 0 && sep.indexOf(right) >= 0) {
981
+ // Separator chars on both side =>
982
+ nt += ot.slice(0, re.lastIndex - m[0].length) + parts.join(newu);
983
+ ot = ot.slice(re.lastIndex);
984
+ }
985
+ }
986
+ m = re.exec(ot);
987
+ }
988
+ if(nt) {
989
+ ax[i].text = nt + ot;
990
+ nr++;
991
+ }
992
+ }
993
+ }
994
+ if(nr) {
995
+ UI.notify(pluralS(nr, 'scale unit') + ' renamed');
996
+ UI.drawDiagram(MODEL);
997
+ }
998
+ }
999
+
1000
+ unitConversionMultiplier(from, to) {
1001
+ // Compute and return the FROM : TO unit conversion rate
1002
+ // NOTE: no conversion if TO is the primitive unit
1003
+ if(from === to || to === '1' || to === '') return 1;
1004
+ const fsu = this.scale_units[from];
1005
+ if(fsu) {
1006
+ const fcr = fsu.conversionRates();
1007
+ if(fcr.hasOwnProperty(to)) return fcr[to];
1008
+ }
1009
+ return VM.UNDEFINED;
1010
+ }
1011
+
836
1012
  addNote(node=null) {
837
1013
  // Add a note to the focal cluster
838
1014
  let n = new Note(this.focal_cluster);
@@ -840,7 +1016,7 @@ class LinnyRModel {
840
1016
  this.focal_cluster.notes.push(n);
841
1017
  return n;
842
1018
  }
843
-
1019
+
844
1020
  addCluster(name, actor_name, node=null) {
845
1021
  // NOTE: Adapt XML saved by legacy Linny-R software
846
1022
  if(name === UI.FORMER_TOP_CLUSTER_NAME ||
@@ -1006,7 +1182,7 @@ class LinnyRModel {
1006
1182
  const id = UI.nameToID(name);
1007
1183
  let d = this.namedObjectByID(id);
1008
1184
  if(d) {
1009
- if(IO_CONTEXT) {
1185
+ if(IO_CONTEXT && d !== this.equations_dataset) {
1010
1186
  IO_CONTEXT.supersede(d);
1011
1187
  } else {
1012
1188
  // Preserve name uniqueness
@@ -1015,11 +1191,27 @@ class LinnyRModel {
1015
1191
  }
1016
1192
  }
1017
1193
  d = new Dataset(name);
1018
- // NOTE: when equations dataset is added, recognize it as such, or its
1019
- // modifier selectors may be rejected while initializing from XML
1020
- if(name === UI.EQUATIONS_DATASET_NAME) this.equations_dataset = d;
1194
+ let eqds = null;
1195
+ if(name === UI.EQUATIONS_DATASET_NAME) {
1196
+ // When including a module, the current equations must be saved,
1197
+ // then the newly parsed dataset must have its modifiers prefixed,
1198
+ // and then be merged with the original equations dataset
1199
+ if(IO_CONTEXT) eqds = this.equations_dataset;
1200
+ // When equations dataset is added, recognize it as such, or its
1201
+ // modifier selectors may be rejected while initializing from XML
1202
+ this.equations_dataset = d;
1203
+ }
1021
1204
  if(node) d.initFromXML(node);
1022
- this.datasets[id] = d;
1205
+ if(eqds) {
1206
+ // Restore pointer to original equations dataset
1207
+ this.equations_dataset = eqds;
1208
+ // Add included equations with prefixed names
1209
+ console.log('HERE', d);
1210
+ // Return the extended equations dataset
1211
+ return eqds;
1212
+ } else {
1213
+ this.datasets[id] = d;
1214
+ }
1023
1215
  return d;
1024
1216
  }
1025
1217
 
@@ -1269,8 +1461,8 @@ class LinnyRModel {
1269
1461
 
1270
1462
  setSelection() {
1271
1463
  // Set selection to contain all selected entities in the focal cluster
1272
- // NOTE: to be called after loading a model, and after UNDO/REDO (and then
1273
- // before drawing the diagram)
1464
+ // NOTE: to be called after loading a model, and after UNDO/REDO (and
1465
+ // then before drawing the diagram)
1274
1466
  const fc = this.focal_cluster;
1275
1467
  this.selection.length = 0;
1276
1468
  this.selection_related_arrows.length = 0;
@@ -1835,67 +2027,90 @@ class LinnyRModel {
1835
2027
  }
1836
2028
  }
1837
2029
 
1838
- replaceEntityInExpressions(en1, en2) {
1839
- // Replace entity name `en1` by `en2` in all variables in all expressions
1840
- // (provided that they are not identical)
1841
- if(en1 === en2) return;
1842
- // NOTE: ignore case and multiple spaces in `en1`, but conserve those in
1843
- // new name `en2` (except for leading and trailing spaces)
1844
- en1 = en1.trim().replace(/\s+/g, ' ').toLowerCase();
1845
- en2 = en2.trim();
1846
- // NOTE: Neither entity name may be empty
1847
- if(!en1 || !en2) return;
1848
- // NOTE: use the `rewrite` method of class IOContext; this will keep track
1849
- // of the number of replacements made
1850
- const ioc = new IOContext();
1851
- // Check all actor weight expressions
2030
+ get allExpressions() {
2031
+ // Returns list of all Expression objects
2032
+ // NOTE: start with dataset expressions, so that when recompiling
2033
+ // their `level-based` property is set before recompiling the
2034
+ // other expressions
2035
+ const x = [];
2036
+ for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
2037
+ const ds = this.datasets[k];
2038
+ // NOTE: dataset modifier expressions include the equations
2039
+ for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
2040
+ x.push(ds.modifiers[m].expression);
2041
+ }
2042
+ }
1852
2043
  for(let k in this.actors) if(this.actors.hasOwnProperty(k)) {
1853
- ioc.rewrite(this.actors[k].weight, en1, en2);
2044
+ x.push(this.actors[k].weight);
1854
2045
  }
1855
- // Check all process attribute expressions
1856
2046
  for(let k in this.processes) if(this.processes.hasOwnProperty(k)) {
1857
2047
  const p = this.processes[k];
1858
- ioc.rewrite(p.lower_bound, en1, en2);
1859
- ioc.rewrite(p.upper_bound, en1, en2);
1860
- ioc.rewrite(p.initial_level, en1, en2);
1861
- ioc.rewrite(p.pace_expression, en1, en2);
2048
+ x.push(p.lower_bound, p.upper_bound, p.initial_level, p.pace_expression);
1862
2049
  }
1863
- // Check all product attribute expressions
1864
2050
  for(let k in this.products) if(this.products.hasOwnProperty(k)) {
1865
2051
  const p = this.products[k];
1866
- ioc.rewrite(p.lower_bound, en1, en2);
1867
- ioc.rewrite(p.upper_bound, en1, en2);
1868
- ioc.rewrite(p.initial_level, en1, en2);
1869
- ioc.rewrite(p.price, en1, en2);
2052
+ x.push(p.lower_bound, p.upper_bound, p.initial_level, p.price);
1870
2053
  }
1871
- // Check all notes in clusters for their color expressions and fields
1872
2054
  for(let k in this.clusters) if(this.clusters.hasOwnProperty(k)) {
1873
2055
  const c = this.clusters[k];
1874
2056
  for(let i = 0; i < c.notes.length; i++) {
1875
2057
  const n = c.notes[i];
1876
- ioc.rewrite(n.color, en1, en2);
1877
- // Also rename entities in note fields
1878
- n.rewriteFields(en1, en2);
2058
+ x.push(n.color);
1879
2059
  }
1880
2060
  }
1881
- // Check all link rate & delay expressions
1882
2061
  for(let k in this.links) if(this.links.hasOwnProperty(k)) {
1883
- ioc.rewrite(this.links[k].relative_rate, en1, en2);
1884
- ioc.rewrite(this.links[k].flow_delay, en1, en2);
2062
+ const l = this.links[k];
2063
+ x.push(l.relative_rate, l.flow_delay);
1885
2064
  }
1886
- // Check all dataset modifier expressions
1887
- for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
1888
- const ds = this.datasets[k];
1889
- for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
1890
- ioc.rewrite(ds.modifiers[m].expression, en1, en2);
2065
+ return x;
2066
+ }
2067
+
2068
+ replaceEntityInExpressions(en1, en2) {
2069
+ // Replace entity name `en1` by `en2` in all variables in all expressions
2070
+ // (provided that they are not identical)
2071
+ if(en1 === en2) return;
2072
+ // NOTE: ignore case and multiple spaces in `en1`, but conserve those in
2073
+ // new name `en2` (except for leading and trailing spaces)
2074
+ en1 = en1.trim().replace(/\s+/g, ' ').toLowerCase();
2075
+ en2 = en2.trim();
2076
+ // NOTE: Neither entity name may be empty
2077
+ if(!en1 || !en2) return;
2078
+ // NOTE: use the `rewrite` method of class IOContext; this will keep track
2079
+ // of the number of replacements made
2080
+ const ioc = new IOContext();
2081
+ // Iterate over all expressions
2082
+ const ax = this.allExpressions;
2083
+ for(let i = 0; i < ax.length; i++) {
2084
+ ioc.rewrite(ax[i], en1, en2);
2085
+ }
2086
+ // Iterate over all notes in clusters to rename entities in note fields
2087
+ for(let k in this.clusters) if(this.clusters.hasOwnProperty(k)) {
2088
+ const cn = this.clusters[k].notes;
2089
+ for(let i = 0; i < cn.length; i++) {
2090
+ cn[i].rewriteFields(en1, en2);
1891
2091
  }
1892
2092
  }
1893
2093
  if(ioc.replace_count) {
1894
2094
  UI.notify(`Renamed ${pluralS(ioc.replace_count, 'variable')} in ` +
1895
2095
  pluralS(ioc.expression_count, 'expression'));
1896
2096
  }
2097
+ // Also rename entities in parameters and outcomes of sensitivity analysis
2098
+ for(let i = 0; i < this.sensitivity_parameters.length; i++) {
2099
+ const sp = this.sensitivity_parameters[i].split('|');
2100
+ if(sp[0].toLowerCase() === en1) {
2101
+ sp[0] = en2;
2102
+ this.sensitivity_parameters[i] = sp.join('|');
2103
+ }
2104
+ }
2105
+ for(let i = 0; i < this.sensitivity_outcomes.length; i++) {
2106
+ const so = this.sensitivity_outcomes[i].split('|');
2107
+ if(so[0].toLowerCase() === en1) {
2108
+ so[0] = en2;
2109
+ this.sensitivity_outcomes[i] = so.join('|');
2110
+ }
2111
+ }
1897
2112
  // Name was changed, so update controller dialogs to display the new name
1898
- UI.updateControllerDialogs('CDEFX');
2113
+ UI.updateControllerDialogs('CDEFJX');
1899
2114
  }
1900
2115
 
1901
2116
  replaceAttributeInExpressions(ena, a) {
@@ -1905,58 +2120,40 @@ class LinnyRModel {
1905
2120
  // or in the new attribute `a` (except for leading and trailing spaces)
1906
2121
  a = a.trim();
1907
2122
  ena = ena.split('|');
1908
- // Double-check that `a` is no empty and `ena` contains a vertical bar
2123
+ // Double-check that `a` is not empty and `ena` contains a vertical bar
1909
2124
  if(!a || ena.length < 2) return;
1910
2125
  // Prepare regex to match [entity|attribute] including brackets, but case-
1911
2126
  // tolerant and spacing-tolerant
1912
2127
  const
1913
2128
  en = escapeRegex(ena[0].trim().replace(/\s+/g, ' ').toLowerCase()),
1914
2129
  at = ena[1].trim(),
1915
- raw = en.replace(/\s/, '\\s+') + '\\s*\\|\\s*' + escapeRegex(at),
1916
- re = new RegExp(
1917
- '\\[\\s*' + raw + '\\s*(\\@[^\\]]+)?\\s*\\]',
1918
- 'gi');
2130
+ raw = en.replace(/\s/, `\s+`) + `\s*\|\s*` + escapeRegex(at),
2131
+ re = new RegExp(`\[\s*${raw}\s*(\@[^\]]+)?\s*\]`, 'gi');
1919
2132
  // Count replacements made
1920
2133
  let n = 0;
1921
- // Check all actor weight expressions
1922
- for(let k in this.actors) if(this.actors.hasOwnProperty(k)) {
1923
- n += this.actors[k].weight.replaceAttribute(re, at, a);
1924
- }
1925
- // Check all process attribute expressions
1926
- for(let k in this.processes) if(this.processes.hasOwnProperty(k)) {
1927
- const p = this.processes[k];
1928
- n += p.lower_bound.replaceAttribute(re, at, a);
1929
- n += p.upper_bound.replaceAttribute(re, at, a);
1930
- n += p.initial_level.replaceAttribute(re, at, a);
1931
- n += p.pace_expression.replaceAttribute(re, at, a);
2134
+ // Iterate over all expressions
2135
+ const ax = this.allExpressions;
2136
+ for(let i = 0; i < ax.length; i++) {
2137
+ n += ax[i].replaceAttribute(re, at, a);
1932
2138
  }
1933
- // Check all product attribute expressions
1934
- for(let k in this.products) if(this.products.hasOwnProperty(k)) {
1935
- const p = this.products[k];
1936
- n += p.lower_bound.replaceAttribute(re, at, a);
1937
- n += p.upper_bound.replaceAttribute(re, at, a);
1938
- n += p.initial_level.replaceAttribute(re, at, a);
1939
- n += p.price.replaceAttribute(re, at, a);
1940
- }
1941
- // Check all notes in clusters for their color expressions and fields
1942
- for(let k in this.clusters) if(this.clusters.hasOwnProperty(k)) {
1943
- const c = this.clusters[k];
1944
- for(let i = 0; i < c.notes.length; i++) {
1945
- n += c.notes[i].color.replaceAttribute(re, at, a);
2139
+ // Also rename attributes in parameters and outcomes of sensitivity analysis
2140
+ let sa_cnt = 0;
2141
+ const enat = en + '|' + at;
2142
+ for(let i = 0; i < this.sensitivity_parameters.length; i++) {
2143
+ const sp = this.sensitivity_parameters[i];
2144
+ if(sp.toLowerCase() === enat) {
2145
+ this.sensitivity_parameters[i] = sp.split('|')[0] + '|' + a;
2146
+ sa_cnt++;
1946
2147
  }
1947
2148
  }
1948
- // Check all link rate and delay expressions
1949
- for(let k in this.links) if(this.links.hasOwnProperty(k)) {
1950
- n += this.links[k].relative_rate.replaceAttribute(re, at, a);
1951
- n += this.links[k].flow_delay.replaceAttribute(re, at, a);
1952
- }
1953
- // Check all dataset modifier expressions
1954
- for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
1955
- const ds = this.datasets[k];
1956
- for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
1957
- n += ds.modifiers[m].expression.replaceAttribute(re, at, a);
2149
+ for(let i = 0; i < this.sensitivity_outcomes.length; i++) {
2150
+ const so = this.sensitivity_outcomes[i];
2151
+ if(so.toLowerCase() === enat) {
2152
+ this.sensitivity_outcomes[i] = so.split('|')[0] + '|' + a;
2153
+ sa_cnt++;
1958
2154
  }
1959
2155
  }
2156
+ if(sa_cnt > 0) SENSITIVITY_ANALYSIS.updateDialog();
1960
2157
  return n;
1961
2158
  }
1962
2159
 
@@ -2056,7 +2253,9 @@ class LinnyRModel {
2056
2253
  for(i = 0; i < n.childNodes.length; i++) {
2057
2254
  c = n.childNodes[i];
2058
2255
  if(c.nodeName === 'scaleunit') {
2059
- this.addScaleUnit(xmlDecoded(nodeContentByTag(c, 'name')));
2256
+ this.addScaleUnit(xmlDecoded(nodeContentByTag(c, 'name')),
2257
+ nodeContentByTag(c, 'scalar'),
2258
+ xmlDecoded(nodeContentByTag(c, 'base-unit')));
2060
2259
  }
2061
2260
  }
2062
2261
  }
@@ -2196,14 +2395,14 @@ class LinnyRModel {
2196
2395
  // will then add the module prefix to the selector
2197
2396
  if(IO_CONTEXT) {
2198
2397
  if(name === UI.EQUATIONS_DATASET_NAME) {
2199
- const mn = childNodeByTag(node, 'modifiers');
2398
+ const mn = childNodeByTag(c, 'modifiers');
2200
2399
  if(mn && mn.childNodes) {
2201
2400
  for(let j = 0; j < mn.childNodes.length; j++) {
2202
- const c = mn.childNodes[j];
2203
- if(c.nodeName === 'modifier') {
2401
+ const cc = mn.childNodes[j];
2402
+ if(cc.nodeName === 'modifier') {
2204
2403
  this.equations_dataset.addModifier(
2205
- xmlDecoded(nodeContentByTag(c, 'selector')),
2206
- c, IO_CONTEXT);
2404
+ xmlDecoded(nodeContentByTag(cc, 'selector')),
2405
+ cc, IO_CONTEXT);
2207
2406
  }
2208
2407
  }
2209
2408
  }
@@ -2367,12 +2566,11 @@ class LinnyRModel {
2367
2566
  '</end-period><look-ahead-period>', this.look_ahead,
2368
2567
  '</look-ahead-period><round-sequence>', this.round_sequence,
2369
2568
  '</round-sequence><scaleunits>'].join('');
2370
- for(let i = 0; i < this.scale_units.length; i++) {
2371
- xml += '<scaleunit><name>' + xmlEncoded(this.scale_units[i]) +
2372
- '</name></scaleunit>';
2569
+ let obj;
2570
+ for(obj in this.scale_units) if(this.scale_units.hasOwnProperty(obj)) {
2571
+ xml += this.scale_units[obj].asXML;
2373
2572
  }
2374
2573
  xml += '</scaleunits><actors>';
2375
- let obj;
2376
2574
  for(obj in this.actors) {
2377
2575
  // NOTE: do not to save "(no actor)"
2378
2576
  if(this.actors.hasOwnProperty(obj) && obj != UI.nameToID(UI.NO_ACTOR)) {
@@ -2485,12 +2683,15 @@ class LinnyRModel {
2485
2683
  const ds_dict = {};
2486
2684
  for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
2487
2685
  const ds = this.datasets[k];
2488
- for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
2489
- const s = ds.modifiers[m].selector;
2490
- if(s in ds_dict) {
2491
- ds_dict[s].push(ds);
2492
- } else {
2493
- ds_dict[s] = [ds];
2686
+ // NOTE: ignore selectors of the equations dataset
2687
+ if(ds !== this.equations_dataset) {
2688
+ for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
2689
+ const s = ds.modifiers[m].selector;
2690
+ if(s in ds_dict) {
2691
+ ds_dict[s].push(ds);
2692
+ } else {
2693
+ ds_dict[s] = [ds];
2694
+ }
2494
2695
  }
2495
2696
  }
2496
2697
  }
@@ -2586,6 +2787,10 @@ class LinnyRModel {
2586
2787
  this.cleanVector(p.cash_flow, 0, 0);
2587
2788
  this.cleanVector(p.cash_in, 0, 0);
2588
2789
  this.cleanVector(p.cash_out, 0, 0);
2790
+ // NOTE: note fields also must be reset
2791
+ for(let i = 0; i < p.notes.length; i++) {
2792
+ p.notes[i].parsed = false;
2793
+ }
2589
2794
  }
2590
2795
  for(obj in this.processes) if(this.processes.hasOwnProperty(obj)) {
2591
2796
  p = this.processes[obj];
@@ -2676,30 +2881,9 @@ class LinnyRModel {
2676
2881
 
2677
2882
  compileExpressions() {
2678
2883
  // Compile all expression attributes of all model entities
2679
- let obj,
2680
- p;
2681
- // NOTE: start with dataset expressions, so that their level-based
2682
- // property is set before compiling the other expressions
2683
- for(obj in this.datasets) if(this.datasets.hasOwnProperty(obj)) {
2684
- this.datasets[obj].compileExpressions();
2685
- }
2686
- for(obj in this.actors) if(this.actors.hasOwnProperty(obj)) {
2687
- this.actors[obj].weight.compile();
2688
- }
2689
- for(obj in this.processes) if(this.processes.hasOwnProperty(obj)) {
2690
- p = this.processes[obj];
2691
- p.lower_bound.compile();
2692
- p.upper_bound.compile();
2693
- }
2694
- for(obj in this.products) if(this.products.hasOwnProperty(obj)) {
2695
- p = this.products[obj];
2696
- p.lower_bound.compile();
2697
- p.upper_bound.compile();
2698
- p.price.compile();
2699
- }
2700
- for(obj in this.links) if(this.links.hasOwnProperty(obj)) {
2701
- this.links[obj].relative_rate.compile();
2702
- this.links[obj].flow_delay.compile();
2884
+ const ax = this.allExpressions;
2885
+ for(let i = 0; i < ax.length; i++) {
2886
+ ax[i].compile();
2703
2887
  }
2704
2888
  }
2705
2889
 
@@ -3341,17 +3525,6 @@ class LinnyRModel {
3341
3525
  // Start with the Linny-R model properties
3342
3526
  let diff = differences(this, m, Object.keys(UI.MC.SETTINGS_PROPS));
3343
3527
  if(Object.keys(diff).length > 0) d.settings = diff;
3344
- // Then check for differences in scale unit lists
3345
- diff = {};
3346
- for(let i = 0; i < this.scale_units.length; i++) {
3347
- const su = this.scale_units[i];
3348
- if(m.scale_units.indexOf(su) < 0) diff[su] = [UI.MC.ADDED, su];
3349
- }
3350
- for(let i = 0; i < m.scale_units.length; i++) {
3351
- const su = m.scale_units[i];
3352
- if(this.scale_units.indexOf(su) < 0) diff[su] = [UI.MC.DELETED, su];
3353
- }
3354
- if(Object.keys(diff).length > 0) d.units = diff;
3355
3528
  // NOTE: dataset differences will also detect equation differences
3356
3529
  for(let i = 0; i < UI.MC.ENTITY_PROPS.length; i++) {
3357
3530
  const ep = UI.MC.ENTITY_PROPS[i];
@@ -3688,8 +3861,8 @@ class IOContext {
3688
3861
  }
3689
3862
 
3690
3863
  bind(fn, an) {
3691
- // Binds the formal name `n` of an entity in a module to the actual name
3692
- // `an` it will have in the current model
3864
+ // Binds the formal name `fn` of an entity in a module to the actual
3865
+ // name `an` it will have in the current model
3693
3866
  const id = UI.nameToID(fn);
3694
3867
  if(this.bindings.hasOwnProperty(id)) {
3695
3868
  this.bindings[id].bind(an);
@@ -3709,7 +3882,6 @@ class IOContext {
3709
3882
  // (and for processes and clusters: with actor name `an` if specified and
3710
3883
  // not "(no actor)")
3711
3884
  // NOTE: do not modify (no actor), nor the "dataset dot"
3712
- // @@TO DO: correctly handle equations!
3713
3885
  if(n === UI.NO_ACTOR || n === '.') return n;
3714
3886
  // NOTE: the top cluster of the included model has the prefix as its name
3715
3887
  if(n === UI.TOP_CLUSTER_NAME || n === UI.FORMER_TOP_CLUSTER_NAME) {
@@ -3844,7 +4016,7 @@ class IOContext {
3844
4016
  a,
3845
4017
  stat;
3846
4018
  while(true) {
3847
- p = x.text.indexOf('[', p + 1);
4019
+ p = x.text.indexOf('[', q + 1);
3848
4020
  if(p < 0) {
3849
4021
  // No more '[' => add remaining part of text, and quit
3850
4022
  s += x.text.slice(q + 1);
@@ -3949,6 +4121,79 @@ class IOContext {
3949
4121
  } // END of class IOContext
3950
4122
 
3951
4123
 
4124
+ // CLASS ScaleUnit
4125
+ class ScaleUnit {
4126
+ constructor(name, scalar, base_unit) {
4127
+ this.name = name;
4128
+ // NOTES:
4129
+ // (1) Undefined or empty strings default to '1'
4130
+ // (2) Multiplier is stored as string to preserve modeler's notation
4131
+ this.scalar = scalar || '1';
4132
+ this.base_unit = base_unit || '1';
4133
+ }
4134
+
4135
+ get multiplier() {
4136
+ // Returns scalar as number
4137
+ return safeStrToFloat(this.scalar, 1);
4138
+ }
4139
+
4140
+ conversionRates() {
4141
+ // Returns a "dictionary" {U1: R1, U2: R2, ...} such that Ui is a
4142
+ // scale unit that can be converted to *this* scaleunit U at rate Ri
4143
+ const cr = {};
4144
+ let p = 0, // previous count of entries
4145
+ n = 1;
4146
+ // At least one conversion: U -> U with rate 1
4147
+ cr[this.name] = 1;
4148
+ if(this.base_unit !== '1') {
4149
+ // Second conversion: U -> base of U with modeler-defined rate
4150
+ cr[this.base_unit] = this.multiplier;
4151
+ n++;
4152
+ }
4153
+ // Keep track of the number of keys; terminate as no new keys
4154
+ while(p < n) {
4155
+ p = n;
4156
+ // Iterate over all convertible scale units discovered so far
4157
+ for(let u in cr) if(cr.hasOwnProperty(u)) {
4158
+ // Look for conversions to units NOT yet detected
4159
+ for(let k in MODEL.scale_units) if(k != '1' &&
4160
+ MODEL.scale_units.hasOwnProperty(k)) {
4161
+ const
4162
+ su = MODEL.scale_units[k],
4163
+ b = su.base_unit;
4164
+ if(b === '1') continue;
4165
+ if(!cr.hasOwnProperty(k) && cr.hasOwnProperty(b)) {
4166
+ // Add unit if new while base unit is convertible
4167
+ cr[k] = cr[b] / su.multiplier;
4168
+ n++;
4169
+ } else if(cr.hasOwnProperty(k) && !cr.hasOwnProperty(b)) {
4170
+ // Likewise, add base unit if new while unit is convertible
4171
+ cr[b] = cr[k] * su.multiplier;
4172
+ n++;
4173
+ }
4174
+ }
4175
+ }
4176
+ }
4177
+ return cr;
4178
+ }
4179
+
4180
+ get asXML() {
4181
+ return ['<scaleunit><name>', xmlEncoded(this.name),
4182
+ '</name><scalar>', this.scalar,
4183
+ '</scalar><base-unit>', xmlEncoded(this.base_unit),
4184
+ '</base-unit></scaleunit>'].join('');
4185
+ }
4186
+
4187
+ // NOTE: NO initFromXML because scale units are added directly
4188
+
4189
+ differences(u) {
4190
+ // Return "dictionary" of differences, or NULL if none
4191
+ const d = differences(this, u, UI.MC.UNIT_PROPS);
4192
+ if(Object.keys(d).length > 0) return d;
4193
+ return null;
4194
+ }
4195
+ }
4196
+
3952
4197
  // CLASS Actor
3953
4198
  class Actor {
3954
4199
  constructor(name) {
@@ -4104,35 +4349,45 @@ class ObjectWithXYWH {
4104
4349
 
4105
4350
  // CLASS NoteField: numeric value of "field" [[dataset]] in note text
4106
4351
  class NoteField {
4107
- constructor(f, o) {
4108
- // `f` holds the unmodified tag string [[dataset]] to be replaced by the
4109
- // value of vector or expression `o` for the current time step
4352
+ constructor(f, o, u='1', m=1) {
4353
+ // `f` holds the unmodified tag string [[dataset]] to be replaced by
4354
+ // the value of vector or expression `o` for the current time step;
4355
+ // if specified, `u` is the unit of the value to be displayed, and
4356
+ // `m` is the multiplier for the value to be displayed
4110
4357
  this.field = f;
4111
4358
  this.object = o;
4359
+ this.unit = u;
4360
+ this.multiplier = m;
4112
4361
  }
4113
4362
 
4114
4363
  get value() {
4115
- // Returns the numeric value of this note field
4364
+ // Returns the numeric value of this note field as a numeric string
4365
+ // followed by its unit (unless this is 1)
4366
+ let v = VM.UNDEFINED;
4116
4367
  const t = MODEL.t;
4117
4368
  if(Array.isArray(this.object)) {
4118
4369
  // Object is a vector
4119
- if(t < this.object.length) {
4120
- return this.object[t];
4121
- } else {
4122
- return VM.UNDEFINED;
4123
- }
4124
- } else if(this.object.hasOwnProperty('c') && this.object.hasOwnProperty('u')) {
4370
+ if(t < this.object.length) v = this.object[t];
4371
+ } else if(this.object.hasOwnProperty('c') &&
4372
+ this.object.hasOwnProperty('u')) {
4125
4373
  // Object holds link lists for cluster balance computation
4126
- return MODEL.flowBalance(this.object, t);
4374
+ v = MODEL.flowBalance(this.object, t);
4127
4375
  } else if(this.object instanceof Expression) {
4128
4376
  // Object is an expression
4129
- return this.object.result(t);
4377
+ v = this.object.result(t);
4130
4378
  } else if(typeof this.object === 'number') {
4131
- return this.object;
4379
+ v = this.object;
4380
+ } else {
4381
+ // NOTE: this fall-through should not occur
4382
+ console.log('Note field value issue:', this.object);
4132
4383
  }
4133
- // NOTE: this fall-through should not occur
4134
- console.log('Note field value issue:', this.object);
4135
- return VM.UNDEFINED;
4384
+ if(Math.abs(this.multiplier - 1) > VM.NEAR_ZERO &&
4385
+ v > VM.MINUS_INFINITY && v < VM.PLUS_INFINITY) {
4386
+ v *= this.multiplier;
4387
+ }
4388
+ v = VM.sig4Dig(v);
4389
+ if(this.unit !== '1') v += ' ' + this.unit;
4390
+ return v;
4136
4391
  }
4137
4392
 
4138
4393
  } // END of class NoteField
@@ -4195,6 +4450,13 @@ class Note extends ObjectWithXYWH {
4195
4450
  this.width = safeStrToInt(nodeContentByTag(node, 'width'));
4196
4451
  this.height = safeStrToInt(nodeContentByTag(node, 'height'));
4197
4452
  this.color.text = xmlDecoded(nodeContentByTag(node, 'color'));
4453
+ if(IO_CONTEXT) {
4454
+ const fel = this.fieldEntities;
4455
+ for(let i = 0; i < fel.length; i++) {
4456
+ this.rewriteTags(fel[i], IO_CONTEXT.actualName(fel[i]));
4457
+ }
4458
+ IO_CONTEXT.rewrite(this.color);
4459
+ }
4198
4460
  }
4199
4461
 
4200
4462
  setCluster(c) {
@@ -4220,24 +4482,93 @@ class Note extends ObjectWithXYWH {
4220
4482
  for(let i = 0; i < tags.length; i++) {
4221
4483
  const
4222
4484
  tag = tags[i],
4223
- ena = tag.slice(2, tag.length - 2).trim().split('|');
4485
+ inner = tag.slice(2, tag.length - 2).trim(),
4486
+ bar = inner.lastIndexOf('|'),
4487
+ arrow = inner.lastIndexOf('->');
4488
+ // Check if a unit conversion scalar was specified
4489
+ let ena,
4490
+ from_unit = '1',
4491
+ to_unit = '',
4492
+ multiplier = 1;
4493
+ if(arrow > bar) {
4494
+ // Now for sure it is entity->unit or entity|attr->unit
4495
+ ena = inner.split('->');
4496
+ // As example, assume that unit = 'kWh' (so the value of the
4497
+ // field should be displayed in kilowatthour)
4498
+ // NOTE: use .trim() instead of UI.cleanName(...) here;
4499
+ // this forces the modeler to be exact, and that permits proper
4500
+ // renaming of scale units in note fields
4501
+ to_unit = ena[1].trim();
4502
+ ena = ena[0].split('|');
4503
+ if(!MODEL.scale_units.hasOwnProperty(to_unit)) {
4504
+ UI.warn(`Unknown scale unit "${to_unit}"`);
4505
+ to_unit = '1';
4506
+ }
4507
+ } else {
4508
+ ena = inner.split('|');
4509
+ }
4224
4510
  // Look up entity for name and attribute
4225
- const obj = MODEL.objectByName(ena[0]);
4511
+ const obj = MODEL.objectByName(ena[0].trim());
4226
4512
  if(obj instanceof DatasetModifier) {
4227
- this.fields.push(new NoteField(tag, obj.expression));
4513
+ // NOTE: equations are (for now) dimensionless => unit '1'
4514
+ if(obj.dataset !== MODEL.equations_dataset) {
4515
+ from_unit = obj.dataset.scale_unit;
4516
+ multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
4517
+ }
4518
+ this.fields.push(new NoteField(tag, obj.expression, to_unit, multiplier));
4228
4519
  } else if(obj) {
4229
4520
  // If attribute omitted, use default attribute of entity type
4230
4521
  const attr = (ena.length > 1 ? ena[1].trim() : obj.defaultAttribute);
4231
- // Variable may specify a vector-type attribute
4232
- let val = obj.attributeValue(attr);
4522
+ let val = null;
4523
+ // NOTE: for datasets, use the active modifier
4524
+ if(!attr && obj instanceof Dataset) {
4525
+ val = obj.activeModifierExpression;
4526
+ } else {
4527
+ // Variable may specify a vector-type attribute
4528
+ val = obj.attributeValue(attr);
4529
+ }
4233
4530
  // If not, it may be a cluster unit balance
4234
4531
  if(!val && attr.startsWith('=') && obj instanceof Cluster) {
4235
4532
  val = {c: obj, u: attr.substring(1).trim()};
4533
+ from_unit = val.u;
4534
+ }
4535
+ if(obj instanceof Dataset) {
4536
+ from_unit = obj.scale_unit;
4537
+ } else if(obj instanceof Product) {
4538
+ if(attr === 'L') {
4539
+ from_unit = obj.scale_unit;
4540
+ } else if(attr === 'CP' || attr === 'HCP') {
4541
+ from_unit = MODEL.currency_unit;
4542
+ }
4543
+ } else if(obj instanceof Link) {
4544
+ const node = (obj.from_node instanceof Process ?
4545
+ obj.to_node : obj.from_node);
4546
+ if(attr === 'F') {
4547
+ if(obj.multiplier <= VM.LM_MEAN) {
4548
+ from_unit = node.scale_unit;
4549
+ } else {
4550
+ from_unit = '1';
4551
+ }
4552
+ }
4553
+ } else if(attr === 'CI' || attr === 'CO' || attr === 'CF') {
4554
+ from_unit = MODEL.currency_unit;
4236
4555
  }
4237
4556
  // If not, it may be an expression-type attribute
4238
- if(!val) val = obj.attributeExpression(attr);
4557
+ if(!val) {
4558
+ val = obj.attributeExpression(attr);
4559
+ if(obj instanceof Product) {
4560
+ if(attr === 'IL' || attr === 'LB' || attr === 'UB') {
4561
+ from_unit = obj.scale_unit;
4562
+ } else if(attr === 'P') {
4563
+ from_unit = MODEL.currency_unit + '/' + obj.scale_unit;
4564
+ }
4565
+ }
4566
+ }
4567
+ // If no TO unit, add the FROM unit
4568
+ if(to_unit === '') to_unit = from_unit;
4239
4569
  if(val) {
4240
- this.fields.push(new NoteField(tag, val));
4570
+ multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
4571
+ this.fields.push(new NoteField(tag, val, to_unit, multiplier));
4241
4572
  } else {
4242
4573
  UI.warn(`Unknown ${obj.type.toLowerCase()} attribute "${attr}"`);
4243
4574
  }
@@ -4248,10 +4579,48 @@ class Note extends ObjectWithXYWH {
4248
4579
  }
4249
4580
  this.parsed = true;
4250
4581
  }
4582
+
4583
+ get fieldEntities() {
4584
+ // Return a list with names of entities used in fields
4585
+ const
4586
+ fel = [],
4587
+ tags = this.contents.match(/\[\[[^\]]+\]\]/g);
4588
+ for(let i = 0; i < tags.length; i++) {
4589
+ const
4590
+ tag = tags[i],
4591
+ inner = tag.slice(2, tag.length - 2).trim(),
4592
+ vb = inner.lastIndexOf('|'),
4593
+ ua = inner.lastIndexOf('->');
4594
+ if(vb >= 0) {
4595
+ addDistinct(inner.slice(0, vb), fel);
4596
+ } else if(ua >= 0 &&
4597
+ MODEL.scale_units.hasOwnProperty(inner.slice(ua + 2))) {
4598
+ addDistinct(inner.slice(0, ua), fel);
4599
+ } else {
4600
+ addDistinct(inner, fel);
4601
+ }
4602
+ }
4603
+ return fel;
4604
+ }
4605
+
4606
+ rewriteTags(en1, en2) {
4607
+ // Rewrite tags that reference entity name `en1` to reference `en2` instead
4608
+ if(en1 === en2) return;
4609
+ const
4610
+ raw = en1.split(/\s+/).join('\\\\s+'),
4611
+ re = new RegExp('\\[\\[\\s*' + raw + '\\s*(\\->|\\||\\])', 'g'),
4612
+ tags = this.contents.match(re);
4613
+ if(tags) {
4614
+ for(let i = 0; i < tags.length; i++) {
4615
+ this.contents = this.contents.replace(tags[i], tags[i].replace(en1, en2));
4616
+ }
4617
+ }
4618
+ }
4251
4619
 
4252
4620
  rewriteFields(en1, en2) {
4253
4621
  // Rename fields that reference entity name `en1` to reference `en2` instead
4254
4622
  // NOTE: this does not affect the expression code
4623
+ if(en1 === en2) return;
4255
4624
  for(let i = 0; i < this.fields.length; i++) {
4256
4625
  const
4257
4626
  f = this.fields[i],
@@ -4260,12 +4629,17 @@ class Note extends ObjectWithXYWH {
4260
4629
  // Separate tag into variable and attribute + offset string (if any)
4261
4630
  let e = tag,
4262
4631
  a = '',
4263
- vb = tag.lastIndexOf('|');
4632
+ vb = tag.lastIndexOf('|'),
4633
+ ua = tag.lastIndexOf('->');
4264
4634
  if(vb >= 0) {
4265
4635
  e = tag.slice(0, vb);
4266
4636
  // NOTE: attribute string includes the vertical bar '|'
4267
4637
  a = tag.slice(vb);
4268
- }
4638
+ } else if(ua >= 0 && MODEL.scale_units.hasOwnProperty(tag.slice(ua + 2))) {
4639
+ e = tag.slice(0, ua);
4640
+ // NOTE: attribute string includes the unit conversion arrow '->'
4641
+ a = tag.slice(ua);
4642
+ }
4269
4643
  // Check for match
4270
4644
  const r = UI.replaceEntity(e, en1, en2);
4271
4645
  if(r) {
@@ -4281,7 +4655,7 @@ class Note extends ObjectWithXYWH {
4281
4655
  let txt = this.contents;
4282
4656
  for(let i = 0; i < this.fields.length; i++) {
4283
4657
  const nf = this.fields[i];
4284
- txt = txt.replace(nf.field, VM.sig4Dig(nf.value));
4658
+ txt = txt.replace(nf.field, nf.value);
4285
4659
  }
4286
4660
  return txt;
4287
4661
  }
@@ -4438,7 +4812,7 @@ class NodeBox extends ObjectWithXYWH {
4438
4812
  delete MODEL.products[old_id];
4439
4813
  } else if(this instanceof Cluster) {
4440
4814
  MODEL.clusters[new_id] = this;
4441
- delete MODEL.products[old_id];
4815
+ delete MODEL.clusters[old_id];
4442
4816
  } else {
4443
4817
  // NOTE: this should never happen => report an error
4444
4818
  UI.alert('Can only rename processes, products and clusters');
@@ -4703,7 +5077,7 @@ class Arrow {
4703
5077
  } else {
4704
5078
  if(p[0] && p[1]) {
4705
5079
  console.log('ERROR: Two distinct flows on monodirectional arrow',
4706
- this, sum);
5080
+ this, sum, p);
4707
5081
  return [0, 0, 0, false, false];
4708
5082
  }
4709
5083
  status = 1;
@@ -6319,6 +6693,8 @@ class Node extends NodeBox {
6319
6693
  ds = MODEL.addDataset(dsn);
6320
6694
  // Use the LB attribute as default value for the dataset
6321
6695
  ds.default_value = parseFloat(this.lower_bound.text);
6696
+ // UB data has same unit as product
6697
+ ds.scale_unit = this.scale_unit;
6322
6698
  ds.data = stringToFloatArray(lb_data);
6323
6699
  ds.computeVector();
6324
6700
  ds.computeStatistics();
@@ -6331,6 +6707,8 @@ class Node extends NodeBox {
6331
6707
  dsn = this.displayName + ' UPPER BOUND DATA',
6332
6708
  ds = MODEL.addDataset(dsn);
6333
6709
  ds.default_value = parseFloat(this.upper_bound.text);
6710
+ // UB data has same unit as product
6711
+ ds.scale_unit = this.scale_unit;
6334
6712
  ds.data = stringToFloatArray(ub_data);
6335
6713
  ds.computeVector();
6336
6714
  ds.computeStatistics();
@@ -6958,6 +7336,8 @@ class Product extends Node {
6958
7336
  ds = MODEL.addDataset(dsn);
6959
7337
  // Use the price attribute as default value for the dataset
6960
7338
  ds.default_value = parseFloat(this.price.text);
7339
+ // NOTE: dataset unit then is a currency
7340
+ ds.scale_unit = MODEL.currency_unit;
6961
7341
  ds.data = stringToFloatArray(data);
6962
7342
  ds.computeVector();
6963
7343
  ds.computeStatistics();
@@ -7340,12 +7720,10 @@ class Link {
7340
7720
 
7341
7721
  // CLASS DatasetModifier
7342
7722
  class DatasetModifier {
7343
- constructor(dataset, selector, params=false) {
7723
+ constructor(dataset, selector) {
7344
7724
  this.dataset = dataset;
7345
7725
  this.selector = selector;
7346
7726
  this.expression = new Expression(dataset, selector, '');
7347
- // Equations may have parameters
7348
- this.parameters = params;
7349
7727
  this.expression_cache = {};
7350
7728
  }
7351
7729
 
@@ -7376,17 +7754,12 @@ class DatasetModifier {
7376
7754
  // NOTE: for some reason, selector may become empty string, so prevent
7377
7755
  // saving such unidentified modifiers
7378
7756
  if(this.selector.trim().length === 0) return '';
7379
- let pstr = (Array.isArray(this.parameters) ?
7380
- this.parameters.join('\\').trim() : '');
7381
- if(pstr) pstr = ' parameters="' + xmlEncoded(pstr) + '"';
7382
- return ['<modifier', pstr, '><selector>', xmlEncoded(this.selector),
7757
+ return ['<modifier><selector>', xmlEncoded(this.selector),
7383
7758
  '</selector><expression>', xmlEncoded(this.expression.text),
7384
7759
  '</expression></modifier>'].join('');
7385
7760
  }
7386
7761
 
7387
7762
  initFromXML(node) {
7388
- const pstr = nodeParameterValue(node, 'parameters').trim();
7389
- this.parameters = (pstr ? xmlDecoded(pstr).split('\\'): false);
7390
7763
  this.expression.text = xmlDecoded(nodeContentByTag(node, 'expression'));
7391
7764
  if(IO_CONTEXT) {
7392
7765
  // Contextualize the included expression
@@ -7418,6 +7791,7 @@ class Dataset {
7418
7791
  this.name = name;
7419
7792
  this.comments = '';
7420
7793
  this.default_value = 0;
7794
+ this.scale_unit = '1';
7421
7795
  this.time_scale = 1;
7422
7796
  this.time_unit = CONFIGURATION.default_time_unit;
7423
7797
  this.method = 'nearest';
@@ -7516,6 +7890,14 @@ class Dataset {
7516
7890
  }
7517
7891
  return this.default_value * MODEL.timeStepDuration / this.timeStepDuration;
7518
7892
  }
7893
+
7894
+ changeScaleUnit(name) {
7895
+ let su = MODEL.addScaleUnit(name);
7896
+ if(su !== this.scale_unit) {
7897
+ this.scale_unit = su;
7898
+ MODEL.cleanUpScaleUnits();
7899
+ }
7900
+ }
7519
7901
 
7520
7902
  matchingModifiers(l) {
7521
7903
  // Returns the list of selectors of this dataset (in order: from most to
@@ -7644,53 +8026,53 @@ class Dataset {
7644
8026
  attributeExpression(a) {
7645
8027
  // Returns expression for selector `a`, or NULL if no such selector exists
7646
8028
  // NOTE: selectors no longer are case-sensitive
7647
- a = UI.nameToID(a);
7648
- for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
7649
- if(m === a) return this.modifiers[m].expression;
8029
+ if(a) {
8030
+ a = UI.nameToID(a);
8031
+ for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
8032
+ if(m === a) return this.modifiers[m].expression;
8033
+ }
7650
8034
  }
7651
8035
  return null;
7652
8036
  }
8037
+
8038
+ get activeModifierExpression() {
8039
+ if(MODEL.running_experiment) {
8040
+ // If an experiment is running, check if dataset modifiers match the
8041
+ // combination of selectors for the active run
8042
+ const mm = this.matchingModifiers(MODEL.running_experiment.activeCombination);
8043
+ // If so, use the first match
8044
+ if(mm.length > 0) return mm[0].expression;
8045
+ }
8046
+ if(this.default_selector) {
8047
+ // If no experiment (so "normal" run), use default selector if specified
8048
+ const dm = this.modifiers[this.default_selector];
8049
+ if(dm) return dm.expression;
8050
+ // Exception should never occur, but check anyway and log it
8051
+ console.log('WARNING: Dataset "' + this.name +
8052
+ `" has no default selector "${this.default_selector}"`);
8053
+ }
8054
+ // Fall-through: return vector instead of expression
8055
+ return this.vector;
8056
+ }
7653
8057
 
7654
8058
  addModifier(selector, node=null, ioc=null) {
7655
- let s = selector,
7656
- params = false;
8059
+ let s = selector;
7657
8060
  // Firstly, sanitize the selector
7658
8061
  if(this === MODEL.equations_dataset) {
7659
8062
  // Equation identifiers cannot contain characters that have special
7660
8063
  // meaning in a variable identifier
7661
- s = s.replace(/[\*\?\|\[\]\{\}\:\@\#]/g, '');
8064
+ s = s.replace(/[\*\?\|\[\]\{\}\@\#]/g, '');
7662
8065
  if(s !== selector) {
7663
- UI.warn('Equation name cannot contain [, ], {, }, |, :, @, #, * or ?');
8066
+ UI.warn('Equation name cannot contain [, ], {, }, |, @, #, * or ?');
7664
8067
  return null;
7665
8068
  }
7666
- // Check whether equation has parameters
7667
- const ss = s.split('\\');
7668
- if(ss.length > 1) {
7669
- s = ss.shift();
7670
- // Store parameter names in lower case
7671
- for(let i = 0; i < ss.length; i++) {
7672
- ss[i] = ss[i].toLowerCase();
7673
- }
7674
- params = ss;
7675
- }
8069
+ // Reduce inner spaces to one, and trim outer spaces
8070
+ s = s.replace(/\s+/g, ' ').trim();
8071
+ // Then prefix it when the IO context argument is defined
8072
+ if(ioc) s = ioc.actualName(s);
7676
8073
  // If equation already exists, return its modifier
7677
8074
  const id = UI.nameToID(s);
7678
- if(this.modifiers.hasOwnProperty(id)) {
7679
- const exm = this.modifiers[id];
7680
- if(params) {
7681
- if(!exm.parameters) {
7682
- UI.warn(`Existing equation ${exm.displayName} has no parameters`);
7683
- } else {
7684
- const
7685
- newp = params.join('\\'),
7686
- oldp = exm.parameters.join('\\');
7687
- if(newp !== oldp) {
7688
- UI.warn(`Parameter mismatch: expected \\${oldp}, not \\${newp}`);
7689
- }
7690
- }
7691
- }
7692
- return exm;
7693
- }
8075
+ if(this.modifiers.hasOwnProperty(id)) return this.modifiers[id];
7694
8076
  // New equation identifier must not equal some entity ID
7695
8077
  const obj = MODEL.objectByName(s);
7696
8078
  if(obj) {
@@ -7698,8 +8080,6 @@ class Dataset {
7698
8080
  UI.warningEntityExists(obj);
7699
8081
  return null;
7700
8082
  }
7701
- // Also reduce inner spaces to one, and trim outer spaces
7702
- s = s.replace(/\s+/g, ' ').trim();
7703
8083
  } else {
7704
8084
  // Standard dataset modifier selectors are much more restricted, but
7705
8085
  // to be user-friendly, special chars are removed automatically
@@ -7717,12 +8097,10 @@ class Dataset {
7717
8097
  UI.warn(UI.WARNING.INVALID_SELECTOR);
7718
8098
  return null;
7719
8099
  }
7720
- // Then prefix it when the IO context argument is defined
7721
- if(ioc) s = ioc.actualName(s);
7722
8100
  // Then add a dataset modifier to this dataset
7723
8101
  const id = UI.nameToID(s);
7724
8102
  if(!this.modifiers.hasOwnProperty(id)) {
7725
- this.modifiers[id] = new DatasetModifier(this, s, params);
8103
+ this.modifiers[id] = new DatasetModifier(this, s);
7726
8104
  }
7727
8105
  // Finally, initialize it when the XML node argument is defined
7728
8106
  if(node) this.modifiers[id].initFromXML(node);
@@ -7757,7 +8135,8 @@ class Dataset {
7757
8135
  const xml = ['<dataset', p, '><name>', xmlEncoded(n),
7758
8136
  '</name><notes>', cmnts,
7759
8137
  '</notes><default>', this.default_value,
7760
- '</default><time-scale>', this.time_scale,
8138
+ '</default><unit>', xmlEncoded(this.scale_unit),
8139
+ '</unit><time-scale>', this.time_scale,
7761
8140
  '</time-scale><time-unit>', this.time_unit,
7762
8141
  '</time-unit><method>', this.method,
7763
8142
  '</method><url>', xmlEncoded(this.url),
@@ -7771,10 +8150,11 @@ class Dataset {
7771
8150
  initFromXML(node) {
7772
8151
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
7773
8152
  this.default_value = safeStrToFloat(nodeContentByTag(node, 'default'));
8153
+ this.scale_unit = xmlDecoded(nodeContentByTag(node, 'unit')) || '1';
7774
8154
  this.time_scale = safeStrToFloat(nodeContentByTag(node, 'time-scale'), 1);
7775
- this.time_unit = nodeContentByTag(node, 'time-unit');
7776
- if(!this.time_unit) this.time_unit = CONFIGURATION.default_time_unit;
7777
- this.method = nodeContentByTag(node, 'method');
8155
+ this.time_unit = nodeContentByTag(node, 'time-unit') ||
8156
+ CONFIGURATION.default_time_unit;
8157
+ this.method = nodeContentByTag(node, 'method') || 'nearest';
7778
8158
  this.periodic = nodeParameterValue(node, 'periodic') === '1';
7779
8159
  this.array = nodeParameterValue(node, 'array') === '1';
7780
8160
  this.black_box = nodeParameterValue(node, 'black-box') === '1';
@@ -9226,7 +9606,12 @@ class ExperimentRunResult {
9226
9606
  obj = MODEL.objectByID(this.object_id),
9227
9607
  dn = obj.displayName;
9228
9608
  // NOTE: for equations dataset, only display the modifier selector
9229
- if(obj === MODEL.equations_dataset) return this.attribute;
9609
+ if(obj === MODEL.equations_dataset) {
9610
+ const m = obj.modifiers[this.attribute.toLowerCase()];
9611
+ if(m) return m.selector;
9612
+ console.log('WARNING: Run result of non-existent equation', this.attribute);
9613
+ return this.attribute;
9614
+ }
9230
9615
  return (this.attribute ? dn + '|' + this.attribute : dn);
9231
9616
  }
9232
9617
 
@@ -9609,8 +9994,13 @@ class Experiment {
9609
9994
  this.variables = [];
9610
9995
  this.configuration_dims = 0;
9611
9996
  this.column_scenario_dims = 0;
9997
+ this.iterator_ranges = [[0,0], [0,0], [0,0]];
9998
+ this.iterator_dimensions = [];
9612
9999
  this.settings_selectors = [];
9613
10000
  this.settings_dimensions = [];
10001
+ this.combination_selectors = [];
10002
+ this.combination_dimensions = [];
10003
+ this.available_dimensions = [];
9614
10004
  this.actor_selectors = [];
9615
10005
  this.actor_dimensions = [];
9616
10006
  this.excluded_selectors = '';
@@ -9670,6 +10060,56 @@ class Experiment {
9670
10060
  return this.combinations[this.active_combination_index];
9671
10061
  }
9672
10062
 
10063
+ get iteratorRangeString() {
10064
+ // Returns the iterator ranges as "from,to" pairs separated by |
10065
+ const ir = [];
10066
+ for(let i = 0; i < 3; i++) {
10067
+ ir.push(this.iterator_ranges[i].join(','));
10068
+ }
10069
+ return ir.join('|');
10070
+ }
10071
+
10072
+ parseIteratorRangeString(s) {
10073
+ // Parses `s` as "from,to" pairs, ignoring syntax errors
10074
+ if(s) {
10075
+ const ir = s.split('|');
10076
+ // Add 2 extra substrings to have at least 3
10077
+ ir.push('', '');
10078
+ for(let i = 0; i < 3; i++) {
10079
+ const r = ir[i].split(',');
10080
+ // Likewise add extra substring to have at least 2
10081
+ r.push('');
10082
+ // Parse integers, defaulting to 0
10083
+ this.iterator_ranges[i] = [safeStrToInt(r[0], 0), safeStrToInt(r[1], 0)];
10084
+ }
10085
+ }
10086
+ }
10087
+
10088
+ updateIteratorDimensions() {
10089
+ // Create iterator selectors for each index variable having a relevant range
10090
+ this.iterator_dimensions = [];
10091
+ const il = ['i', 'j', 'k'];
10092
+ for(let i = 0; i < 3; i++) {
10093
+ const r = this.iterator_ranges[i];
10094
+ if(r[0] || r[1]) {
10095
+ const
10096
+ sel = [],
10097
+ k = il[i] + '=';
10098
+ // NOTE: iterate from FROM to TO limit also when FROM > TO
10099
+ if(r[0] <= r[1]) {
10100
+ for(let j = r[0]; j <= r[1]; j++) {
10101
+ sel.push(k + j);
10102
+ }
10103
+ } else {
10104
+ for(let j = r[0]; j >= r[1]; j--) {
10105
+ sel.push(k + j);
10106
+ }
10107
+ }
10108
+ this.iterator_dimensions.push(sel);
10109
+ }
10110
+ }
10111
+ }
10112
+
9673
10113
  matchingCombinationIndex(sl) {
9674
10114
  // Returns index of combination with most selectors in common wilt `sl`
9675
10115
  let high = 0,
@@ -9705,6 +10145,16 @@ class Experiment {
9705
10145
  `<sdim>${xmlEncoded(this.settings_dimensions[i].join(','))}</sdim>`;
9706
10146
  if(sd.indexOf(dim) < 0) sd += dim;
9707
10147
  }
10148
+ let cs = '';
10149
+ for(let i = 0; i < this.combination_selectors.length; i++) {
10150
+ cs += `<csel>${xmlEncoded(this.combination_selectors[i])}</csel>`;
10151
+ }
10152
+ let cd = '';
10153
+ for(let i = 0; i < this.combination_dimensions.length; i++) {
10154
+ const dim =
10155
+ `<cdim>${xmlEncoded(this.combination_dimensions[i].join(','))}</cdim>`;
10156
+ if(cd.indexOf(dim) < 0) cd += dim;
10157
+ }
9708
10158
  let as = '';
9709
10159
  for(let i = 0; i < this.actor_selectors.length; i++) {
9710
10160
  as += this.actor_selectors[i].asXML;
@@ -9723,6 +10173,7 @@ class Experiment {
9723
10173
  return ['<experiment configuration-dims="', this.configuration_dims,
9724
10174
  '" column_scenario-dims="', this.column_scenario_dims,
9725
10175
  (this.completed ? '" completed="1' : ''),
10176
+ '" iterator-ranges="', this.iteratorRangeString,
9726
10177
  '" started="', this.time_started,
9727
10178
  '" stopped="', this.time_stopped,
9728
10179
  '" variables="', this.download_settings.variables,
@@ -9739,7 +10190,9 @@ class Experiment {
9739
10190
  '</dimensions><chart-titles>', ct,
9740
10191
  '</chart-titles><settings-selectors>', ss,
9741
10192
  '</settings-selectors><settings-dimensions>', sd,
9742
- '</settings-dimensions><actor-selectors>', as,
10193
+ '</settings-dimensions><combination-selectors>', cs,
10194
+ '</combination-selectors><combination-dimensions>', cd,
10195
+ '</combination-dimensions><actor-selectors>', as,
9743
10196
  '</actor-selectors><excluded-selectors>',
9744
10197
  xmlEncoded(this.excluded_selectors),
9745
10198
  '</excluded-selectors><clusters-to-ignore>', cti,
@@ -9752,6 +10205,7 @@ class Experiment {
9752
10205
  nodeParameterValue(node, 'configuration-dims'));
9753
10206
  this.column_scenario_dims = safeStrToInt(
9754
10207
  nodeParameterValue(node, 'column-scenario-dims'));
10208
+ this.parseIteratorRangeString(nodeParameterValue(node, 'iterator-ranges'));
9755
10209
  this.completed = nodeParameterValue(node, 'completed') === '1';
9756
10210
  this.time_started = safeStrToInt(nodeParameterValue(node, 'started'));
9757
10211
  this.time_stopped = safeStrToInt(nodeParameterValue(node, 'stopped'));
@@ -9807,6 +10261,24 @@ class Experiment {
9807
10261
  }
9808
10262
  }
9809
10263
  }
10264
+ n = childNodeByTag(node, 'combination-selectors');
10265
+ if(n && n.childNodes) {
10266
+ for(let i = 0; i < n.childNodes.length; i++) {
10267
+ c = n.childNodes[i];
10268
+ if(c.nodeName === 'csel') {
10269
+ this.combination_selectors.push(xmlDecoded(nodeContent(c)));
10270
+ }
10271
+ }
10272
+ }
10273
+ n = childNodeByTag(node, 'combination-dimensions');
10274
+ if(n && n.childNodes) {
10275
+ for(let i = 0; i < n.childNodes.length; i++) {
10276
+ c = n.childNodes[i];
10277
+ if(c.nodeName === 'cdim') {
10278
+ this.combination_dimensions.push(xmlDecoded(nodeContent(c)).split(','));
10279
+ }
10280
+ }
10281
+ }
9810
10282
  n = childNodeByTag(node, 'actor-selectors');
9811
10283
  if(n && n.childNodes) {
9812
10284
  for(let i = 0; i < n.childNodes.length; i++) {
@@ -9855,7 +10327,9 @@ class Experiment {
9855
10327
  // Returns dimension index if any dimension contains any selector in
9856
10328
  // dimension `d`, or -1 otherwise
9857
10329
  for(let i = 0; i < this.dimensions.length; i++) {
9858
- if(intersection(this.dimensions[i], d).length > 0) return i;
10330
+ const xd = this.dimensions[i].slice();
10331
+ this.expandCombinationSelectors(xd);
10332
+ if(intersection(xd, d).length > 0) return i;
9859
10333
  }
9860
10334
  return -1;
9861
10335
  }
@@ -9864,7 +10338,7 @@ class Experiment {
9864
10338
  // Removes dimension `d` from list and returns its old index
9865
10339
  for(let i = 0; i < this.dimensions.length; i++) {
9866
10340
  if(intersection(this.dimensions[i], d).length > 0) {
9867
- this.dimensions.splice(i);
10341
+ this.dimensions.splice(i, 1);
9868
10342
  return i;
9869
10343
  }
9870
10344
  }
@@ -9901,7 +10375,170 @@ class Experiment {
9901
10375
  if(adi >= 0) this.dimensions[adi] = d;
9902
10376
  }
9903
10377
  }
10378
+
10379
+ get allDimensionSelectors() {
10380
+ const sl = Object.keys(MODEL.listOfAllSelectors);
10381
+ // Add selectors of actor, iterator and settings dimensions
10382
+ return sl;
10383
+ }
9904
10384
 
10385
+ orthogonalSelectors(c) {
10386
+ // Returns TRUE iff the selectors in set `c` all are elements of
10387
+ // different experiment dimensions
10388
+ const
10389
+ // Make a copy of `c` so it can be safely expanded
10390
+ xc = c.slice(),
10391
+ // Start with a copy of all model dimensions
10392
+ dl = MODEL.dimensions.slice(),
10393
+ issues = [];
10394
+ // Add dimensions defined for this experiment
10395
+ for(let i = 0; i < this.settings_dimensions.length; i++) {
10396
+ dl.push(this.settings_dimensions[i]);
10397
+ }
10398
+ for(let i = 0; i < this.actor_dimensions.length; i++) {
10399
+ dl.push(this.actor_dimensions[i]);
10400
+ }
10401
+ // Expand `c` as it may contain combination selectors
10402
+ this.expandCombinationSelectors(xc);
10403
+ // Check for all these dimensions that `c` contains known selectors
10404
+ // and that no two or more selectors occur in the same dimension
10405
+ let unknown = xc.slice();
10406
+ for(let i = 0; i < dl.length; i++) {
10407
+ const idc = intersection(dl[i], xc);
10408
+ unknown = complement(unknown, idc);
10409
+ if(idc.length > 1) {
10410
+ const pair = idc.join(' & ');
10411
+ if(issues.indexOf(pair) < 0) issues.push(pair);
10412
+ }
10413
+ }
10414
+ if(unknown.length > 0) {
10415
+ UI.warn('Combination contains ' +
10416
+ pluralS(unknown.length, 'undefined selector') +
10417
+ ' (' + unknown.join(', ') + ')');
10418
+ return false;
10419
+ }
10420
+ if(issues.length > 0) {
10421
+ UI.warn('Combination contains multiple selectors from same dimension (' +
10422
+ issues.join(', ') + ')');
10423
+ return false;
10424
+ }
10425
+ return true;
10426
+ }
10427
+
10428
+ expandCombinationSelectors(cs) {
10429
+ // Expansion of combination selectors in a selector set `cs` means
10430
+ // that if, for example, `cs` = (A, C1) where C1 is a combination
10431
+ // selector defined as C1 = (B, C2) with A and B being "normal"
10432
+ // selectors, then C1 must be removed from `cs`, while B and the
10433
+ // expansion of C2 must be appended to `cs`.
10434
+ // NOTE: the original selectors C1 and C2 must be removed because
10435
+ // *dimension* selectors cannot be a used as "normal" selectors
10436
+ // (e.g., for dataset modifiers, actor settings or model setting)
10437
+ // NOTE: traverse `cs` in reverse order to ensure that deleting and
10438
+ // appending produce the intended result
10439
+ for(let i = cs.length - 1; i >= 0; i--) {
10440
+ const s = cs[i];
10441
+ // Check whether selector `s` defines a combination
10442
+ for(let j = 0; j < this.combination_selectors.length; j++) {
10443
+ const tuple = this.combination_selectors[j].split('|');
10444
+ if(tuple[0] === s) {
10445
+ // First remove `s` from the original set...
10446
+ cs.splice(i, 1);
10447
+ // Let `xs` be the selector set to replace `s`
10448
+ const xs = tuple[1].split(' ');
10449
+ // Recursively expand `xs`, as it may contain combination selectors
10450
+ this.expandCombinationSelectors(xs);
10451
+ // ... and append its expansion
10452
+ cs.push(...xs);
10453
+ }
10454
+ }
10455
+ }
10456
+ }
10457
+
10458
+ orthogonalCombinationDimensions(sl) {
10459
+ // Returns TRUE iff the expansions of the selectors in set `sl`
10460
+ // are mutually exclusive
10461
+ const
10462
+ xl = {},
10463
+ issues = {};
10464
+ for(let i = 0; i < sl.length; i++) {
10465
+ const s = sl[i];
10466
+ xl[s] = [s];
10467
+ this.expandCombinationSelectors(xl[s]);
10468
+ issues[s] = [];
10469
+ }
10470
+ let ok = true;
10471
+ for(let i = 0; i < sl.length; i++) {
10472
+ const s1 = sl[i];
10473
+ for(let j = i + 1; j < sl.length; j++) {
10474
+ const
10475
+ s2 = sl[j],
10476
+ shared = intersection(xl[s1], xl[s2]);
10477
+ if(shared.length > 0) {
10478
+ issues[s1].push(`${s2}: ${shared.join(', ')}`);
10479
+ ok = false;
10480
+ }
10481
+ }
10482
+ }
10483
+ if(!ok) {
10484
+ const il = [];
10485
+ for(let i = 0; i < sl.length; i++) {
10486
+ const s = sl[i];
10487
+ if(issues[s].length > 0) {
10488
+ il.push(`${s} (${issues[s].join('; ')})`);
10489
+ }
10490
+ }
10491
+ UI.warn('Combination dimension is not orthogonal: ' + il.join(', '));
10492
+ }
10493
+ return ok;
10494
+ }
10495
+
10496
+ inferAvailableDimensions() {
10497
+ // Creates list of dimensions that are orthogonal to those already
10498
+ // selected for this experiment
10499
+ this.available_dimensions.length = 0;
10500
+ // For efficiency, do not use hasDimension but expand the dimensions
10501
+ // that are already selected once, and define a lookup function that
10502
+ // checks for orthogonality
10503
+ const
10504
+ axes = [],
10505
+ orthogonal = (d) => {
10506
+ for(let i = 0; i < axes.length; i++) {
10507
+ if(intersection(axes[i], d).length > 0) return false;
10508
+ }
10509
+ return true;
10510
+ };
10511
+ for(let i = 0; i < this.dimensions.length; i++) {
10512
+ axes.push(this.dimensions[i].slice());
10513
+ this.expandCombinationSelectors(axes[i]);
10514
+ }
10515
+ for(let i = 0; i < MODEL.dimensions.length; i++) {
10516
+ const d = MODEL.dimensions[i];
10517
+ if(orthogonal(d)) this.available_dimensions.push(d);
10518
+ }
10519
+ for(let i = 0; i < this.settings_dimensions.length; i++) {
10520
+ const d = this.settings_dimensions[i];
10521
+ if(orthogonal(d)) this.available_dimensions.push(d);
10522
+ }
10523
+ for(let i = 0; i < this.iterator_dimensions.length; i++) {
10524
+ const d = this.iterator_dimensions[i];
10525
+ if(orthogonal(d)) this.available_dimensions.push(d);
10526
+ }
10527
+ for(let i = 0; i < this.actor_dimensions.length; i++) {
10528
+ const d = this.actor_dimensions[i];
10529
+ if(orthogonal(d)) this.available_dimensions.push(d);
10530
+ }
10531
+ for(let i = 0; i < this.combination_dimensions.length; i++) {
10532
+ // NOTE: combination dimensions must be expanded before checking...
10533
+ const
10534
+ d = this.combination_dimensions[i],
10535
+ xd = d.slice();
10536
+ this.expandCombinationSelectors(xd);
10537
+ // ... but the original combination dimension must be added
10538
+ if(orthogonal(xd)) this.available_dimensions.push(d);
10539
+ }
10540
+ }
10541
+
9905
10542
  inferActualDimensions() {
9906
10543
  // Creates list of dimensions without excluded selectors
9907
10544
  this.actual_dimensions.length = 0;
@@ -9918,6 +10555,9 @@ class Experiment {
9918
10555
  if(n >= this.actual_dimensions.length) {
9919
10556
  // NOTE: do not push an empty selector list (can occur if no dimensions)
9920
10557
  if(s.length > 0) this.combinations.push(s);
10558
+ // NOTE: combinations may include *dimension* selectors
10559
+ // These then must be "expanded"
10560
+ this.expandCombinationSelectors(s);
9921
10561
  return;
9922
10562
  }
9923
10563
  const d = this.actual_dimensions[n];
@@ -9929,14 +10569,33 @@ class Experiment {
9929
10569
  }
9930
10570
  }
9931
10571
 
10572
+ renameSelectorInDimensions(olds, news) {
10573
+ // Update the combination dimensions that contain `olds`
10574
+ for(let i = 0; i < this.settings_dimensions.length; i++) {
10575
+ const si = this.settings_dimensions[i].indexOf(olds);
10576
+ if(si >= 0) this.settings_dimensions[i][si] = news;
10577
+ }
10578
+ for(let i = 0; i < this.combination_selectors.length; i++) {
10579
+ const
10580
+ c = this.combination_selectors[i].split('|'),
10581
+ sl = c[1].split(' '),
10582
+ si = sl.indexOf(olds);
10583
+ if(si >= 0) {
10584
+ sl[si] = news;
10585
+ c[1] = sl.join(' ');
10586
+ this.combination_selectors[i] = c.join('|');
10587
+ }
10588
+ }
10589
+ }
10590
+
9932
10591
  mayBeIgnored(c) {
9933
- // Returns TRUE iff `c` is on the list to be ignored
10592
+ // Returns TRUE iff cluster `c` is on the list to be ignored
9934
10593
  for(let i = 0; i < this.clusters_to_ignore.length; i++) {
9935
10594
  if(this.clusters_to_ignore[i].cluster === c) return true;
9936
10595
  }
9937
10596
  return false;
9938
10597
  }
9939
-
10598
+
9940
10599
  inferVariables() {
9941
10600
  // Create list of distinct variables in charts
9942
10601
  this.variables.length = 0;
@@ -9994,7 +10653,19 @@ class Experiment {
9994
10653
 
9995
10654
  get resultsAsCSV() {
9996
10655
  // Return results as specfied by the download settings
10656
+ // NOTE: no runs => no results => return empty string
10657
+ if(this.runs.length === 0) return '';
9997
10658
  const
10659
+ // Local function to convert number to string
10660
+ numval = (v, p) => {
10661
+ // Return 0 as single digit
10662
+ if(Math.abs(v) < VM.NEAR_ZERO) return '0';
10663
+ // Return empty string for undefined or exceptional values
10664
+ if(!v || v < VM.MINUS_INFINITY || v > VM.PLUS_INFINITY) return '';
10665
+ // Return other values as float with specified precision
10666
+ return v.toPrecision(p);
10667
+ },
10668
+ prec = this.download_settings.precision,
9998
10669
  allruns = this.download_settings.runs === 'all',
9999
10670
  sep = (this.download_settings.separator === 'tab' ? '\t' :
10000
10671
  (this.download_settings.separator === 'comma' ? ',' : ';')),
@@ -10019,24 +10690,21 @@ class Experiment {
10019
10690
  exceptions: `${quo}Exceptions${quo}${sep}`,
10020
10691
  run: []
10021
10692
  };
10022
- // Make list of indices of variables to include
10023
- if(this.download_settings.variables === 'selected') {
10024
- // Only one variable
10025
- vars.push(this.resultIndex(this.selected_variable));
10026
- } else {
10027
- // All variables
10028
- for(let i = 0; i < this.variables.length; i++) {
10029
- vars.push(i);
10030
- }
10031
- }
10032
- const nvars = vars.length;
10033
10693
  for(let i = 0; i < this.combinations.length; i++) {
10034
10694
  if(i < this.runs.length &&
10035
10695
  (allruns || this.chart_combinations.indexOf(i) >= 0)) {
10036
10696
  data.run.push(i);
10037
10697
  }
10038
10698
  }
10039
- let series_length = 0;
10699
+ let series_length = 0,
10700
+ // By default, assume all variables to be output
10701
+ start = 0,
10702
+ stop = this.runs[0].results.length;
10703
+ if(this.download_settings.variables === 'selected') {
10704
+ // Only one variable
10705
+ start = this.resultIndex(this.selected_variable);
10706
+ stop = start + 1;
10707
+ }
10040
10708
  for(let i = 0; i < data.run.length; i++) {
10041
10709
  const
10042
10710
  rnr = data.run[i],
@@ -10044,31 +10712,35 @@ class Experiment {
10044
10712
  data.nr += r.number;
10045
10713
  data.combi += quo + this.combinations[rnr].join('|') + quo;
10046
10714
  // Run duration in seconds
10047
- data.rsecs += VM.sig2Dig((r.time_recorded - r.time_started) * 0.001);
10048
- data.ssecs += VM.sig2Dig(r.solver_seconds);
10715
+ data.rsecs += numval((r.time_recorded - r.time_started) * 0.001, 4);
10716
+ data.ssecs += numval(r.solver_seconds, 4);
10049
10717
  data.warnings += r.warning_count;
10050
- for(let j = 0; j < nvars; j++) {
10718
+ for(let j = start; j < stop; j++) {
10051
10719
  // Add empty cells for run attributes
10052
10720
  data.nr += sep;
10053
10721
  data.combi += sep;
10054
10722
  data.rsecs += sep;
10055
10723
  data.ssecs += sep;
10056
10724
  data.warnings += sep;
10057
- const rr = r.results[vars[j]];
10058
- data.variable += rr.displayName + sep;
10059
- // Series may differ in length; the longest determines the
10060
- // number of rows of series data to be added
10061
- series_length = Math.max(series_length, rr.vector.length);
10062
- if(this.download_settings.statistics) {
10063
- data.N += rr.N + sep;
10064
- data.sum += rr.sum + sep;
10065
- data.mean += rr.mean + sep;
10066
- data.variance += rr.variance + sep;
10067
- data.minimum += rr.minimum + sep;
10068
- data.maximum += rr.maximum + sep;
10069
- data.NZ += rr.non_zero_tally + sep;
10070
- data.last += rr.last + sep;
10071
- data.exceptions += rr.exceptions + sep;
10725
+ const rr = r.results[j];
10726
+ if(rr) {
10727
+ data.variable += rr.displayName + sep;
10728
+ // Series may differ in length; the longest determines the
10729
+ // number of rows of series data to be added
10730
+ series_length = Math.max(series_length, rr.vector.length);
10731
+ if(this.download_settings.statistics) {
10732
+ data.N += rr.N + sep;
10733
+ data.sum += numval(rr.sum, prec) + sep;
10734
+ data.mean += numval(rr.mean, prec) + sep;
10735
+ data.variance += numval(rr.variance, prec) + sep;
10736
+ data.minimum += numval(rr.minimum, prec) + sep;
10737
+ data.maximum += numval(rr.maximum, prec) + sep;
10738
+ data.NZ += rr.non_zero_tally + sep;
10739
+ data.last += numval(rr.last, prec) + sep;
10740
+ data.exceptions += rr.exceptions + sep;
10741
+ }
10742
+ } else {
10743
+ console.log('No run results for ', this.variables[vars[j]].displayName);
10072
10744
  }
10073
10745
  }
10074
10746
  }
@@ -10084,20 +10756,18 @@ class Experiment {
10084
10756
  }
10085
10757
  if(this.download_settings.series) {
10086
10758
  ds.push('t');
10087
- const
10088
- prec = this.download_settings.precision,
10089
- row = [];
10759
+ const row = [];
10090
10760
  for(let i = 0; i < series_length; i++) {
10091
10761
  row.length = 0;
10092
10762
  row.push(i);
10093
10763
  for(let j = 0; j < data.run.length; j++) {
10094
10764
  const rnr = data.run[j];
10095
- for(let k = 0; k < vars.length; k++) {
10096
- const rr = this.runs[rnr].results[vars[k]];
10097
- if(i < rr.vector.length) {
10098
- const v = rr.vector[i];
10099
- if(v >= VM.MINUS_INFINITY && v <= VM.PLUS_INFINITY) {
10100
- row.push(v.toPrecision(prec));
10765
+ for(let k = start; k < stop; k++) {
10766
+ const rr = this.runs[rnr].results[k];
10767
+ if(rr) {
10768
+ // NOTE: only experiment variables have vector data
10769
+ if(rr.x_variable && i <= rr.N) {
10770
+ row.push(numval(rr.vector[i], prec));
10101
10771
  } else {
10102
10772
  row.push('');
10103
10773
  }