linny-r 1.1.22 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +201 -16
- package/static/linny-r.css +214 -33
- package/static/scripts/linny-r-config.js +6 -0
- package/static/scripts/linny-r-ctrl.js +28 -8
- package/static/scripts/linny-r-gui.js +709 -113
- package/static/scripts/linny-r-model.js +943 -273
- package/static/scripts/linny-r-utils.js +5 -0
- package/static/scripts/linny-r-vm.js +324 -93
@@ -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);
|
2134
|
+
// Iterate over all expressions
|
2135
|
+
const ax = this.allExpressions;
|
2136
|
+
for(let i = 0; i < ax.length; i++) {
|
2137
|
+
n += ax[i].replaceAttribute(re, at, a);
|
1932
2138
|
}
|
1933
|
-
//
|
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);
|
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)) {
|
@@ -2485,12 +2683,15 @@ class LinnyRModel {
|
|
2485
2683
|
const ds_dict = {};
|
2486
2684
|
for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
|
2487
2685
|
const ds = this.datasets[k];
|
2488
|
-
|
2489
|
-
|
2490
|
-
|
2491
|
-
|
2492
|
-
|
2493
|
-
|
2686
|
+
// NOTE: ignore selectors of the equations dataset
|
2687
|
+
if(ds !== this.equations_dataset) {
|
2688
|
+
for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
|
2689
|
+
const s = ds.modifiers[m].selector;
|
2690
|
+
if(s in ds_dict) {
|
2691
|
+
ds_dict[s].push(ds);
|
2692
|
+
} else {
|
2693
|
+
ds_dict[s] = [ds];
|
2694
|
+
}
|
2494
2695
|
}
|
2495
2696
|
}
|
2496
2697
|
}
|
@@ -2586,6 +2787,10 @@ class LinnyRModel {
|
|
2586
2787
|
this.cleanVector(p.cash_flow, 0, 0);
|
2587
2788
|
this.cleanVector(p.cash_in, 0, 0);
|
2588
2789
|
this.cleanVector(p.cash_out, 0, 0);
|
2790
|
+
// NOTE: note fields also must be reset
|
2791
|
+
for(let i = 0; i < p.notes.length; i++) {
|
2792
|
+
p.notes[i].parsed = false;
|
2793
|
+
}
|
2589
2794
|
}
|
2590
2795
|
for(obj in this.processes) if(this.processes.hasOwnProperty(obj)) {
|
2591
2796
|
p = this.processes[obj];
|
@@ -2676,30 +2881,9 @@ class LinnyRModel {
|
|
2676
2881
|
|
2677
2882
|
compileExpressions() {
|
2678
2883
|
// Compile all expression attributes of all model entities
|
2679
|
-
|
2680
|
-
|
2681
|
-
|
2682
|
-
// property is set before compiling the other expressions
|
2683
|
-
for(obj in this.datasets) if(this.datasets.hasOwnProperty(obj)) {
|
2684
|
-
this.datasets[obj].compileExpressions();
|
2685
|
-
}
|
2686
|
-
for(obj in this.actors) if(this.actors.hasOwnProperty(obj)) {
|
2687
|
-
this.actors[obj].weight.compile();
|
2688
|
-
}
|
2689
|
-
for(obj in this.processes) if(this.processes.hasOwnProperty(obj)) {
|
2690
|
-
p = this.processes[obj];
|
2691
|
-
p.lower_bound.compile();
|
2692
|
-
p.upper_bound.compile();
|
2693
|
-
}
|
2694
|
-
for(obj in this.products) if(this.products.hasOwnProperty(obj)) {
|
2695
|
-
p = this.products[obj];
|
2696
|
-
p.lower_bound.compile();
|
2697
|
-
p.upper_bound.compile();
|
2698
|
-
p.price.compile();
|
2699
|
-
}
|
2700
|
-
for(obj in this.links) if(this.links.hasOwnProperty(obj)) {
|
2701
|
-
this.links[obj].relative_rate.compile();
|
2702
|
-
this.links[obj].flow_delay.compile();
|
2884
|
+
const ax = this.allExpressions;
|
2885
|
+
for(let i = 0; i < ax.length; i++) {
|
2886
|
+
ax[i].compile();
|
2703
2887
|
}
|
2704
2888
|
}
|
2705
2889
|
|
@@ -3341,17 +3525,6 @@ class LinnyRModel {
|
|
3341
3525
|
// Start with the Linny-R model properties
|
3342
3526
|
let diff = differences(this, m, Object.keys(UI.MC.SETTINGS_PROPS));
|
3343
3527
|
if(Object.keys(diff).length > 0) d.settings = diff;
|
3344
|
-
// Then check for differences in scale unit lists
|
3345
|
-
diff = {};
|
3346
|
-
for(let i = 0; i < this.scale_units.length; i++) {
|
3347
|
-
const su = this.scale_units[i];
|
3348
|
-
if(m.scale_units.indexOf(su) < 0) diff[su] = [UI.MC.ADDED, su];
|
3349
|
-
}
|
3350
|
-
for(let i = 0; i < m.scale_units.length; i++) {
|
3351
|
-
const su = m.scale_units[i];
|
3352
|
-
if(this.scale_units.indexOf(su) < 0) diff[su] = [UI.MC.DELETED, su];
|
3353
|
-
}
|
3354
|
-
if(Object.keys(diff).length > 0) d.units = diff;
|
3355
3528
|
// NOTE: dataset differences will also detect equation differences
|
3356
3529
|
for(let i = 0; i < UI.MC.ENTITY_PROPS.length; i++) {
|
3357
3530
|
const ep = UI.MC.ENTITY_PROPS[i];
|
@@ -3688,8 +3861,8 @@ class IOContext {
|
|
3688
3861
|
}
|
3689
3862
|
|
3690
3863
|
bind(fn, an) {
|
3691
|
-
// Binds the formal name `
|
3692
|
-
// `an` it will have in the current model
|
3864
|
+
// Binds the formal name `fn` of an entity in a module to the actual
|
3865
|
+
// name `an` it will have in the current model
|
3693
3866
|
const id = UI.nameToID(fn);
|
3694
3867
|
if(this.bindings.hasOwnProperty(id)) {
|
3695
3868
|
this.bindings[id].bind(an);
|
@@ -3709,7 +3882,6 @@ class IOContext {
|
|
3709
3882
|
// (and for processes and clusters: with actor name `an` if specified and
|
3710
3883
|
// not "(no actor)")
|
3711
3884
|
// NOTE: do not modify (no actor), nor the "dataset dot"
|
3712
|
-
// @@TO DO: correctly handle equations!
|
3713
3885
|
if(n === UI.NO_ACTOR || n === '.') return n;
|
3714
3886
|
// NOTE: the top cluster of the included model has the prefix as its name
|
3715
3887
|
if(n === UI.TOP_CLUSTER_NAME || n === UI.FORMER_TOP_CLUSTER_NAME) {
|
@@ -3844,7 +4016,7 @@ class IOContext {
|
|
3844
4016
|
a,
|
3845
4017
|
stat;
|
3846
4018
|
while(true) {
|
3847
|
-
p = x.text.indexOf('[',
|
4019
|
+
p = x.text.indexOf('[', q + 1);
|
3848
4020
|
if(p < 0) {
|
3849
4021
|
// No more '[' => add remaining part of text, and quit
|
3850
4022
|
s += x.text.slice(q + 1);
|
@@ -3949,6 +4121,79 @@ class IOContext {
|
|
3949
4121
|
} // END of class IOContext
|
3950
4122
|
|
3951
4123
|
|
4124
|
+
// CLASS ScaleUnit
|
4125
|
+
class ScaleUnit {
|
4126
|
+
constructor(name, scalar, base_unit) {
|
4127
|
+
this.name = name;
|
4128
|
+
// NOTES:
|
4129
|
+
// (1) Undefined or empty strings default to '1'
|
4130
|
+
// (2) Multiplier is stored as string to preserve modeler's notation
|
4131
|
+
this.scalar = scalar || '1';
|
4132
|
+
this.base_unit = base_unit || '1';
|
4133
|
+
}
|
4134
|
+
|
4135
|
+
get multiplier() {
|
4136
|
+
// Returns scalar as number
|
4137
|
+
return safeStrToFloat(this.scalar, 1);
|
4138
|
+
}
|
4139
|
+
|
4140
|
+
conversionRates() {
|
4141
|
+
// Returns a "dictionary" {U1: R1, U2: R2, ...} such that Ui is a
|
4142
|
+
// scale unit that can be converted to *this* scaleunit U at rate Ri
|
4143
|
+
const cr = {};
|
4144
|
+
let p = 0, // previous count of entries
|
4145
|
+
n = 1;
|
4146
|
+
// At least one conversion: U -> U with rate 1
|
4147
|
+
cr[this.name] = 1;
|
4148
|
+
if(this.base_unit !== '1') {
|
4149
|
+
// Second conversion: U -> base of U with modeler-defined rate
|
4150
|
+
cr[this.base_unit] = this.multiplier;
|
4151
|
+
n++;
|
4152
|
+
}
|
4153
|
+
// Keep track of the number of keys; terminate as no new keys
|
4154
|
+
while(p < n) {
|
4155
|
+
p = n;
|
4156
|
+
// Iterate over all convertible scale units discovered so far
|
4157
|
+
for(let u in cr) if(cr.hasOwnProperty(u)) {
|
4158
|
+
// Look for conversions to units NOT yet detected
|
4159
|
+
for(let k in MODEL.scale_units) if(k != '1' &&
|
4160
|
+
MODEL.scale_units.hasOwnProperty(k)) {
|
4161
|
+
const
|
4162
|
+
su = MODEL.scale_units[k],
|
4163
|
+
b = su.base_unit;
|
4164
|
+
if(b === '1') continue;
|
4165
|
+
if(!cr.hasOwnProperty(k) && cr.hasOwnProperty(b)) {
|
4166
|
+
// Add unit if new while base unit is convertible
|
4167
|
+
cr[k] = cr[b] / su.multiplier;
|
4168
|
+
n++;
|
4169
|
+
} else if(cr.hasOwnProperty(k) && !cr.hasOwnProperty(b)) {
|
4170
|
+
// Likewise, add base unit if new while unit is convertible
|
4171
|
+
cr[b] = cr[k] * su.multiplier;
|
4172
|
+
n++;
|
4173
|
+
}
|
4174
|
+
}
|
4175
|
+
}
|
4176
|
+
}
|
4177
|
+
return cr;
|
4178
|
+
}
|
4179
|
+
|
4180
|
+
get asXML() {
|
4181
|
+
return ['<scaleunit><name>', xmlEncoded(this.name),
|
4182
|
+
'</name><scalar>', this.scalar,
|
4183
|
+
'</scalar><base-unit>', xmlEncoded(this.base_unit),
|
4184
|
+
'</base-unit></scaleunit>'].join('');
|
4185
|
+
}
|
4186
|
+
|
4187
|
+
// NOTE: NO initFromXML because scale units are added directly
|
4188
|
+
|
4189
|
+
differences(u) {
|
4190
|
+
// Return "dictionary" of differences, or NULL if none
|
4191
|
+
const d = differences(this, u, UI.MC.UNIT_PROPS);
|
4192
|
+
if(Object.keys(d).length > 0) return d;
|
4193
|
+
return null;
|
4194
|
+
}
|
4195
|
+
}
|
4196
|
+
|
3952
4197
|
// CLASS Actor
|
3953
4198
|
class Actor {
|
3954
4199
|
constructor(name) {
|
@@ -4104,35 +4349,45 @@ class ObjectWithXYWH {
|
|
4104
4349
|
|
4105
4350
|
// CLASS NoteField: numeric value of "field" [[dataset]] in note text
|
4106
4351
|
class NoteField {
|
4107
|
-
constructor(f, o) {
|
4108
|
-
// `f` holds the unmodified tag string [[dataset]] to be replaced by
|
4109
|
-
// value of vector or expression `o` for the current time step
|
4352
|
+
constructor(f, o, u='1', m=1) {
|
4353
|
+
// `f` holds the unmodified tag string [[dataset]] to be replaced by
|
4354
|
+
// the value of vector or expression `o` for the current time step;
|
4355
|
+
// if specified, `u` is the unit of the value to be displayed, and
|
4356
|
+
// `m` is the multiplier for the value to be displayed
|
4110
4357
|
this.field = f;
|
4111
4358
|
this.object = o;
|
4359
|
+
this.unit = u;
|
4360
|
+
this.multiplier = m;
|
4112
4361
|
}
|
4113
4362
|
|
4114
4363
|
get value() {
|
4115
|
-
// Returns the numeric value of this note field
|
4364
|
+
// Returns the numeric value of this note field as a numeric string
|
4365
|
+
// followed by its unit (unless this is 1)
|
4366
|
+
let v = VM.UNDEFINED;
|
4116
4367
|
const t = MODEL.t;
|
4117
4368
|
if(Array.isArray(this.object)) {
|
4118
4369
|
// Object is a vector
|
4119
|
-
if(t < this.object.length)
|
4120
|
-
|
4121
|
-
|
4122
|
-
return VM.UNDEFINED;
|
4123
|
-
}
|
4124
|
-
} else if(this.object.hasOwnProperty('c') && this.object.hasOwnProperty('u')) {
|
4370
|
+
if(t < this.object.length) v = this.object[t];
|
4371
|
+
} else if(this.object.hasOwnProperty('c') &&
|
4372
|
+
this.object.hasOwnProperty('u')) {
|
4125
4373
|
// Object holds link lists for cluster balance computation
|
4126
|
-
|
4374
|
+
v = MODEL.flowBalance(this.object, t);
|
4127
4375
|
} else if(this.object instanceof Expression) {
|
4128
4376
|
// Object is an expression
|
4129
|
-
|
4377
|
+
v = this.object.result(t);
|
4130
4378
|
} else if(typeof this.object === 'number') {
|
4131
|
-
|
4379
|
+
v = this.object;
|
4380
|
+
} else {
|
4381
|
+
// NOTE: this fall-through should not occur
|
4382
|
+
console.log('Note field value issue:', this.object);
|
4132
4383
|
}
|
4133
|
-
|
4134
|
-
|
4135
|
-
|
4384
|
+
if(Math.abs(this.multiplier - 1) > VM.NEAR_ZERO &&
|
4385
|
+
v > VM.MINUS_INFINITY && v < VM.PLUS_INFINITY) {
|
4386
|
+
v *= this.multiplier;
|
4387
|
+
}
|
4388
|
+
v = VM.sig4Dig(v);
|
4389
|
+
if(this.unit !== '1') v += ' ' + this.unit;
|
4390
|
+
return v;
|
4136
4391
|
}
|
4137
4392
|
|
4138
4393
|
} // END of class NoteField
|
@@ -4195,6 +4450,13 @@ class Note extends ObjectWithXYWH {
|
|
4195
4450
|
this.width = safeStrToInt(nodeContentByTag(node, 'width'));
|
4196
4451
|
this.height = safeStrToInt(nodeContentByTag(node, 'height'));
|
4197
4452
|
this.color.text = xmlDecoded(nodeContentByTag(node, 'color'));
|
4453
|
+
if(IO_CONTEXT) {
|
4454
|
+
const fel = this.fieldEntities;
|
4455
|
+
for(let i = 0; i < fel.length; i++) {
|
4456
|
+
this.rewriteTags(fel[i], IO_CONTEXT.actualName(fel[i]));
|
4457
|
+
}
|
4458
|
+
IO_CONTEXT.rewrite(this.color);
|
4459
|
+
}
|
4198
4460
|
}
|
4199
4461
|
|
4200
4462
|
setCluster(c) {
|
@@ -4220,24 +4482,93 @@ class Note extends ObjectWithXYWH {
|
|
4220
4482
|
for(let i = 0; i < tags.length; i++) {
|
4221
4483
|
const
|
4222
4484
|
tag = tags[i],
|
4223
|
-
|
4485
|
+
inner = tag.slice(2, tag.length - 2).trim(),
|
4486
|
+
bar = inner.lastIndexOf('|'),
|
4487
|
+
arrow = inner.lastIndexOf('->');
|
4488
|
+
// Check if a unit conversion scalar was specified
|
4489
|
+
let ena,
|
4490
|
+
from_unit = '1',
|
4491
|
+
to_unit = '',
|
4492
|
+
multiplier = 1;
|
4493
|
+
if(arrow > bar) {
|
4494
|
+
// Now for sure it is entity->unit or entity|attr->unit
|
4495
|
+
ena = inner.split('->');
|
4496
|
+
// As example, assume that unit = 'kWh' (so the value of the
|
4497
|
+
// field should be displayed in kilowatthour)
|
4498
|
+
// NOTE: use .trim() instead of UI.cleanName(...) here;
|
4499
|
+
// this forces the modeler to be exact, and that permits proper
|
4500
|
+
// renaming of scale units in note fields
|
4501
|
+
to_unit = ena[1].trim();
|
4502
|
+
ena = ena[0].split('|');
|
4503
|
+
if(!MODEL.scale_units.hasOwnProperty(to_unit)) {
|
4504
|
+
UI.warn(`Unknown scale unit "${to_unit}"`);
|
4505
|
+
to_unit = '1';
|
4506
|
+
}
|
4507
|
+
} else {
|
4508
|
+
ena = inner.split('|');
|
4509
|
+
}
|
4224
4510
|
// Look up entity for name and attribute
|
4225
|
-
const obj = MODEL.objectByName(ena[0]);
|
4511
|
+
const obj = MODEL.objectByName(ena[0].trim());
|
4226
4512
|
if(obj instanceof DatasetModifier) {
|
4227
|
-
|
4513
|
+
// NOTE: equations are (for now) dimensionless => unit '1'
|
4514
|
+
if(obj.dataset !== MODEL.equations_dataset) {
|
4515
|
+
from_unit = obj.dataset.scale_unit;
|
4516
|
+
multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
|
4517
|
+
}
|
4518
|
+
this.fields.push(new NoteField(tag, obj.expression, to_unit, multiplier));
|
4228
4519
|
} else if(obj) {
|
4229
4520
|
// If attribute omitted, use default attribute of entity type
|
4230
4521
|
const attr = (ena.length > 1 ? ena[1].trim() : obj.defaultAttribute);
|
4231
|
-
|
4232
|
-
|
4522
|
+
let val = null;
|
4523
|
+
// NOTE: for datasets, use the active modifier
|
4524
|
+
if(!attr && obj instanceof Dataset) {
|
4525
|
+
val = obj.activeModifierExpression;
|
4526
|
+
} else {
|
4527
|
+
// Variable may specify a vector-type attribute
|
4528
|
+
val = obj.attributeValue(attr);
|
4529
|
+
}
|
4233
4530
|
// If not, it may be a cluster unit balance
|
4234
4531
|
if(!val && attr.startsWith('=') && obj instanceof Cluster) {
|
4235
4532
|
val = {c: obj, u: attr.substring(1).trim()};
|
4533
|
+
from_unit = val.u;
|
4534
|
+
}
|
4535
|
+
if(obj instanceof Dataset) {
|
4536
|
+
from_unit = obj.scale_unit;
|
4537
|
+
} else if(obj instanceof Product) {
|
4538
|
+
if(attr === 'L') {
|
4539
|
+
from_unit = obj.scale_unit;
|
4540
|
+
} else if(attr === 'CP' || attr === 'HCP') {
|
4541
|
+
from_unit = MODEL.currency_unit;
|
4542
|
+
}
|
4543
|
+
} else if(obj instanceof Link) {
|
4544
|
+
const node = (obj.from_node instanceof Process ?
|
4545
|
+
obj.to_node : obj.from_node);
|
4546
|
+
if(attr === 'F') {
|
4547
|
+
if(obj.multiplier <= VM.LM_MEAN) {
|
4548
|
+
from_unit = node.scale_unit;
|
4549
|
+
} else {
|
4550
|
+
from_unit = '1';
|
4551
|
+
}
|
4552
|
+
}
|
4553
|
+
} else if(attr === 'CI' || attr === 'CO' || attr === 'CF') {
|
4554
|
+
from_unit = MODEL.currency_unit;
|
4236
4555
|
}
|
4237
4556
|
// If not, it may be an expression-type attribute
|
4238
|
-
if(!val)
|
4557
|
+
if(!val) {
|
4558
|
+
val = obj.attributeExpression(attr);
|
4559
|
+
if(obj instanceof Product) {
|
4560
|
+
if(attr === 'IL' || attr === 'LB' || attr === 'UB') {
|
4561
|
+
from_unit = obj.scale_unit;
|
4562
|
+
} else if(attr === 'P') {
|
4563
|
+
from_unit = MODEL.currency_unit + '/' + obj.scale_unit;
|
4564
|
+
}
|
4565
|
+
}
|
4566
|
+
}
|
4567
|
+
// If no TO unit, add the FROM unit
|
4568
|
+
if(to_unit === '') to_unit = from_unit;
|
4239
4569
|
if(val) {
|
4240
|
-
|
4570
|
+
multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
|
4571
|
+
this.fields.push(new NoteField(tag, val, to_unit, multiplier));
|
4241
4572
|
} else {
|
4242
4573
|
UI.warn(`Unknown ${obj.type.toLowerCase()} attribute "${attr}"`);
|
4243
4574
|
}
|
@@ -4248,10 +4579,48 @@ class Note extends ObjectWithXYWH {
|
|
4248
4579
|
}
|
4249
4580
|
this.parsed = true;
|
4250
4581
|
}
|
4582
|
+
|
4583
|
+
get fieldEntities() {
|
4584
|
+
// Return a list with names of entities used in fields
|
4585
|
+
const
|
4586
|
+
fel = [],
|
4587
|
+
tags = this.contents.match(/\[\[[^\]]+\]\]/g);
|
4588
|
+
for(let i = 0; i < tags.length; i++) {
|
4589
|
+
const
|
4590
|
+
tag = tags[i],
|
4591
|
+
inner = tag.slice(2, tag.length - 2).trim(),
|
4592
|
+
vb = inner.lastIndexOf('|'),
|
4593
|
+
ua = inner.lastIndexOf('->');
|
4594
|
+
if(vb >= 0) {
|
4595
|
+
addDistinct(inner.slice(0, vb), fel);
|
4596
|
+
} else if(ua >= 0 &&
|
4597
|
+
MODEL.scale_units.hasOwnProperty(inner.slice(ua + 2))) {
|
4598
|
+
addDistinct(inner.slice(0, ua), fel);
|
4599
|
+
} else {
|
4600
|
+
addDistinct(inner, fel);
|
4601
|
+
}
|
4602
|
+
}
|
4603
|
+
return fel;
|
4604
|
+
}
|
4605
|
+
|
4606
|
+
rewriteTags(en1, en2) {
|
4607
|
+
// Rewrite tags that reference entity name `en1` to reference `en2` instead
|
4608
|
+
if(en1 === en2) return;
|
4609
|
+
const
|
4610
|
+
raw = en1.split(/\s+/).join('\\\\s+'),
|
4611
|
+
re = new RegExp('\\[\\[\\s*' + raw + '\\s*(\\->|\\||\\])', 'g'),
|
4612
|
+
tags = this.contents.match(re);
|
4613
|
+
if(tags) {
|
4614
|
+
for(let i = 0; i < tags.length; i++) {
|
4615
|
+
this.contents = this.contents.replace(tags[i], tags[i].replace(en1, en2));
|
4616
|
+
}
|
4617
|
+
}
|
4618
|
+
}
|
4251
4619
|
|
4252
4620
|
rewriteFields(en1, en2) {
|
4253
4621
|
// Rename fields that reference entity name `en1` to reference `en2` instead
|
4254
4622
|
// NOTE: this does not affect the expression code
|
4623
|
+
if(en1 === en2) return;
|
4255
4624
|
for(let i = 0; i < this.fields.length; i++) {
|
4256
4625
|
const
|
4257
4626
|
f = this.fields[i],
|
@@ -4260,12 +4629,17 @@ class Note extends ObjectWithXYWH {
|
|
4260
4629
|
// Separate tag into variable and attribute + offset string (if any)
|
4261
4630
|
let e = tag,
|
4262
4631
|
a = '',
|
4263
|
-
vb = tag.lastIndexOf('|')
|
4632
|
+
vb = tag.lastIndexOf('|'),
|
4633
|
+
ua = tag.lastIndexOf('->');
|
4264
4634
|
if(vb >= 0) {
|
4265
4635
|
e = tag.slice(0, vb);
|
4266
4636
|
// NOTE: attribute string includes the vertical bar '|'
|
4267
4637
|
a = tag.slice(vb);
|
4268
|
-
}
|
4638
|
+
} else if(ua >= 0 && MODEL.scale_units.hasOwnProperty(tag.slice(ua + 2))) {
|
4639
|
+
e = tag.slice(0, ua);
|
4640
|
+
// NOTE: attribute string includes the unit conversion arrow '->'
|
4641
|
+
a = tag.slice(ua);
|
4642
|
+
}
|
4269
4643
|
// Check for match
|
4270
4644
|
const r = UI.replaceEntity(e, en1, en2);
|
4271
4645
|
if(r) {
|
@@ -4281,7 +4655,7 @@ class Note extends ObjectWithXYWH {
|
|
4281
4655
|
let txt = this.contents;
|
4282
4656
|
for(let i = 0; i < this.fields.length; i++) {
|
4283
4657
|
const nf = this.fields[i];
|
4284
|
-
txt = txt.replace(nf.field,
|
4658
|
+
txt = txt.replace(nf.field, nf.value);
|
4285
4659
|
}
|
4286
4660
|
return txt;
|
4287
4661
|
}
|
@@ -4438,7 +4812,7 @@ class NodeBox extends ObjectWithXYWH {
|
|
4438
4812
|
delete MODEL.products[old_id];
|
4439
4813
|
} else if(this instanceof Cluster) {
|
4440
4814
|
MODEL.clusters[new_id] = this;
|
4441
|
-
delete MODEL.
|
4815
|
+
delete MODEL.clusters[old_id];
|
4442
4816
|
} else {
|
4443
4817
|
// NOTE: this should never happen => report an error
|
4444
4818
|
UI.alert('Can only rename processes, products and clusters');
|
@@ -4703,7 +5077,7 @@ class Arrow {
|
|
4703
5077
|
} else {
|
4704
5078
|
if(p[0] && p[1]) {
|
4705
5079
|
console.log('ERROR: Two distinct flows on monodirectional arrow',
|
4706
|
-
this, sum);
|
5080
|
+
this, sum, p);
|
4707
5081
|
return [0, 0, 0, false, false];
|
4708
5082
|
}
|
4709
5083
|
status = 1;
|
@@ -6319,6 +6693,8 @@ class Node extends NodeBox {
|
|
6319
6693
|
ds = MODEL.addDataset(dsn);
|
6320
6694
|
// Use the LB attribute as default value for the dataset
|
6321
6695
|
ds.default_value = parseFloat(this.lower_bound.text);
|
6696
|
+
// UB data has same unit as product
|
6697
|
+
ds.scale_unit = this.scale_unit;
|
6322
6698
|
ds.data = stringToFloatArray(lb_data);
|
6323
6699
|
ds.computeVector();
|
6324
6700
|
ds.computeStatistics();
|
@@ -6331,6 +6707,8 @@ class Node extends NodeBox {
|
|
6331
6707
|
dsn = this.displayName + ' UPPER BOUND DATA',
|
6332
6708
|
ds = MODEL.addDataset(dsn);
|
6333
6709
|
ds.default_value = parseFloat(this.upper_bound.text);
|
6710
|
+
// UB data has same unit as product
|
6711
|
+
ds.scale_unit = this.scale_unit;
|
6334
6712
|
ds.data = stringToFloatArray(ub_data);
|
6335
6713
|
ds.computeVector();
|
6336
6714
|
ds.computeStatistics();
|
@@ -6958,6 +7336,8 @@ class Product extends Node {
|
|
6958
7336
|
ds = MODEL.addDataset(dsn);
|
6959
7337
|
// Use the price attribute as default value for the dataset
|
6960
7338
|
ds.default_value = parseFloat(this.price.text);
|
7339
|
+
// NOTE: dataset unit then is a currency
|
7340
|
+
ds.scale_unit = MODEL.currency_unit;
|
6961
7341
|
ds.data = stringToFloatArray(data);
|
6962
7342
|
ds.computeVector();
|
6963
7343
|
ds.computeStatistics();
|
@@ -7340,12 +7720,10 @@ class Link {
|
|
7340
7720
|
|
7341
7721
|
// CLASS DatasetModifier
|
7342
7722
|
class DatasetModifier {
|
7343
|
-
constructor(dataset, selector
|
7723
|
+
constructor(dataset, selector) {
|
7344
7724
|
this.dataset = dataset;
|
7345
7725
|
this.selector = selector;
|
7346
7726
|
this.expression = new Expression(dataset, selector, '');
|
7347
|
-
// Equations may have parameters
|
7348
|
-
this.parameters = params;
|
7349
7727
|
this.expression_cache = {};
|
7350
7728
|
}
|
7351
7729
|
|
@@ -7376,17 +7754,12 @@ class DatasetModifier {
|
|
7376
7754
|
// NOTE: for some reason, selector may become empty string, so prevent
|
7377
7755
|
// saving such unidentified modifiers
|
7378
7756
|
if(this.selector.trim().length === 0) return '';
|
7379
|
-
|
7380
|
-
this.parameters.join('\\').trim() : '');
|
7381
|
-
if(pstr) pstr = ' parameters="' + xmlEncoded(pstr) + '"';
|
7382
|
-
return ['<modifier', pstr, '><selector>', xmlEncoded(this.selector),
|
7757
|
+
return ['<modifier><selector>', xmlEncoded(this.selector),
|
7383
7758
|
'</selector><expression>', xmlEncoded(this.expression.text),
|
7384
7759
|
'</expression></modifier>'].join('');
|
7385
7760
|
}
|
7386
7761
|
|
7387
7762
|
initFromXML(node) {
|
7388
|
-
const pstr = nodeParameterValue(node, 'parameters').trim();
|
7389
|
-
this.parameters = (pstr ? xmlDecoded(pstr).split('\\'): false);
|
7390
7763
|
this.expression.text = xmlDecoded(nodeContentByTag(node, 'expression'));
|
7391
7764
|
if(IO_CONTEXT) {
|
7392
7765
|
// Contextualize the included expression
|
@@ -7418,6 +7791,7 @@ class Dataset {
|
|
7418
7791
|
this.name = name;
|
7419
7792
|
this.comments = '';
|
7420
7793
|
this.default_value = 0;
|
7794
|
+
this.scale_unit = '1';
|
7421
7795
|
this.time_scale = 1;
|
7422
7796
|
this.time_unit = CONFIGURATION.default_time_unit;
|
7423
7797
|
this.method = 'nearest';
|
@@ -7516,6 +7890,14 @@ class Dataset {
|
|
7516
7890
|
}
|
7517
7891
|
return this.default_value * MODEL.timeStepDuration / this.timeStepDuration;
|
7518
7892
|
}
|
7893
|
+
|
7894
|
+
changeScaleUnit(name) {
|
7895
|
+
let su = MODEL.addScaleUnit(name);
|
7896
|
+
if(su !== this.scale_unit) {
|
7897
|
+
this.scale_unit = su;
|
7898
|
+
MODEL.cleanUpScaleUnits();
|
7899
|
+
}
|
7900
|
+
}
|
7519
7901
|
|
7520
7902
|
matchingModifiers(l) {
|
7521
7903
|
// Returns the list of selectors of this dataset (in order: from most to
|
@@ -7644,53 +8026,53 @@ class Dataset {
|
|
7644
8026
|
attributeExpression(a) {
|
7645
8027
|
// Returns expression for selector `a`, or NULL if no such selector exists
|
7646
8028
|
// NOTE: selectors no longer are case-sensitive
|
7647
|
-
|
7648
|
-
|
7649
|
-
|
8029
|
+
if(a) {
|
8030
|
+
a = UI.nameToID(a);
|
8031
|
+
for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
|
8032
|
+
if(m === a) return this.modifiers[m].expression;
|
8033
|
+
}
|
7650
8034
|
}
|
7651
8035
|
return null;
|
7652
8036
|
}
|
8037
|
+
|
8038
|
+
get activeModifierExpression() {
|
8039
|
+
if(MODEL.running_experiment) {
|
8040
|
+
// If an experiment is running, check if dataset modifiers match the
|
8041
|
+
// combination of selectors for the active run
|
8042
|
+
const mm = this.matchingModifiers(MODEL.running_experiment.activeCombination);
|
8043
|
+
// If so, use the first match
|
8044
|
+
if(mm.length > 0) return mm[0].expression;
|
8045
|
+
}
|
8046
|
+
if(this.default_selector) {
|
8047
|
+
// If no experiment (so "normal" run), use default selector if specified
|
8048
|
+
const dm = this.modifiers[this.default_selector];
|
8049
|
+
if(dm) return dm.expression;
|
8050
|
+
// Exception should never occur, but check anyway and log it
|
8051
|
+
console.log('WARNING: Dataset "' + this.name +
|
8052
|
+
`" has no default selector "${this.default_selector}"`);
|
8053
|
+
}
|
8054
|
+
// Fall-through: return vector instead of expression
|
8055
|
+
return this.vector;
|
8056
|
+
}
|
7653
8057
|
|
7654
8058
|
addModifier(selector, node=null, ioc=null) {
|
7655
|
-
let s = selector
|
7656
|
-
params = false;
|
8059
|
+
let s = selector;
|
7657
8060
|
// Firstly, sanitize the selector
|
7658
8061
|
if(this === MODEL.equations_dataset) {
|
7659
8062
|
// Equation identifiers cannot contain characters that have special
|
7660
8063
|
// meaning in a variable identifier
|
7661
|
-
s = s.replace(/[\*\?\|\[\]\{\}
|
8064
|
+
s = s.replace(/[\*\?\|\[\]\{\}\@\#]/g, '');
|
7662
8065
|
if(s !== selector) {
|
7663
|
-
UI.warn('Equation name cannot contain [, ], {, }, |,
|
8066
|
+
UI.warn('Equation name cannot contain [, ], {, }, |, @, #, * or ?');
|
7664
8067
|
return null;
|
7665
8068
|
}
|
7666
|
-
//
|
7667
|
-
|
7668
|
-
|
7669
|
-
|
7670
|
-
// Store parameter names in lower case
|
7671
|
-
for(let i = 0; i < ss.length; i++) {
|
7672
|
-
ss[i] = ss[i].toLowerCase();
|
7673
|
-
}
|
7674
|
-
params = ss;
|
7675
|
-
}
|
8069
|
+
// Reduce inner spaces to one, and trim outer spaces
|
8070
|
+
s = s.replace(/\s+/g, ' ').trim();
|
8071
|
+
// Then prefix it when the IO context argument is defined
|
8072
|
+
if(ioc) s = ioc.actualName(s);
|
7676
8073
|
// If equation already exists, return its modifier
|
7677
8074
|
const id = UI.nameToID(s);
|
7678
|
-
if(this.modifiers.hasOwnProperty(id))
|
7679
|
-
const exm = this.modifiers[id];
|
7680
|
-
if(params) {
|
7681
|
-
if(!exm.parameters) {
|
7682
|
-
UI.warn(`Existing equation ${exm.displayName} has no parameters`);
|
7683
|
-
} else {
|
7684
|
-
const
|
7685
|
-
newp = params.join('\\'),
|
7686
|
-
oldp = exm.parameters.join('\\');
|
7687
|
-
if(newp !== oldp) {
|
7688
|
-
UI.warn(`Parameter mismatch: expected \\${oldp}, not \\${newp}`);
|
7689
|
-
}
|
7690
|
-
}
|
7691
|
-
}
|
7692
|
-
return exm;
|
7693
|
-
}
|
8075
|
+
if(this.modifiers.hasOwnProperty(id)) return this.modifiers[id];
|
7694
8076
|
// New equation identifier must not equal some entity ID
|
7695
8077
|
const obj = MODEL.objectByName(s);
|
7696
8078
|
if(obj) {
|
@@ -7698,8 +8080,6 @@ class Dataset {
|
|
7698
8080
|
UI.warningEntityExists(obj);
|
7699
8081
|
return null;
|
7700
8082
|
}
|
7701
|
-
// Also reduce inner spaces to one, and trim outer spaces
|
7702
|
-
s = s.replace(/\s+/g, ' ').trim();
|
7703
8083
|
} else {
|
7704
8084
|
// Standard dataset modifier selectors are much more restricted, but
|
7705
8085
|
// to be user-friendly, special chars are removed automatically
|
@@ -7717,12 +8097,10 @@ class Dataset {
|
|
7717
8097
|
UI.warn(UI.WARNING.INVALID_SELECTOR);
|
7718
8098
|
return null;
|
7719
8099
|
}
|
7720
|
-
// Then prefix it when the IO context argument is defined
|
7721
|
-
if(ioc) s = ioc.actualName(s);
|
7722
8100
|
// Then add a dataset modifier to this dataset
|
7723
8101
|
const id = UI.nameToID(s);
|
7724
8102
|
if(!this.modifiers.hasOwnProperty(id)) {
|
7725
|
-
this.modifiers[id] = new DatasetModifier(this, s
|
8103
|
+
this.modifiers[id] = new DatasetModifier(this, s);
|
7726
8104
|
}
|
7727
8105
|
// Finally, initialize it when the XML node argument is defined
|
7728
8106
|
if(node) this.modifiers[id].initFromXML(node);
|
@@ -7757,7 +8135,8 @@ class Dataset {
|
|
7757
8135
|
const xml = ['<dataset', p, '><name>', xmlEncoded(n),
|
7758
8136
|
'</name><notes>', cmnts,
|
7759
8137
|
'</notes><default>', this.default_value,
|
7760
|
-
'</default><
|
8138
|
+
'</default><unit>', xmlEncoded(this.scale_unit),
|
8139
|
+
'</unit><time-scale>', this.time_scale,
|
7761
8140
|
'</time-scale><time-unit>', this.time_unit,
|
7762
8141
|
'</time-unit><method>', this.method,
|
7763
8142
|
'</method><url>', xmlEncoded(this.url),
|
@@ -7771,10 +8150,11 @@ class Dataset {
|
|
7771
8150
|
initFromXML(node) {
|
7772
8151
|
this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
|
7773
8152
|
this.default_value = safeStrToFloat(nodeContentByTag(node, 'default'));
|
8153
|
+
this.scale_unit = xmlDecoded(nodeContentByTag(node, 'unit')) || '1';
|
7774
8154
|
this.time_scale = safeStrToFloat(nodeContentByTag(node, 'time-scale'), 1);
|
7775
|
-
this.time_unit = nodeContentByTag(node, 'time-unit')
|
7776
|
-
|
7777
|
-
this.method = nodeContentByTag(node, 'method');
|
8155
|
+
this.time_unit = nodeContentByTag(node, 'time-unit') ||
|
8156
|
+
CONFIGURATION.default_time_unit;
|
8157
|
+
this.method = nodeContentByTag(node, 'method') || 'nearest';
|
7778
8158
|
this.periodic = nodeParameterValue(node, 'periodic') === '1';
|
7779
8159
|
this.array = nodeParameterValue(node, 'array') === '1';
|
7780
8160
|
this.black_box = nodeParameterValue(node, 'black-box') === '1';
|
@@ -9226,7 +9606,12 @@ class ExperimentRunResult {
|
|
9226
9606
|
obj = MODEL.objectByID(this.object_id),
|
9227
9607
|
dn = obj.displayName;
|
9228
9608
|
// NOTE: for equations dataset, only display the modifier selector
|
9229
|
-
if(obj === MODEL.equations_dataset)
|
9609
|
+
if(obj === MODEL.equations_dataset) {
|
9610
|
+
const m = obj.modifiers[this.attribute.toLowerCase()];
|
9611
|
+
if(m) return m.selector;
|
9612
|
+
console.log('WARNING: Run result of non-existent equation', this.attribute);
|
9613
|
+
return this.attribute;
|
9614
|
+
}
|
9230
9615
|
return (this.attribute ? dn + '|' + this.attribute : dn);
|
9231
9616
|
}
|
9232
9617
|
|
@@ -9609,8 +9994,13 @@ class Experiment {
|
|
9609
9994
|
this.variables = [];
|
9610
9995
|
this.configuration_dims = 0;
|
9611
9996
|
this.column_scenario_dims = 0;
|
9997
|
+
this.iterator_ranges = [[0,0], [0,0], [0,0]];
|
9998
|
+
this.iterator_dimensions = [];
|
9612
9999
|
this.settings_selectors = [];
|
9613
10000
|
this.settings_dimensions = [];
|
10001
|
+
this.combination_selectors = [];
|
10002
|
+
this.combination_dimensions = [];
|
10003
|
+
this.available_dimensions = [];
|
9614
10004
|
this.actor_selectors = [];
|
9615
10005
|
this.actor_dimensions = [];
|
9616
10006
|
this.excluded_selectors = '';
|
@@ -9670,6 +10060,56 @@ class Experiment {
|
|
9670
10060
|
return this.combinations[this.active_combination_index];
|
9671
10061
|
}
|
9672
10062
|
|
10063
|
+
get iteratorRangeString() {
|
10064
|
+
// Returns the iterator ranges as "from,to" pairs separated by |
|
10065
|
+
const ir = [];
|
10066
|
+
for(let i = 0; i < 3; i++) {
|
10067
|
+
ir.push(this.iterator_ranges[i].join(','));
|
10068
|
+
}
|
10069
|
+
return ir.join('|');
|
10070
|
+
}
|
10071
|
+
|
10072
|
+
parseIteratorRangeString(s) {
|
10073
|
+
// Parses `s` as "from,to" pairs, ignoring syntax errors
|
10074
|
+
if(s) {
|
10075
|
+
const ir = s.split('|');
|
10076
|
+
// Add 2 extra substrings to have at least 3
|
10077
|
+
ir.push('', '');
|
10078
|
+
for(let i = 0; i < 3; i++) {
|
10079
|
+
const r = ir[i].split(',');
|
10080
|
+
// Likewise add extra substring to have at least 2
|
10081
|
+
r.push('');
|
10082
|
+
// Parse integers, defaulting to 0
|
10083
|
+
this.iterator_ranges[i] = [safeStrToInt(r[0], 0), safeStrToInt(r[1], 0)];
|
10084
|
+
}
|
10085
|
+
}
|
10086
|
+
}
|
10087
|
+
|
10088
|
+
updateIteratorDimensions() {
|
10089
|
+
// Create iterator selectors for each index variable having a relevant range
|
10090
|
+
this.iterator_dimensions = [];
|
10091
|
+
const il = ['i', 'j', 'k'];
|
10092
|
+
for(let i = 0; i < 3; i++) {
|
10093
|
+
const r = this.iterator_ranges[i];
|
10094
|
+
if(r[0] || r[1]) {
|
10095
|
+
const
|
10096
|
+
sel = [],
|
10097
|
+
k = il[i] + '=';
|
10098
|
+
// NOTE: iterate from FROM to TO limit also when FROM > TO
|
10099
|
+
if(r[0] <= r[1]) {
|
10100
|
+
for(let j = r[0]; j <= r[1]; j++) {
|
10101
|
+
sel.push(k + j);
|
10102
|
+
}
|
10103
|
+
} else {
|
10104
|
+
for(let j = r[0]; j >= r[1]; j--) {
|
10105
|
+
sel.push(k + j);
|
10106
|
+
}
|
10107
|
+
}
|
10108
|
+
this.iterator_dimensions.push(sel);
|
10109
|
+
}
|
10110
|
+
}
|
10111
|
+
}
|
10112
|
+
|
9673
10113
|
matchingCombinationIndex(sl) {
|
9674
10114
|
// Returns index of combination with most selectors in common wilt `sl`
|
9675
10115
|
let high = 0,
|
@@ -9705,6 +10145,16 @@ class Experiment {
|
|
9705
10145
|
`<sdim>${xmlEncoded(this.settings_dimensions[i].join(','))}</sdim>`;
|
9706
10146
|
if(sd.indexOf(dim) < 0) sd += dim;
|
9707
10147
|
}
|
10148
|
+
let cs = '';
|
10149
|
+
for(let i = 0; i < this.combination_selectors.length; i++) {
|
10150
|
+
cs += `<csel>${xmlEncoded(this.combination_selectors[i])}</csel>`;
|
10151
|
+
}
|
10152
|
+
let cd = '';
|
10153
|
+
for(let i = 0; i < this.combination_dimensions.length; i++) {
|
10154
|
+
const dim =
|
10155
|
+
`<cdim>${xmlEncoded(this.combination_dimensions[i].join(','))}</cdim>`;
|
10156
|
+
if(cd.indexOf(dim) < 0) cd += dim;
|
10157
|
+
}
|
9708
10158
|
let as = '';
|
9709
10159
|
for(let i = 0; i < this.actor_selectors.length; i++) {
|
9710
10160
|
as += this.actor_selectors[i].asXML;
|
@@ -9723,6 +10173,7 @@ class Experiment {
|
|
9723
10173
|
return ['<experiment configuration-dims="', this.configuration_dims,
|
9724
10174
|
'" column_scenario-dims="', this.column_scenario_dims,
|
9725
10175
|
(this.completed ? '" completed="1' : ''),
|
10176
|
+
'" iterator-ranges="', this.iteratorRangeString,
|
9726
10177
|
'" started="', this.time_started,
|
9727
10178
|
'" stopped="', this.time_stopped,
|
9728
10179
|
'" variables="', this.download_settings.variables,
|
@@ -9739,7 +10190,9 @@ class Experiment {
|
|
9739
10190
|
'</dimensions><chart-titles>', ct,
|
9740
10191
|
'</chart-titles><settings-selectors>', ss,
|
9741
10192
|
'</settings-selectors><settings-dimensions>', sd,
|
9742
|
-
'</settings-dimensions><
|
10193
|
+
'</settings-dimensions><combination-selectors>', cs,
|
10194
|
+
'</combination-selectors><combination-dimensions>', cd,
|
10195
|
+
'</combination-dimensions><actor-selectors>', as,
|
9743
10196
|
'</actor-selectors><excluded-selectors>',
|
9744
10197
|
xmlEncoded(this.excluded_selectors),
|
9745
10198
|
'</excluded-selectors><clusters-to-ignore>', cti,
|
@@ -9752,6 +10205,7 @@ class Experiment {
|
|
9752
10205
|
nodeParameterValue(node, 'configuration-dims'));
|
9753
10206
|
this.column_scenario_dims = safeStrToInt(
|
9754
10207
|
nodeParameterValue(node, 'column-scenario-dims'));
|
10208
|
+
this.parseIteratorRangeString(nodeParameterValue(node, 'iterator-ranges'));
|
9755
10209
|
this.completed = nodeParameterValue(node, 'completed') === '1';
|
9756
10210
|
this.time_started = safeStrToInt(nodeParameterValue(node, 'started'));
|
9757
10211
|
this.time_stopped = safeStrToInt(nodeParameterValue(node, 'stopped'));
|
@@ -9807,6 +10261,24 @@ class Experiment {
|
|
9807
10261
|
}
|
9808
10262
|
}
|
9809
10263
|
}
|
10264
|
+
n = childNodeByTag(node, 'combination-selectors');
|
10265
|
+
if(n && n.childNodes) {
|
10266
|
+
for(let i = 0; i < n.childNodes.length; i++) {
|
10267
|
+
c = n.childNodes[i];
|
10268
|
+
if(c.nodeName === 'csel') {
|
10269
|
+
this.combination_selectors.push(xmlDecoded(nodeContent(c)));
|
10270
|
+
}
|
10271
|
+
}
|
10272
|
+
}
|
10273
|
+
n = childNodeByTag(node, 'combination-dimensions');
|
10274
|
+
if(n && n.childNodes) {
|
10275
|
+
for(let i = 0; i < n.childNodes.length; i++) {
|
10276
|
+
c = n.childNodes[i];
|
10277
|
+
if(c.nodeName === 'cdim') {
|
10278
|
+
this.combination_dimensions.push(xmlDecoded(nodeContent(c)).split(','));
|
10279
|
+
}
|
10280
|
+
}
|
10281
|
+
}
|
9810
10282
|
n = childNodeByTag(node, 'actor-selectors');
|
9811
10283
|
if(n && n.childNodes) {
|
9812
10284
|
for(let i = 0; i < n.childNodes.length; i++) {
|
@@ -9855,7 +10327,9 @@ class Experiment {
|
|
9855
10327
|
// Returns dimension index if any dimension contains any selector in
|
9856
10328
|
// dimension `d`, or -1 otherwise
|
9857
10329
|
for(let i = 0; i < this.dimensions.length; i++) {
|
9858
|
-
|
10330
|
+
const xd = this.dimensions[i].slice();
|
10331
|
+
this.expandCombinationSelectors(xd);
|
10332
|
+
if(intersection(xd, d).length > 0) return i;
|
9859
10333
|
}
|
9860
10334
|
return -1;
|
9861
10335
|
}
|
@@ -9864,7 +10338,7 @@ class Experiment {
|
|
9864
10338
|
// Removes dimension `d` from list and returns its old index
|
9865
10339
|
for(let i = 0; i < this.dimensions.length; i++) {
|
9866
10340
|
if(intersection(this.dimensions[i], d).length > 0) {
|
9867
|
-
this.dimensions.splice(i);
|
10341
|
+
this.dimensions.splice(i, 1);
|
9868
10342
|
return i;
|
9869
10343
|
}
|
9870
10344
|
}
|
@@ -9901,7 +10375,170 @@ class Experiment {
|
|
9901
10375
|
if(adi >= 0) this.dimensions[adi] = d;
|
9902
10376
|
}
|
9903
10377
|
}
|
10378
|
+
|
10379
|
+
get allDimensionSelectors() {
|
10380
|
+
const sl = Object.keys(MODEL.listOfAllSelectors);
|
10381
|
+
// Add selectors of actor, iterator and settings dimensions
|
10382
|
+
return sl;
|
10383
|
+
}
|
9904
10384
|
|
10385
|
+
orthogonalSelectors(c) {
|
10386
|
+
// Returns TRUE iff the selectors in set `c` all are elements of
|
10387
|
+
// different experiment dimensions
|
10388
|
+
const
|
10389
|
+
// Make a copy of `c` so it can be safely expanded
|
10390
|
+
xc = c.slice(),
|
10391
|
+
// Start with a copy of all model dimensions
|
10392
|
+
dl = MODEL.dimensions.slice(),
|
10393
|
+
issues = [];
|
10394
|
+
// Add dimensions defined for this experiment
|
10395
|
+
for(let i = 0; i < this.settings_dimensions.length; i++) {
|
10396
|
+
dl.push(this.settings_dimensions[i]);
|
10397
|
+
}
|
10398
|
+
for(let i = 0; i < this.actor_dimensions.length; i++) {
|
10399
|
+
dl.push(this.actor_dimensions[i]);
|
10400
|
+
}
|
10401
|
+
// Expand `c` as it may contain combination selectors
|
10402
|
+
this.expandCombinationSelectors(xc);
|
10403
|
+
// Check for all these dimensions that `c` contains known selectors
|
10404
|
+
// and that no two or more selectors occur in the same dimension
|
10405
|
+
let unknown = xc.slice();
|
10406
|
+
for(let i = 0; i < dl.length; i++) {
|
10407
|
+
const idc = intersection(dl[i], xc);
|
10408
|
+
unknown = complement(unknown, idc);
|
10409
|
+
if(idc.length > 1) {
|
10410
|
+
const pair = idc.join(' & ');
|
10411
|
+
if(issues.indexOf(pair) < 0) issues.push(pair);
|
10412
|
+
}
|
10413
|
+
}
|
10414
|
+
if(unknown.length > 0) {
|
10415
|
+
UI.warn('Combination contains ' +
|
10416
|
+
pluralS(unknown.length, 'undefined selector') +
|
10417
|
+
' (' + unknown.join(', ') + ')');
|
10418
|
+
return false;
|
10419
|
+
}
|
10420
|
+
if(issues.length > 0) {
|
10421
|
+
UI.warn('Combination contains multiple selectors from same dimension (' +
|
10422
|
+
issues.join(', ') + ')');
|
10423
|
+
return false;
|
10424
|
+
}
|
10425
|
+
return true;
|
10426
|
+
}
|
10427
|
+
|
10428
|
+
expandCombinationSelectors(cs) {
|
10429
|
+
// Expansion of combination selectors in a selector set `cs` means
|
10430
|
+
// that if, for example, `cs` = (A, C1) where C1 is a combination
|
10431
|
+
// selector defined as C1 = (B, C2) with A and B being "normal"
|
10432
|
+
// selectors, then C1 must be removed from `cs`, while B and the
|
10433
|
+
// expansion of C2 must be appended to `cs`.
|
10434
|
+
// NOTE: the original selectors C1 and C2 must be removed because
|
10435
|
+
// *dimension* selectors cannot be a used as "normal" selectors
|
10436
|
+
// (e.g., for dataset modifiers, actor settings or model setting)
|
10437
|
+
// NOTE: traverse `cs` in reverse order to ensure that deleting and
|
10438
|
+
// appending produce the intended result
|
10439
|
+
for(let i = cs.length - 1; i >= 0; i--) {
|
10440
|
+
const s = cs[i];
|
10441
|
+
// Check whether selector `s` defines a combination
|
10442
|
+
for(let j = 0; j < this.combination_selectors.length; j++) {
|
10443
|
+
const tuple = this.combination_selectors[j].split('|');
|
10444
|
+
if(tuple[0] === s) {
|
10445
|
+
// First remove `s` from the original set...
|
10446
|
+
cs.splice(i, 1);
|
10447
|
+
// Let `xs` be the selector set to replace `s`
|
10448
|
+
const xs = tuple[1].split(' ');
|
10449
|
+
// Recursively expand `xs`, as it may contain combination selectors
|
10450
|
+
this.expandCombinationSelectors(xs);
|
10451
|
+
// ... and append its expansion
|
10452
|
+
cs.push(...xs);
|
10453
|
+
}
|
10454
|
+
}
|
10455
|
+
}
|
10456
|
+
}
|
10457
|
+
|
10458
|
+
orthogonalCombinationDimensions(sl) {
|
10459
|
+
// Returns TRUE iff the expansions of the selectors in set `sl`
|
10460
|
+
// are mutually exclusive
|
10461
|
+
const
|
10462
|
+
xl = {},
|
10463
|
+
issues = {};
|
10464
|
+
for(let i = 0; i < sl.length; i++) {
|
10465
|
+
const s = sl[i];
|
10466
|
+
xl[s] = [s];
|
10467
|
+
this.expandCombinationSelectors(xl[s]);
|
10468
|
+
issues[s] = [];
|
10469
|
+
}
|
10470
|
+
let ok = true;
|
10471
|
+
for(let i = 0; i < sl.length; i++) {
|
10472
|
+
const s1 = sl[i];
|
10473
|
+
for(let j = i + 1; j < sl.length; j++) {
|
10474
|
+
const
|
10475
|
+
s2 = sl[j],
|
10476
|
+
shared = intersection(xl[s1], xl[s2]);
|
10477
|
+
if(shared.length > 0) {
|
10478
|
+
issues[s1].push(`${s2}: ${shared.join(', ')}`);
|
10479
|
+
ok = false;
|
10480
|
+
}
|
10481
|
+
}
|
10482
|
+
}
|
10483
|
+
if(!ok) {
|
10484
|
+
const il = [];
|
10485
|
+
for(let i = 0; i < sl.length; i++) {
|
10486
|
+
const s = sl[i];
|
10487
|
+
if(issues[s].length > 0) {
|
10488
|
+
il.push(`${s} (${issues[s].join('; ')})`);
|
10489
|
+
}
|
10490
|
+
}
|
10491
|
+
UI.warn('Combination dimension is not orthogonal: ' + il.join(', '));
|
10492
|
+
}
|
10493
|
+
return ok;
|
10494
|
+
}
|
10495
|
+
|
10496
|
+
inferAvailableDimensions() {
|
10497
|
+
// Creates list of dimensions that are orthogonal to those already
|
10498
|
+
// selected for this experiment
|
10499
|
+
this.available_dimensions.length = 0;
|
10500
|
+
// For efficiency, do not use hasDimension but expand the dimensions
|
10501
|
+
// that are already selected once, and define a lookup function that
|
10502
|
+
// checks for orthogonality
|
10503
|
+
const
|
10504
|
+
axes = [],
|
10505
|
+
orthogonal = (d) => {
|
10506
|
+
for(let i = 0; i < axes.length; i++) {
|
10507
|
+
if(intersection(axes[i], d).length > 0) return false;
|
10508
|
+
}
|
10509
|
+
return true;
|
10510
|
+
};
|
10511
|
+
for(let i = 0; i < this.dimensions.length; i++) {
|
10512
|
+
axes.push(this.dimensions[i].slice());
|
10513
|
+
this.expandCombinationSelectors(axes[i]);
|
10514
|
+
}
|
10515
|
+
for(let i = 0; i < MODEL.dimensions.length; i++) {
|
10516
|
+
const d = MODEL.dimensions[i];
|
10517
|
+
if(orthogonal(d)) this.available_dimensions.push(d);
|
10518
|
+
}
|
10519
|
+
for(let i = 0; i < this.settings_dimensions.length; i++) {
|
10520
|
+
const d = this.settings_dimensions[i];
|
10521
|
+
if(orthogonal(d)) this.available_dimensions.push(d);
|
10522
|
+
}
|
10523
|
+
for(let i = 0; i < this.iterator_dimensions.length; i++) {
|
10524
|
+
const d = this.iterator_dimensions[i];
|
10525
|
+
if(orthogonal(d)) this.available_dimensions.push(d);
|
10526
|
+
}
|
10527
|
+
for(let i = 0; i < this.actor_dimensions.length; i++) {
|
10528
|
+
const d = this.actor_dimensions[i];
|
10529
|
+
if(orthogonal(d)) this.available_dimensions.push(d);
|
10530
|
+
}
|
10531
|
+
for(let i = 0; i < this.combination_dimensions.length; i++) {
|
10532
|
+
// NOTE: combination dimensions must be expanded before checking...
|
10533
|
+
const
|
10534
|
+
d = this.combination_dimensions[i],
|
10535
|
+
xd = d.slice();
|
10536
|
+
this.expandCombinationSelectors(xd);
|
10537
|
+
// ... but the original combination dimension must be added
|
10538
|
+
if(orthogonal(xd)) this.available_dimensions.push(d);
|
10539
|
+
}
|
10540
|
+
}
|
10541
|
+
|
9905
10542
|
inferActualDimensions() {
|
9906
10543
|
// Creates list of dimensions without excluded selectors
|
9907
10544
|
this.actual_dimensions.length = 0;
|
@@ -9918,6 +10555,9 @@ class Experiment {
|
|
9918
10555
|
if(n >= this.actual_dimensions.length) {
|
9919
10556
|
// NOTE: do not push an empty selector list (can occur if no dimensions)
|
9920
10557
|
if(s.length > 0) this.combinations.push(s);
|
10558
|
+
// NOTE: combinations may include *dimension* selectors
|
10559
|
+
// These then must be "expanded"
|
10560
|
+
this.expandCombinationSelectors(s);
|
9921
10561
|
return;
|
9922
10562
|
}
|
9923
10563
|
const d = this.actual_dimensions[n];
|
@@ -9929,14 +10569,33 @@ class Experiment {
|
|
9929
10569
|
}
|
9930
10570
|
}
|
9931
10571
|
|
10572
|
+
renameSelectorInDimensions(olds, news) {
|
10573
|
+
// Update the combination dimensions that contain `olds`
|
10574
|
+
for(let i = 0; i < this.settings_dimensions.length; i++) {
|
10575
|
+
const si = this.settings_dimensions[i].indexOf(olds);
|
10576
|
+
if(si >= 0) this.settings_dimensions[i][si] = news;
|
10577
|
+
}
|
10578
|
+
for(let i = 0; i < this.combination_selectors.length; i++) {
|
10579
|
+
const
|
10580
|
+
c = this.combination_selectors[i].split('|'),
|
10581
|
+
sl = c[1].split(' '),
|
10582
|
+
si = sl.indexOf(olds);
|
10583
|
+
if(si >= 0) {
|
10584
|
+
sl[si] = news;
|
10585
|
+
c[1] = sl.join(' ');
|
10586
|
+
this.combination_selectors[i] = c.join('|');
|
10587
|
+
}
|
10588
|
+
}
|
10589
|
+
}
|
10590
|
+
|
9932
10591
|
mayBeIgnored(c) {
|
9933
|
-
// Returns TRUE iff `c` is on the list to be ignored
|
10592
|
+
// Returns TRUE iff cluster `c` is on the list to be ignored
|
9934
10593
|
for(let i = 0; i < this.clusters_to_ignore.length; i++) {
|
9935
10594
|
if(this.clusters_to_ignore[i].cluster === c) return true;
|
9936
10595
|
}
|
9937
10596
|
return false;
|
9938
10597
|
}
|
9939
|
-
|
10598
|
+
|
9940
10599
|
inferVariables() {
|
9941
10600
|
// Create list of distinct variables in charts
|
9942
10601
|
this.variables.length = 0;
|
@@ -9994,7 +10653,19 @@ class Experiment {
|
|
9994
10653
|
|
9995
10654
|
get resultsAsCSV() {
|
9996
10655
|
// Return results as specfied by the download settings
|
10656
|
+
// NOTE: no runs => no results => return empty string
|
10657
|
+
if(this.runs.length === 0) return '';
|
9997
10658
|
const
|
10659
|
+
// Local function to convert number to string
|
10660
|
+
numval = (v, p) => {
|
10661
|
+
// Return 0 as single digit
|
10662
|
+
if(Math.abs(v) < VM.NEAR_ZERO) return '0';
|
10663
|
+
// Return empty string for undefined or exceptional values
|
10664
|
+
if(!v || v < VM.MINUS_INFINITY || v > VM.PLUS_INFINITY) return '';
|
10665
|
+
// Return other values as float with specified precision
|
10666
|
+
return v.toPrecision(p);
|
10667
|
+
},
|
10668
|
+
prec = this.download_settings.precision,
|
9998
10669
|
allruns = this.download_settings.runs === 'all',
|
9999
10670
|
sep = (this.download_settings.separator === 'tab' ? '\t' :
|
10000
10671
|
(this.download_settings.separator === 'comma' ? ',' : ';')),
|
@@ -10019,24 +10690,21 @@ class Experiment {
|
|
10019
10690
|
exceptions: `${quo}Exceptions${quo}${sep}`,
|
10020
10691
|
run: []
|
10021
10692
|
};
|
10022
|
-
// Make list of indices of variables to include
|
10023
|
-
if(this.download_settings.variables === 'selected') {
|
10024
|
-
// Only one variable
|
10025
|
-
vars.push(this.resultIndex(this.selected_variable));
|
10026
|
-
} else {
|
10027
|
-
// All variables
|
10028
|
-
for(let i = 0; i < this.variables.length; i++) {
|
10029
|
-
vars.push(i);
|
10030
|
-
}
|
10031
|
-
}
|
10032
|
-
const nvars = vars.length;
|
10033
10693
|
for(let i = 0; i < this.combinations.length; i++) {
|
10034
10694
|
if(i < this.runs.length &&
|
10035
10695
|
(allruns || this.chart_combinations.indexOf(i) >= 0)) {
|
10036
10696
|
data.run.push(i);
|
10037
10697
|
}
|
10038
10698
|
}
|
10039
|
-
let series_length = 0
|
10699
|
+
let series_length = 0,
|
10700
|
+
// By default, assume all variables to be output
|
10701
|
+
start = 0,
|
10702
|
+
stop = this.runs[0].results.length;
|
10703
|
+
if(this.download_settings.variables === 'selected') {
|
10704
|
+
// Only one variable
|
10705
|
+
start = this.resultIndex(this.selected_variable);
|
10706
|
+
stop = start + 1;
|
10707
|
+
}
|
10040
10708
|
for(let i = 0; i < data.run.length; i++) {
|
10041
10709
|
const
|
10042
10710
|
rnr = data.run[i],
|
@@ -10044,31 +10712,35 @@ class Experiment {
|
|
10044
10712
|
data.nr += r.number;
|
10045
10713
|
data.combi += quo + this.combinations[rnr].join('|') + quo;
|
10046
10714
|
// Run duration in seconds
|
10047
|
-
data.rsecs +=
|
10048
|
-
data.ssecs +=
|
10715
|
+
data.rsecs += numval((r.time_recorded - r.time_started) * 0.001, 4);
|
10716
|
+
data.ssecs += numval(r.solver_seconds, 4);
|
10049
10717
|
data.warnings += r.warning_count;
|
10050
|
-
for(let j =
|
10718
|
+
for(let j = start; j < stop; j++) {
|
10051
10719
|
// Add empty cells for run attributes
|
10052
10720
|
data.nr += sep;
|
10053
10721
|
data.combi += sep;
|
10054
10722
|
data.rsecs += sep;
|
10055
10723
|
data.ssecs += sep;
|
10056
10724
|
data.warnings += sep;
|
10057
|
-
const rr = r.results[
|
10058
|
-
|
10059
|
-
|
10060
|
-
|
10061
|
-
|
10062
|
-
|
10063
|
-
|
10064
|
-
|
10065
|
-
|
10066
|
-
|
10067
|
-
|
10068
|
-
|
10069
|
-
|
10070
|
-
|
10071
|
-
|
10725
|
+
const rr = r.results[j];
|
10726
|
+
if(rr) {
|
10727
|
+
data.variable += rr.displayName + sep;
|
10728
|
+
// Series may differ in length; the longest determines the
|
10729
|
+
// number of rows of series data to be added
|
10730
|
+
series_length = Math.max(series_length, rr.vector.length);
|
10731
|
+
if(this.download_settings.statistics) {
|
10732
|
+
data.N += rr.N + sep;
|
10733
|
+
data.sum += numval(rr.sum, prec) + sep;
|
10734
|
+
data.mean += numval(rr.mean, prec) + sep;
|
10735
|
+
data.variance += numval(rr.variance, prec) + sep;
|
10736
|
+
data.minimum += numval(rr.minimum, prec) + sep;
|
10737
|
+
data.maximum += numval(rr.maximum, prec) + sep;
|
10738
|
+
data.NZ += rr.non_zero_tally + sep;
|
10739
|
+
data.last += numval(rr.last, prec) + sep;
|
10740
|
+
data.exceptions += rr.exceptions + sep;
|
10741
|
+
}
|
10742
|
+
} else {
|
10743
|
+
console.log('No run results for ', this.variables[vars[j]].displayName);
|
10072
10744
|
}
|
10073
10745
|
}
|
10074
10746
|
}
|
@@ -10084,20 +10756,18 @@ class Experiment {
|
|
10084
10756
|
}
|
10085
10757
|
if(this.download_settings.series) {
|
10086
10758
|
ds.push('t');
|
10087
|
-
const
|
10088
|
-
prec = this.download_settings.precision,
|
10089
|
-
row = [];
|
10759
|
+
const row = [];
|
10090
10760
|
for(let i = 0; i < series_length; i++) {
|
10091
10761
|
row.length = 0;
|
10092
10762
|
row.push(i);
|
10093
10763
|
for(let j = 0; j < data.run.length; j++) {
|
10094
10764
|
const rnr = data.run[j];
|
10095
|
-
for(let k =
|
10096
|
-
const rr = this.runs[rnr].results[
|
10097
|
-
if(
|
10098
|
-
|
10099
|
-
if(
|
10100
|
-
row.push(
|
10765
|
+
for(let k = start; k < stop; k++) {
|
10766
|
+
const rr = this.runs[rnr].results[k];
|
10767
|
+
if(rr) {
|
10768
|
+
// NOTE: only experiment variables have vector data
|
10769
|
+
if(rr.x_variable && i <= rr.N) {
|
10770
|
+
row.push(numval(rr.vector[i], prec));
|
10101
10771
|
} else {
|
10102
10772
|
row.push('');
|
10103
10773
|
}
|