linny-r 1.1.23 → 1.2.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.
- package/README.md +7 -6
- package/package.json +1 -1
- package/static/images/combination.png +0 -0
- package/static/images/iterator.png +0 -0
- package/static/images/scale.png +0 -0
- package/static/index.html +218 -13
- package/static/linny-r.css +220 -33
- package/static/scripts/linny-r-config.js +6 -0
- package/static/scripts/linny-r-ctrl.js +27 -7
- package/static/scripts/linny-r-gui.js +968 -166
- package/static/scripts/linny-r-model.js +889 -230
- package/static/scripts/linny-r-utils.js +5 -0
- package/static/scripts/linny-r-vm.js +310 -89
@@ -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
|
-
|
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 = {};
|
@@ -688,8 +689,8 @@ class LinnyRModel {
|
|
688
689
|
// Merge into dimension if there are shared selectors
|
689
690
|
for(let i = 0; i < this.dimensions.length; i++) {
|
690
691
|
const c = complement(sl, this.dimensions[i]);
|
691
|
-
if(c.length
|
692
|
-
this.dimensions[i].push(...c);
|
692
|
+
if(c.length < sl.length) {
|
693
|
+
if(c.length > 0) this.dimensions[i].push(...c);
|
693
694
|
newdim = false;
|
694
695
|
break;
|
695
696
|
}
|
@@ -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
|
739
|
-
let removed = 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
|
-
|
829
|
-
//
|
830
|
-
if(
|
831
|
-
|
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 ||
|
@@ -1005,7 +1181,7 @@ class LinnyRModel {
|
|
1005
1181
|
}
|
1006
1182
|
const id = UI.nameToID(name);
|
1007
1183
|
let d = this.namedObjectByID(id);
|
1008
|
-
if(d) {
|
1184
|
+
if(d && d !== this.equations_dataset) {
|
1009
1185
|
if(IO_CONTEXT) {
|
1010
1186
|
IO_CONTEXT.supersede(d);
|
1011
1187
|
} else {
|
@@ -1015,11 +1191,25 @@ class LinnyRModel {
|
|
1015
1191
|
}
|
1016
1192
|
}
|
1017
1193
|
d = new Dataset(name);
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
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
|
-
|
1205
|
+
if(eqds) {
|
1206
|
+
// Restore pointer to original equations dataset
|
1207
|
+
this.equations_dataset = eqds;
|
1208
|
+
// Return the extended equations dataset
|
1209
|
+
return eqds;
|
1210
|
+
} else {
|
1211
|
+
this.datasets[id] = d;
|
1212
|
+
}
|
1023
1213
|
return d;
|
1024
1214
|
}
|
1025
1215
|
|
@@ -1269,8 +1459,8 @@ class LinnyRModel {
|
|
1269
1459
|
|
1270
1460
|
setSelection() {
|
1271
1461
|
// 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
|
1273
|
-
// before drawing the diagram)
|
1462
|
+
// NOTE: to be called after loading a model, and after UNDO/REDO (and
|
1463
|
+
// then before drawing the diagram)
|
1274
1464
|
const fc = this.focal_cluster;
|
1275
1465
|
this.selection.length = 0;
|
1276
1466
|
this.selection_related_arrows.length = 0;
|
@@ -1835,67 +2025,90 @@ class LinnyRModel {
|
|
1835
2025
|
}
|
1836
2026
|
}
|
1837
2027
|
|
1838
|
-
|
1839
|
-
//
|
1840
|
-
//
|
1841
|
-
|
1842
|
-
//
|
1843
|
-
|
1844
|
-
|
1845
|
-
|
1846
|
-
|
1847
|
-
|
1848
|
-
|
1849
|
-
|
1850
|
-
|
1851
|
-
// Check all actor weight expressions
|
2028
|
+
get allExpressions() {
|
2029
|
+
// Returns list of all Expression objects
|
2030
|
+
// NOTE: start with dataset expressions, so that when recompiling
|
2031
|
+
// their `level-based` property is set before recompiling the
|
2032
|
+
// other expressions
|
2033
|
+
const x = [];
|
2034
|
+
for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
|
2035
|
+
const ds = this.datasets[k];
|
2036
|
+
// NOTE: dataset modifier expressions include the equations
|
2037
|
+
for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
|
2038
|
+
x.push(ds.modifiers[m].expression);
|
2039
|
+
}
|
2040
|
+
}
|
1852
2041
|
for(let k in this.actors) if(this.actors.hasOwnProperty(k)) {
|
1853
|
-
|
2042
|
+
x.push(this.actors[k].weight);
|
1854
2043
|
}
|
1855
|
-
// Check all process attribute expressions
|
1856
2044
|
for(let k in this.processes) if(this.processes.hasOwnProperty(k)) {
|
1857
2045
|
const p = this.processes[k];
|
1858
|
-
|
1859
|
-
ioc.rewrite(p.upper_bound, en1, en2);
|
1860
|
-
ioc.rewrite(p.initial_level, en1, en2);
|
1861
|
-
ioc.rewrite(p.pace_expression, en1, en2);
|
2046
|
+
x.push(p.lower_bound, p.upper_bound, p.initial_level, p.pace_expression);
|
1862
2047
|
}
|
1863
|
-
// Check all product attribute expressions
|
1864
2048
|
for(let k in this.products) if(this.products.hasOwnProperty(k)) {
|
1865
2049
|
const p = this.products[k];
|
1866
|
-
|
1867
|
-
ioc.rewrite(p.upper_bound, en1, en2);
|
1868
|
-
ioc.rewrite(p.initial_level, en1, en2);
|
1869
|
-
ioc.rewrite(p.price, en1, en2);
|
2050
|
+
x.push(p.lower_bound, p.upper_bound, p.initial_level, p.price);
|
1870
2051
|
}
|
1871
|
-
// Check all notes in clusters for their color expressions and fields
|
1872
2052
|
for(let k in this.clusters) if(this.clusters.hasOwnProperty(k)) {
|
1873
2053
|
const c = this.clusters[k];
|
1874
2054
|
for(let i = 0; i < c.notes.length; i++) {
|
1875
2055
|
const n = c.notes[i];
|
1876
|
-
|
1877
|
-
// Also rename entities in note fields
|
1878
|
-
n.rewriteFields(en1, en2);
|
2056
|
+
x.push(n.color);
|
1879
2057
|
}
|
1880
2058
|
}
|
1881
|
-
// Check all link rate & delay expressions
|
1882
2059
|
for(let k in this.links) if(this.links.hasOwnProperty(k)) {
|
1883
|
-
|
1884
|
-
|
2060
|
+
const l = this.links[k];
|
2061
|
+
x.push(l.relative_rate, l.flow_delay);
|
1885
2062
|
}
|
1886
|
-
|
1887
|
-
|
1888
|
-
|
1889
|
-
|
1890
|
-
|
2063
|
+
return x;
|
2064
|
+
}
|
2065
|
+
|
2066
|
+
replaceEntityInExpressions(en1, en2) {
|
2067
|
+
// Replace entity name `en1` by `en2` in all variables in all expressions
|
2068
|
+
// (provided that they are not identical)
|
2069
|
+
if(en1 === en2) return;
|
2070
|
+
// NOTE: ignore case and multiple spaces in `en1`, but conserve those in
|
2071
|
+
// new name `en2` (except for leading and trailing spaces)
|
2072
|
+
en1 = en1.trim().replace(/\s+/g, ' ').toLowerCase();
|
2073
|
+
en2 = en2.trim();
|
2074
|
+
// NOTE: Neither entity name may be empty
|
2075
|
+
if(!en1 || !en2) return;
|
2076
|
+
// NOTE: use the `rewrite` method of class IOContext; this will keep track
|
2077
|
+
// of the number of replacements made
|
2078
|
+
const ioc = new IOContext();
|
2079
|
+
// Iterate over all expressions
|
2080
|
+
const ax = this.allExpressions;
|
2081
|
+
for(let i = 0; i < ax.length; i++) {
|
2082
|
+
ioc.rewrite(ax[i], en1, en2);
|
2083
|
+
}
|
2084
|
+
// Iterate over all notes in clusters to rename entities in note fields
|
2085
|
+
for(let k in this.clusters) if(this.clusters.hasOwnProperty(k)) {
|
2086
|
+
const cn = this.clusters[k].notes;
|
2087
|
+
for(let i = 0; i < cn.length; i++) {
|
2088
|
+
cn[i].rewriteFields(en1, en2);
|
1891
2089
|
}
|
1892
2090
|
}
|
1893
2091
|
if(ioc.replace_count) {
|
1894
2092
|
UI.notify(`Renamed ${pluralS(ioc.replace_count, 'variable')} in ` +
|
1895
2093
|
pluralS(ioc.expression_count, 'expression'));
|
1896
2094
|
}
|
2095
|
+
// Also rename entities in parameters and outcomes of sensitivity analysis
|
2096
|
+
for(let i = 0; i < this.sensitivity_parameters.length; i++) {
|
2097
|
+
const sp = this.sensitivity_parameters[i].split('|');
|
2098
|
+
if(sp[0].toLowerCase() === en1) {
|
2099
|
+
sp[0] = en2;
|
2100
|
+
this.sensitivity_parameters[i] = sp.join('|');
|
2101
|
+
}
|
2102
|
+
}
|
2103
|
+
for(let i = 0; i < this.sensitivity_outcomes.length; i++) {
|
2104
|
+
const so = this.sensitivity_outcomes[i].split('|');
|
2105
|
+
if(so[0].toLowerCase() === en1) {
|
2106
|
+
so[0] = en2;
|
2107
|
+
this.sensitivity_outcomes[i] = so.join('|');
|
2108
|
+
}
|
2109
|
+
}
|
1897
2110
|
// Name was changed, so update controller dialogs to display the new name
|
1898
|
-
UI.updateControllerDialogs('
|
2111
|
+
UI.updateControllerDialogs('CDEFJX');
|
1899
2112
|
}
|
1900
2113
|
|
1901
2114
|
replaceAttributeInExpressions(ena, a) {
|
@@ -1905,58 +2118,40 @@ class LinnyRModel {
|
|
1905
2118
|
// or in the new attribute `a` (except for leading and trailing spaces)
|
1906
2119
|
a = a.trim();
|
1907
2120
|
ena = ena.split('|');
|
1908
|
-
// Double-check that `a` is
|
2121
|
+
// Double-check that `a` is not empty and `ena` contains a vertical bar
|
1909
2122
|
if(!a || ena.length < 2) return;
|
1910
2123
|
// Prepare regex to match [entity|attribute] including brackets, but case-
|
1911
2124
|
// tolerant and spacing-tolerant
|
1912
2125
|
const
|
1913
2126
|
en = escapeRegex(ena[0].trim().replace(/\s+/g, ' ').toLowerCase()),
|
1914
2127
|
at = ena[1].trim(),
|
1915
|
-
raw = en.replace(/\s/,
|
1916
|
-
re = new RegExp(
|
1917
|
-
'\\[\\s*' + raw + '\\s*(\\@[^\\]]+)?\\s*\\]',
|
1918
|
-
'gi');
|
2128
|
+
raw = en.replace(/\s/, `\s+`) + `\s*\|\s*` + escapeRegex(at),
|
2129
|
+
re = new RegExp(`\[\s*${raw}\s*(\@[^\]]+)?\s*\]`, 'gi');
|
1919
2130
|
// Count replacements made
|
1920
2131
|
let n = 0;
|
1921
|
-
//
|
1922
|
-
|
1923
|
-
|
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);
|
2132
|
+
// Iterate over all expressions
|
2133
|
+
const ax = this.allExpressions;
|
2134
|
+
for(let i = 0; i < ax.length; i++) {
|
2135
|
+
n += ax[i].replaceAttribute(re, at, a);
|
1932
2136
|
}
|
1933
|
-
//
|
1934
|
-
|
1935
|
-
|
1936
|
-
|
1937
|
-
|
1938
|
-
|
1939
|
-
|
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);
|
2137
|
+
// Also rename attributes in parameters and outcomes of sensitivity analysis
|
2138
|
+
let sa_cnt = 0;
|
2139
|
+
const enat = en + '|' + at;
|
2140
|
+
for(let i = 0; i < this.sensitivity_parameters.length; i++) {
|
2141
|
+
const sp = this.sensitivity_parameters[i];
|
2142
|
+
if(sp.toLowerCase() === enat) {
|
2143
|
+
this.sensitivity_parameters[i] = sp.split('|')[0] + '|' + a;
|
2144
|
+
sa_cnt++;
|
1946
2145
|
}
|
1947
2146
|
}
|
1948
|
-
|
1949
|
-
|
1950
|
-
|
1951
|
-
|
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);
|
2147
|
+
for(let i = 0; i < this.sensitivity_outcomes.length; i++) {
|
2148
|
+
const so = this.sensitivity_outcomes[i];
|
2149
|
+
if(so.toLowerCase() === enat) {
|
2150
|
+
this.sensitivity_outcomes[i] = so.split('|')[0] + '|' + a;
|
2151
|
+
sa_cnt++;
|
1958
2152
|
}
|
1959
2153
|
}
|
2154
|
+
if(sa_cnt > 0) SENSITIVITY_ANALYSIS.updateDialog();
|
1960
2155
|
return n;
|
1961
2156
|
}
|
1962
2157
|
|
@@ -2056,7 +2251,9 @@ class LinnyRModel {
|
|
2056
2251
|
for(i = 0; i < n.childNodes.length; i++) {
|
2057
2252
|
c = n.childNodes[i];
|
2058
2253
|
if(c.nodeName === 'scaleunit') {
|
2059
|
-
this.addScaleUnit(xmlDecoded(nodeContentByTag(c, 'name'))
|
2254
|
+
this.addScaleUnit(xmlDecoded(nodeContentByTag(c, 'name')),
|
2255
|
+
nodeContentByTag(c, 'scalar'),
|
2256
|
+
xmlDecoded(nodeContentByTag(c, 'base-unit')));
|
2060
2257
|
}
|
2061
2258
|
}
|
2062
2259
|
}
|
@@ -2196,14 +2393,14 @@ class LinnyRModel {
|
|
2196
2393
|
// will then add the module prefix to the selector
|
2197
2394
|
if(IO_CONTEXT) {
|
2198
2395
|
if(name === UI.EQUATIONS_DATASET_NAME) {
|
2199
|
-
const mn = childNodeByTag(
|
2396
|
+
const mn = childNodeByTag(c, 'modifiers');
|
2200
2397
|
if(mn && mn.childNodes) {
|
2201
2398
|
for(let j = 0; j < mn.childNodes.length; j++) {
|
2202
|
-
const
|
2203
|
-
if(
|
2399
|
+
const cc = mn.childNodes[j];
|
2400
|
+
if(cc.nodeName === 'modifier') {
|
2204
2401
|
this.equations_dataset.addModifier(
|
2205
|
-
xmlDecoded(nodeContentByTag(
|
2206
|
-
|
2402
|
+
xmlDecoded(nodeContentByTag(cc, 'selector')),
|
2403
|
+
cc, IO_CONTEXT);
|
2207
2404
|
}
|
2208
2405
|
}
|
2209
2406
|
}
|
@@ -2367,12 +2564,11 @@ class LinnyRModel {
|
|
2367
2564
|
'</end-period><look-ahead-period>', this.look_ahead,
|
2368
2565
|
'</look-ahead-period><round-sequence>', this.round_sequence,
|
2369
2566
|
'</round-sequence><scaleunits>'].join('');
|
2370
|
-
|
2371
|
-
|
2372
|
-
|
2567
|
+
let obj;
|
2568
|
+
for(obj in this.scale_units) if(this.scale_units.hasOwnProperty(obj)) {
|
2569
|
+
xml += this.scale_units[obj].asXML;
|
2373
2570
|
}
|
2374
2571
|
xml += '</scaleunits><actors>';
|
2375
|
-
let obj;
|
2376
2572
|
for(obj in this.actors) {
|
2377
2573
|
// NOTE: do not to save "(no actor)"
|
2378
2574
|
if(this.actors.hasOwnProperty(obj) && obj != UI.nameToID(UI.NO_ACTOR)) {
|
@@ -2589,6 +2785,10 @@ class LinnyRModel {
|
|
2589
2785
|
this.cleanVector(p.cash_flow, 0, 0);
|
2590
2786
|
this.cleanVector(p.cash_in, 0, 0);
|
2591
2787
|
this.cleanVector(p.cash_out, 0, 0);
|
2788
|
+
// NOTE: note fields also must be reset
|
2789
|
+
for(let i = 0; i < p.notes.length; i++) {
|
2790
|
+
p.notes[i].parsed = false;
|
2791
|
+
}
|
2592
2792
|
}
|
2593
2793
|
for(obj in this.processes) if(this.processes.hasOwnProperty(obj)) {
|
2594
2794
|
p = this.processes[obj];
|
@@ -2679,30 +2879,9 @@ class LinnyRModel {
|
|
2679
2879
|
|
2680
2880
|
compileExpressions() {
|
2681
2881
|
// Compile all expression attributes of all model entities
|
2682
|
-
|
2683
|
-
|
2684
|
-
|
2685
|
-
// property is set before compiling the other expressions
|
2686
|
-
for(obj in this.datasets) if(this.datasets.hasOwnProperty(obj)) {
|
2687
|
-
this.datasets[obj].compileExpressions();
|
2688
|
-
}
|
2689
|
-
for(obj in this.actors) if(this.actors.hasOwnProperty(obj)) {
|
2690
|
-
this.actors[obj].weight.compile();
|
2691
|
-
}
|
2692
|
-
for(obj in this.processes) if(this.processes.hasOwnProperty(obj)) {
|
2693
|
-
p = this.processes[obj];
|
2694
|
-
p.lower_bound.compile();
|
2695
|
-
p.upper_bound.compile();
|
2696
|
-
}
|
2697
|
-
for(obj in this.products) if(this.products.hasOwnProperty(obj)) {
|
2698
|
-
p = this.products[obj];
|
2699
|
-
p.lower_bound.compile();
|
2700
|
-
p.upper_bound.compile();
|
2701
|
-
p.price.compile();
|
2702
|
-
}
|
2703
|
-
for(obj in this.links) if(this.links.hasOwnProperty(obj)) {
|
2704
|
-
this.links[obj].relative_rate.compile();
|
2705
|
-
this.links[obj].flow_delay.compile();
|
2882
|
+
const ax = this.allExpressions;
|
2883
|
+
for(let i = 0; i < ax.length; i++) {
|
2884
|
+
ax[i].compile();
|
2706
2885
|
}
|
2707
2886
|
}
|
2708
2887
|
|
@@ -3344,17 +3523,6 @@ class LinnyRModel {
|
|
3344
3523
|
// Start with the Linny-R model properties
|
3345
3524
|
let diff = differences(this, m, Object.keys(UI.MC.SETTINGS_PROPS));
|
3346
3525
|
if(Object.keys(diff).length > 0) d.settings = diff;
|
3347
|
-
// Then check for differences in scale unit lists
|
3348
|
-
diff = {};
|
3349
|
-
for(let i = 0; i < this.scale_units.length; i++) {
|
3350
|
-
const su = this.scale_units[i];
|
3351
|
-
if(m.scale_units.indexOf(su) < 0) diff[su] = [UI.MC.ADDED, su];
|
3352
|
-
}
|
3353
|
-
for(let i = 0; i < m.scale_units.length; i++) {
|
3354
|
-
const su = m.scale_units[i];
|
3355
|
-
if(this.scale_units.indexOf(su) < 0) diff[su] = [UI.MC.DELETED, su];
|
3356
|
-
}
|
3357
|
-
if(Object.keys(diff).length > 0) d.units = diff;
|
3358
3526
|
// NOTE: dataset differences will also detect equation differences
|
3359
3527
|
for(let i = 0; i < UI.MC.ENTITY_PROPS.length; i++) {
|
3360
3528
|
const ep = UI.MC.ENTITY_PROPS[i];
|
@@ -3691,8 +3859,8 @@ class IOContext {
|
|
3691
3859
|
}
|
3692
3860
|
|
3693
3861
|
bind(fn, an) {
|
3694
|
-
// Binds the formal name `
|
3695
|
-
// `an` it will have in the current model
|
3862
|
+
// Binds the formal name `fn` of an entity in a module to the actual
|
3863
|
+
// name `an` it will have in the current model
|
3696
3864
|
const id = UI.nameToID(fn);
|
3697
3865
|
if(this.bindings.hasOwnProperty(id)) {
|
3698
3866
|
this.bindings[id].bind(an);
|
@@ -3712,7 +3880,6 @@ class IOContext {
|
|
3712
3880
|
// (and for processes and clusters: with actor name `an` if specified and
|
3713
3881
|
// not "(no actor)")
|
3714
3882
|
// NOTE: do not modify (no actor), nor the "dataset dot"
|
3715
|
-
// @@TO DO: correctly handle equations!
|
3716
3883
|
if(n === UI.NO_ACTOR || n === '.') return n;
|
3717
3884
|
// NOTE: the top cluster of the included model has the prefix as its name
|
3718
3885
|
if(n === UI.TOP_CLUSTER_NAME || n === UI.FORMER_TOP_CLUSTER_NAME) {
|
@@ -3847,7 +4014,7 @@ class IOContext {
|
|
3847
4014
|
a,
|
3848
4015
|
stat;
|
3849
4016
|
while(true) {
|
3850
|
-
p = x.text.indexOf('[',
|
4017
|
+
p = x.text.indexOf('[', q + 1);
|
3851
4018
|
if(p < 0) {
|
3852
4019
|
// No more '[' => add remaining part of text, and quit
|
3853
4020
|
s += x.text.slice(q + 1);
|
@@ -3952,6 +4119,79 @@ class IOContext {
|
|
3952
4119
|
} // END of class IOContext
|
3953
4120
|
|
3954
4121
|
|
4122
|
+
// CLASS ScaleUnit
|
4123
|
+
class ScaleUnit {
|
4124
|
+
constructor(name, scalar, base_unit) {
|
4125
|
+
this.name = name;
|
4126
|
+
// NOTES:
|
4127
|
+
// (1) Undefined or empty strings default to '1'
|
4128
|
+
// (2) Multiplier is stored as string to preserve modeler's notation
|
4129
|
+
this.scalar = scalar || '1';
|
4130
|
+
this.base_unit = base_unit || '1';
|
4131
|
+
}
|
4132
|
+
|
4133
|
+
get multiplier() {
|
4134
|
+
// Returns scalar as number
|
4135
|
+
return safeStrToFloat(this.scalar, 1);
|
4136
|
+
}
|
4137
|
+
|
4138
|
+
conversionRates() {
|
4139
|
+
// Returns a "dictionary" {U1: R1, U2: R2, ...} such that Ui is a
|
4140
|
+
// scale unit that can be converted to *this* scaleunit U at rate Ri
|
4141
|
+
const cr = {};
|
4142
|
+
let p = 0, // previous count of entries
|
4143
|
+
n = 1;
|
4144
|
+
// At least one conversion: U -> U with rate 1
|
4145
|
+
cr[this.name] = 1;
|
4146
|
+
if(this.base_unit !== '1') {
|
4147
|
+
// Second conversion: U -> base of U with modeler-defined rate
|
4148
|
+
cr[this.base_unit] = this.multiplier;
|
4149
|
+
n++;
|
4150
|
+
}
|
4151
|
+
// Keep track of the number of keys; terminate as no new keys
|
4152
|
+
while(p < n) {
|
4153
|
+
p = n;
|
4154
|
+
// Iterate over all convertible scale units discovered so far
|
4155
|
+
for(let u in cr) if(cr.hasOwnProperty(u)) {
|
4156
|
+
// Look for conversions to units NOT yet detected
|
4157
|
+
for(let k in MODEL.scale_units) if(k != '1' &&
|
4158
|
+
MODEL.scale_units.hasOwnProperty(k)) {
|
4159
|
+
const
|
4160
|
+
su = MODEL.scale_units[k],
|
4161
|
+
b = su.base_unit;
|
4162
|
+
if(b === '1') continue;
|
4163
|
+
if(!cr.hasOwnProperty(k) && cr.hasOwnProperty(b)) {
|
4164
|
+
// Add unit if new while base unit is convertible
|
4165
|
+
cr[k] = cr[b] / su.multiplier;
|
4166
|
+
n++;
|
4167
|
+
} else if(cr.hasOwnProperty(k) && !cr.hasOwnProperty(b)) {
|
4168
|
+
// Likewise, add base unit if new while unit is convertible
|
4169
|
+
cr[b] = cr[k] * su.multiplier;
|
4170
|
+
n++;
|
4171
|
+
}
|
4172
|
+
}
|
4173
|
+
}
|
4174
|
+
}
|
4175
|
+
return cr;
|
4176
|
+
}
|
4177
|
+
|
4178
|
+
get asXML() {
|
4179
|
+
return ['<scaleunit><name>', xmlEncoded(this.name),
|
4180
|
+
'</name><scalar>', this.scalar,
|
4181
|
+
'</scalar><base-unit>', xmlEncoded(this.base_unit),
|
4182
|
+
'</base-unit></scaleunit>'].join('');
|
4183
|
+
}
|
4184
|
+
|
4185
|
+
// NOTE: NO initFromXML because scale units are added directly
|
4186
|
+
|
4187
|
+
differences(u) {
|
4188
|
+
// Return "dictionary" of differences, or NULL if none
|
4189
|
+
const d = differences(this, u, UI.MC.UNIT_PROPS);
|
4190
|
+
if(Object.keys(d).length > 0) return d;
|
4191
|
+
return null;
|
4192
|
+
}
|
4193
|
+
}
|
4194
|
+
|
3955
4195
|
// CLASS Actor
|
3956
4196
|
class Actor {
|
3957
4197
|
constructor(name) {
|
@@ -4107,35 +4347,45 @@ class ObjectWithXYWH {
|
|
4107
4347
|
|
4108
4348
|
// CLASS NoteField: numeric value of "field" [[dataset]] in note text
|
4109
4349
|
class NoteField {
|
4110
|
-
constructor(f, o) {
|
4111
|
-
// `f` holds the unmodified tag string [[dataset]] to be replaced by
|
4112
|
-
// value of vector or expression `o` for the current time step
|
4350
|
+
constructor(f, o, u='1', m=1) {
|
4351
|
+
// `f` holds the unmodified tag string [[dataset]] to be replaced by
|
4352
|
+
// the value of vector or expression `o` for the current time step;
|
4353
|
+
// if specified, `u` is the unit of the value to be displayed, and
|
4354
|
+
// `m` is the multiplier for the value to be displayed
|
4113
4355
|
this.field = f;
|
4114
4356
|
this.object = o;
|
4357
|
+
this.unit = u;
|
4358
|
+
this.multiplier = m;
|
4115
4359
|
}
|
4116
4360
|
|
4117
4361
|
get value() {
|
4118
|
-
// Returns the numeric value of this note field
|
4362
|
+
// Returns the numeric value of this note field as a numeric string
|
4363
|
+
// followed by its unit (unless this is 1)
|
4364
|
+
let v = VM.UNDEFINED;
|
4119
4365
|
const t = MODEL.t;
|
4120
4366
|
if(Array.isArray(this.object)) {
|
4121
4367
|
// Object is a vector
|
4122
|
-
if(t < this.object.length)
|
4123
|
-
|
4124
|
-
|
4125
|
-
return VM.UNDEFINED;
|
4126
|
-
}
|
4127
|
-
} else if(this.object.hasOwnProperty('c') && this.object.hasOwnProperty('u')) {
|
4368
|
+
if(t < this.object.length) v = this.object[t];
|
4369
|
+
} else if(this.object.hasOwnProperty('c') &&
|
4370
|
+
this.object.hasOwnProperty('u')) {
|
4128
4371
|
// Object holds link lists for cluster balance computation
|
4129
|
-
|
4372
|
+
v = MODEL.flowBalance(this.object, t);
|
4130
4373
|
} else if(this.object instanceof Expression) {
|
4131
4374
|
// Object is an expression
|
4132
|
-
|
4375
|
+
v = this.object.result(t);
|
4133
4376
|
} else if(typeof this.object === 'number') {
|
4134
|
-
|
4377
|
+
v = this.object;
|
4378
|
+
} else {
|
4379
|
+
// NOTE: this fall-through should not occur
|
4380
|
+
console.log('Note field value issue:', this.object);
|
4135
4381
|
}
|
4136
|
-
|
4137
|
-
|
4138
|
-
|
4382
|
+
if(Math.abs(this.multiplier - 1) > VM.NEAR_ZERO &&
|
4383
|
+
v > VM.MINUS_INFINITY && v < VM.PLUS_INFINITY) {
|
4384
|
+
v *= this.multiplier;
|
4385
|
+
}
|
4386
|
+
v = VM.sig4Dig(v);
|
4387
|
+
if(this.unit !== '1') v += ' ' + this.unit;
|
4388
|
+
return v;
|
4139
4389
|
}
|
4140
4390
|
|
4141
4391
|
} // END of class NoteField
|
@@ -4198,6 +4448,13 @@ class Note extends ObjectWithXYWH {
|
|
4198
4448
|
this.width = safeStrToInt(nodeContentByTag(node, 'width'));
|
4199
4449
|
this.height = safeStrToInt(nodeContentByTag(node, 'height'));
|
4200
4450
|
this.color.text = xmlDecoded(nodeContentByTag(node, 'color'));
|
4451
|
+
if(IO_CONTEXT) {
|
4452
|
+
const fel = this.fieldEntities;
|
4453
|
+
for(let i = 0; i < fel.length; i++) {
|
4454
|
+
this.rewriteTags(fel[i], IO_CONTEXT.actualName(fel[i]));
|
4455
|
+
}
|
4456
|
+
IO_CONTEXT.rewrite(this.color);
|
4457
|
+
}
|
4201
4458
|
}
|
4202
4459
|
|
4203
4460
|
setCluster(c) {
|
@@ -4223,24 +4480,93 @@ class Note extends ObjectWithXYWH {
|
|
4223
4480
|
for(let i = 0; i < tags.length; i++) {
|
4224
4481
|
const
|
4225
4482
|
tag = tags[i],
|
4226
|
-
|
4483
|
+
inner = tag.slice(2, tag.length - 2).trim(),
|
4484
|
+
bar = inner.lastIndexOf('|'),
|
4485
|
+
arrow = inner.lastIndexOf('->');
|
4486
|
+
// Check if a unit conversion scalar was specified
|
4487
|
+
let ena,
|
4488
|
+
from_unit = '1',
|
4489
|
+
to_unit = '',
|
4490
|
+
multiplier = 1;
|
4491
|
+
if(arrow > bar) {
|
4492
|
+
// Now for sure it is entity->unit or entity|attr->unit
|
4493
|
+
ena = inner.split('->');
|
4494
|
+
// As example, assume that unit = 'kWh' (so the value of the
|
4495
|
+
// field should be displayed in kilowatthour)
|
4496
|
+
// NOTE: use .trim() instead of UI.cleanName(...) here;
|
4497
|
+
// this forces the modeler to be exact, and that permits proper
|
4498
|
+
// renaming of scale units in note fields
|
4499
|
+
to_unit = ena[1].trim();
|
4500
|
+
ena = ena[0].split('|');
|
4501
|
+
if(!MODEL.scale_units.hasOwnProperty(to_unit)) {
|
4502
|
+
UI.warn(`Unknown scale unit "${to_unit}"`);
|
4503
|
+
to_unit = '1';
|
4504
|
+
}
|
4505
|
+
} else {
|
4506
|
+
ena = inner.split('|');
|
4507
|
+
}
|
4227
4508
|
// Look up entity for name and attribute
|
4228
|
-
const obj = MODEL.objectByName(ena[0]);
|
4509
|
+
const obj = MODEL.objectByName(ena[0].trim());
|
4229
4510
|
if(obj instanceof DatasetModifier) {
|
4230
|
-
|
4511
|
+
// NOTE: equations are (for now) dimensionless => unit '1'
|
4512
|
+
if(obj.dataset !== MODEL.equations_dataset) {
|
4513
|
+
from_unit = obj.dataset.scale_unit;
|
4514
|
+
multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
|
4515
|
+
}
|
4516
|
+
this.fields.push(new NoteField(tag, obj.expression, to_unit, multiplier));
|
4231
4517
|
} else if(obj) {
|
4232
4518
|
// If attribute omitted, use default attribute of entity type
|
4233
4519
|
const attr = (ena.length > 1 ? ena[1].trim() : obj.defaultAttribute);
|
4234
|
-
|
4235
|
-
|
4520
|
+
let val = null;
|
4521
|
+
// NOTE: for datasets, use the active modifier
|
4522
|
+
if(!attr && obj instanceof Dataset) {
|
4523
|
+
val = obj.activeModifierExpression;
|
4524
|
+
} else {
|
4525
|
+
// Variable may specify a vector-type attribute
|
4526
|
+
val = obj.attributeValue(attr);
|
4527
|
+
}
|
4236
4528
|
// If not, it may be a cluster unit balance
|
4237
4529
|
if(!val && attr.startsWith('=') && obj instanceof Cluster) {
|
4238
4530
|
val = {c: obj, u: attr.substring(1).trim()};
|
4531
|
+
from_unit = val.u;
|
4532
|
+
}
|
4533
|
+
if(obj instanceof Dataset) {
|
4534
|
+
from_unit = obj.scale_unit;
|
4535
|
+
} else if(obj instanceof Product) {
|
4536
|
+
if(attr === 'L') {
|
4537
|
+
from_unit = obj.scale_unit;
|
4538
|
+
} else if(attr === 'CP' || attr === 'HCP') {
|
4539
|
+
from_unit = MODEL.currency_unit;
|
4540
|
+
}
|
4541
|
+
} else if(obj instanceof Link) {
|
4542
|
+
const node = (obj.from_node instanceof Process ?
|
4543
|
+
obj.to_node : obj.from_node);
|
4544
|
+
if(attr === 'F') {
|
4545
|
+
if(obj.multiplier <= VM.LM_MEAN) {
|
4546
|
+
from_unit = node.scale_unit;
|
4547
|
+
} else {
|
4548
|
+
from_unit = '1';
|
4549
|
+
}
|
4550
|
+
}
|
4551
|
+
} else if(attr === 'CI' || attr === 'CO' || attr === 'CF') {
|
4552
|
+
from_unit = MODEL.currency_unit;
|
4239
4553
|
}
|
4240
4554
|
// If not, it may be an expression-type attribute
|
4241
|
-
if(!val)
|
4555
|
+
if(!val) {
|
4556
|
+
val = obj.attributeExpression(attr);
|
4557
|
+
if(obj instanceof Product) {
|
4558
|
+
if(attr === 'IL' || attr === 'LB' || attr === 'UB') {
|
4559
|
+
from_unit = obj.scale_unit;
|
4560
|
+
} else if(attr === 'P') {
|
4561
|
+
from_unit = MODEL.currency_unit + '/' + obj.scale_unit;
|
4562
|
+
}
|
4563
|
+
}
|
4564
|
+
}
|
4565
|
+
// If no TO unit, add the FROM unit
|
4566
|
+
if(to_unit === '') to_unit = from_unit;
|
4242
4567
|
if(val) {
|
4243
|
-
|
4568
|
+
multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
|
4569
|
+
this.fields.push(new NoteField(tag, val, to_unit, multiplier));
|
4244
4570
|
} else {
|
4245
4571
|
UI.warn(`Unknown ${obj.type.toLowerCase()} attribute "${attr}"`);
|
4246
4572
|
}
|
@@ -4251,10 +4577,48 @@ class Note extends ObjectWithXYWH {
|
|
4251
4577
|
}
|
4252
4578
|
this.parsed = true;
|
4253
4579
|
}
|
4580
|
+
|
4581
|
+
get fieldEntities() {
|
4582
|
+
// Return a list with names of entities used in fields
|
4583
|
+
const
|
4584
|
+
fel = [],
|
4585
|
+
tags = this.contents.match(/\[\[[^\]]+\]\]/g);
|
4586
|
+
for(let i = 0; i < tags.length; i++) {
|
4587
|
+
const
|
4588
|
+
tag = tags[i],
|
4589
|
+
inner = tag.slice(2, tag.length - 2).trim(),
|
4590
|
+
vb = inner.lastIndexOf('|'),
|
4591
|
+
ua = inner.lastIndexOf('->');
|
4592
|
+
if(vb >= 0) {
|
4593
|
+
addDistinct(inner.slice(0, vb), fel);
|
4594
|
+
} else if(ua >= 0 &&
|
4595
|
+
MODEL.scale_units.hasOwnProperty(inner.slice(ua + 2))) {
|
4596
|
+
addDistinct(inner.slice(0, ua), fel);
|
4597
|
+
} else {
|
4598
|
+
addDistinct(inner, fel);
|
4599
|
+
}
|
4600
|
+
}
|
4601
|
+
return fel;
|
4602
|
+
}
|
4603
|
+
|
4604
|
+
rewriteTags(en1, en2) {
|
4605
|
+
// Rewrite tags that reference entity name `en1` to reference `en2` instead
|
4606
|
+
if(en1 === en2) return;
|
4607
|
+
const
|
4608
|
+
raw = en1.split(/\s+/).join('\\\\s+'),
|
4609
|
+
re = new RegExp('\\[\\[\\s*' + raw + '\\s*(\\->|\\||\\])', 'g'),
|
4610
|
+
tags = this.contents.match(re);
|
4611
|
+
if(tags) {
|
4612
|
+
for(let i = 0; i < tags.length; i++) {
|
4613
|
+
this.contents = this.contents.replace(tags[i], tags[i].replace(en1, en2));
|
4614
|
+
}
|
4615
|
+
}
|
4616
|
+
}
|
4254
4617
|
|
4255
4618
|
rewriteFields(en1, en2) {
|
4256
4619
|
// Rename fields that reference entity name `en1` to reference `en2` instead
|
4257
4620
|
// NOTE: this does not affect the expression code
|
4621
|
+
if(en1 === en2) return;
|
4258
4622
|
for(let i = 0; i < this.fields.length; i++) {
|
4259
4623
|
const
|
4260
4624
|
f = this.fields[i],
|
@@ -4263,12 +4627,17 @@ class Note extends ObjectWithXYWH {
|
|
4263
4627
|
// Separate tag into variable and attribute + offset string (if any)
|
4264
4628
|
let e = tag,
|
4265
4629
|
a = '',
|
4266
|
-
vb = tag.lastIndexOf('|')
|
4630
|
+
vb = tag.lastIndexOf('|'),
|
4631
|
+
ua = tag.lastIndexOf('->');
|
4267
4632
|
if(vb >= 0) {
|
4268
4633
|
e = tag.slice(0, vb);
|
4269
4634
|
// NOTE: attribute string includes the vertical bar '|'
|
4270
4635
|
a = tag.slice(vb);
|
4271
|
-
}
|
4636
|
+
} else if(ua >= 0 && MODEL.scale_units.hasOwnProperty(tag.slice(ua + 2))) {
|
4637
|
+
e = tag.slice(0, ua);
|
4638
|
+
// NOTE: attribute string includes the unit conversion arrow '->'
|
4639
|
+
a = tag.slice(ua);
|
4640
|
+
}
|
4272
4641
|
// Check for match
|
4273
4642
|
const r = UI.replaceEntity(e, en1, en2);
|
4274
4643
|
if(r) {
|
@@ -4284,7 +4653,7 @@ class Note extends ObjectWithXYWH {
|
|
4284
4653
|
let txt = this.contents;
|
4285
4654
|
for(let i = 0; i < this.fields.length; i++) {
|
4286
4655
|
const nf = this.fields[i];
|
4287
|
-
txt = txt.replace(nf.field,
|
4656
|
+
txt = txt.replace(nf.field, nf.value);
|
4288
4657
|
}
|
4289
4658
|
return txt;
|
4290
4659
|
}
|
@@ -4402,7 +4771,14 @@ class NodeBox extends ObjectWithXYWH {
|
|
4402
4771
|
get numberContext() {
|
4403
4772
|
// Returns the string to be used to evaluate #, so for clusters, processes
|
4404
4773
|
// and products this is the string of trailing digits (or empty if none)
|
4405
|
-
|
4774
|
+
// of the node name, or if that does not end with a number, the trailing
|
4775
|
+
// digits of the first prefix (from right to left) that does
|
4776
|
+
const sn = this.name.split(UI.PREFIXER);
|
4777
|
+
let nc = endsWithDigits(sn.pop());
|
4778
|
+
while(!nc && sn.length > 0) {
|
4779
|
+
nc = endsWithDigits(sn.pop());
|
4780
|
+
}
|
4781
|
+
return nc;
|
4406
4782
|
}
|
4407
4783
|
|
4408
4784
|
rename(name, actor_name) {
|
@@ -4441,7 +4817,7 @@ class NodeBox extends ObjectWithXYWH {
|
|
4441
4817
|
delete MODEL.products[old_id];
|
4442
4818
|
} else if(this instanceof Cluster) {
|
4443
4819
|
MODEL.clusters[new_id] = this;
|
4444
|
-
delete MODEL.
|
4820
|
+
delete MODEL.clusters[old_id];
|
4445
4821
|
} else {
|
4446
4822
|
// NOTE: this should never happen => report an error
|
4447
4823
|
UI.alert('Can only rename processes, products and clusters');
|
@@ -4706,7 +5082,7 @@ class Arrow {
|
|
4706
5082
|
} else {
|
4707
5083
|
if(p[0] && p[1]) {
|
4708
5084
|
console.log('ERROR: Two distinct flows on monodirectional arrow',
|
4709
|
-
this, sum);
|
5085
|
+
this, sum, p);
|
4710
5086
|
return [0, 0, 0, false, false];
|
4711
5087
|
}
|
4712
5088
|
status = 1;
|
@@ -6322,6 +6698,8 @@ class Node extends NodeBox {
|
|
6322
6698
|
ds = MODEL.addDataset(dsn);
|
6323
6699
|
// Use the LB attribute as default value for the dataset
|
6324
6700
|
ds.default_value = parseFloat(this.lower_bound.text);
|
6701
|
+
// UB data has same unit as product
|
6702
|
+
ds.scale_unit = this.scale_unit;
|
6325
6703
|
ds.data = stringToFloatArray(lb_data);
|
6326
6704
|
ds.computeVector();
|
6327
6705
|
ds.computeStatistics();
|
@@ -6334,6 +6712,8 @@ class Node extends NodeBox {
|
|
6334
6712
|
dsn = this.displayName + ' UPPER BOUND DATA',
|
6335
6713
|
ds = MODEL.addDataset(dsn);
|
6336
6714
|
ds.default_value = parseFloat(this.upper_bound.text);
|
6715
|
+
// UB data has same unit as product
|
6716
|
+
ds.scale_unit = this.scale_unit;
|
6337
6717
|
ds.data = stringToFloatArray(ub_data);
|
6338
6718
|
ds.computeVector();
|
6339
6719
|
ds.computeStatistics();
|
@@ -6961,6 +7341,8 @@ class Product extends Node {
|
|
6961
7341
|
ds = MODEL.addDataset(dsn);
|
6962
7342
|
// Use the price attribute as default value for the dataset
|
6963
7343
|
ds.default_value = parseFloat(this.price.text);
|
7344
|
+
// NOTE: dataset unit then is a currency
|
7345
|
+
ds.scale_unit = MODEL.currency_unit;
|
6964
7346
|
ds.data = stringToFloatArray(data);
|
6965
7347
|
ds.computeVector();
|
6966
7348
|
ds.computeStatistics();
|
@@ -7216,8 +7598,8 @@ class Link {
|
|
7216
7598
|
tn = this.from_node;
|
7217
7599
|
}
|
7218
7600
|
// Otherwise, the FROM node is checked first
|
7219
|
-
let nc =
|
7220
|
-
if(!nc) nc =
|
7601
|
+
let nc = fn.numberContext;
|
7602
|
+
if(!nc) nc = tn.numberContext;
|
7221
7603
|
return nc;
|
7222
7604
|
}
|
7223
7605
|
|
@@ -7343,12 +7725,10 @@ class Link {
|
|
7343
7725
|
|
7344
7726
|
// CLASS DatasetModifier
|
7345
7727
|
class DatasetModifier {
|
7346
|
-
constructor(dataset, selector
|
7728
|
+
constructor(dataset, selector) {
|
7347
7729
|
this.dataset = dataset;
|
7348
7730
|
this.selector = selector;
|
7349
7731
|
this.expression = new Expression(dataset, selector, '');
|
7350
|
-
// Equations may have parameters
|
7351
|
-
this.parameters = params;
|
7352
7732
|
this.expression_cache = {};
|
7353
7733
|
}
|
7354
7734
|
|
@@ -7379,17 +7759,12 @@ class DatasetModifier {
|
|
7379
7759
|
// NOTE: for some reason, selector may become empty string, so prevent
|
7380
7760
|
// saving such unidentified modifiers
|
7381
7761
|
if(this.selector.trim().length === 0) return '';
|
7382
|
-
|
7383
|
-
this.parameters.join('\\').trim() : '');
|
7384
|
-
if(pstr) pstr = ' parameters="' + xmlEncoded(pstr) + '"';
|
7385
|
-
return ['<modifier', pstr, '><selector>', xmlEncoded(this.selector),
|
7762
|
+
return ['<modifier><selector>', xmlEncoded(this.selector),
|
7386
7763
|
'</selector><expression>', xmlEncoded(this.expression.text),
|
7387
7764
|
'</expression></modifier>'].join('');
|
7388
7765
|
}
|
7389
7766
|
|
7390
7767
|
initFromXML(node) {
|
7391
|
-
const pstr = nodeParameterValue(node, 'parameters').trim();
|
7392
|
-
this.parameters = (pstr ? xmlDecoded(pstr).split('\\'): false);
|
7393
7768
|
this.expression.text = xmlDecoded(nodeContentByTag(node, 'expression'));
|
7394
7769
|
if(IO_CONTEXT) {
|
7395
7770
|
// Contextualize the included expression
|
@@ -7421,6 +7796,7 @@ class Dataset {
|
|
7421
7796
|
this.name = name;
|
7422
7797
|
this.comments = '';
|
7423
7798
|
this.default_value = 0;
|
7799
|
+
this.scale_unit = '1';
|
7424
7800
|
this.time_scale = 1;
|
7425
7801
|
this.time_unit = CONFIGURATION.default_time_unit;
|
7426
7802
|
this.method = 'nearest';
|
@@ -7476,7 +7852,12 @@ class Dataset {
|
|
7476
7852
|
|
7477
7853
|
get numberContext() {
|
7478
7854
|
// Returns the string to be used to evaluate # (empty string if undefined)
|
7479
|
-
|
7855
|
+
const sn = this.name.split(UI.PREFIXER);
|
7856
|
+
let nc = endsWithDigits(sn.pop());
|
7857
|
+
while(!nc && sn.length > 0) {
|
7858
|
+
nc = endsWithDigits(sn.pop());
|
7859
|
+
}
|
7860
|
+
return nc;
|
7480
7861
|
}
|
7481
7862
|
|
7482
7863
|
get selectorList() {
|
@@ -7519,6 +7900,14 @@ class Dataset {
|
|
7519
7900
|
}
|
7520
7901
|
return this.default_value * MODEL.timeStepDuration / this.timeStepDuration;
|
7521
7902
|
}
|
7903
|
+
|
7904
|
+
changeScaleUnit(name) {
|
7905
|
+
let su = MODEL.addScaleUnit(name);
|
7906
|
+
if(su !== this.scale_unit) {
|
7907
|
+
this.scale_unit = su;
|
7908
|
+
MODEL.cleanUpScaleUnits();
|
7909
|
+
}
|
7910
|
+
}
|
7522
7911
|
|
7523
7912
|
matchingModifiers(l) {
|
7524
7913
|
// Returns the list of selectors of this dataset (in order: from most to
|
@@ -7655,47 +8044,45 @@ class Dataset {
|
|
7655
8044
|
}
|
7656
8045
|
return null;
|
7657
8046
|
}
|
8047
|
+
|
8048
|
+
get activeModifierExpression() {
|
8049
|
+
if(MODEL.running_experiment) {
|
8050
|
+
// If an experiment is running, check if dataset modifiers match the
|
8051
|
+
// combination of selectors for the active run
|
8052
|
+
const mm = this.matchingModifiers(MODEL.running_experiment.activeCombination);
|
8053
|
+
// If so, use the first match
|
8054
|
+
if(mm.length > 0) return mm[0].expression;
|
8055
|
+
}
|
8056
|
+
if(this.default_selector) {
|
8057
|
+
// If no experiment (so "normal" run), use default selector if specified
|
8058
|
+
const dm = this.modifiers[this.default_selector];
|
8059
|
+
if(dm) return dm.expression;
|
8060
|
+
// Exception should never occur, but check anyway and log it
|
8061
|
+
console.log('WARNING: Dataset "' + this.name +
|
8062
|
+
`" has no default selector "${this.default_selector}"`);
|
8063
|
+
}
|
8064
|
+
// Fall-through: return vector instead of expression
|
8065
|
+
return this.vector;
|
8066
|
+
}
|
7658
8067
|
|
7659
8068
|
addModifier(selector, node=null, ioc=null) {
|
7660
|
-
let s = selector
|
7661
|
-
params = false;
|
8069
|
+
let s = selector;
|
7662
8070
|
// Firstly, sanitize the selector
|
7663
8071
|
if(this === MODEL.equations_dataset) {
|
7664
8072
|
// Equation identifiers cannot contain characters that have special
|
7665
8073
|
// meaning in a variable identifier
|
7666
|
-
s = s.replace(/[\*\?\|\[\]\{\}
|
8074
|
+
s = s.replace(/[\*\?\|\[\]\{\}\@\#]/g, '');
|
7667
8075
|
if(s !== selector) {
|
7668
|
-
UI.warn('Equation name cannot contain [, ], {, }, |,
|
8076
|
+
UI.warn('Equation name cannot contain [, ], {, }, |, @, #, * or ?');
|
7669
8077
|
return null;
|
7670
8078
|
}
|
7671
|
-
//
|
7672
|
-
|
7673
|
-
|
7674
|
-
|
7675
|
-
// Store parameter names in lower case
|
7676
|
-
for(let i = 0; i < ss.length; i++) {
|
7677
|
-
ss[i] = ss[i].toLowerCase();
|
7678
|
-
}
|
7679
|
-
params = ss;
|
7680
|
-
}
|
8079
|
+
// Reduce inner spaces to one, and trim outer spaces
|
8080
|
+
s = s.replace(/\s+/g, ' ').trim();
|
8081
|
+
// Then prefix it when the IO context argument is defined
|
8082
|
+
if(ioc) s = ioc.actualName(s);
|
7681
8083
|
// If equation already exists, return its modifier
|
7682
8084
|
const id = UI.nameToID(s);
|
7683
|
-
if(this.modifiers.hasOwnProperty(id))
|
7684
|
-
const exm = this.modifiers[id];
|
7685
|
-
if(params) {
|
7686
|
-
if(!exm.parameters) {
|
7687
|
-
UI.warn(`Existing equation ${exm.displayName} has no parameters`);
|
7688
|
-
} else {
|
7689
|
-
const
|
7690
|
-
newp = params.join('\\'),
|
7691
|
-
oldp = exm.parameters.join('\\');
|
7692
|
-
if(newp !== oldp) {
|
7693
|
-
UI.warn(`Parameter mismatch: expected \\${oldp}, not \\${newp}`);
|
7694
|
-
}
|
7695
|
-
}
|
7696
|
-
}
|
7697
|
-
return exm;
|
7698
|
-
}
|
8085
|
+
if(this.modifiers.hasOwnProperty(id)) return this.modifiers[id];
|
7699
8086
|
// New equation identifier must not equal some entity ID
|
7700
8087
|
const obj = MODEL.objectByName(s);
|
7701
8088
|
if(obj) {
|
@@ -7703,8 +8090,6 @@ class Dataset {
|
|
7703
8090
|
UI.warningEntityExists(obj);
|
7704
8091
|
return null;
|
7705
8092
|
}
|
7706
|
-
// Also reduce inner spaces to one, and trim outer spaces
|
7707
|
-
s = s.replace(/\s+/g, ' ').trim();
|
7708
8093
|
} else {
|
7709
8094
|
// Standard dataset modifier selectors are much more restricted, but
|
7710
8095
|
// to be user-friendly, special chars are removed automatically
|
@@ -7722,12 +8107,10 @@ class Dataset {
|
|
7722
8107
|
UI.warn(UI.WARNING.INVALID_SELECTOR);
|
7723
8108
|
return null;
|
7724
8109
|
}
|
7725
|
-
// Then prefix it when the IO context argument is defined
|
7726
|
-
if(ioc) s = ioc.actualName(s);
|
7727
8110
|
// Then add a dataset modifier to this dataset
|
7728
8111
|
const id = UI.nameToID(s);
|
7729
8112
|
if(!this.modifiers.hasOwnProperty(id)) {
|
7730
|
-
this.modifiers[id] = new DatasetModifier(this, s
|
8113
|
+
this.modifiers[id] = new DatasetModifier(this, s);
|
7731
8114
|
}
|
7732
8115
|
// Finally, initialize it when the XML node argument is defined
|
7733
8116
|
if(node) this.modifiers[id].initFromXML(node);
|
@@ -7762,7 +8145,8 @@ class Dataset {
|
|
7762
8145
|
const xml = ['<dataset', p, '><name>', xmlEncoded(n),
|
7763
8146
|
'</name><notes>', cmnts,
|
7764
8147
|
'</notes><default>', this.default_value,
|
7765
|
-
'</default><
|
8148
|
+
'</default><unit>', xmlEncoded(this.scale_unit),
|
8149
|
+
'</unit><time-scale>', this.time_scale,
|
7766
8150
|
'</time-scale><time-unit>', this.time_unit,
|
7767
8151
|
'</time-unit><method>', this.method,
|
7768
8152
|
'</method><url>', xmlEncoded(this.url),
|
@@ -7776,10 +8160,11 @@ class Dataset {
|
|
7776
8160
|
initFromXML(node) {
|
7777
8161
|
this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
|
7778
8162
|
this.default_value = safeStrToFloat(nodeContentByTag(node, 'default'));
|
8163
|
+
this.scale_unit = xmlDecoded(nodeContentByTag(node, 'unit')) || '1';
|
7779
8164
|
this.time_scale = safeStrToFloat(nodeContentByTag(node, 'time-scale'), 1);
|
7780
|
-
this.time_unit = nodeContentByTag(node, 'time-unit')
|
7781
|
-
|
7782
|
-
this.method = nodeContentByTag(node, 'method');
|
8165
|
+
this.time_unit = nodeContentByTag(node, 'time-unit') ||
|
8166
|
+
CONFIGURATION.default_time_unit;
|
8167
|
+
this.method = nodeContentByTag(node, 'method') || 'nearest';
|
7783
8168
|
this.periodic = nodeParameterValue(node, 'periodic') === '1';
|
7784
8169
|
this.array = nodeParameterValue(node, 'array') === '1';
|
7785
8170
|
this.black_box = nodeParameterValue(node, 'black-box') === '1';
|
@@ -9619,8 +10004,13 @@ class Experiment {
|
|
9619
10004
|
this.variables = [];
|
9620
10005
|
this.configuration_dims = 0;
|
9621
10006
|
this.column_scenario_dims = 0;
|
10007
|
+
this.iterator_ranges = [[0,0], [0,0], [0,0]];
|
10008
|
+
this.iterator_dimensions = [];
|
9622
10009
|
this.settings_selectors = [];
|
9623
10010
|
this.settings_dimensions = [];
|
10011
|
+
this.combination_selectors = [];
|
10012
|
+
this.combination_dimensions = [];
|
10013
|
+
this.available_dimensions = [];
|
9624
10014
|
this.actor_selectors = [];
|
9625
10015
|
this.actor_dimensions = [];
|
9626
10016
|
this.excluded_selectors = '';
|
@@ -9680,6 +10070,56 @@ class Experiment {
|
|
9680
10070
|
return this.combinations[this.active_combination_index];
|
9681
10071
|
}
|
9682
10072
|
|
10073
|
+
get iteratorRangeString() {
|
10074
|
+
// Returns the iterator ranges as "from,to" pairs separated by |
|
10075
|
+
const ir = [];
|
10076
|
+
for(let i = 0; i < 3; i++) {
|
10077
|
+
ir.push(this.iterator_ranges[i].join(','));
|
10078
|
+
}
|
10079
|
+
return ir.join('|');
|
10080
|
+
}
|
10081
|
+
|
10082
|
+
parseIteratorRangeString(s) {
|
10083
|
+
// Parses `s` as "from,to" pairs, ignoring syntax errors
|
10084
|
+
if(s) {
|
10085
|
+
const ir = s.split('|');
|
10086
|
+
// Add 2 extra substrings to have at least 3
|
10087
|
+
ir.push('', '');
|
10088
|
+
for(let i = 0; i < 3; i++) {
|
10089
|
+
const r = ir[i].split(',');
|
10090
|
+
// Likewise add extra substring to have at least 2
|
10091
|
+
r.push('');
|
10092
|
+
// Parse integers, defaulting to 0
|
10093
|
+
this.iterator_ranges[i] = [safeStrToInt(r[0], 0), safeStrToInt(r[1], 0)];
|
10094
|
+
}
|
10095
|
+
}
|
10096
|
+
}
|
10097
|
+
|
10098
|
+
updateIteratorDimensions() {
|
10099
|
+
// Create iterator selectors for each index variable having a relevant range
|
10100
|
+
this.iterator_dimensions = [];
|
10101
|
+
const il = ['i', 'j', 'k'];
|
10102
|
+
for(let i = 0; i < 3; i++) {
|
10103
|
+
const r = this.iterator_ranges[i];
|
10104
|
+
if(r[0] || r[1]) {
|
10105
|
+
const
|
10106
|
+
sel = [],
|
10107
|
+
k = il[i] + '=';
|
10108
|
+
// NOTE: iterate from FROM to TO limit also when FROM > TO
|
10109
|
+
if(r[0] <= r[1]) {
|
10110
|
+
for(let j = r[0]; j <= r[1]; j++) {
|
10111
|
+
sel.push(k + j);
|
10112
|
+
}
|
10113
|
+
} else {
|
10114
|
+
for(let j = r[0]; j >= r[1]; j--) {
|
10115
|
+
sel.push(k + j);
|
10116
|
+
}
|
10117
|
+
}
|
10118
|
+
this.iterator_dimensions.push(sel);
|
10119
|
+
}
|
10120
|
+
}
|
10121
|
+
}
|
10122
|
+
|
9683
10123
|
matchingCombinationIndex(sl) {
|
9684
10124
|
// Returns index of combination with most selectors in common wilt `sl`
|
9685
10125
|
let high = 0,
|
@@ -9715,6 +10155,16 @@ class Experiment {
|
|
9715
10155
|
`<sdim>${xmlEncoded(this.settings_dimensions[i].join(','))}</sdim>`;
|
9716
10156
|
if(sd.indexOf(dim) < 0) sd += dim;
|
9717
10157
|
}
|
10158
|
+
let cs = '';
|
10159
|
+
for(let i = 0; i < this.combination_selectors.length; i++) {
|
10160
|
+
cs += `<csel>${xmlEncoded(this.combination_selectors[i])}</csel>`;
|
10161
|
+
}
|
10162
|
+
let cd = '';
|
10163
|
+
for(let i = 0; i < this.combination_dimensions.length; i++) {
|
10164
|
+
const dim =
|
10165
|
+
`<cdim>${xmlEncoded(this.combination_dimensions[i].join(','))}</cdim>`;
|
10166
|
+
if(cd.indexOf(dim) < 0) cd += dim;
|
10167
|
+
}
|
9718
10168
|
let as = '';
|
9719
10169
|
for(let i = 0; i < this.actor_selectors.length; i++) {
|
9720
10170
|
as += this.actor_selectors[i].asXML;
|
@@ -9733,6 +10183,7 @@ class Experiment {
|
|
9733
10183
|
return ['<experiment configuration-dims="', this.configuration_dims,
|
9734
10184
|
'" column_scenario-dims="', this.column_scenario_dims,
|
9735
10185
|
(this.completed ? '" completed="1' : ''),
|
10186
|
+
'" iterator-ranges="', this.iteratorRangeString,
|
9736
10187
|
'" started="', this.time_started,
|
9737
10188
|
'" stopped="', this.time_stopped,
|
9738
10189
|
'" variables="', this.download_settings.variables,
|
@@ -9749,7 +10200,9 @@ class Experiment {
|
|
9749
10200
|
'</dimensions><chart-titles>', ct,
|
9750
10201
|
'</chart-titles><settings-selectors>', ss,
|
9751
10202
|
'</settings-selectors><settings-dimensions>', sd,
|
9752
|
-
'</settings-dimensions><
|
10203
|
+
'</settings-dimensions><combination-selectors>', cs,
|
10204
|
+
'</combination-selectors><combination-dimensions>', cd,
|
10205
|
+
'</combination-dimensions><actor-selectors>', as,
|
9753
10206
|
'</actor-selectors><excluded-selectors>',
|
9754
10207
|
xmlEncoded(this.excluded_selectors),
|
9755
10208
|
'</excluded-selectors><clusters-to-ignore>', cti,
|
@@ -9762,6 +10215,7 @@ class Experiment {
|
|
9762
10215
|
nodeParameterValue(node, 'configuration-dims'));
|
9763
10216
|
this.column_scenario_dims = safeStrToInt(
|
9764
10217
|
nodeParameterValue(node, 'column-scenario-dims'));
|
10218
|
+
this.parseIteratorRangeString(nodeParameterValue(node, 'iterator-ranges'));
|
9765
10219
|
this.completed = nodeParameterValue(node, 'completed') === '1';
|
9766
10220
|
this.time_started = safeStrToInt(nodeParameterValue(node, 'started'));
|
9767
10221
|
this.time_stopped = safeStrToInt(nodeParameterValue(node, 'stopped'));
|
@@ -9817,6 +10271,24 @@ class Experiment {
|
|
9817
10271
|
}
|
9818
10272
|
}
|
9819
10273
|
}
|
10274
|
+
n = childNodeByTag(node, 'combination-selectors');
|
10275
|
+
if(n && n.childNodes) {
|
10276
|
+
for(let i = 0; i < n.childNodes.length; i++) {
|
10277
|
+
c = n.childNodes[i];
|
10278
|
+
if(c.nodeName === 'csel') {
|
10279
|
+
this.combination_selectors.push(xmlDecoded(nodeContent(c)));
|
10280
|
+
}
|
10281
|
+
}
|
10282
|
+
}
|
10283
|
+
n = childNodeByTag(node, 'combination-dimensions');
|
10284
|
+
if(n && n.childNodes) {
|
10285
|
+
for(let i = 0; i < n.childNodes.length; i++) {
|
10286
|
+
c = n.childNodes[i];
|
10287
|
+
if(c.nodeName === 'cdim') {
|
10288
|
+
this.combination_dimensions.push(xmlDecoded(nodeContent(c)).split(','));
|
10289
|
+
}
|
10290
|
+
}
|
10291
|
+
}
|
9820
10292
|
n = childNodeByTag(node, 'actor-selectors');
|
9821
10293
|
if(n && n.childNodes) {
|
9822
10294
|
for(let i = 0; i < n.childNodes.length; i++) {
|
@@ -9865,7 +10337,9 @@ class Experiment {
|
|
9865
10337
|
// Returns dimension index if any dimension contains any selector in
|
9866
10338
|
// dimension `d`, or -1 otherwise
|
9867
10339
|
for(let i = 0; i < this.dimensions.length; i++) {
|
9868
|
-
|
10340
|
+
const xd = this.dimensions[i].slice();
|
10341
|
+
this.expandCombinationSelectors(xd);
|
10342
|
+
if(intersection(xd, d).length > 0) return i;
|
9869
10343
|
}
|
9870
10344
|
return -1;
|
9871
10345
|
}
|
@@ -9874,7 +10348,7 @@ class Experiment {
|
|
9874
10348
|
// Removes dimension `d` from list and returns its old index
|
9875
10349
|
for(let i = 0; i < this.dimensions.length; i++) {
|
9876
10350
|
if(intersection(this.dimensions[i], d).length > 0) {
|
9877
|
-
this.dimensions.splice(i);
|
10351
|
+
this.dimensions.splice(i, 1);
|
9878
10352
|
return i;
|
9879
10353
|
}
|
9880
10354
|
}
|
@@ -9911,7 +10385,170 @@ class Experiment {
|
|
9911
10385
|
if(adi >= 0) this.dimensions[adi] = d;
|
9912
10386
|
}
|
9913
10387
|
}
|
10388
|
+
|
10389
|
+
get allDimensionSelectors() {
|
10390
|
+
const sl = Object.keys(MODEL.listOfAllSelectors);
|
10391
|
+
// Add selectors of actor, iterator and settings dimensions
|
10392
|
+
return sl;
|
10393
|
+
}
|
9914
10394
|
|
10395
|
+
orthogonalSelectors(c) {
|
10396
|
+
// Returns TRUE iff the selectors in set `c` all are elements of
|
10397
|
+
// different experiment dimensions
|
10398
|
+
const
|
10399
|
+
// Make a copy of `c` so it can be safely expanded
|
10400
|
+
xc = c.slice(),
|
10401
|
+
// Start with a copy of all model dimensions
|
10402
|
+
dl = MODEL.dimensions.slice(),
|
10403
|
+
issues = [];
|
10404
|
+
// Add dimensions defined for this experiment
|
10405
|
+
for(let i = 0; i < this.settings_dimensions.length; i++) {
|
10406
|
+
dl.push(this.settings_dimensions[i]);
|
10407
|
+
}
|
10408
|
+
for(let i = 0; i < this.actor_dimensions.length; i++) {
|
10409
|
+
dl.push(this.actor_dimensions[i]);
|
10410
|
+
}
|
10411
|
+
// Expand `c` as it may contain combination selectors
|
10412
|
+
this.expandCombinationSelectors(xc);
|
10413
|
+
// Check for all these dimensions that `c` contains known selectors
|
10414
|
+
// and that no two or more selectors occur in the same dimension
|
10415
|
+
let unknown = xc.slice();
|
10416
|
+
for(let i = 0; i < dl.length; i++) {
|
10417
|
+
const idc = intersection(dl[i], xc);
|
10418
|
+
unknown = complement(unknown, idc);
|
10419
|
+
if(idc.length > 1) {
|
10420
|
+
const pair = idc.join(' & ');
|
10421
|
+
if(issues.indexOf(pair) < 0) issues.push(pair);
|
10422
|
+
}
|
10423
|
+
}
|
10424
|
+
if(unknown.length > 0) {
|
10425
|
+
UI.warn('Combination contains ' +
|
10426
|
+
pluralS(unknown.length, 'undefined selector') +
|
10427
|
+
' (' + unknown.join(', ') + ')');
|
10428
|
+
return false;
|
10429
|
+
}
|
10430
|
+
if(issues.length > 0) {
|
10431
|
+
UI.warn('Combination contains multiple selectors from same dimension (' +
|
10432
|
+
issues.join(', ') + ')');
|
10433
|
+
return false;
|
10434
|
+
}
|
10435
|
+
return true;
|
10436
|
+
}
|
10437
|
+
|
10438
|
+
expandCombinationSelectors(cs) {
|
10439
|
+
// Expansion of combination selectors in a selector set `cs` means
|
10440
|
+
// that if, for example, `cs` = (A, C1) where C1 is a combination
|
10441
|
+
// selector defined as C1 = (B, C2) with A and B being "normal"
|
10442
|
+
// selectors, then C1 must be removed from `cs`, while B and the
|
10443
|
+
// expansion of C2 must be appended to `cs`.
|
10444
|
+
// NOTE: the original selectors C1 and C2 must be removed because
|
10445
|
+
// *dimension* selectors cannot be a used as "normal" selectors
|
10446
|
+
// (e.g., for dataset modifiers, actor settings or model setting)
|
10447
|
+
// NOTE: traverse `cs` in reverse order to ensure that deleting and
|
10448
|
+
// appending produce the intended result
|
10449
|
+
for(let i = cs.length - 1; i >= 0; i--) {
|
10450
|
+
const s = cs[i];
|
10451
|
+
// Check whether selector `s` defines a combination
|
10452
|
+
for(let j = 0; j < this.combination_selectors.length; j++) {
|
10453
|
+
const tuple = this.combination_selectors[j].split('|');
|
10454
|
+
if(tuple[0] === s) {
|
10455
|
+
// First remove `s` from the original set...
|
10456
|
+
cs.splice(i, 1);
|
10457
|
+
// Let `xs` be the selector set to replace `s`
|
10458
|
+
const xs = tuple[1].split(' ');
|
10459
|
+
// Recursively expand `xs`, as it may contain combination selectors
|
10460
|
+
this.expandCombinationSelectors(xs);
|
10461
|
+
// ... and append its expansion
|
10462
|
+
cs.push(...xs);
|
10463
|
+
}
|
10464
|
+
}
|
10465
|
+
}
|
10466
|
+
}
|
10467
|
+
|
10468
|
+
orthogonalCombinationDimensions(sl) {
|
10469
|
+
// Returns TRUE iff the expansions of the selectors in set `sl`
|
10470
|
+
// are mutually exclusive
|
10471
|
+
const
|
10472
|
+
xl = {},
|
10473
|
+
issues = {};
|
10474
|
+
for(let i = 0; i < sl.length; i++) {
|
10475
|
+
const s = sl[i];
|
10476
|
+
xl[s] = [s];
|
10477
|
+
this.expandCombinationSelectors(xl[s]);
|
10478
|
+
issues[s] = [];
|
10479
|
+
}
|
10480
|
+
let ok = true;
|
10481
|
+
for(let i = 0; i < sl.length; i++) {
|
10482
|
+
const s1 = sl[i];
|
10483
|
+
for(let j = i + 1; j < sl.length; j++) {
|
10484
|
+
const
|
10485
|
+
s2 = sl[j],
|
10486
|
+
shared = intersection(xl[s1], xl[s2]);
|
10487
|
+
if(shared.length > 0) {
|
10488
|
+
issues[s1].push(`${s2}: ${shared.join(', ')}`);
|
10489
|
+
ok = false;
|
10490
|
+
}
|
10491
|
+
}
|
10492
|
+
}
|
10493
|
+
if(!ok) {
|
10494
|
+
const il = [];
|
10495
|
+
for(let i = 0; i < sl.length; i++) {
|
10496
|
+
const s = sl[i];
|
10497
|
+
if(issues[s].length > 0) {
|
10498
|
+
il.push(`${s} (${issues[s].join('; ')})`);
|
10499
|
+
}
|
10500
|
+
}
|
10501
|
+
UI.warn('Combination dimension is not orthogonal: ' + il.join(', '));
|
10502
|
+
}
|
10503
|
+
return ok;
|
10504
|
+
}
|
10505
|
+
|
10506
|
+
inferAvailableDimensions() {
|
10507
|
+
// Creates list of dimensions that are orthogonal to those already
|
10508
|
+
// selected for this experiment
|
10509
|
+
this.available_dimensions.length = 0;
|
10510
|
+
// For efficiency, do not use hasDimension but expand the dimensions
|
10511
|
+
// that are already selected once, and define a lookup function that
|
10512
|
+
// checks for orthogonality
|
10513
|
+
const
|
10514
|
+
axes = [],
|
10515
|
+
orthogonal = (d) => {
|
10516
|
+
for(let i = 0; i < axes.length; i++) {
|
10517
|
+
if(intersection(axes[i], d).length > 0) return false;
|
10518
|
+
}
|
10519
|
+
return true;
|
10520
|
+
};
|
10521
|
+
for(let i = 0; i < this.dimensions.length; i++) {
|
10522
|
+
axes.push(this.dimensions[i].slice());
|
10523
|
+
this.expandCombinationSelectors(axes[i]);
|
10524
|
+
}
|
10525
|
+
for(let i = 0; i < MODEL.dimensions.length; i++) {
|
10526
|
+
const d = MODEL.dimensions[i];
|
10527
|
+
if(orthogonal(d)) this.available_dimensions.push(d);
|
10528
|
+
}
|
10529
|
+
for(let i = 0; i < this.settings_dimensions.length; i++) {
|
10530
|
+
const d = this.settings_dimensions[i];
|
10531
|
+
if(orthogonal(d)) this.available_dimensions.push(d);
|
10532
|
+
}
|
10533
|
+
for(let i = 0; i < this.iterator_dimensions.length; i++) {
|
10534
|
+
const d = this.iterator_dimensions[i];
|
10535
|
+
if(orthogonal(d)) this.available_dimensions.push(d);
|
10536
|
+
}
|
10537
|
+
for(let i = 0; i < this.actor_dimensions.length; i++) {
|
10538
|
+
const d = this.actor_dimensions[i];
|
10539
|
+
if(orthogonal(d)) this.available_dimensions.push(d);
|
10540
|
+
}
|
10541
|
+
for(let i = 0; i < this.combination_dimensions.length; i++) {
|
10542
|
+
// NOTE: combination dimensions must be expanded before checking...
|
10543
|
+
const
|
10544
|
+
d = this.combination_dimensions[i],
|
10545
|
+
xd = d.slice();
|
10546
|
+
this.expandCombinationSelectors(xd);
|
10547
|
+
// ... but the original combination dimension must be added
|
10548
|
+
if(orthogonal(xd)) this.available_dimensions.push(d);
|
10549
|
+
}
|
10550
|
+
}
|
10551
|
+
|
9915
10552
|
inferActualDimensions() {
|
9916
10553
|
// Creates list of dimensions without excluded selectors
|
9917
10554
|
this.actual_dimensions.length = 0;
|
@@ -9928,6 +10565,9 @@ class Experiment {
|
|
9928
10565
|
if(n >= this.actual_dimensions.length) {
|
9929
10566
|
// NOTE: do not push an empty selector list (can occur if no dimensions)
|
9930
10567
|
if(s.length > 0) this.combinations.push(s);
|
10568
|
+
// NOTE: combinations may include *dimension* selectors
|
10569
|
+
// These then must be "expanded"
|
10570
|
+
this.expandCombinationSelectors(s);
|
9931
10571
|
return;
|
9932
10572
|
}
|
9933
10573
|
const d = this.actual_dimensions[n];
|
@@ -9939,14 +10579,33 @@ class Experiment {
|
|
9939
10579
|
}
|
9940
10580
|
}
|
9941
10581
|
|
10582
|
+
renameSelectorInDimensions(olds, news) {
|
10583
|
+
// Update the combination dimensions that contain `olds`
|
10584
|
+
for(let i = 0; i < this.settings_dimensions.length; i++) {
|
10585
|
+
const si = this.settings_dimensions[i].indexOf(olds);
|
10586
|
+
if(si >= 0) this.settings_dimensions[i][si] = news;
|
10587
|
+
}
|
10588
|
+
for(let i = 0; i < this.combination_selectors.length; i++) {
|
10589
|
+
const
|
10590
|
+
c = this.combination_selectors[i].split('|'),
|
10591
|
+
sl = c[1].split(' '),
|
10592
|
+
si = sl.indexOf(olds);
|
10593
|
+
if(si >= 0) {
|
10594
|
+
sl[si] = news;
|
10595
|
+
c[1] = sl.join(' ');
|
10596
|
+
this.combination_selectors[i] = c.join('|');
|
10597
|
+
}
|
10598
|
+
}
|
10599
|
+
}
|
10600
|
+
|
9942
10601
|
mayBeIgnored(c) {
|
9943
|
-
// Returns TRUE iff `c` is on the list to be ignored
|
10602
|
+
// Returns TRUE iff cluster `c` is on the list to be ignored
|
9944
10603
|
for(let i = 0; i < this.clusters_to_ignore.length; i++) {
|
9945
10604
|
if(this.clusters_to_ignore[i].cluster === c) return true;
|
9946
10605
|
}
|
9947
10606
|
return false;
|
9948
10607
|
}
|
9949
|
-
|
10608
|
+
|
9950
10609
|
inferVariables() {
|
9951
10610
|
// Create list of distinct variables in charts
|
9952
10611
|
this.variables.length = 0;
|