linny-r 2.1.5 → 2.1.7
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 +18 -2
- package/static/linny-r.css +44 -0
- package/static/scripts/linny-r-config.js +2 -2
- package/static/scripts/linny-r-gui-chart-manager.js +3 -2
- package/static/scripts/linny-r-gui-controller.js +32 -25
- package/static/scripts/linny-r-gui-dataset-manager.js +8 -1
- package/static/scripts/linny-r-gui-experiment-manager.js +28 -16
- package/static/scripts/linny-r-gui-file-manager.js +2 -1
- package/static/scripts/linny-r-gui-finder.js +260 -22
- package/static/scripts/linny-r-gui-power-grid-manager.js +6 -0
- package/static/scripts/linny-r-milp.js +7 -1
- package/static/scripts/linny-r-model.js +94 -133
- package/static/scripts/linny-r-utils.js +210 -30
- package/static/scripts/linny-r-vm.js +21 -7
@@ -50,12 +50,27 @@ class Finder {
|
|
50
50
|
this.chart_btn = document.getElementById('finder-chart-btn');
|
51
51
|
this.chart_btn.addEventListener(
|
52
52
|
'click', () => FINDER.confirmAddChartVariables());
|
53
|
+
this.table_btn = document.getElementById('finder-table-btn');
|
54
|
+
this.table_btn.addEventListener(
|
55
|
+
'click', () => FINDER.toggleViewAttributes());
|
56
|
+
this.experiment_btn = document.getElementById('finder-experiment-btn');
|
57
|
+
this.experiment_btn.addEventListener(
|
58
|
+
'click', () => FINDER.toggleViewExperiment());
|
53
59
|
this.copy_btn = document.getElementById('finder-copy-btn');
|
54
60
|
this.copy_btn.addEventListener(
|
55
61
|
'click', (event) => FINDER.copyAttributesToClipboard(event.shiftKey));
|
62
|
+
this.entity_scroll_area = document.getElementById('finder-scroll-area');
|
63
|
+
this.entity_scroll_area.addEventListener(
|
64
|
+
'scroll', () => FINDER.scrollEntityArea());
|
56
65
|
this.entity_table = document.getElementById('finder-table');
|
57
66
|
this.item_table = document.getElementById('finder-item-table');
|
58
67
|
this.expression_table = document.getElementById('finder-expression-table');
|
68
|
+
this.data_pane = document.getElementById('finder-data-pane');
|
69
|
+
this.data_header = document.getElementById('finder-data-header');
|
70
|
+
this.data_scroll_area = document.getElementById('finder-data-scroll-area');
|
71
|
+
this.data_scroll_area.addEventListener(
|
72
|
+
'scroll', () => FINDER.scrollDataArea());
|
73
|
+
this.data_table = document.getElementById('finder-data-table');
|
59
74
|
|
60
75
|
// The Confirm add chart variables modal.
|
61
76
|
this.add_chart_variables_modal = new ModalDialog('confirm-add-chart-variables');
|
@@ -97,6 +112,8 @@ class Finder {
|
|
97
112
|
// Product cluster index "remembers" for which cluster a product was
|
98
113
|
// last revealed, so it can reveal the next cluster when clicked again.
|
99
114
|
this.product_cluster_index = 0;
|
115
|
+
this.tabular_view = false;
|
116
|
+
this.experiment_view = false;
|
100
117
|
}
|
101
118
|
|
102
119
|
doubleClicked(obj) {
|
@@ -150,8 +167,23 @@ class Finder {
|
|
150
167
|
let imgs = '';
|
151
168
|
this.entities.length = 0;
|
152
169
|
this.filtered_types.length = 0;
|
153
|
-
|
154
|
-
|
170
|
+
if(this.experiment_view) {
|
171
|
+
// List outcome variables of selected experiment.
|
172
|
+
const x = EXPERIMENT_MANAGER.selected_experiment;
|
173
|
+
if(x) {
|
174
|
+
x.inferVariables();
|
175
|
+
for(const v of x.variables) {
|
176
|
+
const obj = v.object;
|
177
|
+
if(et !== VM.entity_letters && et.indexOf(obj.typeLetter) >= 0) {
|
178
|
+
if(!fp || patternMatch(obj.displayName, this.filter_pattern)) {
|
179
|
+
this.entities.push(v);
|
180
|
+
enl.push(v.displayName);
|
181
|
+
}
|
182
|
+
}
|
183
|
+
}
|
184
|
+
}
|
185
|
+
} else if(fp || et && et !== VM.entity_letters) {
|
186
|
+
// No list unless a pattern OR a specified SUB-set of entity types.
|
155
187
|
if(et.indexOf('A') >= 0) {
|
156
188
|
imgs += '<img src="images/actor.png">';
|
157
189
|
for(let k in MODEL.actors) if(MODEL.actors.hasOwnProperty(k)) {
|
@@ -315,32 +347,44 @@ class Finder {
|
|
315
347
|
}
|
316
348
|
}
|
317
349
|
}
|
350
|
+
// NOTE: Pass TRUE to indicate "comparison of identifiers".
|
318
351
|
enl.sort((a, b) => UI.compareFullNames(a, b, true));
|
319
352
|
}
|
320
353
|
document.getElementById('finder-entity-imgs').innerHTML = imgs;
|
321
|
-
let
|
322
|
-
|
323
|
-
|
324
|
-
if(
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
354
|
+
let n = enl.length,
|
355
|
+
seid = 'etr';
|
356
|
+
for(let i = 0; i < n; i++) {
|
357
|
+
if(this.experiment_view) {
|
358
|
+
el.push(['<tr id="etr', i, '" class="dataset"><td>',
|
359
|
+
'<div class="series">', enl[i], '</div></td></tr>'].join(''));
|
360
|
+
} else {
|
361
|
+
const e = MODEL.objectByID(enl[i]);
|
362
|
+
if(e === se) seid += i;
|
363
|
+
el.push(['<tr id="etr', i, '" class="dataset',
|
364
|
+
(e === se ? ' sel-set' : ''), '" onclick="FINDER.selectEntity(\'',
|
365
|
+
enl[i], '\', event.altKey);" onmouseover="FINDER.showInfo(\'', enl[i],
|
366
|
+
'\', event.shiftKey);"><td draggable="true" ',
|
367
|
+
'ondragstart="FINDER.drag(event);"><img class="finder" src="images/',
|
368
|
+
e.type.toLowerCase(), '.png">', e.displayName,
|
369
|
+
'</td></tr>'].join(''));
|
370
|
+
}
|
332
371
|
}
|
333
372
|
// NOTE: Reset `selected_entity` if not in the new list.
|
334
373
|
if(seid === 'etr') this.selected_entity = null;
|
335
374
|
this.entity_table.innerHTML = el.join('');
|
336
375
|
UI.scrollIntoView(document.getElementById(seid));
|
337
|
-
document.getElementById('finder-count').innerHTML = pluralS(
|
338
|
-
|
339
|
-
// Only show the edit button if all filtered entities are of the
|
340
|
-
// same type.
|
341
|
-
let n = el.length;
|
376
|
+
document.getElementById('finder-count').innerHTML = pluralS(n,
|
377
|
+
'entity', 'entities');
|
342
378
|
this.edit_btn.style.display = 'none';
|
379
|
+
this.chart_btn.style.display = 'none';
|
380
|
+
this.table_btn.style.display = 'none';
|
343
381
|
this.copy_btn.style.display = 'none';
|
382
|
+
/*
|
383
|
+
// Show the experiment button only when at least 1 experiment exists.
|
384
|
+
this.experiment_btn.style.display = (MODEL.experiments.length ?
|
385
|
+
'inline-block' : 'none');
|
386
|
+
*/
|
387
|
+
// Only show other buttons if the set of filtered entities is not empty.
|
344
388
|
if(n > 0) {
|
345
389
|
this.copy_btn.style.display = 'inline-block';
|
346
390
|
if(CHART_MANAGER.visible && CHART_MANAGER.chart_index >= 0) {
|
@@ -351,13 +395,27 @@ class Finder {
|
|
351
395
|
this.chart_btn.style.display = 'inline-block';
|
352
396
|
}
|
353
397
|
}
|
398
|
+
// NOTE: Enable editing and tabular view only when filter results
|
399
|
+
// in a single entity type.
|
354
400
|
n = this.entityGroup.length;
|
355
401
|
if(n > 0) {
|
356
402
|
this.edit_btn.title = 'Edit attributes of ' +
|
357
403
|
pluralS(n, this.entities[0].type.toLowerCase());
|
358
404
|
this.edit_btn.style.display = 'inline-block';
|
405
|
+
this.table_btn.style.display = 'inline-block';
|
359
406
|
}
|
360
407
|
}
|
408
|
+
// Show toggle button status.
|
409
|
+
if(this.tabular_view) {
|
410
|
+
this.table_btn.classList.add('stay-activ');
|
411
|
+
} else {
|
412
|
+
this.table_btn.classList.remove('stay-activ');
|
413
|
+
}
|
414
|
+
if(this.experiment_view) {
|
415
|
+
this.experiment_btn.classList.add('stay-activ');
|
416
|
+
} else {
|
417
|
+
this.experiment_btn.classList.remove('stay-activ');
|
418
|
+
}
|
361
419
|
this.updateRightPane();
|
362
420
|
}
|
363
421
|
|
@@ -379,10 +437,10 @@ class Finder {
|
|
379
437
|
if(this.filtered_types.length === 1 && ft !== 'E') {
|
380
438
|
for(const e of this.entities) {
|
381
439
|
// Exclude "no actor" and top cluster.
|
382
|
-
if(e.name
|
440
|
+
if(!e.name || (e.name !== '(no_actor)' && e.name !== '(top_cluster)' &&
|
383
441
|
// Also exclude actor cash flow data products because
|
384
442
|
// many of their properties should not be changed.
|
385
|
-
!e.name.startsWith('$')) {
|
443
|
+
!e.name.startsWith('$'))) {
|
386
444
|
eg.push(e);
|
387
445
|
}
|
388
446
|
}
|
@@ -406,7 +464,13 @@ class Finder {
|
|
406
464
|
for(const a of ca) {
|
407
465
|
html += `<option value="${a}">${VM.attribute_names[a]}</option>`;
|
408
466
|
}
|
409
|
-
|
467
|
+
if(html) {
|
468
|
+
md.element('attr-of').style.display = 'inline-block';
|
469
|
+
md.element('attribute').innerHTML = html;
|
470
|
+
} else {
|
471
|
+
md.element('attr-of').style.display = 'none';
|
472
|
+
md.element('attribute').innerHTML = '';
|
473
|
+
}
|
410
474
|
md.element('count').innerText = et;
|
411
475
|
md.show();
|
412
476
|
}
|
@@ -440,7 +504,33 @@ class Finder {
|
|
440
504
|
md.hide();
|
441
505
|
}
|
442
506
|
|
507
|
+
scrollEntityArea() {
|
508
|
+
// When in tabular view, the data table must scroll along with the
|
509
|
+
// entity table.
|
510
|
+
if(this.tabular_view) {
|
511
|
+
this.data_scroll_area.scrollTop = this.entity_scroll_area.scrollTop;
|
512
|
+
}
|
513
|
+
}
|
514
|
+
|
515
|
+
scrollDataArea() {
|
516
|
+
// When in tabular view, the entity table must scroll along with the
|
517
|
+
// data table.
|
518
|
+
if(this.tabular_view) {
|
519
|
+
this.entity_scroll_area.scrollTop = this.data_scroll_area.scrollTop;
|
520
|
+
}
|
521
|
+
}
|
522
|
+
|
443
523
|
updateRightPane() {
|
524
|
+
// Right pane can display attribute data...
|
525
|
+
if(this.tabular_view) {
|
526
|
+
this.data_pane.style.display = 'block';
|
527
|
+
this.updateTabularView();
|
528
|
+
return;
|
529
|
+
}
|
530
|
+
// ... or no data...
|
531
|
+
this.data_pane.style.display = 'none';
|
532
|
+
this.data_table.innerHTML = '';
|
533
|
+
// ... but information on the occurence of the selected entity.
|
444
534
|
const
|
445
535
|
se = this.selected_entity,
|
446
536
|
occ = [], // list with occurrences (clusters, processes or charts)
|
@@ -622,6 +712,152 @@ class Finder {
|
|
622
712
|
document.getElementById('finder-expression-hdr').innerHTML =
|
623
713
|
pluralS(el.length, 'expression');
|
624
714
|
}
|
715
|
+
|
716
|
+
toggleViewAttributes() {
|
717
|
+
// Show/hide tabular display of entity attributes.
|
718
|
+
this.tabular_view = !this.tabular_view;
|
719
|
+
this.updateRightPane();
|
720
|
+
if(this.tabular_view) {
|
721
|
+
this.table_btn.classList.add('stay-activ');
|
722
|
+
} else {
|
723
|
+
this.table_btn.classList.remove('stay-activ');
|
724
|
+
}
|
725
|
+
}
|
726
|
+
|
727
|
+
toggleViewExperiment() {
|
728
|
+
// Switch between model entities and experiment outcomes.
|
729
|
+
this.experiment_view = !this.experiment_view;
|
730
|
+
if(this.experiment_view) this.tabular_view = true;
|
731
|
+
this.updateDialog();
|
732
|
+
}
|
733
|
+
|
734
|
+
updateTabularView() {
|
735
|
+
// Display data values when tabular view is active.
|
736
|
+
if(!this.entities.length ||
|
737
|
+
(this.filtered_types.length !== 1 && !this.experiment_view)) {
|
738
|
+
this.data_table.innerHTML = '';
|
739
|
+
return;
|
740
|
+
}
|
741
|
+
const
|
742
|
+
special = ['\u221E', '-\u221E', '\u2047', '\u00A2'],
|
743
|
+
rows = [],
|
744
|
+
etl = this.entities[0].typeLetter,
|
745
|
+
data_list = [],
|
746
|
+
data = {};
|
747
|
+
// Collect data and sort list by name, so it coresponds with the
|
748
|
+
// entities listed in the left pane.
|
749
|
+
if(this.experiment_view) {
|
750
|
+
// Get selected runs.
|
751
|
+
const
|
752
|
+
x = EXPERIMENT_MANAGER.selected_experiment,
|
753
|
+
runs = (x ? x.chart_combinations : []);
|
754
|
+
if(!runs.length) {
|
755
|
+
UI.notify('');
|
756
|
+
this.data_table.innerHTML = '';
|
757
|
+
return;
|
758
|
+
}
|
759
|
+
// Add aray for each run.
|
760
|
+
data[0] = [];
|
761
|
+
for(const e of this.entities) {
|
762
|
+
const run_data = {name: e.object.displayName};
|
763
|
+
// Add value for each run.
|
764
|
+
data_list.push(run_data);
|
765
|
+
}
|
766
|
+
data_list.sort((a, b) => UI.compareFullNames(a.name, b.name));
|
767
|
+
} else {
|
768
|
+
for(const e of this.entities) data_list.push(e.attributes);
|
769
|
+
data_list.sort((a, b) => UI.compareFullNames(a.name, b.name));
|
770
|
+
// The data "matrix" then holds values as an array per attribute code.
|
771
|
+
// NOTE: Datasets are special in that their data is a multi-line
|
772
|
+
// string of tab-separated key-value pairs where the first pair has no
|
773
|
+
// key (dataset default value) and the other pairs have a dataset
|
774
|
+
// modifier selector as key.
|
775
|
+
if(etl === 'D') {
|
776
|
+
// First compile the list of unique selectors.
|
777
|
+
const sel = [];
|
778
|
+
for(const ed of data_list) {
|
779
|
+
// NOTE: Dataset modifier lines start with a tab.
|
780
|
+
const lines = ed.D.split('\n\t');
|
781
|
+
// Store default value in entity data object for second iteration.
|
782
|
+
ed.dv = VM.sig4Dig(safeStrToFloat(lines[0].trim(), 0));
|
783
|
+
for(let i = 1; i < lines.length; i++) {
|
784
|
+
const pair = lines[i].split('\t');
|
785
|
+
if(pair[0]) {
|
786
|
+
addDistinct(pair[0], sel);
|
787
|
+
// Store pair value in entity data object for second iteration.
|
788
|
+
ed[pair[0]] = (pair.length > 1 ? pair[1] : '');
|
789
|
+
}
|
790
|
+
}
|
791
|
+
}
|
792
|
+
sel.sort(compareSelectors);
|
793
|
+
// Initialize arrays for default values and for selectors.
|
794
|
+
// NOTE: The parentheses of '(default)'ensure that there is no doubling
|
795
|
+
// with a selector defined by the modeler.
|
796
|
+
data['(default)'] = [];
|
797
|
+
for(const s of sel) data[s] = [];
|
798
|
+
// Perform second iteration.
|
799
|
+
for(const ed of data_list) {
|
800
|
+
data['(default)'].push(ed.dv);
|
801
|
+
for(const s of sel) {
|
802
|
+
if(ed[s]) {
|
803
|
+
const f = parseFloat(ed[s]);
|
804
|
+
data[s].push(isNaN(f) ? ed[s] : VM.sig4Dig(f));
|
805
|
+
} else {
|
806
|
+
// Empty string to denote "no modifier => not calculated".
|
807
|
+
data[s].push('\u2047');
|
808
|
+
}
|
809
|
+
}
|
810
|
+
}
|
811
|
+
} else {
|
812
|
+
// Initialize array per selector.
|
813
|
+
let atcodes = VM.attribute_codes[etl];
|
814
|
+
if(!MODEL.solved) atcodes = complement(atcodes, VM.level_based_attr);
|
815
|
+
if(!MODEL.infer_cost_prices) atcodes = complement(atcodes, ['CP', 'HCP', 'SOC']);
|
816
|
+
for(const ac of atcodes) data[ac] = [];
|
817
|
+
for(const ed of data_list) {
|
818
|
+
for(const ac of atcodes) {
|
819
|
+
let v = ed[ac];
|
820
|
+
if(v === '') {
|
821
|
+
// Empty strings denote "undefined".
|
822
|
+
v = '\u2047';
|
823
|
+
// Keep special values such as infinity and exception codes.
|
824
|
+
} else if(special.indexOf(v) < 0) {
|
825
|
+
// When model is not solved, expression values will be the
|
826
|
+
// expression string, and this is likely to be not parsable.
|
827
|
+
const f = parseFloat(v);
|
828
|
+
if(isNaN(f)) {
|
829
|
+
v = '\u2297'; // Circled X to denote "not computed".
|
830
|
+
} else {
|
831
|
+
v = VM.sig4Dig(parseFloat(f.toPrecision(4)));
|
832
|
+
}
|
833
|
+
}
|
834
|
+
data[ac].push(v);
|
835
|
+
}
|
836
|
+
}
|
837
|
+
}
|
838
|
+
}
|
839
|
+
// Create header.
|
840
|
+
const
|
841
|
+
keys = Object.keys(data),
|
842
|
+
row = [],
|
843
|
+
perc = (97 / keys.length).toPrecision(3),
|
844
|
+
style = `min-width: ${perc}%; max-width: ${perc}%`;
|
845
|
+
for(const k of keys) {
|
846
|
+
row.push(`<td style="${style}">${k}</td>`);
|
847
|
+
}
|
848
|
+
this.data_header.innerHTML = '<tr>' + row.join('') + '</tr>';
|
849
|
+
// Format each array with uniform decimals.
|
850
|
+
for(const k of keys) uniformDecimals(data[k]);
|
851
|
+
const n = data_list.length;
|
852
|
+
for(let index = 0; index < n; index++) {
|
853
|
+
const row = [];
|
854
|
+
for(const k of keys) {
|
855
|
+
row.push(`<td style="${style}">${data[k][index]}</td>`);
|
856
|
+
}
|
857
|
+
rows.push('<tr>' + row.join('') + '</tr>');
|
858
|
+
}
|
859
|
+
this.data_table.innerHTML = rows.join('');
|
860
|
+
}
|
625
861
|
|
626
862
|
drag(ev) {
|
627
863
|
// Start dragging the selected entity.
|
@@ -977,9 +1213,11 @@ class Finder {
|
|
977
1213
|
// Also update the draggable dialogs that may be affected.
|
978
1214
|
UI.updateControllerDialogs('CDEFIJX');
|
979
1215
|
}
|
980
|
-
|
1216
|
+
|
981
1217
|
copyAttributesToClipboard(shift) {
|
982
1218
|
// Copy relevant entity attributes as tab-separated text to clipboard.
|
1219
|
+
// When copy button is Shift-clicked, only data for the selected entity
|
1220
|
+
// is copied.
|
983
1221
|
// NOTE: All entity types have "get" method `attributes` that returns an
|
984
1222
|
// object that for each defined attribute (and if model has been
|
985
1223
|
// solved also each inferred attribute) has a property with its value.
|
@@ -173,6 +173,12 @@ class PowerGridManager {
|
|
173
173
|
MODEL.ignore_KVL = UI.boxChecked('power-grids-KVL');
|
174
174
|
MODEL.ignore_power_losses = UI.boxChecked('power-grids-losses');
|
175
175
|
this.dialog.hide();
|
176
|
+
const pg_btn = document.getElementById('settings-power-btn');
|
177
|
+
if(MODEL.ignore_grid_capacity || MODEL.ignore_KVL || MODEL.ignore_power_losses) {
|
178
|
+
pg_btn.classList.add('ignore');
|
179
|
+
} else {
|
180
|
+
pg_btn.classList.remove('ignore');
|
181
|
+
}
|
176
182
|
}
|
177
183
|
|
178
184
|
selectPowerGrid(event, id, focus) {
|
@@ -581,7 +581,13 @@ module.exports = class MILPSolver {
|
|
581
581
|
col++;
|
582
582
|
}
|
583
583
|
// Return near-zero values as 0.
|
584
|
-
|
584
|
+
let xv = x_dict[v];
|
585
|
+
const xfv = parseFloat(xv);
|
586
|
+
if(xfv && Math.abs(xfv) < this.near_zero) {
|
587
|
+
console.log('NOTE: Truncated ', xfv, ' to zero for variable', v);
|
588
|
+
xv = '0';
|
589
|
+
}
|
590
|
+
x_values.push(xv);
|
585
591
|
col++;
|
586
592
|
}
|
587
593
|
// Add zeros to vector for remaining columns.
|