linny-r 2.1.5 → 2.1.6
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-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-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 +13 -13
- package/static/scripts/linny-r-utils.js +42 -30
- package/static/scripts/linny-r-vm.js +19 -5
package/package.json
CHANGED
package/static/index.html
CHANGED
@@ -3178,6 +3178,12 @@ where X can be one or several of these letters: ABCDELPQ">
|
|
3178
3178
|
<img id="finder-chart-btn" class="btn enab"
|
3179
3179
|
src="images/chart.png"
|
3180
3180
|
title="Add attribute to chart">
|
3181
|
+
<img id="finder-experiment-btn" class="btn enab"
|
3182
|
+
src="images/experiment.png"
|
3183
|
+
title="Only consider experiment outcomes">
|
3184
|
+
<img id="finder-table-btn" class="btn enab"
|
3185
|
+
src="images/table.png"
|
3186
|
+
title="View entity attribute values">
|
3181
3187
|
<img id="finder-copy-btn" class="btn enab"
|
3182
3188
|
src="images/table-to-clpbrd.png"
|
3183
3189
|
title="Copy entity attributes to clipboard">
|
@@ -3193,6 +3199,14 @@ where X can be one or several of these letters: ABCDELPQ">
|
|
3193
3199
|
<table id="finder-expression-table">
|
3194
3200
|
</table>
|
3195
3201
|
</div>
|
3202
|
+
<div id="finder-data-pane">
|
3203
|
+
<table id="finder-data-header">
|
3204
|
+
</table>
|
3205
|
+
<div id="finder-data-scroll-area">
|
3206
|
+
<table id="finder-data-table">
|
3207
|
+
</table>
|
3208
|
+
</div>
|
3209
|
+
</div>
|
3196
3210
|
<div id="finder-resize" class="resizer"></div>
|
3197
3211
|
</div>
|
3198
3212
|
|
@@ -3206,8 +3220,10 @@ where X can be one or several of these letters: ABCDELPQ">
|
|
3206
3220
|
<img class="ok-btn" src="images/ok.png">
|
3207
3221
|
</div>
|
3208
3222
|
<div style="margin: 2px; padding-right: 2px; white-space: nowrap">
|
3209
|
-
<
|
3210
|
-
|
3223
|
+
<div id="confirm-add-chart-variables-attr-of">
|
3224
|
+
<select id="confirm-add-chart-variables-attribute"></select> of
|
3225
|
+
</div>
|
3226
|
+
<span id="confirm-add-chart-variables-count"></span>
|
3211
3227
|
</div>
|
3212
3228
|
<div>
|
3213
3229
|
<div id="confirm-add-chart-variables-absolute" class="box clear"></div>
|
package/static/linny-r.css
CHANGED
@@ -1167,6 +1167,10 @@ table.power-flow th:not(:first-child) {
|
|
1167
1167
|
filter: brightness(160%);
|
1168
1168
|
}
|
1169
1169
|
|
1170
|
+
#settings-power-btn.ignore {
|
1171
|
+
filter: hue-rotate(230deg);
|
1172
|
+
}
|
1173
|
+
|
1170
1174
|
#password-dlg {
|
1171
1175
|
width: min-content;
|
1172
1176
|
height: min-content;
|
@@ -2660,6 +2664,7 @@ div.io-box {
|
|
2660
2664
|
#finder-table,
|
2661
2665
|
#finder-item-table,
|
2662
2666
|
#finder-expression-table,
|
2667
|
+
#finder-data-table,
|
2663
2668
|
#boundline-data-series-table,
|
2664
2669
|
#boundline-data-sel-table,
|
2665
2670
|
#restore-table {
|
@@ -5160,6 +5165,8 @@ img.finder {
|
|
5160
5165
|
|
5161
5166
|
#finder-edit-btn,
|
5162
5167
|
#finder-chart-btn,
|
5168
|
+
#finder-experiment-btn,
|
5169
|
+
#finder-table-btn,
|
5163
5170
|
#finder-copy-btn {
|
5164
5171
|
height: 15px;
|
5165
5172
|
width: 15px;
|
@@ -5211,9 +5218,46 @@ img.finder {
|
|
5211
5218
|
border-top: 1px solid Silver;
|
5212
5219
|
}
|
5213
5220
|
|
5221
|
+
#finder-data-pane {
|
5222
|
+
position: absolute;
|
5223
|
+
background-color: inherit;
|
5224
|
+
top: 23px;
|
5225
|
+
left: calc(50% + 4px);
|
5226
|
+
width: calc(50% - 4px);
|
5227
|
+
height: calc(100% - 30px);
|
5228
|
+
}
|
5229
|
+
|
5230
|
+
#finder-data-header {
|
5231
|
+
height: 20px;
|
5232
|
+
font-weight: bold;
|
5233
|
+
text-align: right;
|
5234
|
+
width: 100%
|
5235
|
+
}
|
5236
|
+
|
5237
|
+
#finder-data-scroll-area {
|
5238
|
+
width: calc(100% - 2px);
|
5239
|
+
height: calc(100% - 32px);
|
5240
|
+
overflow-y: auto;
|
5241
|
+
border-top: 1px solid Silver;
|
5242
|
+
}
|
5243
|
+
|
5244
|
+
#finder-data-table {
|
5245
|
+
text-align: right;
|
5246
|
+
}
|
5247
|
+
|
5248
|
+
#finder-data-header > tbody > tr > td:last-child,
|
5249
|
+
#finder-data-table > tbody > tr > td:last-child {
|
5250
|
+
padding-right: 3%;
|
5251
|
+
}
|
5252
|
+
|
5214
5253
|
#confirm-add-chart-variables-dlg {
|
5215
5254
|
width: min-content;
|
5216
5255
|
height: min-content;
|
5256
|
+
min-width: 135px;
|
5257
|
+
}
|
5258
|
+
|
5259
|
+
#confirm-add-chart-variables-attr-of {
|
5260
|
+
display: inline-block;
|
5217
5261
|
}
|
5218
5262
|
|
5219
5263
|
#confirm-add-chart-variables-attribute {
|
@@ -209,7 +209,7 @@ class GUIChartManager extends ChartManager {
|
|
209
209
|
this.variable_index = -1;
|
210
210
|
this.stretch_factor = 1;
|
211
211
|
this.drawing_graph = false;
|
212
|
-
// Clear the model-related DOM elements
|
212
|
+
// Clear the model-related DOM elements.
|
213
213
|
this.chart_selector.innerHTML = '';
|
214
214
|
this.variables_table.innerHTML = '';
|
215
215
|
this.options_shown = true;
|
@@ -641,7 +641,8 @@ class GUIChartManager extends ChartManager {
|
|
641
641
|
for(const cv of c.variables) {
|
642
642
|
const nv = new ChartVariable(nc);
|
643
643
|
nv.setProperties(cv.object, cv.attribute, cv.stacked,
|
644
|
-
cv.color, cv.scale_factor, cv.absolute, cv.line_width,
|
644
|
+
cv.color, cv.scale_factor, cv.absolute, cv.line_width,
|
645
|
+
cv.visible, cv.sorted);
|
645
646
|
nc.variables.push(nv);
|
646
647
|
}
|
647
648
|
this.chart_index = MODEL.indexOfChart(nc.title);
|
@@ -3078,6 +3078,7 @@ class GUIController extends Controller {
|
|
3078
3078
|
MODEL.t = Math.max(1, MODEL.t - dt);
|
3079
3079
|
UI.updateTimeStep();
|
3080
3080
|
UI.drawDiagram(MODEL);
|
3081
|
+
if(FINDER.visible && FINDER.tabular_view) FINDER.updateTabularView();
|
3081
3082
|
}
|
3082
3083
|
}
|
3083
3084
|
|
@@ -3088,6 +3089,7 @@ class GUIController extends Controller {
|
|
3088
3089
|
MODEL.t = Math.min(MODEL.end_period - MODEL.start_period + 1, MODEL.t + dt);
|
3089
3090
|
UI.updateTimeStep();
|
3090
3091
|
UI.drawDiagram(MODEL);
|
3092
|
+
if(FINDER.visible && FINDER.tabular_view) FINDER.updateTabularView();
|
3091
3093
|
}
|
3092
3094
|
}
|
3093
3095
|
|
@@ -3096,39 +3098,39 @@ class GUIController extends Controller {
|
|
3096
3098
|
//
|
3097
3099
|
|
3098
3100
|
copyStringToClipboard(string) {
|
3099
|
-
//
|
3100
|
-
let msg = pluralS(string.split('\n').length, 'line') +
|
3101
|
-
' copied to clipboard',
|
3102
|
-
type = 'notification';
|
3101
|
+
// Copy string to clipboard and notifies user of #lines copied.
|
3103
3102
|
if(navigator.clipboard) {
|
3104
|
-
|
3105
|
-
|
3103
|
+
const msg = pluralS(string.split('\n').length, 'line') +
|
3104
|
+
' copied to clipboard';
|
3105
|
+
navigator.clipboard.writeText(string)
|
3106
|
+
.then(() => UI.setMessage(msg, 'notification'))
|
3107
|
+
.catch(() => UI.setMessage('Failed to copy to clipboard', 'warning'));
|
3106
3108
|
} else {
|
3107
|
-
|
3108
|
-
|
3109
|
-
document.body.appendChild(ta);
|
3110
|
-
ta.value = string;
|
3111
|
-
ta.select();
|
3112
|
-
document.execCommand('copy');
|
3113
|
-
document.body.removeChild(ta);
|
3109
|
+
UI.setMessage('Your browser does not support copying to clipboard',
|
3110
|
+
'warning');
|
3114
3111
|
}
|
3115
|
-
UI.setMessage(msg, type);
|
3116
3112
|
}
|
3117
3113
|
|
3118
|
-
copyHtmlToClipboard(html) {
|
3119
|
-
// Copy HTML to clipboard
|
3120
|
-
|
3121
|
-
|
3122
|
-
|
3114
|
+
copyHtmlToClipboard(html, plain=false) {
|
3115
|
+
// Copy HTML (as such or as plain text) to clipboard and notify user.
|
3116
|
+
if(navigator.clipboard) {
|
3117
|
+
const
|
3118
|
+
item = (plain ? {'text/plain': html} : {'text/html': html}),
|
3119
|
+
data = [new ClipboardItem(item)],
|
3120
|
+
msg = 'HTML copied to clipboard' + (plain ? ' as plain text' : '');
|
3121
|
+
navigator.clipboard.write(data)
|
3122
|
+
.then(() => UI.setMessage(msg, 'notification'))
|
3123
|
+
.catch((error) => UI.setMessage('Failed to copy HTML to clipboard',
|
3124
|
+
'warning', error));
|
3125
|
+
} else {
|
3126
|
+
UI.setMessage('Your browser does not support copying HTML to clipboard',
|
3127
|
+
'warning');
|
3123
3128
|
}
|
3124
|
-
document.addEventListener('copy', listener);
|
3125
|
-
document.execCommand('copy');
|
3126
|
-
document.removeEventListener('copy', listener);
|
3127
3129
|
}
|
3128
3130
|
|
3129
3131
|
logHeapSize(msg='') {
|
3130
|
-
//
|
3131
|
-
// NOTE:
|
3132
|
+
// Log MB's of used heap memory to console (to detect memory leaks).
|
3133
|
+
// NOTE: This feature is supported only by Chrome.
|
3132
3134
|
if(msg) msg += ' -- ';
|
3133
3135
|
if(performance.memory !== undefined) {
|
3134
3136
|
console.log(msg + 'Allocated memory: ' + Math.round(
|
@@ -4035,11 +4037,16 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
4035
4037
|
this.setBox('settings-encrypt', model.encrypt);
|
4036
4038
|
const pg_btn = md.element('power-btn');
|
4037
4039
|
pg_btn.style.display = (model.with_power_flow ? 'inline-block' : 'none');
|
4040
|
+
if(model.ignore_grid_capacity || model.ignore_KVL || model.ignore_power_losses) {
|
4041
|
+
pg_btn.classList.add('ignore');
|
4042
|
+
} else {
|
4043
|
+
pg_btn.classList.remove('ignore');
|
4044
|
+
}
|
4038
4045
|
md.show('name');
|
4039
4046
|
}
|
4040
4047
|
|
4041
4048
|
updateSettings(model) {
|
4042
|
-
// Valdidate inputs
|
4049
|
+
// Valdidate inputs.
|
4043
4050
|
const px = this.validNumericInput('settings-grid-pixels', 'grid resolution');
|
4044
4051
|
if(px === false) return false;
|
4045
4052
|
const ts = this.validNumericInput('settings-time-scale', 'time step');
|
@@ -288,7 +288,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
288
288
|
for(const r of this.dataset_table.rows) if(r.dataset.prefix === lcp) return r;
|
289
289
|
return null;
|
290
290
|
}
|
291
|
-
|
291
|
+
|
292
292
|
selectPrefixRow(e) {
|
293
293
|
// Select expand/collapse prefix row.
|
294
294
|
this.focal_table = this.dataset_table;
|
@@ -579,6 +579,13 @@ class GUIDatasetManager extends DatasetManager {
|
|
579
579
|
// NOTE: Updating entire dialog may be very time-consuming
|
580
580
|
// when model contains numerous prefixed datasets.
|
581
581
|
this.updatePanes();
|
582
|
+
// NOTE: The selected row now has to be highlighted, as the table
|
583
|
+
// HTML is not changed.
|
584
|
+
let r = event.target;
|
585
|
+
while(r && r.tagName !== 'TR') r = r.parentNode;
|
586
|
+
const sel = this.dataset_table.getElementsByClassName('sel-set');
|
587
|
+
if(sel.length > 0) sel[0].classList.remove('sel-set');
|
588
|
+
r.classList.add('sel-set');
|
582
589
|
}
|
583
590
|
|
584
591
|
selectModifier(event, id, x=true) {
|
@@ -343,6 +343,8 @@ class GUIExperimentManager extends ExperimentManager {
|
|
343
343
|
// NOTE: When UpdateDialog is called after an entity has been renamed,
|
344
344
|
// its variable list should be updated.
|
345
345
|
this.updateViewerVariable();
|
346
|
+
// NOTE: Finder may need updating as well.
|
347
|
+
if(FINDER.experiment_view) FINDER.updateDialog();
|
346
348
|
}
|
347
349
|
|
348
350
|
updateParameters() {
|
@@ -727,6 +729,11 @@ class GUIExperimentManager extends ExperimentManager {
|
|
727
729
|
// Toggle `n` consecutive rows, starting at row `r` (0 = top), to be
|
728
730
|
// (no longer) part of the chart combination set.
|
729
731
|
// @@TO DO: shift-key indicates "add row(s) to selection"
|
732
|
+
if(MODEL.running_experiment) {
|
733
|
+
// NOTE: do NOT change run selection while VM is solving!
|
734
|
+
UI.notify('Run selection cannot be changed when an experiment is running');
|
735
|
+
return;
|
736
|
+
}
|
730
737
|
const
|
731
738
|
x = this.selected_experiment,
|
732
739
|
// Let `first` be the number of the first run on row `r`.
|
@@ -751,7 +758,10 @@ class GUIExperimentManager extends ExperimentManager {
|
|
751
758
|
}
|
752
759
|
}
|
753
760
|
this.updateData();
|
761
|
+
CHART_MANAGER.resetChartVectors();
|
754
762
|
CHART_MANAGER.updateDialog();
|
763
|
+
// NOTE: Finder may need updating as well.
|
764
|
+
if(FINDER.experiment_view) FINDER.updateDialog();
|
755
765
|
}
|
756
766
|
}
|
757
767
|
|
@@ -761,34 +771,36 @@ class GUIExperimentManager extends ExperimentManager {
|
|
761
771
|
|
762
772
|
toggleChartCombi(n, shift, alt) {
|
763
773
|
// Set `n` to be the chart combination, or toggle if Shift-key is pressed,
|
764
|
-
// or execute single run if Alt-key is pressed
|
774
|
+
// or execute single run if Alt-key is pressed.
|
775
|
+
if(MODEL.running_experiment) {
|
776
|
+
// NOTE: do NOT do this while VM is solving, as this would interfere!
|
777
|
+
UI.notify('Run selection cannot be changed when an experiment is running');
|
778
|
+
return;
|
779
|
+
}
|
765
780
|
const x = this.selected_experiment;
|
766
781
|
if(x && alt && n >= 0) {
|
767
782
|
this.startExperiment(n);
|
768
783
|
return;
|
769
784
|
}
|
770
785
|
if(x && n < x.combinations.length) {
|
771
|
-
//
|
772
|
-
if(!shift) x.chart_combinations.length = 0;
|
773
|
-
// Toggle => add if not in selection, otherwise remove
|
786
|
+
// Toggle => add if not in selection, otherwise remove.
|
774
787
|
const ci = x.chart_combinations.indexOf(n);
|
775
788
|
if(ci < 0) {
|
789
|
+
// Clear current selection unless Shift-key is pressed.
|
790
|
+
if(!shift) x.chart_combinations.length = 0;
|
776
791
|
x.chart_combinations.push(n);
|
777
792
|
} else {
|
778
793
|
x.chart_combinations.splice(ci, 1);
|
779
794
|
}
|
780
795
|
}
|
781
796
|
this.updateData();
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
CHART_MANAGER.resetChartVectors();
|
790
|
-
CHART_MANAGER.updateDialog();
|
791
|
-
}
|
797
|
+
// Show the messages for this run in the monitor.
|
798
|
+
VM.setRunMessages(n);
|
799
|
+
// Update the chart.
|
800
|
+
CHART_MANAGER.resetChartVectors();
|
801
|
+
CHART_MANAGER.updateDialog();
|
802
|
+
// NOTE: Finder may need updating as well.
|
803
|
+
if(FINDER.experiment_view) FINDER.updateDialog();
|
792
804
|
}
|
793
805
|
|
794
806
|
runInfo(n) {
|
@@ -839,7 +851,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
839
851
|
info.html = html.join('');
|
840
852
|
return info;
|
841
853
|
}
|
842
|
-
// Fall-through (should not occur)
|
854
|
+
// Fall-through (should not occur).
|
843
855
|
return null;
|
844
856
|
}
|
845
857
|
|
@@ -848,7 +860,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
848
860
|
// NOTE: Skip when viewer is showing!
|
849
861
|
if(!UI.hidden('experiment-viewer')) return;
|
850
862
|
if(n < MODEL.experiments.length) {
|
851
|
-
// NOTE:
|
863
|
+
// NOTE: Mouse move over title in viewer passes n = -1.
|
852
864
|
const x = (n < 0 ? this.selected_experiment : MODEL.experiments[n]);
|
853
865
|
DOCUMENTATION_MANAGER.update(x, shift);
|
854
866
|
}
|
@@ -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.
|
@@ -542,7 +542,7 @@ class LinnyRModel {
|
|
542
542
|
e = this.objectByName(en);
|
543
543
|
if(!e) return `Unknown model entity "${en}"`;
|
544
544
|
const
|
545
|
-
ao = ea[1].split('@'),
|
545
|
+
ao = (ea.length > 1 ? ea[1].split('@') : ['']),
|
546
546
|
a = ao[0].trim();
|
547
547
|
// Valid if no attribute, as all entity types have a default attribute.
|
548
548
|
if(!a) return true;
|
@@ -1636,8 +1636,7 @@ class LinnyRModel {
|
|
1636
1636
|
const ci = this.indexOfChart(title);
|
1637
1637
|
if(ci >= 0) return this.charts[ci];
|
1638
1638
|
// Otherwise, add it. NOTE: Unlike datasets, charts are not "entities".
|
1639
|
-
let c = new Chart();
|
1640
|
-
c.title = title;
|
1639
|
+
let c = new Chart(title);
|
1641
1640
|
if(node) c.initFromXML(node);
|
1642
1641
|
this.charts.push(c);
|
1643
1642
|
// Sort the chart titles alphabetically...
|
@@ -3323,7 +3322,7 @@ class LinnyRModel {
|
|
3323
3322
|
vbls.sort((a, b) => UI.compareFullNames(a.displayName, b.displayName));
|
3324
3323
|
// Create a new chart as dummy, so without adding it to this model.
|
3325
3324
|
const
|
3326
|
-
c = new Chart(),
|
3325
|
+
c = new Chart('__d_u_m_m_y__c_h_a_r_t__'),
|
3327
3326
|
wcdm = [];
|
3328
3327
|
for(const v of vbls) {
|
3329
3328
|
// NOTE: Prevent adding wildcard dataset modifiers more than once.
|
@@ -6242,19 +6241,19 @@ class Cluster extends NodeBox {
|
|
6242
6241
|
}
|
6243
6242
|
|
6244
6243
|
get nestingLevel() {
|
6245
|
-
// Return the "depth" of this cluster in the cluster hierarchy
|
6244
|
+
// Return the "depth" of this cluster in the cluster hierarchy.
|
6246
6245
|
if(this.cluster) return this.cluster.nestingLevel + 1; // recursion!
|
6247
6246
|
return 0;
|
6248
6247
|
}
|
6249
6248
|
|
6250
6249
|
get toBeIgnored() {
|
6251
|
-
// Return TRUE if this cluster or some parent cluster is set to be ignored
|
6250
|
+
// Return TRUE if this cluster or some parent cluster is set to be ignored.
|
6252
6251
|
return this.ignore || MODEL.ignoreClusterInThisRun(this) ||
|
6253
6252
|
(this.cluster && this.cluster.toBeIgnored); // recursion!
|
6254
6253
|
}
|
6255
6254
|
|
6256
6255
|
get blackBoxed() {
|
6257
|
-
// Return TRUE if this cluster or some parent cluster is marked as black box
|
6256
|
+
// Return TRUE if this cluster or some parent cluster is marked as black box.
|
6258
6257
|
return this.black_box ||
|
6259
6258
|
(this.cluster && this.cluster.blackBoxed); // recursion!
|
6260
6259
|
}
|
@@ -7930,6 +7929,7 @@ class Process extends Node {
|
|
7930
7929
|
const a = {name: this.displayName};
|
7931
7930
|
a.LB = this.lower_bound.asAttribute;
|
7932
7931
|
a.UB = (this.equal_bounds ? a.LB : this.upper_bound.asAttribute);
|
7932
|
+
if(this.grid) a.LB = -a.UB;
|
7933
7933
|
a.IL = this.initial_level.asAttribute;
|
7934
7934
|
a.LCF = this.pace_expression.asAttribute;
|
7935
7935
|
if(MODEL.solved) {
|
@@ -8863,13 +8863,13 @@ class Link {
|
|
8863
8863
|
}
|
8864
8864
|
|
8865
8865
|
get identifier() {
|
8866
|
-
// NOTE:
|
8867
|
-
// prevents problems when nodes are renamed
|
8866
|
+
// NOTE: Link IDs are based on the node codes rather than IDs, as this
|
8867
|
+
// prevents problems when nodes are renamed.
|
8868
8868
|
return this.from_node.code + '___' + this.to_node.code;
|
8869
8869
|
}
|
8870
8870
|
|
8871
8871
|
get attributes() {
|
8872
|
-
// NOTE:
|
8872
|
+
// NOTE: Link is named by its tab-separated node names.
|
8873
8873
|
const a = {name: this.from_node.displayName + '\t' + this.to_node.displayName};
|
8874
8874
|
a.R = this.relative_rate.asAttribute;
|
8875
8875
|
if(MODEL.infer_cost_prices) a.SOC = this.share_of_cost;
|
@@ -9222,7 +9222,7 @@ class Dataset {
|
|
9222
9222
|
}
|
9223
9223
|
|
9224
9224
|
get attributes() {
|
9225
|
-
// NOTE:
|
9225
|
+
// NOTE: Modifiers are appended as additional lines of text.
|
9226
9226
|
const a = {name: this.displayName};
|
9227
9227
|
a.D = '\t' + (this.vector ? this.vector[MODEL.t] : this.default_value);
|
9228
9228
|
for(let k in this.modifiers) if(this.modifiers.hasOwnProperty(k)) {
|
@@ -9981,8 +9981,8 @@ class ChartVariable {
|
|
9981
9981
|
}
|
9982
9982
|
// Scale the value unless run result (these are already scaled!).
|
9983
9983
|
if(!rr) {
|
9984
|
-
v *= this.scale_factor;
|
9985
9984
|
if(this.absolute) v = Math.abs(v);
|
9985
|
+
v *= this.scale_factor;
|
9986
9986
|
}
|
9987
9987
|
this.vector.push(v);
|
9988
9988
|
// Do not include values for t = 0 in statistics.
|
@@ -13051,7 +13051,7 @@ class Constraint {
|
|
13051
13051
|
}
|
13052
13052
|
|
13053
13053
|
get typeLetter() {
|
13054
|
-
return '
|
13054
|
+
return 'B';
|
13055
13055
|
}
|
13056
13056
|
|
13057
13057
|
get identifier() {
|
@@ -189,11 +189,16 @@ function uniformDecimals(data) {
|
|
189
189
|
}
|
190
190
|
maxi = Math.max(maxi, ss[0].length);
|
191
191
|
}
|
192
|
-
// STEP 2: Convert the data to a uniform format
|
192
|
+
// STEP 2: Convert the data to a uniform format.
|
193
|
+
const special = ['\u221E', '-\u221E', '\u2047', '\u00A2'];
|
193
194
|
for(let i = 0; i < data.length; i++) {
|
194
|
-
const
|
195
|
+
const
|
196
|
+
v = data[i],
|
197
|
+
f = parseFloat(v);
|
195
198
|
if(isNaN(f)) {
|
196
|
-
|
199
|
+
// Keep special values such as infinity, and replace error values
|
200
|
+
// by Unicode warning sign.
|
201
|
+
if(special.indexOf(v) < 0) data[i] = '\u26A0';
|
197
202
|
} else if(maxe > 0) {
|
198
203
|
// Convert ALL numbers to exponential notation with two decimals (1.23e+7)
|
199
204
|
const v = f.toExponential(2);
|
@@ -405,51 +410,59 @@ function patternList(str) {
|
|
405
410
|
|
406
411
|
function patternMatch(str, patterns) {
|
407
412
|
// Returns TRUE when `str` matches the &|^-pattern.
|
408
|
-
// NOTE: If a pattern starts with
|
409
|
-
//
|
410
|
-
//
|
413
|
+
// NOTE: If a pattern starts with an opening bracket [ then `str` must
|
414
|
+
// start with the rest of the pattern to match. If it ends with a closing
|
415
|
+
// bracket ] then `str` must end with the first part of the pattern.
|
416
|
+
// In this way, [pattern] denotes that `str` should exactly match
|
411
417
|
for(let i = 0; i < patterns.length; i++) {
|
412
418
|
const p = patterns[i];
|
413
419
|
// NOTE: `p` is an OR sub-pattern that tests for a set of "plus"
|
414
420
|
// sub-sub-patterns (all of which should match) and a set of "min"
|
415
421
|
// sub-sub-patters (all should NOT match)
|
416
422
|
let pm,
|
423
|
+
swob,
|
424
|
+
ewcb,
|
417
425
|
re,
|
418
426
|
match = true;
|
419
427
|
for(let j = 0; match && j < p.plus.length; j++) {
|
420
428
|
pm = p.plus[j];
|
421
|
-
|
422
|
-
|
423
|
-
|
429
|
+
swob = pm.startsWith('[');
|
430
|
+
ewcb = pm.endsWith(']');
|
431
|
+
if(swob && ewcb) {
|
432
|
+
match = (str === pm.slice(1, -1));
|
433
|
+
} else if(swob) {
|
424
434
|
match = str.startsWith(pm.substring(1));
|
435
|
+
} else if(ewcb) {
|
436
|
+
match = str.endsWith(pm.slice(0, -1));
|
425
437
|
} else {
|
426
438
|
match = (str.indexOf(pm) >= 0);
|
427
439
|
}
|
428
|
-
// If no match, check whether pattern contains wildcards
|
440
|
+
// If no match, check whether pattern contains wildcards.
|
429
441
|
if(!match && pm.indexOf('#') >= 0) {
|
430
442
|
// If so, rematch using regular expression that tests for a
|
431
|
-
// number or a ?? wildcard
|
443
|
+
// number or a ?? wildcard.
|
432
444
|
let res = pm.split('#');
|
433
445
|
for(let i = 0; i < res.length; i++) {
|
434
446
|
res[i] = escapeRegex(res[i]);
|
435
447
|
}
|
436
448
|
res = res.join('(\\d+|\\?\\?)');
|
437
|
-
if(
|
438
|
-
|
439
|
-
} else if(pm.startsWith('~')) {
|
440
|
-
res = '^' + res;
|
441
|
-
}
|
449
|
+
if(swob) res = '^' + res;
|
450
|
+
if(ewcb) res += '$';
|
442
451
|
re = new RegExp(res, 'g');
|
443
452
|
match = re.test(str);
|
444
453
|
}
|
445
454
|
}
|
446
|
-
// Any "min" match indicates NO match for this sub-pattern
|
455
|
+
// Any "min" match indicates NO match for this sub-pattern.
|
447
456
|
for(let j = 0; match && j < p.min.length; j++) {
|
448
457
|
pm = p.min[j];
|
449
|
-
|
450
|
-
|
451
|
-
|
458
|
+
swob = pm.startsWith('[');
|
459
|
+
ewcb = pm.endsWith(']');
|
460
|
+
if(swob && ewcb) {
|
461
|
+
match = (str !== pm.slice(1, -1));
|
462
|
+
} else if(swob) {
|
452
463
|
match = !str.startsWith(pm.substring(1));
|
464
|
+
} else if(ewcb) {
|
465
|
+
match = !str.endsWith(pm.slice(0, -1));
|
453
466
|
} else {
|
454
467
|
match = (str.indexOf(pm) < 0);
|
455
468
|
}
|
@@ -461,11 +474,8 @@ function patternMatch(str, patterns) {
|
|
461
474
|
res[i] = escapeRegex(res[i]);
|
462
475
|
}
|
463
476
|
res = res.join('(\\d+|\\?\\?)');
|
464
|
-
if(
|
465
|
-
|
466
|
-
} else if(pm.startsWith('~')) {
|
467
|
-
res = '^' + res;
|
468
|
-
}
|
477
|
+
if(swob) res = '^' + res;
|
478
|
+
if(ewcb) res += '$';
|
469
479
|
re = new RegExp(res, 'g');
|
470
480
|
match = !re.test(str);
|
471
481
|
}
|
@@ -973,18 +983,20 @@ function nameToLines(name, actor_name = '') {
|
|
973
983
|
// the node box.
|
974
984
|
let m = actor_name.length;
|
975
985
|
const
|
976
|
-
d = Math.floor(Math.sqrt(0.
|
986
|
+
d = Math.floor(Math.sqrt(0.25 * name.length)),
|
977
987
|
// Do not wrap strings shorter than 13 characters (about 50 pixels).
|
978
988
|
limit = Math.max(Math.ceil(name.length / d), m, 13),
|
979
|
-
|
980
|
-
|
989
|
+
// NOTE: Do not split on spaces followed by a number or a single
|
990
|
+
// capital letter.
|
991
|
+
a = name.split(/\s(?!\d+:|\d+$|[A-Z]\W)/);
|
992
|
+
// Split words at '-' when wider than limit.
|
981
993
|
for(let j = 0; j < a.length; j++) {
|
982
994
|
if(a[j].length > limit) {
|
983
995
|
const sw = a[j].split('-');
|
984
996
|
if(sw.length > 1) {
|
985
|
-
// Replace j-th word by last fragment of split string
|
997
|
+
// Replace j-th word by last fragment of split string.
|
986
998
|
a[j] = sw.pop();
|
987
|
-
// Insert remaining fragments before
|
999
|
+
// Insert remaining fragments before.
|
988
1000
|
while(sw.length > 0) a.splice(j, 0, sw.pop() + '-');
|
989
1001
|
}
|
990
1002
|
}
|
@@ -1364,7 +1364,10 @@ class ExpressionParser {
|
|
1364
1364
|
const
|
1365
1365
|
parts = name.split(UI.PREFIXER),
|
1366
1366
|
tail = parts.pop();
|
1367
|
-
if(parts.length
|
1367
|
+
if(!tail && parts.length) {
|
1368
|
+
// Prefix without its trailing colon+space could identify an entity.
|
1369
|
+
obj = MODEL.objectByID(UI.nameToID(parts.join(UI.PREFIXER)));
|
1370
|
+
} else if(parts.length > 0) {
|
1368
1371
|
// Name contains at least one prefix => last part *could* be a
|
1369
1372
|
// method name, so look it up after adding a leading colon.
|
1370
1373
|
const method = MODEL.equationByID(UI.nameToID(':' + tail));
|
@@ -3391,13 +3394,19 @@ class VirtualMachine {
|
|
3391
3394
|
// Infer cycle basis for combined power grids for which Kirchhoff's
|
3392
3395
|
// voltage law must be enforced.
|
3393
3396
|
if(MODEL.with_power_flow) {
|
3394
|
-
|
3397
|
+
this.logMessage(1, 'POWER FLOW: ' +
|
3395
3398
|
pluralS(Object.keys(MODEL.power_grids).length, 'grid'));
|
3399
|
+
if(MODEL.ignore_grid_capacity) this.logMessage(1,
|
3400
|
+
'NOTE: Assuming infinite grid line cacity');
|
3401
|
+
if(MODEL.ignore_KVL) this.logMessage(1,
|
3402
|
+
'NOTE: Disregarding Kirchhoff\'s Voltage Law');
|
3403
|
+
if(MODEL.ignore_power_losses) this.logMessage(1,
|
3404
|
+
'NOTE: Disregarding transmission losses');
|
3396
3405
|
POWER_GRID_MANAGER.inferCycleBasis();
|
3397
3406
|
if(POWER_GRID_MANAGER.messages.length > 1) {
|
3398
3407
|
UI.warn('Check monitor for power grid warnings');
|
3399
3408
|
}
|
3400
|
-
|
3409
|
+
this.logMessage(1, POWER_GRID_MANAGER.messages.join('\n'));
|
3401
3410
|
if(POWER_GRID_MANAGER.cycle_basis.length) this.logMessage(1,
|
3402
3411
|
'Enforcing Kirchhoff\'s voltage law for ' +
|
3403
3412
|
POWER_GRID_MANAGER.cycleBasisAsString);
|
@@ -6204,7 +6213,7 @@ Solver status = ${json.status}`);
|
|
6204
6213
|
} catch(err) {
|
6205
6214
|
const msg = `ERROR while processing solver data for block ${bnr}: ${err}`;
|
6206
6215
|
console.log(msg);
|
6207
|
-
|
6216
|
+
this.logMessage(bnr, msg);
|
6208
6217
|
UI.alert(msg);
|
6209
6218
|
this.stopSolving();
|
6210
6219
|
this.halted = true;
|
@@ -7340,7 +7349,10 @@ function VMI_push_run_result(x, args) {
|
|
7340
7349
|
}
|
7341
7350
|
}
|
7342
7351
|
// Truncate near-zero values.
|
7343
|
-
if(Math.abs(v) < VM.SIG_DIF_FROM_ZERO)
|
7352
|
+
if(v && Math.abs(v) < VM.SIG_DIF_FROM_ZERO) {
|
7353
|
+
console.log('NOTE: Truncated experiment run result', v, 'to zero');
|
7354
|
+
v = 0;
|
7355
|
+
}
|
7344
7356
|
x.push(v);
|
7345
7357
|
}
|
7346
7358
|
|
@@ -8432,6 +8444,8 @@ function VMI_set_bounds(args) {
|
|
8432
8444
|
// Check the difference, as this may be negligible.
|
8433
8445
|
if(u - l < VM.SIG_DIF_FROM_ZERO) {
|
8434
8446
|
u = Math.round(u * 1e5) / 1e5;
|
8447
|
+
// NOTE: This may result in -0 (minus zero) => then set to 0.
|
8448
|
+
if(u < 0 && u > -VM.NEAR_ZERO) u = 0;
|
8435
8449
|
} else {
|
8436
8450
|
// If substantial, warn that "impossible" bounds would have been set.
|
8437
8451
|
const vk = vbl.displayName;
|