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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "2.1.5",
3
+ "version": "2.1.6",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
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
- <select id="confirm-add-chart-variables-attribute"></select>
3210
- of <span id="confirm-add-chart-variables-count"></span>
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>
@@ -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, cv.sorted);
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
- // Copies string to clipboard and notifies user of #lines copied
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
- navigator.clipboard.writeText(string).catch(
3105
- () => UI.setMessage('Failed to copy to clipboard', 'warning'));
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
- // Workaround using deprecated execCommand
3108
- const ta = document.createElement('textarea');
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
- function listener(event) {
3121
- event.clipboardData.setData('text/html', html);
3122
- event.preventDefault();
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
- // Logs MB's of used heap memory to console (to detect memory leaks)
3131
- // NOTE: this feature is supported only by Chrome
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
- // Clear current selection unless Shift-key is pressed
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
- if(MODEL.running_experiment) {
783
- // NOTE: do NOT do this while VM is solving, as this would interfer!
784
- UI.notify('Selected run cannot be viewed while running an experiment');
785
- } else {
786
- // Show the messages for this run in the monitor
787
- VM.setRunMessages(n);
788
- // Update the chart
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: mouse move over title in viewer passes n = -1
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
- // No list unless a pattern OR a specified SUB-set of entity types.
154
- if(fp || et && et !== VM.entity_letters) {
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 seid = 'etr';
322
- for(let i = 0; i < enl.length; i++) {
323
- const e = MODEL.objectByID(enl[i]);
324
- if(e === se) seid += i;
325
- el.push(['<tr id="etr', i, '" class="dataset',
326
- (e === se ? ' sel-set' : ''), '" onclick="FINDER.selectEntity(\'',
327
- enl[i], '\', event.altKey);" onmouseover="FINDER.showInfo(\'', enl[i],
328
- '\', event.shiftKey);"><td draggable="true" ',
329
- 'ondragstart="FINDER.drag(event);"><img class="finder" src="images/',
330
- e.type.toLowerCase(), '.png">', e.displayName,
331
- '</td></tr>'].join(''));
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
- el.length, 'entity', 'entities');
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 && e.name !== '(no_actor)' && e.name !== '(top_cluster)' &&
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
- md.element('attribute').innerHTML = html;
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
- x_values.push(Math.abs(x_dict[v]) < this.near_zero ? 0 : x_dict[v]);
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: link IDs are based on the node codes rather than IDs, as this
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: link is named by its tab-separated node names
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: modifiers are appended as additional lines of text
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 'C';
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 f = parseFloat(data[i]);
195
+ const
196
+ v = data[i],
197
+ f = parseFloat(v);
195
198
  if(isNaN(f)) {
196
- data[i] = '\u26A0'; // Unicode warning sign
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 equals sign = then `str` must
409
- // equal the rest of the pattern to match; if it starts with a tilde
410
- // ~ then `str` must start with the rest of the pattern to match.
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
- if(pm.startsWith('=')) {
422
- match = (str === pm.substring(1));
423
- } else if(pm.startsWith('~')) {
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(pm.startsWith('=')) {
438
- res = '^' + res + '$';
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
- if(pm.startsWith('=')) {
450
- match = (str !== pm.substring(1));
451
- } else if(pm.startsWith('~')) {
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(pm.startsWith('=')) {
465
- res = '^' + res + '$';
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.3 * name.length)),
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
- a = name.split(' ');
980
- // Split words at '-' when wider than limit
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 > 0) {
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
- MONITOR.logMessage(1, 'POWER FLOW: ' +
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
- MONITOR.logMessage(1, POWER_GRID_MANAGER.messages.join('\n'));
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
- MONITOR.logMessage(bnr, msg);
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) v = 0;
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;