linny-r 1.1.23 → 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.
- 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 +198 -13
- package/static/linny-r.css +214 -33
- package/static/scripts/linny-r-config.js +6 -0
- package/static/scripts/linny-r-ctrl.js +23 -7
- package/static/scripts/linny-r-gui.js +666 -111
- package/static/scripts/linny-r-model.js +873 -224
- 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 = {};
|
@@ -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 ||
|
@@ -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
|
-
|
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
|
+
// 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
|
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
|
-
|
1839
|
-
//
|
1840
|
-
//
|
1841
|
-
|
1842
|
-
//
|
1843
|
-
|
1844
|
-
|
1845
|
-
|
1846
|
-
|
1847
|
-
|
1848
|
-
|
1849
|
-
|
1850
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1884
|
-
|
2062
|
+
const l = this.links[k];
|
2063
|
+
x.push(l.relative_rate, l.flow_delay);
|
1885
2064
|
}
|
1886
|
-
|
1887
|
-
|
1888
|
-
|
1889
|
-
|
1890
|
-
|
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('
|
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
|
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/,
|
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
|
-
//
|
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);
|
1932
|
-
}
|
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);
|
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);
|
1940
2138
|
}
|
1941
|
-
//
|
1942
|
-
|
1943
|
-
|
1944
|
-
|
1945
|
-
|
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
|
-
|
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);
|
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(
|
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
|
2203
|
-
if(
|
2401
|
+
const cc = mn.childNodes[j];
|
2402
|
+
if(cc.nodeName === 'modifier') {
|
2204
2403
|
this.equations_dataset.addModifier(
|
2205
|
-
xmlDecoded(nodeContentByTag(
|
2206
|
-
|
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
|
-
|
2371
|
-
|
2372
|
-
|
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)) {
|
@@ -2589,6 +2787,10 @@ class LinnyRModel {
|
|
2589
2787
|
this.cleanVector(p.cash_flow, 0, 0);
|
2590
2788
|
this.cleanVector(p.cash_in, 0, 0);
|
2591
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
|
+
}
|
2592
2794
|
}
|
2593
2795
|
for(obj in this.processes) if(this.processes.hasOwnProperty(obj)) {
|
2594
2796
|
p = this.processes[obj];
|
@@ -2679,30 +2881,9 @@ class LinnyRModel {
|
|
2679
2881
|
|
2680
2882
|
compileExpressions() {
|
2681
2883
|
// 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();
|
2884
|
+
const ax = this.allExpressions;
|
2885
|
+
for(let i = 0; i < ax.length; i++) {
|
2886
|
+
ax[i].compile();
|
2706
2887
|
}
|
2707
2888
|
}
|
2708
2889
|
|
@@ -3344,17 +3525,6 @@ class LinnyRModel {
|
|
3344
3525
|
// Start with the Linny-R model properties
|
3345
3526
|
let diff = differences(this, m, Object.keys(UI.MC.SETTINGS_PROPS));
|
3346
3527
|
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
3528
|
// NOTE: dataset differences will also detect equation differences
|
3359
3529
|
for(let i = 0; i < UI.MC.ENTITY_PROPS.length; i++) {
|
3360
3530
|
const ep = UI.MC.ENTITY_PROPS[i];
|
@@ -3691,8 +3861,8 @@ class IOContext {
|
|
3691
3861
|
}
|
3692
3862
|
|
3693
3863
|
bind(fn, an) {
|
3694
|
-
// Binds the formal name `
|
3695
|
-
// `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
|
3696
3866
|
const id = UI.nameToID(fn);
|
3697
3867
|
if(this.bindings.hasOwnProperty(id)) {
|
3698
3868
|
this.bindings[id].bind(an);
|
@@ -3712,7 +3882,6 @@ class IOContext {
|
|
3712
3882
|
// (and for processes and clusters: with actor name `an` if specified and
|
3713
3883
|
// not "(no actor)")
|
3714
3884
|
// NOTE: do not modify (no actor), nor the "dataset dot"
|
3715
|
-
// @@TO DO: correctly handle equations!
|
3716
3885
|
if(n === UI.NO_ACTOR || n === '.') return n;
|
3717
3886
|
// NOTE: the top cluster of the included model has the prefix as its name
|
3718
3887
|
if(n === UI.TOP_CLUSTER_NAME || n === UI.FORMER_TOP_CLUSTER_NAME) {
|
@@ -3847,7 +4016,7 @@ class IOContext {
|
|
3847
4016
|
a,
|
3848
4017
|
stat;
|
3849
4018
|
while(true) {
|
3850
|
-
p = x.text.indexOf('[',
|
4019
|
+
p = x.text.indexOf('[', q + 1);
|
3851
4020
|
if(p < 0) {
|
3852
4021
|
// No more '[' => add remaining part of text, and quit
|
3853
4022
|
s += x.text.slice(q + 1);
|
@@ -3952,6 +4121,79 @@ class IOContext {
|
|
3952
4121
|
} // END of class IOContext
|
3953
4122
|
|
3954
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
|
+
|
3955
4197
|
// CLASS Actor
|
3956
4198
|
class Actor {
|
3957
4199
|
constructor(name) {
|
@@ -4107,35 +4349,45 @@ class ObjectWithXYWH {
|
|
4107
4349
|
|
4108
4350
|
// CLASS NoteField: numeric value of "field" [[dataset]] in note text
|
4109
4351
|
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
|
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
|
4113
4357
|
this.field = f;
|
4114
4358
|
this.object = o;
|
4359
|
+
this.unit = u;
|
4360
|
+
this.multiplier = m;
|
4115
4361
|
}
|
4116
4362
|
|
4117
4363
|
get value() {
|
4118
|
-
// 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;
|
4119
4367
|
const t = MODEL.t;
|
4120
4368
|
if(Array.isArray(this.object)) {
|
4121
4369
|
// 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')) {
|
4370
|
+
if(t < this.object.length) v = this.object[t];
|
4371
|
+
} else if(this.object.hasOwnProperty('c') &&
|
4372
|
+
this.object.hasOwnProperty('u')) {
|
4128
4373
|
// Object holds link lists for cluster balance computation
|
4129
|
-
|
4374
|
+
v = MODEL.flowBalance(this.object, t);
|
4130
4375
|
} else if(this.object instanceof Expression) {
|
4131
4376
|
// Object is an expression
|
4132
|
-
|
4377
|
+
v = this.object.result(t);
|
4133
4378
|
} else if(typeof this.object === 'number') {
|
4134
|
-
|
4379
|
+
v = this.object;
|
4380
|
+
} else {
|
4381
|
+
// NOTE: this fall-through should not occur
|
4382
|
+
console.log('Note field value issue:', this.object);
|
4135
4383
|
}
|
4136
|
-
|
4137
|
-
|
4138
|
-
|
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;
|
4139
4391
|
}
|
4140
4392
|
|
4141
4393
|
} // END of class NoteField
|
@@ -4198,6 +4450,13 @@ class Note extends ObjectWithXYWH {
|
|
4198
4450
|
this.width = safeStrToInt(nodeContentByTag(node, 'width'));
|
4199
4451
|
this.height = safeStrToInt(nodeContentByTag(node, 'height'));
|
4200
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
|
+
}
|
4201
4460
|
}
|
4202
4461
|
|
4203
4462
|
setCluster(c) {
|
@@ -4223,24 +4482,93 @@ class Note extends ObjectWithXYWH {
|
|
4223
4482
|
for(let i = 0; i < tags.length; i++) {
|
4224
4483
|
const
|
4225
4484
|
tag = tags[i],
|
4226
|
-
|
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
|
+
}
|
4227
4510
|
// Look up entity for name and attribute
|
4228
|
-
const obj = MODEL.objectByName(ena[0]);
|
4511
|
+
const obj = MODEL.objectByName(ena[0].trim());
|
4229
4512
|
if(obj instanceof DatasetModifier) {
|
4230
|
-
|
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));
|
4231
4519
|
} else if(obj) {
|
4232
4520
|
// If attribute omitted, use default attribute of entity type
|
4233
4521
|
const attr = (ena.length > 1 ? ena[1].trim() : obj.defaultAttribute);
|
4234
|
-
|
4235
|
-
|
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
|
+
}
|
4236
4530
|
// If not, it may be a cluster unit balance
|
4237
4531
|
if(!val && attr.startsWith('=') && obj instanceof Cluster) {
|
4238
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;
|
4239
4555
|
}
|
4240
4556
|
// If not, it may be an expression-type attribute
|
4241
|
-
if(!val)
|
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;
|
4242
4569
|
if(val) {
|
4243
|
-
|
4570
|
+
multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
|
4571
|
+
this.fields.push(new NoteField(tag, val, to_unit, multiplier));
|
4244
4572
|
} else {
|
4245
4573
|
UI.warn(`Unknown ${obj.type.toLowerCase()} attribute "${attr}"`);
|
4246
4574
|
}
|
@@ -4251,10 +4579,48 @@ class Note extends ObjectWithXYWH {
|
|
4251
4579
|
}
|
4252
4580
|
this.parsed = true;
|
4253
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
|
+
}
|
4254
4619
|
|
4255
4620
|
rewriteFields(en1, en2) {
|
4256
4621
|
// Rename fields that reference entity name `en1` to reference `en2` instead
|
4257
4622
|
// NOTE: this does not affect the expression code
|
4623
|
+
if(en1 === en2) return;
|
4258
4624
|
for(let i = 0; i < this.fields.length; i++) {
|
4259
4625
|
const
|
4260
4626
|
f = this.fields[i],
|
@@ -4263,12 +4629,17 @@ class Note extends ObjectWithXYWH {
|
|
4263
4629
|
// Separate tag into variable and attribute + offset string (if any)
|
4264
4630
|
let e = tag,
|
4265
4631
|
a = '',
|
4266
|
-
vb = tag.lastIndexOf('|')
|
4632
|
+
vb = tag.lastIndexOf('|'),
|
4633
|
+
ua = tag.lastIndexOf('->');
|
4267
4634
|
if(vb >= 0) {
|
4268
4635
|
e = tag.slice(0, vb);
|
4269
4636
|
// NOTE: attribute string includes the vertical bar '|'
|
4270
4637
|
a = tag.slice(vb);
|
4271
|
-
}
|
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
|
+
}
|
4272
4643
|
// Check for match
|
4273
4644
|
const r = UI.replaceEntity(e, en1, en2);
|
4274
4645
|
if(r) {
|
@@ -4284,7 +4655,7 @@ class Note extends ObjectWithXYWH {
|
|
4284
4655
|
let txt = this.contents;
|
4285
4656
|
for(let i = 0; i < this.fields.length; i++) {
|
4286
4657
|
const nf = this.fields[i];
|
4287
|
-
txt = txt.replace(nf.field,
|
4658
|
+
txt = txt.replace(nf.field, nf.value);
|
4288
4659
|
}
|
4289
4660
|
return txt;
|
4290
4661
|
}
|
@@ -4441,7 +4812,7 @@ class NodeBox extends ObjectWithXYWH {
|
|
4441
4812
|
delete MODEL.products[old_id];
|
4442
4813
|
} else if(this instanceof Cluster) {
|
4443
4814
|
MODEL.clusters[new_id] = this;
|
4444
|
-
delete MODEL.
|
4815
|
+
delete MODEL.clusters[old_id];
|
4445
4816
|
} else {
|
4446
4817
|
// NOTE: this should never happen => report an error
|
4447
4818
|
UI.alert('Can only rename processes, products and clusters');
|
@@ -4706,7 +5077,7 @@ class Arrow {
|
|
4706
5077
|
} else {
|
4707
5078
|
if(p[0] && p[1]) {
|
4708
5079
|
console.log('ERROR: Two distinct flows on monodirectional arrow',
|
4709
|
-
this, sum);
|
5080
|
+
this, sum, p);
|
4710
5081
|
return [0, 0, 0, false, false];
|
4711
5082
|
}
|
4712
5083
|
status = 1;
|
@@ -6322,6 +6693,8 @@ class Node extends NodeBox {
|
|
6322
6693
|
ds = MODEL.addDataset(dsn);
|
6323
6694
|
// Use the LB attribute as default value for the dataset
|
6324
6695
|
ds.default_value = parseFloat(this.lower_bound.text);
|
6696
|
+
// UB data has same unit as product
|
6697
|
+
ds.scale_unit = this.scale_unit;
|
6325
6698
|
ds.data = stringToFloatArray(lb_data);
|
6326
6699
|
ds.computeVector();
|
6327
6700
|
ds.computeStatistics();
|
@@ -6334,6 +6707,8 @@ class Node extends NodeBox {
|
|
6334
6707
|
dsn = this.displayName + ' UPPER BOUND DATA',
|
6335
6708
|
ds = MODEL.addDataset(dsn);
|
6336
6709
|
ds.default_value = parseFloat(this.upper_bound.text);
|
6710
|
+
// UB data has same unit as product
|
6711
|
+
ds.scale_unit = this.scale_unit;
|
6337
6712
|
ds.data = stringToFloatArray(ub_data);
|
6338
6713
|
ds.computeVector();
|
6339
6714
|
ds.computeStatistics();
|
@@ -6961,6 +7336,8 @@ class Product extends Node {
|
|
6961
7336
|
ds = MODEL.addDataset(dsn);
|
6962
7337
|
// Use the price attribute as default value for the dataset
|
6963
7338
|
ds.default_value = parseFloat(this.price.text);
|
7339
|
+
// NOTE: dataset unit then is a currency
|
7340
|
+
ds.scale_unit = MODEL.currency_unit;
|
6964
7341
|
ds.data = stringToFloatArray(data);
|
6965
7342
|
ds.computeVector();
|
6966
7343
|
ds.computeStatistics();
|
@@ -7343,12 +7720,10 @@ class Link {
|
|
7343
7720
|
|
7344
7721
|
// CLASS DatasetModifier
|
7345
7722
|
class DatasetModifier {
|
7346
|
-
constructor(dataset, selector
|
7723
|
+
constructor(dataset, selector) {
|
7347
7724
|
this.dataset = dataset;
|
7348
7725
|
this.selector = selector;
|
7349
7726
|
this.expression = new Expression(dataset, selector, '');
|
7350
|
-
// Equations may have parameters
|
7351
|
-
this.parameters = params;
|
7352
7727
|
this.expression_cache = {};
|
7353
7728
|
}
|
7354
7729
|
|
@@ -7379,17 +7754,12 @@ class DatasetModifier {
|
|
7379
7754
|
// NOTE: for some reason, selector may become empty string, so prevent
|
7380
7755
|
// saving such unidentified modifiers
|
7381
7756
|
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),
|
7757
|
+
return ['<modifier><selector>', xmlEncoded(this.selector),
|
7386
7758
|
'</selector><expression>', xmlEncoded(this.expression.text),
|
7387
7759
|
'</expression></modifier>'].join('');
|
7388
7760
|
}
|
7389
7761
|
|
7390
7762
|
initFromXML(node) {
|
7391
|
-
const pstr = nodeParameterValue(node, 'parameters').trim();
|
7392
|
-
this.parameters = (pstr ? xmlDecoded(pstr).split('\\'): false);
|
7393
7763
|
this.expression.text = xmlDecoded(nodeContentByTag(node, 'expression'));
|
7394
7764
|
if(IO_CONTEXT) {
|
7395
7765
|
// Contextualize the included expression
|
@@ -7421,6 +7791,7 @@ class Dataset {
|
|
7421
7791
|
this.name = name;
|
7422
7792
|
this.comments = '';
|
7423
7793
|
this.default_value = 0;
|
7794
|
+
this.scale_unit = '1';
|
7424
7795
|
this.time_scale = 1;
|
7425
7796
|
this.time_unit = CONFIGURATION.default_time_unit;
|
7426
7797
|
this.method = 'nearest';
|
@@ -7519,6 +7890,14 @@ class Dataset {
|
|
7519
7890
|
}
|
7520
7891
|
return this.default_value * MODEL.timeStepDuration / this.timeStepDuration;
|
7521
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
|
+
}
|
7522
7901
|
|
7523
7902
|
matchingModifiers(l) {
|
7524
7903
|
// Returns the list of selectors of this dataset (in order: from most to
|
@@ -7655,47 +8034,45 @@ class Dataset {
|
|
7655
8034
|
}
|
7656
8035
|
return null;
|
7657
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
|
+
}
|
7658
8057
|
|
7659
8058
|
addModifier(selector, node=null, ioc=null) {
|
7660
|
-
let s = selector
|
7661
|
-
params = false;
|
8059
|
+
let s = selector;
|
7662
8060
|
// Firstly, sanitize the selector
|
7663
8061
|
if(this === MODEL.equations_dataset) {
|
7664
8062
|
// Equation identifiers cannot contain characters that have special
|
7665
8063
|
// meaning in a variable identifier
|
7666
|
-
s = s.replace(/[\*\?\|\[\]\{\}
|
8064
|
+
s = s.replace(/[\*\?\|\[\]\{\}\@\#]/g, '');
|
7667
8065
|
if(s !== selector) {
|
7668
|
-
UI.warn('Equation name cannot contain [, ], {, }, |,
|
8066
|
+
UI.warn('Equation name cannot contain [, ], {, }, |, @, #, * or ?');
|
7669
8067
|
return null;
|
7670
8068
|
}
|
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
|
-
}
|
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);
|
7681
8073
|
// If equation already exists, return its modifier
|
7682
8074
|
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
|
-
}
|
8075
|
+
if(this.modifiers.hasOwnProperty(id)) return this.modifiers[id];
|
7699
8076
|
// New equation identifier must not equal some entity ID
|
7700
8077
|
const obj = MODEL.objectByName(s);
|
7701
8078
|
if(obj) {
|
@@ -7703,8 +8080,6 @@ class Dataset {
|
|
7703
8080
|
UI.warningEntityExists(obj);
|
7704
8081
|
return null;
|
7705
8082
|
}
|
7706
|
-
// Also reduce inner spaces to one, and trim outer spaces
|
7707
|
-
s = s.replace(/\s+/g, ' ').trim();
|
7708
8083
|
} else {
|
7709
8084
|
// Standard dataset modifier selectors are much more restricted, but
|
7710
8085
|
// to be user-friendly, special chars are removed automatically
|
@@ -7722,12 +8097,10 @@ class Dataset {
|
|
7722
8097
|
UI.warn(UI.WARNING.INVALID_SELECTOR);
|
7723
8098
|
return null;
|
7724
8099
|
}
|
7725
|
-
// Then prefix it when the IO context argument is defined
|
7726
|
-
if(ioc) s = ioc.actualName(s);
|
7727
8100
|
// Then add a dataset modifier to this dataset
|
7728
8101
|
const id = UI.nameToID(s);
|
7729
8102
|
if(!this.modifiers.hasOwnProperty(id)) {
|
7730
|
-
this.modifiers[id] = new DatasetModifier(this, s
|
8103
|
+
this.modifiers[id] = new DatasetModifier(this, s);
|
7731
8104
|
}
|
7732
8105
|
// Finally, initialize it when the XML node argument is defined
|
7733
8106
|
if(node) this.modifiers[id].initFromXML(node);
|
@@ -7762,7 +8135,8 @@ class Dataset {
|
|
7762
8135
|
const xml = ['<dataset', p, '><name>', xmlEncoded(n),
|
7763
8136
|
'</name><notes>', cmnts,
|
7764
8137
|
'</notes><default>', this.default_value,
|
7765
|
-
'</default><
|
8138
|
+
'</default><unit>', xmlEncoded(this.scale_unit),
|
8139
|
+
'</unit><time-scale>', this.time_scale,
|
7766
8140
|
'</time-scale><time-unit>', this.time_unit,
|
7767
8141
|
'</time-unit><method>', this.method,
|
7768
8142
|
'</method><url>', xmlEncoded(this.url),
|
@@ -7776,10 +8150,11 @@ class Dataset {
|
|
7776
8150
|
initFromXML(node) {
|
7777
8151
|
this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
|
7778
8152
|
this.default_value = safeStrToFloat(nodeContentByTag(node, 'default'));
|
8153
|
+
this.scale_unit = xmlDecoded(nodeContentByTag(node, 'unit')) || '1';
|
7779
8154
|
this.time_scale = safeStrToFloat(nodeContentByTag(node, 'time-scale'), 1);
|
7780
|
-
this.time_unit = nodeContentByTag(node, 'time-unit')
|
7781
|
-
|
7782
|
-
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';
|
7783
8158
|
this.periodic = nodeParameterValue(node, 'periodic') === '1';
|
7784
8159
|
this.array = nodeParameterValue(node, 'array') === '1';
|
7785
8160
|
this.black_box = nodeParameterValue(node, 'black-box') === '1';
|
@@ -9619,8 +9994,13 @@ class Experiment {
|
|
9619
9994
|
this.variables = [];
|
9620
9995
|
this.configuration_dims = 0;
|
9621
9996
|
this.column_scenario_dims = 0;
|
9997
|
+
this.iterator_ranges = [[0,0], [0,0], [0,0]];
|
9998
|
+
this.iterator_dimensions = [];
|
9622
9999
|
this.settings_selectors = [];
|
9623
10000
|
this.settings_dimensions = [];
|
10001
|
+
this.combination_selectors = [];
|
10002
|
+
this.combination_dimensions = [];
|
10003
|
+
this.available_dimensions = [];
|
9624
10004
|
this.actor_selectors = [];
|
9625
10005
|
this.actor_dimensions = [];
|
9626
10006
|
this.excluded_selectors = '';
|
@@ -9680,6 +10060,56 @@ class Experiment {
|
|
9680
10060
|
return this.combinations[this.active_combination_index];
|
9681
10061
|
}
|
9682
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
|
+
|
9683
10113
|
matchingCombinationIndex(sl) {
|
9684
10114
|
// Returns index of combination with most selectors in common wilt `sl`
|
9685
10115
|
let high = 0,
|
@@ -9715,6 +10145,16 @@ class Experiment {
|
|
9715
10145
|
`<sdim>${xmlEncoded(this.settings_dimensions[i].join(','))}</sdim>`;
|
9716
10146
|
if(sd.indexOf(dim) < 0) sd += dim;
|
9717
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
|
+
}
|
9718
10158
|
let as = '';
|
9719
10159
|
for(let i = 0; i < this.actor_selectors.length; i++) {
|
9720
10160
|
as += this.actor_selectors[i].asXML;
|
@@ -9733,6 +10173,7 @@ class Experiment {
|
|
9733
10173
|
return ['<experiment configuration-dims="', this.configuration_dims,
|
9734
10174
|
'" column_scenario-dims="', this.column_scenario_dims,
|
9735
10175
|
(this.completed ? '" completed="1' : ''),
|
10176
|
+
'" iterator-ranges="', this.iteratorRangeString,
|
9736
10177
|
'" started="', this.time_started,
|
9737
10178
|
'" stopped="', this.time_stopped,
|
9738
10179
|
'" variables="', this.download_settings.variables,
|
@@ -9749,7 +10190,9 @@ class Experiment {
|
|
9749
10190
|
'</dimensions><chart-titles>', ct,
|
9750
10191
|
'</chart-titles><settings-selectors>', ss,
|
9751
10192
|
'</settings-selectors><settings-dimensions>', sd,
|
9752
|
-
'</settings-dimensions><
|
10193
|
+
'</settings-dimensions><combination-selectors>', cs,
|
10194
|
+
'</combination-selectors><combination-dimensions>', cd,
|
10195
|
+
'</combination-dimensions><actor-selectors>', as,
|
9753
10196
|
'</actor-selectors><excluded-selectors>',
|
9754
10197
|
xmlEncoded(this.excluded_selectors),
|
9755
10198
|
'</excluded-selectors><clusters-to-ignore>', cti,
|
@@ -9762,6 +10205,7 @@ class Experiment {
|
|
9762
10205
|
nodeParameterValue(node, 'configuration-dims'));
|
9763
10206
|
this.column_scenario_dims = safeStrToInt(
|
9764
10207
|
nodeParameterValue(node, 'column-scenario-dims'));
|
10208
|
+
this.parseIteratorRangeString(nodeParameterValue(node, 'iterator-ranges'));
|
9765
10209
|
this.completed = nodeParameterValue(node, 'completed') === '1';
|
9766
10210
|
this.time_started = safeStrToInt(nodeParameterValue(node, 'started'));
|
9767
10211
|
this.time_stopped = safeStrToInt(nodeParameterValue(node, 'stopped'));
|
@@ -9817,6 +10261,24 @@ class Experiment {
|
|
9817
10261
|
}
|
9818
10262
|
}
|
9819
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
|
+
}
|
9820
10282
|
n = childNodeByTag(node, 'actor-selectors');
|
9821
10283
|
if(n && n.childNodes) {
|
9822
10284
|
for(let i = 0; i < n.childNodes.length; i++) {
|
@@ -9865,7 +10327,9 @@ class Experiment {
|
|
9865
10327
|
// Returns dimension index if any dimension contains any selector in
|
9866
10328
|
// dimension `d`, or -1 otherwise
|
9867
10329
|
for(let i = 0; i < this.dimensions.length; i++) {
|
9868
|
-
|
10330
|
+
const xd = this.dimensions[i].slice();
|
10331
|
+
this.expandCombinationSelectors(xd);
|
10332
|
+
if(intersection(xd, d).length > 0) return i;
|
9869
10333
|
}
|
9870
10334
|
return -1;
|
9871
10335
|
}
|
@@ -9874,7 +10338,7 @@ class Experiment {
|
|
9874
10338
|
// Removes dimension `d` from list and returns its old index
|
9875
10339
|
for(let i = 0; i < this.dimensions.length; i++) {
|
9876
10340
|
if(intersection(this.dimensions[i], d).length > 0) {
|
9877
|
-
this.dimensions.splice(i);
|
10341
|
+
this.dimensions.splice(i, 1);
|
9878
10342
|
return i;
|
9879
10343
|
}
|
9880
10344
|
}
|
@@ -9911,7 +10375,170 @@ class Experiment {
|
|
9911
10375
|
if(adi >= 0) this.dimensions[adi] = d;
|
9912
10376
|
}
|
9913
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
|
+
}
|
9914
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
|
+
|
9915
10542
|
inferActualDimensions() {
|
9916
10543
|
// Creates list of dimensions without excluded selectors
|
9917
10544
|
this.actual_dimensions.length = 0;
|
@@ -9928,6 +10555,9 @@ class Experiment {
|
|
9928
10555
|
if(n >= this.actual_dimensions.length) {
|
9929
10556
|
// NOTE: do not push an empty selector list (can occur if no dimensions)
|
9930
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);
|
9931
10561
|
return;
|
9932
10562
|
}
|
9933
10563
|
const d = this.actual_dimensions[n];
|
@@ -9939,14 +10569,33 @@ class Experiment {
|
|
9939
10569
|
}
|
9940
10570
|
}
|
9941
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
|
+
|
9942
10591
|
mayBeIgnored(c) {
|
9943
|
-
// Returns TRUE iff `c` is on the list to be ignored
|
10592
|
+
// Returns TRUE iff cluster `c` is on the list to be ignored
|
9944
10593
|
for(let i = 0; i < this.clusters_to_ignore.length; i++) {
|
9945
10594
|
if(this.clusters_to_ignore[i].cluster === c) return true;
|
9946
10595
|
}
|
9947
10596
|
return false;
|
9948
10597
|
}
|
9949
|
-
|
10598
|
+
|
9950
10599
|
inferVariables() {
|
9951
10600
|
// Create list of distinct variables in charts
|
9952
10601
|
this.variables.length = 0;
|