linny-r 1.4.1 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/static/index.html +1 -5
- package/static/linny-r.css +0 -14
- package/static/scripts/linny-r-ctrl.js +59 -5
- package/static/scripts/linny-r-gui.js +11 -16
- package/static/scripts/linny-r-model.js +124 -41
- package/static/scripts/linny-r-utils.js +25 -9
- package/static/scripts/linny-r-vm.js +30 -14
package/package.json
CHANGED
package/static/index.html
CHANGED
@@ -165,8 +165,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
165
165
|
// Inform user that newer version exists
|
166
166
|
UI.check_update_modal.element('msg').innerHTML = [
|
167
167
|
'<a href="', GITHUB_REPOSITORY,
|
168
|
-
'/
|
169
|
-
info[1].replaceAll('.', ''), '" ',
|
168
|
+
'/releases/tag/v', info[1], '" ',
|
170
169
|
'title="Click to view version release notes" ',
|
171
170
|
'target="_blank">Version <strong>',
|
172
171
|
info[1], '</strong></a> released on ',
|
@@ -1288,9 +1287,6 @@ NOTE: Unit symbols are case-sensitive, so BTU ≠ Btu">
|
|
1288
1287
|
<div id="process-dlg" class="inp-dlg">
|
1289
1288
|
<div class="dlg-title">
|
1290
1289
|
Process properties
|
1291
|
-
<div class="simbtn">
|
1292
|
-
<img id="process-sim-btn" class="btn sim enab" src="images/process.png">
|
1293
|
-
</div>
|
1294
1290
|
<img class="cancel-btn" src="images/cancel.png">
|
1295
1291
|
<img class="ok-btn" src="images/ok.png">
|
1296
1292
|
</div>
|
package/static/linny-r.css
CHANGED
@@ -346,20 +346,6 @@ div.contbtn {
|
|
346
346
|
cursor: pointer;
|
347
347
|
}
|
348
348
|
|
349
|
-
div.simbtn {
|
350
|
-
display: none;
|
351
|
-
margin-left: 5px;
|
352
|
-
}
|
353
|
-
|
354
|
-
img.sim {
|
355
|
-
height: 14px;
|
356
|
-
width: 14px;
|
357
|
-
}
|
358
|
-
|
359
|
-
img.sim.enab:hover {
|
360
|
-
background-color: #9e96e5;
|
361
|
-
}
|
362
|
-
|
363
349
|
input {
|
364
350
|
vertical-align: baseline;
|
365
351
|
}
|
@@ -302,14 +302,20 @@ class Controller {
|
|
302
302
|
(name.startsWith(this.BLACK_BOX) || name[0].match(/[\w]/));
|
303
303
|
}
|
304
304
|
|
305
|
-
prefixesAndName(name) {
|
305
|
+
prefixesAndName(name, key=false) {
|
306
306
|
// Returns name split exclusively at '[non-space]: [non-space]'
|
307
|
+
let sep = this.PREFIXER,
|
308
|
+
space = ' ';
|
309
|
+
if(key) {
|
310
|
+
sep = ':_';
|
311
|
+
space = '_';
|
312
|
+
}
|
307
313
|
const
|
308
|
-
s = name.split(
|
314
|
+
s = name.split(sep),
|
309
315
|
pan = [s[0]];
|
310
316
|
for(let i = 1; i < s.length; i++) {
|
311
317
|
const j = pan.length - 1;
|
312
|
-
if(s[i].startsWith(
|
318
|
+
if(s[i].startsWith(space) || (i > 0 && pan[j].endsWith(space))) {
|
313
319
|
pan[j] += s[i];
|
314
320
|
} else {
|
315
321
|
pan.push(s[i]);
|
@@ -350,14 +356,20 @@ class Controller {
|
|
350
356
|
this.LINK_ARROW : this.CONSTRAINT_ARROW),
|
351
357
|
nodes = name.split(arrow);
|
352
358
|
for(let i = 0; i < nodes.length; i++) {
|
353
|
-
nodes[i] = nodes[i].replace(/^:\s*/, prefix)
|
359
|
+
nodes[i] = nodes[i].replace(/^:\s*/, prefix)
|
360
|
+
// NOTE: An embedded double prefix, e.g., "xxx: : yyy" indicates
|
361
|
+
// that the second colon+space should be replaced by the prefix.
|
362
|
+
// This "double prefix" may occur only once in an entity name,
|
363
|
+
// hence no global regexp.
|
364
|
+
.replace(/(\w+):\s+:\s+(\w+)/, `$1: ${prefix}$2`);
|
354
365
|
}
|
355
366
|
return nodes.join(arrow);
|
356
367
|
}
|
357
368
|
|
358
369
|
tailNumber(name) {
|
359
370
|
// Returns the string of digits at the end of `name`. If not there,
|
360
|
-
// check prefixes (if any) from right to left for a tail number.
|
371
|
+
// check prefixes (if any) *from right to left* for a tail number.
|
372
|
+
// Thus, the number that is "closest" to the name part is returned.
|
361
373
|
const pan = UI.prefixesAndName(name);
|
362
374
|
let n = endsWithDigits(pan.pop());
|
363
375
|
while(!n && pan.length > 0) {
|
@@ -366,6 +378,48 @@ class Controller {
|
|
366
378
|
return n;
|
367
379
|
}
|
368
380
|
|
381
|
+
compareFullNames(n1, n2, key=false) {
|
382
|
+
// Compare full names, considering prefixes in *left-to-right* order
|
383
|
+
// while taking into account the tailnumber for each part so that
|
384
|
+
// "xx: yy2: nnn" comes before "xx: yy10: nnn".
|
385
|
+
if(n1 === n2) return 0;
|
386
|
+
if(key) {
|
387
|
+
// NOTE: Replacing link and constraint arrows by two prefixers
|
388
|
+
// ensures that sort wil be first on FROM node, and then on TO node.
|
389
|
+
const p2 = UI.PREFIXER + UI.PREFIXER;
|
390
|
+
// Keys for links and constraints are not based on their names,
|
391
|
+
// so look up their names before comparing.
|
392
|
+
if(n1.indexOf('____') > 0 && MODEL.constraints[n1]) {
|
393
|
+
n1 = MODEL.constraints[n1].displayName
|
394
|
+
.replace(UI.CONSTRAINT_ARROW, p2);
|
395
|
+
} else if(n1.indexOf('___') > 0 && MODEL.links[n1]) {
|
396
|
+
n1 = MODEL.links[n1].displayName
|
397
|
+
.replace(UI.LINK_ARROW, p2);
|
398
|
+
}
|
399
|
+
if(n2.indexOf('____') > 0 && MODEL.constraints[n2]) {
|
400
|
+
n2 = MODEL.constraints[n2].displayName.
|
401
|
+
replace(UI.CONSTRAINT_ARROW, p2);
|
402
|
+
} else if(n2.indexOf('___') > 0 && MODEL.links[n2]) {
|
403
|
+
n2 = MODEL.links[n2].displayName
|
404
|
+
.replace(UI.LINK_ARROW, p2);
|
405
|
+
}
|
406
|
+
n1 = n1.toLowerCase().replaceAll(' ', '_');
|
407
|
+
n2 = n2.toLowerCase().replaceAll(' ', '_');
|
408
|
+
}
|
409
|
+
const
|
410
|
+
pan1 = UI.prefixesAndName(n1, key),
|
411
|
+
pan2 = UI.prefixesAndName(n2, key),
|
412
|
+
sl = Math.min(pan1.length, pan2.length);
|
413
|
+
let i = 0;
|
414
|
+
while(i < sl) {
|
415
|
+
const c = compareWithTailNumbers(pan1[i], pan2[i]);
|
416
|
+
if(c !== 0) return c;
|
417
|
+
i++;
|
418
|
+
}
|
419
|
+
return pan1.length - pan2.length;
|
420
|
+
}
|
421
|
+
|
422
|
+
|
369
423
|
nameToID(name) {
|
370
424
|
// Returns a name in lower case with link arrow replaced by three
|
371
425
|
// underscores, constraint link arrow by four underscores, and spaces
|
@@ -7356,7 +7356,8 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
7356
7356
|
// is passed to differentiate between the DOM elements to be used
|
7357
7357
|
const
|
7358
7358
|
type = document.getElementById(prefix + 'variable-obj').value,
|
7359
|
-
n_list = this.namesByType(VM.object_types[type]).sort(
|
7359
|
+
n_list = this.namesByType(VM.object_types[type]).sort(
|
7360
|
+
(a, b) => UI.compareFullNames(a, b)),
|
7360
7361
|
vn = document.getElementById(prefix + 'variable-name'),
|
7361
7362
|
options = [];
|
7362
7363
|
// Add "empty" as first and initial option, but disable it.
|
@@ -7398,7 +7399,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
7398
7399
|
slist.push(d.modifiers[m].selector);
|
7399
7400
|
}
|
7400
7401
|
// Sort to present equations in alphabetical order
|
7401
|
-
slist.sort(
|
7402
|
+
slist.sort((a, b) => UI.compareFullNames(a, b));
|
7402
7403
|
for(let i = 0; i < slist.length; i++) {
|
7403
7404
|
options.push(`<option value="${slist[i]}">${slist[i]}</option>`);
|
7404
7405
|
}
|
@@ -9900,13 +9901,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9900
9901
|
dl = [],
|
9901
9902
|
dnl = [],
|
9902
9903
|
sd = this.selected_dataset,
|
9903
|
-
ioclass = ['', 'import', 'export']
|
9904
|
-
ciPrefixCompare = (a, b) => {
|
9905
|
-
const
|
9906
|
-
pa = a.split(':_').join(' '),
|
9907
|
-
pb = b.split(':_').join(' ');
|
9908
|
-
return ciCompare(pa, pb);
|
9909
|
-
};
|
9904
|
+
ioclass = ['', 'import', 'export'];
|
9910
9905
|
for(let d in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(d) &&
|
9911
9906
|
// NOTE: do not list "black-boxed" entities
|
9912
9907
|
!d.startsWith(UI.BLACK_BOX) &&
|
@@ -9917,7 +9912,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9917
9912
|
dnl.push(d);
|
9918
9913
|
}
|
9919
9914
|
}
|
9920
|
-
dnl.sort(
|
9915
|
+
dnl.sort((a, b) => UI.compareFullNames(a, b, true));
|
9921
9916
|
// First determine indentation levels, prefixes and names
|
9922
9917
|
const
|
9923
9918
|
indent = [],
|
@@ -10678,7 +10673,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
10678
10673
|
const
|
10679
10674
|
ln = document.getElementById('series-line-number'),
|
10680
10675
|
lc = document.getElementById('series-line-count');
|
10681
|
-
ln.innerHTML = this.series_data.value.
|
10676
|
+
ln.innerHTML = this.series_data.value.substring(0,
|
10682
10677
|
this.series_data.selectionStart).split('\n').length;
|
10683
10678
|
lc.innerHTML = this.series_data.value.split('\n').length;
|
10684
10679
|
}
|
@@ -12190,7 +12185,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
12190
12185
|
const
|
12191
12186
|
ds_dict = MODEL.listOfAllSelectors,
|
12192
12187
|
html = [],
|
12193
|
-
sl = Object.keys(ds_dict).sort(
|
12188
|
+
sl = Object.keys(ds_dict).sort((a, b) => UI.compareFullNames(a, b, true));
|
12194
12189
|
for(let i = 0; i < sl.length; i++) {
|
12195
12190
|
const
|
12196
12191
|
s = sl[i],
|
@@ -13175,7 +13170,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
13175
13170
|
for(let i = 0; i < x.variables.length; i++) {
|
13176
13171
|
addDistinct(x.variables[i].displayName, vl);
|
13177
13172
|
}
|
13178
|
-
vl.sort(
|
13173
|
+
vl.sort((a, b) => UI.compareFullNames(a, b));
|
13179
13174
|
for(let i = 0; i < vl.length; i++) {
|
13180
13175
|
ol.push(['<option value="', vl[i], '"',
|
13181
13176
|
(vl[i] == x.selected_variable ? ' selected="selected"' : ''),
|
@@ -15524,7 +15519,7 @@ class Finder {
|
|
15524
15519
|
}
|
15525
15520
|
}
|
15526
15521
|
}
|
15527
|
-
enl.sort(
|
15522
|
+
enl.sort((a, b) => UI.compareFullNames(a, b, true));
|
15528
15523
|
}
|
15529
15524
|
document.getElementById('finder-entity-imgs').innerHTML = imgs;
|
15530
15525
|
let seid = 'etr';
|
@@ -15921,7 +15916,7 @@ class Finder {
|
|
15921
15916
|
// No cost price calculation => trim associated attributes from header
|
15922
15917
|
let p = ah.indexOf('\tCost price');
|
15923
15918
|
if(p > 0) {
|
15924
|
-
ah = ah.
|
15919
|
+
ah = ah.substring(0, p);
|
15925
15920
|
} else {
|
15926
15921
|
// SOC is exogenous, and hence comes before F in header => replace
|
15927
15922
|
ah = ah.replace('\tShare of cost', '');
|
@@ -16191,7 +16186,7 @@ class GUIReceiver {
|
|
16191
16186
|
return response.text();
|
16192
16187
|
})
|
16193
16188
|
.then((data) => {
|
16194
|
-
// For experiments, only display server response if warning or error
|
16189
|
+
// For experiments, only display server response if warning or error.
|
16195
16190
|
UI.postResponseOK(data, !RECEIVER.experiment);
|
16196
16191
|
// If execution completed, perform the call-back action if the
|
16197
16192
|
// receiver is active (so not when auto-reporting a run).
|
@@ -2940,24 +2940,72 @@ class LinnyRModel {
|
|
2940
2940
|
}
|
2941
2941
|
|
2942
2942
|
get outputData() {
|
2943
|
-
// Returns model results [data, statistics] in tab-separated format
|
2944
|
-
|
2945
|
-
|
2943
|
+
// Returns model results [data, statistics] in tab-separated format.
|
2944
|
+
const
|
2945
|
+
vbls = [],
|
2946
|
+
names = [],
|
2947
|
+
scale_re = /\s+\(x[0-9\.\,]+\)$/;
|
2948
|
+
// First create list of distinct variables used in charts.
|
2949
|
+
// NOTE: Also include those that are not "visible" in a chart.
|
2946
2950
|
for(let i = 0; i < this.charts.length; i++) {
|
2947
2951
|
const c = this.charts[i];
|
2948
2952
|
for(let j = 0; j < c.variables.length; j++) {
|
2949
|
-
|
2953
|
+
let v = c.variables[j],
|
2954
|
+
vn = v.displayName;
|
2955
|
+
// If variable is scaled, do not include it as such, but include
|
2956
|
+
// a new unscaled chart variable.
|
2957
|
+
if(vn.match(scale_re)) {
|
2958
|
+
vn = vn.replace(scale_re, '');
|
2959
|
+
// Add only if (now unscaled) variable has not been added already.
|
2960
|
+
if(names.indexOf(vn) < 0) {
|
2961
|
+
// NOTE: Chart variable object is used ony as adummy, so NULL
|
2962
|
+
// can be used as its "owner chart".
|
2963
|
+
const cv = new ChartVariable(null);
|
2964
|
+
cv.setProperties(v.object, v.attribute, false, '#000000');
|
2965
|
+
vbls.push(cv);
|
2966
|
+
names.push(uvn);
|
2967
|
+
}
|
2968
|
+
} else if(names.indexOf(vn) < 0) {
|
2969
|
+
// Keep track of the dataset and dataset modifier variables,
|
2970
|
+
// so they will not be added in the next FOR loop.
|
2971
|
+
vbls.push(v);
|
2972
|
+
names.push(vn);
|
2973
|
+
}
|
2974
|
+
}
|
2975
|
+
}
|
2976
|
+
// Add new variables for each outcome dataset and each equation that
|
2977
|
+
// is not a chart variable.
|
2978
|
+
for(let id in this.datasets) if(this.datasets.hasOwnProperty(id)) {
|
2979
|
+
const
|
2980
|
+
ds = this.datasets[id],
|
2981
|
+
eq = (ds === this.equations_dataset);
|
2982
|
+
if(ds.outcome || eq) {
|
2983
|
+
for(let ms in ds.modifiers) if(ds.modifiers.hasOwnProperty(ms)) {
|
2984
|
+
const
|
2985
|
+
dm = ds.modifiers[ms],
|
2986
|
+
n = dm.displayName;
|
2987
|
+
// Do not add if already in the list.
|
2988
|
+
if(names.indexOf(n) < 0) {
|
2989
|
+
// Here, too, NULL can be used as "owner chart".
|
2990
|
+
const cv = new ChartVariable(null);
|
2991
|
+
cv.setProperties(ds, dm.selector, false, '#000000');
|
2992
|
+
vbls.push(cv);
|
2993
|
+
}
|
2994
|
+
}
|
2950
2995
|
}
|
2951
2996
|
}
|
2952
|
-
//
|
2997
|
+
// Sort variables by their name.
|
2998
|
+
vbls.sort((a, b) => UI.compareFullNames(a.displayName, b.displayName));
|
2999
|
+
// Create a new chart as dummy, so without adding it to this model.
|
2953
3000
|
const c = new Chart();
|
2954
3001
|
for(let i = 0; i < vbls.length; i++) {
|
2955
3002
|
const v = vbls[i];
|
2956
3003
|
c.addVariable(v.object.displayName, v.attribute);
|
2957
3004
|
}
|
2958
|
-
|
3005
|
+
// NOTE: Call `draw` with FALSE to prevent display in the chart manager.
|
3006
|
+
c.draw(false);
|
3007
|
+
// After drawing, all variables and their statistics have been computed.
|
2959
3008
|
return [c.dataAsString, c.statisticsAsString];
|
2960
|
-
// @@@TO DO: also add statistics on "outcome" datasets
|
2961
3009
|
}
|
2962
3010
|
|
2963
3011
|
get listOfAllSelectors() {
|
@@ -5278,14 +5326,14 @@ class NodeBox extends ObjectWithXYWH {
|
|
5278
5326
|
MODEL.replaceEntityInExpressions(old_name, this.displayName);
|
5279
5327
|
MODEL.inferIgnoredEntities();
|
5280
5328
|
// NOTE: Renaming may affect the node's display size.
|
5281
|
-
if(this.resize())
|
5329
|
+
if(this.resize()) UI.drawSelection(MODEL);
|
5282
5330
|
// NOTE: Only TRUE indicates a successful (cosmetic) name change.
|
5283
5331
|
return true;
|
5284
5332
|
}
|
5285
5333
|
|
5286
5334
|
resize() {
|
5287
|
-
// Resizes this node; returns TRUE iff size has changed
|
5288
|
-
//
|
5335
|
+
// Resizes this node; returns TRUE iff size has changed.
|
5336
|
+
// Therefore, keep track of original width and height.
|
5289
5337
|
const
|
5290
5338
|
ow = this.width,
|
5291
5339
|
oh = this.height,
|
@@ -5341,12 +5389,13 @@ class NodeBox extends ObjectWithXYWH {
|
|
5341
5389
|
}
|
5342
5390
|
|
5343
5391
|
drawWithLinks() {
|
5344
|
-
// TO DO:
|
5392
|
+
// TO DO: Also draw relevant arrows when this is a cluster.
|
5345
5393
|
UI.drawObject(this);
|
5346
5394
|
if(this instanceof Cluster) return;
|
5347
|
-
//
|
5395
|
+
// Draw ALL arrows associated with this node.
|
5348
5396
|
const fc = this.cluster;
|
5349
|
-
|
5397
|
+
fc.categorizeEntities();
|
5398
|
+
// Make list of arrows that represent a link related to this node.
|
5350
5399
|
let a,
|
5351
5400
|
alist = [];
|
5352
5401
|
for(let j = 0; j < fc.arrows.length; j++) {
|
@@ -5362,7 +5411,7 @@ class NodeBox extends ObjectWithXYWH {
|
|
5362
5411
|
}
|
5363
5412
|
}
|
5364
5413
|
}
|
5365
|
-
//
|
5414
|
+
// Draw all arrows in this list.
|
5366
5415
|
for(let i = 0; i < alist.length; i++) UI.drawObject(alist[i]);
|
5367
5416
|
}
|
5368
5417
|
|
@@ -7323,7 +7372,7 @@ class Process extends Node {
|
|
7323
7372
|
// without comments or their (X, Y) position
|
7324
7373
|
n = MODEL.black_box_entities[id];
|
7325
7374
|
// `n` is just the name, so remove the actor name if it was added
|
7326
|
-
if(an) n = n.
|
7375
|
+
if(an) n = n.substring(0, n.lastIndexOf(an));
|
7327
7376
|
col = true;
|
7328
7377
|
cmnts = '';
|
7329
7378
|
x = 0;
|
@@ -8829,10 +8878,11 @@ class ChartVariable {
|
|
8829
8878
|
this.non_zero_tally = 0;
|
8830
8879
|
this.exceptions = 0;
|
8831
8880
|
this.bin_tallies = [];
|
8881
|
+
this.wildcard_index = false;
|
8832
8882
|
}
|
8833
8883
|
|
8834
8884
|
setProperties(obj, attr, stck, clr, sf=1, lw=1, vis=true) {
|
8835
|
-
// Sets the defining properties for this chart variable
|
8885
|
+
// Sets the defining properties for this chart variable.
|
8836
8886
|
this.object = obj;
|
8837
8887
|
this.attribute = attr;
|
8838
8888
|
this.stacked = stck;
|
@@ -8844,12 +8894,18 @@ class ChartVariable {
|
|
8844
8894
|
|
8845
8895
|
get displayName() {
|
8846
8896
|
// Returns the name of the Linny-R entity and its attribute, followed
|
8847
|
-
// by its scale factor unless it equals 1 (no scaling)
|
8897
|
+
// by its scale factor unless it equals 1 (no scaling).
|
8848
8898
|
const sf = (this.scale_factor === 1 ? '' :
|
8849
8899
|
` (x${VM.sig4Dig(this.scale_factor)})`);
|
8850
|
-
// NOTE:
|
8851
|
-
if(this.object === MODEL.equations_dataset)
|
8852
|
-
|
8900
|
+
// NOTE: Display name of equation is just the equations dataset modifier.
|
8901
|
+
if(this.object === MODEL.equations_dataset) {
|
8902
|
+
let eqn = this.attribute;
|
8903
|
+
if(this.wildcard_index !== false) {
|
8904
|
+
eqn = eqn.replace('??', this.wildcard_index);
|
8905
|
+
}
|
8906
|
+
return eqn + sf;
|
8907
|
+
}
|
8908
|
+
// NOTE: Do not display the vertical bar if no attribute is specified.
|
8853
8909
|
if(!this.attribute) return this.object.displayName + sf;
|
8854
8910
|
return this.object.displayName + UI.OA_SEPARATOR + this.attribute + sf;
|
8855
8911
|
}
|
@@ -8919,7 +8975,7 @@ class ChartVariable {
|
|
8919
8975
|
}
|
8920
8976
|
|
8921
8977
|
computeVector() {
|
8922
|
-
// Compute vector for this variable (using run results if specified)
|
8978
|
+
// Compute vector for this variable (using run results if specified).
|
8923
8979
|
let xrun = null,
|
8924
8980
|
rr = null,
|
8925
8981
|
ri = this.chart.run_index;
|
@@ -8934,10 +8990,10 @@ class ChartVariable {
|
|
8934
8990
|
this.vector.length = 0;
|
8935
8991
|
}
|
8936
8992
|
}
|
8937
|
-
// Compute vector and statistics only if vector is still empty
|
8993
|
+
// Compute vector and statistics only if vector is still empty.
|
8938
8994
|
if(this.vector.length > 0) return;
|
8939
|
-
// NOTE: expression vectors start at t = 0 with initial values that
|
8940
|
-
// not be included in statistics
|
8995
|
+
// NOTE: expression vectors start at t = 0 with initial values that
|
8996
|
+
// should not be included in statistics.
|
8941
8997
|
let v,
|
8942
8998
|
av = null,
|
8943
8999
|
t_end;
|
@@ -8948,22 +9004,23 @@ class ChartVariable {
|
|
8948
9004
|
this.non_zero_tally = 0;
|
8949
9005
|
this.exceptions = 0;
|
8950
9006
|
if(rr) {
|
8951
|
-
// Use run results (time scaled) as "actual vector" `av` for this
|
9007
|
+
// Use run results (time scaled) as "actual vector" `av` for this
|
9008
|
+
// variable.
|
8952
9009
|
const tsteps = Math.ceil(this.chart.time_horizon / this.chart.time_scale);
|
8953
9010
|
av = [];
|
8954
|
-
// NOTE:
|
9011
|
+
// NOTE: `scaleDataToVector` expects "pure" data, so slice off v[0].
|
8955
9012
|
VM.scaleDataToVector(rr.vector.slice(1), av, xrun.time_step_duration,
|
8956
9013
|
this.chart.time_scale, tsteps, 1);
|
8957
9014
|
t_end = tsteps;
|
8958
9015
|
} else {
|
8959
9016
|
// Get the variable's own value (number, vector or expression)
|
8960
|
-
// Special case: when an experiment is running, variables that
|
8961
|
-
// dataset with no explicit modifier must recompute the
|
8962
|
-
// current experiment run combination
|
9017
|
+
// Special case: when an experiment is running, variables that
|
9018
|
+
// depict a dataset with no explicit modifier must recompute the
|
9019
|
+
// vector using the current experiment run combination.
|
8963
9020
|
if(MODEL.running_experiment &&
|
8964
9021
|
this.object instanceof Dataset && !this.attribute) {
|
8965
9022
|
// Check if dataset modifiers match the combination of selectors
|
8966
|
-
// for the active run
|
9023
|
+
// for the active run.
|
8967
9024
|
const mm = this.object.matchingModifiers(
|
8968
9025
|
MODEL.running_experiment.activeCombination);
|
8969
9026
|
// If so, use the first (the list should contain at most 1 selector)
|
@@ -8982,21 +9039,24 @@ class ChartVariable {
|
|
8982
9039
|
}
|
8983
9040
|
t_end = MODEL.end_period - MODEL.start_period + 1;
|
8984
9041
|
}
|
8985
|
-
// NOTE: when a chart combines run results with dataset vectors, the
|
8986
|
-
// may be longer than the # of time steps displayed in the chart
|
9042
|
+
// NOTE: when a chart combines run results with dataset vectors, the
|
9043
|
+
// latter may be longer than the # of time steps displayed in the chart.
|
8987
9044
|
t_end = Math.min(t_end, this.chart.total_time_steps);
|
8988
9045
|
this.N = t_end;
|
8989
9046
|
for(let t = 0; t <= t_end; t++) {
|
8990
|
-
// Get the result, store it, and incorporate it in statistics
|
9047
|
+
// Get the result, store it, and incorporate it in statistics.
|
8991
9048
|
if(!av) {
|
8992
9049
|
// Undefined attribute => zero (no error)
|
8993
9050
|
v = 0;
|
8994
9051
|
} else if(Array.isArray(av)) {
|
8995
|
-
// Attribute value is a vector
|
9052
|
+
// Attribute value is a vector.
|
9053
|
+
// NOTE: This vector may be shorter than t; then use 0.
|
8996
9054
|
v = (t < av.length ? av[t] : 0);
|
8997
9055
|
} else if(av instanceof Expression) {
|
8998
|
-
// Attribute value is an expression
|
8999
|
-
|
9056
|
+
// Attribute value is an expression. If this chart variable has
|
9057
|
+
// its wildcard vector index set, evaluate the expression with
|
9058
|
+
// this index as context number.
|
9059
|
+
v = av.result(t, this.wildcard_index);
|
9000
9060
|
} else {
|
9001
9061
|
// Attribute value must be a number
|
9002
9062
|
v = av;
|
@@ -9165,7 +9225,7 @@ class Chart {
|
|
9165
9225
|
|
9166
9226
|
variableIndexByName(n) {
|
9167
9227
|
for(let i = 0; i < this.variables.length; i++) {
|
9168
|
-
if(this.variables[i].displayName
|
9228
|
+
if(this.variables[i].displayName === n) return i;
|
9169
9229
|
}
|
9170
9230
|
return -1;
|
9171
9231
|
}
|
@@ -9190,20 +9250,39 @@ class Chart {
|
|
9190
9250
|
if(n === UI.EQUATIONS_DATASET_NAME) {
|
9191
9251
|
// For equations only the attribute (modifier selector)
|
9192
9252
|
dn = a;
|
9253
|
+
n = a;
|
9193
9254
|
} else if(!a) {
|
9194
9255
|
// If no attribute specified (=> dataset) only the entity name
|
9195
9256
|
dn = n;
|
9196
9257
|
}
|
9197
9258
|
let vi = this.variableIndexByName(dn);
|
9198
9259
|
if(vi >= 0) return vi;
|
9199
|
-
//
|
9260
|
+
// Check whether name refers to a Linny-R entity defined by the model.
|
9200
9261
|
let obj = MODEL.objectByName(n);
|
9201
9262
|
if(obj === null) {
|
9202
9263
|
UI.warn(`Unknown entity "${n}"`);
|
9203
9264
|
return null;
|
9204
|
-
}
|
9205
|
-
|
9265
|
+
}
|
9266
|
+
const eq = obj instanceof DatasetModifier;
|
9267
|
+
if(!eq) {
|
9268
|
+
// No equation and no attribute specified? Then assume default.
|
9206
9269
|
if(a === '') a = obj.defaultAttribute;
|
9270
|
+
} else if(n.indexOf('??') >= 0) {
|
9271
|
+
// Special case: for wildcard equations, add dummy variables
|
9272
|
+
// for each vector in the wildcard vector set of the equation
|
9273
|
+
// expression.
|
9274
|
+
const
|
9275
|
+
vlist = [],
|
9276
|
+
clr = this.nextAvailableDefaultColor,
|
9277
|
+
indices = Object.keys(obj.expression.wildcard_vectors);
|
9278
|
+
for(let i = 0; i < indices.length; i++) {
|
9279
|
+
const v = new ChartVariable(this);
|
9280
|
+
v.setProperties(MODEL.equations_dataset, dn, false, clr);
|
9281
|
+
v.wildcard_index = parseInt(indices[i]);
|
9282
|
+
this.variables.push(v);
|
9283
|
+
vlist.push(v);
|
9284
|
+
}
|
9285
|
+
return vlist;
|
9207
9286
|
}
|
9208
9287
|
const v = new ChartVariable(this);
|
9209
9288
|
v.setProperties(obj, a, false, this.nextAvailableDefaultColor, 1, 1);
|
@@ -9296,7 +9375,7 @@ class Chart {
|
|
9296
9375
|
return VM.sig2Dig(s / 8760) + 'y';
|
9297
9376
|
}
|
9298
9377
|
|
9299
|
-
draw() {
|
9378
|
+
draw(display=true) {
|
9300
9379
|
// NOTE: The SVG drawing area is fixed to be 500 pixels high, so that
|
9301
9380
|
// when saved as a file, it will (at 300 dpi) be about 2 inches high.
|
9302
9381
|
// Its width will equal its hight times the W/H-ratio of the chart
|
@@ -9471,6 +9550,10 @@ class Chart {
|
|
9471
9550
|
}
|
9472
9551
|
}
|
9473
9552
|
|
9553
|
+
// Now all vectors have been computed. If `display` is FALSE, this
|
9554
|
+
// indicates that data is used only to save model results.
|
9555
|
+
if(!display) return;
|
9556
|
+
|
9474
9557
|
// Define the bins when drawing as histogram
|
9475
9558
|
if(this.histogram) {
|
9476
9559
|
this.value_range = maxv - minv;
|
@@ -124,9 +124,9 @@ function msecToTime(msec) {
|
|
124
124
|
const ts = new Date(msec).toISOString().slice(11, -1).split('.');
|
125
125
|
let hms = ts[0], ms = ts[1];
|
126
126
|
// Trim zero hours and minutes
|
127
|
-
while(hms.startsWith('00:')) hms = hms.
|
127
|
+
while(hms.startsWith('00:')) hms = hms.substring(3);
|
128
128
|
// Trim leading zero on first number
|
129
|
-
if(hms.startsWith('00')) hms = hms.
|
129
|
+
if(hms.startsWith('00')) hms = hms.substring(1);
|
130
130
|
// Trim msec when minutes > 0
|
131
131
|
if(hms.indexOf(':') > 0) return hms;
|
132
132
|
// If < 1 second, return as milliseconds
|
@@ -173,7 +173,7 @@ function uniformDecimals(data) {
|
|
173
173
|
ss = v.split('e');
|
174
174
|
x = ss[1];
|
175
175
|
if(x.length < maxe) {
|
176
|
-
x = x[0] + '0' + x.
|
176
|
+
x = x[0] + '0' + x.substring(1);
|
177
177
|
}
|
178
178
|
data[i] = ss[0] + 'e' + x;
|
179
179
|
} else if(maxi > 3) {
|
@@ -478,6 +478,21 @@ function matchingNumber(m, s) {
|
|
478
478
|
return (n == m ? n : false);
|
479
479
|
}
|
480
480
|
|
481
|
+
function compareWithTailNumbers(s1, s2) {
|
482
|
+
// Returns 0 on equal, an integer < 0 if `s1` comes before `s2`, and
|
483
|
+
// an integer > 0 if `s2` comes before `s1`.
|
484
|
+
if(s1 === s2) return 0;
|
485
|
+
let tn1 = endsWithDigits(s1),
|
486
|
+
tn2 = endsWithDigits(s2);
|
487
|
+
if(tn1) s1 = s1.slice(0, -tn1.length);
|
488
|
+
if(tn2) s2 = s2.slice(0, -tn2.length);
|
489
|
+
let c = ciCompare(s1, s2);
|
490
|
+
if(c !== 0 || !(tn1 || tn2)) return c;
|
491
|
+
if(tn1 && tn2) return parseInt(tn1) - parseInt(tn2);
|
492
|
+
if(tn2) return -1;
|
493
|
+
return 1;
|
494
|
+
}
|
495
|
+
|
481
496
|
function compareSelectors(s1, s2) {
|
482
497
|
// Dataset selectors comparison is case-insensitive, and puts wildcards
|
483
498
|
// last, where * comes later than ?
|
@@ -492,7 +507,7 @@ function compareSelectors(s1, s2) {
|
|
492
507
|
star2 = s2.indexOf('*');
|
493
508
|
if(star1 >= 0) {
|
494
509
|
if(star2 < 0) return 1;
|
495
|
-
return s1
|
510
|
+
return ciCompare(s1, s2);
|
496
511
|
}
|
497
512
|
if(star2 >= 0) return -1;
|
498
513
|
// Replace ? by | because | has a higher ASCII value than all other chars
|
@@ -525,7 +540,7 @@ function compareSelectors(s1, s2) {
|
|
525
540
|
while(i >= 0 && s_1[i] === '-') i--;
|
526
541
|
// If trailing minuses, replace by as many spaces and add an exclamation point
|
527
542
|
if(i < n - 1) {
|
528
|
-
s_1 = s_1.
|
543
|
+
s_1 = s_1.substring(0, i);
|
529
544
|
while(s_1.length < n) s_1 += ' ';
|
530
545
|
s_1 += '!';
|
531
546
|
}
|
@@ -534,7 +549,7 @@ function compareSelectors(s1, s2) {
|
|
534
549
|
i = n - 1;
|
535
550
|
while(i >= 0 && s_2[i] === '-') i--;
|
536
551
|
if(i < n - 1) {
|
537
|
-
s_2 = s_2.
|
552
|
+
s_2 = s_2.substring(0, i);
|
538
553
|
while(s_2.length < n) s_2 += ' ';
|
539
554
|
s_2 += '!';
|
540
555
|
}
|
@@ -814,8 +829,9 @@ function stringToFloatArray(s) {
|
|
814
829
|
a = [];
|
815
830
|
while(i <= s.length) {
|
816
831
|
const
|
817
|
-
h = s.
|
818
|
-
r = h.
|
832
|
+
h = s.substring(i - 8, i),
|
833
|
+
r = h.substring(6, 2) + h.substring(4, 2) +
|
834
|
+
h.substring(2, 2) + h.substring(0, 2);
|
819
835
|
a.push(hexToFloat(r));
|
820
836
|
i += 8;
|
821
837
|
}
|
@@ -830,7 +846,7 @@ function hexToBytes(hex) {
|
|
830
846
|
// Converts a hex string to a Uint8Array
|
831
847
|
const bytes = [];
|
832
848
|
for(let i = 0; i < hex.length; i += 2) {
|
833
|
-
bytes.push(parseInt(hex.
|
849
|
+
bytes.push(parseInt(hex.substring(i, i + 2), 16));
|
834
850
|
}
|
835
851
|
return new Uint8Array(bytes);
|
836
852
|
}
|
@@ -713,14 +713,14 @@ class ExpressionParser {
|
|
713
713
|
// t (current time step, this is the default),
|
714
714
|
if('#cfijklnprst'.includes(offs[0].charAt(0))) {
|
715
715
|
anchor1 = offs[0].charAt(0);
|
716
|
-
offset1 = safeStrToInt(offs[0].
|
716
|
+
offset1 = safeStrToInt(offs[0].substring(1));
|
717
717
|
} else {
|
718
718
|
offset1 = safeStrToInt(offs[0]);
|
719
719
|
}
|
720
720
|
if(offs.length > 1) {
|
721
721
|
if('#cfijklnprst'.includes(offs[1].charAt(0))) {
|
722
722
|
anchor2 = offs[1].charAt(0);
|
723
|
-
offset2 = safeStrToInt(offs[1].
|
723
|
+
offset2 = safeStrToInt(offs[1].substring(1));
|
724
724
|
} else {
|
725
725
|
offset2 = safeStrToInt(offs[1]);
|
726
726
|
}
|
@@ -757,7 +757,7 @@ class ExpressionParser {
|
|
757
757
|
};
|
758
758
|
// NOTE: name should then be in the experiment's variable list
|
759
759
|
name = s[1].trim();
|
760
|
-
s = s[0].
|
760
|
+
s = s[0].substring(1);
|
761
761
|
// Check for scaling method
|
762
762
|
// NOTE: simply ignore $ unless it indicates a valid method
|
763
763
|
const msep = s.indexOf('$');
|
@@ -967,6 +967,10 @@ class ExpressionParser {
|
|
967
967
|
return false;
|
968
968
|
}
|
969
969
|
}
|
970
|
+
/*
|
971
|
+
// DEPRECATED -- Modeler can deal with this by smartly using AND
|
972
|
+
// clauses like "&x: &y:" to limit set to specific prefixes.
|
973
|
+
|
970
974
|
// Deal with "prefix inheritance" when pattern starts with a colon.
|
971
975
|
if(pat.startsWith(':') && this.owner_prefix) {
|
972
976
|
// Add a "must start with" AND condition to all OR clauses of the
|
@@ -979,6 +983,7 @@ class ExpressionParser {
|
|
979
983
|
}
|
980
984
|
pat = oc.join('|');
|
981
985
|
}
|
986
|
+
*/
|
982
987
|
// NOTE: For patterns, assume that # *always* denotes the context-
|
983
988
|
// sensitive number #, because if modelers wishes to include
|
984
989
|
// ANY number, they can make their pattern less selective.
|
@@ -1449,7 +1454,7 @@ class ExpressionParser {
|
|
1449
1454
|
this.los = 1;
|
1450
1455
|
this.error = 'Missing closing bracket \']\'';
|
1451
1456
|
} else {
|
1452
|
-
v = this.expr.
|
1457
|
+
v = this.expr.substring(this.pit + 1, i);
|
1453
1458
|
this.pit = i + 1;
|
1454
1459
|
// NOTE: Enclosing brackets are also part of this symbol
|
1455
1460
|
this.los = v.length + 2;
|
@@ -1467,7 +1472,7 @@ class ExpressionParser {
|
|
1467
1472
|
this.los = 1;
|
1468
1473
|
this.error = 'Unmatched quote';
|
1469
1474
|
} else {
|
1470
|
-
v = this.expr.
|
1475
|
+
v = this.expr.substring(this.pit + 1, i);
|
1471
1476
|
this.pit = i + 1;
|
1472
1477
|
// NOTE: Enclosing quotes are also part of this symbol
|
1473
1478
|
this.los = v.length + 2;
|
@@ -1520,7 +1525,7 @@ class ExpressionParser {
|
|
1520
1525
|
this.los++;
|
1521
1526
|
}
|
1522
1527
|
// ... but trim spaces from the symbol
|
1523
|
-
v = this.expr.
|
1528
|
+
v = this.expr.substring(this.pit, this.pit + this.los).trim();
|
1524
1529
|
// Ignore case
|
1525
1530
|
l = v.toLowerCase();
|
1526
1531
|
if(l === '#') {
|
@@ -5929,7 +5934,9 @@ function VMI_push_dataset_modifier(x, args) {
|
|
5929
5934
|
// NOTE: Use the "local" time step for expression x, i.e., the top
|
5930
5935
|
// value of the expression's time step stack `x.step`.
|
5931
5936
|
tot = twoOffsetTimeStep(x.step[x.step.length - 1],
|
5932
|
-
args[1], args[2], args[3], args[4], 1, x)
|
5937
|
+
args[1], args[2], args[3], args[4], 1, x),
|
5938
|
+
// Record whether either anchor uses the context-sensitive number.
|
5939
|
+
hashtag_index = (args[1] === '#' || args[3] === '#');
|
5933
5940
|
// NOTE: Sanity check to facilitate debugging; if no dataset is provided,
|
5934
5941
|
// the script will still break at the LET statement below.
|
5935
5942
|
if(!ds) console.log('ERROR: VMI_push_dataset_modifier without dataset',
|
@@ -5947,7 +5954,7 @@ function VMI_push_dataset_modifier(x, args) {
|
|
5947
5954
|
t = t % obj.length;
|
5948
5955
|
if(t < 0) t += obj.length;
|
5949
5956
|
}
|
5950
|
-
if(
|
5957
|
+
if(hashtag_index) {
|
5951
5958
|
// NOTE: Add 1 because (parent) anchors are 1-based.
|
5952
5959
|
ds.parent_anchor = t + 1;
|
5953
5960
|
if(DEBUGGING) {
|
@@ -5991,12 +5998,21 @@ function VMI_push_dataset_modifier(x, args) {
|
|
5991
5998
|
if(t >= 0 && t < obj.length) {
|
5992
5999
|
v = obj[t];
|
5993
6000
|
} else if(ds.array && t >= obj.length) {
|
5994
|
-
//
|
5995
|
-
|
5996
|
-
VM.
|
5997
|
-
|
5998
|
-
|
5999
|
-
|
6001
|
+
// Ensure that value of t is human-readable.
|
6002
|
+
// NOTE: Add 1 to compensate for earlier t-- to make `t` zero-based.
|
6003
|
+
const index = VM.sig2Dig(t + 1);
|
6004
|
+
// Special case: index is undefined because # was undefined.
|
6005
|
+
if(hashtag_index && index === '\u2047') {
|
6006
|
+
// In such cases, return the default value of the dataset.
|
6007
|
+
v = ds.default_value;
|
6008
|
+
} else {
|
6009
|
+
// Set error value to indicate that array index is out of bounds.
|
6010
|
+
v = VM.ARRAY_INDEX;
|
6011
|
+
VM.out_of_bounds_array = ds.displayName;
|
6012
|
+
VM.out_of_bounds_msg = `Index ${index} not in array dataset ` +
|
6013
|
+
`${ds.displayName}, which has length ${obj.length}`;
|
6014
|
+
console.log(VM.out_of_bounds_msg);
|
6015
|
+
}
|
6000
6016
|
}
|
6001
6017
|
// Fall through: no change to `v` => dataset default value is pushed.
|
6002
6018
|
} else {
|