linny-r 1.1.21 → 1.1.23

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": "1.1.21",
3
+ "version": "1.1.23",
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
@@ -159,7 +159,9 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
159
159
  '[LINNY_R_VERSION]', v);
160
160
  // Update the version number in the browser's upper left corner
161
161
  document.getElementById('linny-r-version-number').innerHTML = v;
162
- if(info[1] !== 'up-to-date') {
162
+ // NOTE: server detects "version 0" when npmjs website was
163
+ // not reached; if so, do not suggest a new version exists
164
+ if(info[1] !== 'up-to-date' && info[1] !== '0') {
163
165
  // Inform user that newer version exists
164
166
  UI.check_update_modal.element('msg').innerHTML = [
165
167
  '<a href="', GITHUB_REPOSITORY,
@@ -1402,7 +1404,7 @@ and move the cursor over the status bar">
1402
1404
  <option value="1">Product</option>
1403
1405
  <option value="2">Cluster</option>
1404
1406
  <option value="3">Link</option>
1405
- <option value="4">Constraint</option>
1407
+ <!-- <option value="4">Constraint</option> -->
1406
1408
  <option value="5">Actor</option>
1407
1409
  <option value="6">Dataset</option>
1408
1410
  <option value="7">Equation</option>
@@ -1818,7 +1820,7 @@ NOTE: * and ? will be interpreted as wildcards"
1818
1820
  <option value="1">Product</option>
1819
1821
  <option value="2">Cluster</option>
1820
1822
  <option value="3">Link</option>
1821
- <option value="4">Constraint</option>
1823
+ <!-- <option value="4">Constraint</option> -->
1822
1824
  <option value="5">Actor</option>
1823
1825
  <option value="6">Dataset</option>
1824
1826
  <option value="7">Equation</option>
@@ -1955,7 +1957,7 @@ NOTE: * and ? will be interpreted as wildcards"
1955
1957
  <option value="1">Product</option>
1956
1958
  <option id="add-sa-variable-cluster" value="2">Cluster</option>
1957
1959
  <option value="3">Link</option>
1958
- <option value="4">Constraint</option>
1960
+ <!-- <option value="4">Constraint</option> -->
1959
1961
  <option value="5">Actor</option>
1960
1962
  <option value="6">Dataset</option>
1961
1963
  <option id="add-sa-variable-equation" value="7">Equation</option>
@@ -2107,6 +2109,8 @@ NOTE: * and ? will be interpreted as wildcards"
2107
2109
  <div id="xv-no-scale" class="color-scale no-colors"></div>
2108
2110
  <img id="xv-copy-btn" class="btn enab" src="images/table-to-clpbrd.png"
2109
2111
  title="Copy table to clipboard (as HTML)">
2112
+ <img id="xv-download-btn" class="btn enab" src="images/save-data.png"
2113
+ title="Download results (CSV file)">
2110
2114
  </div>
2111
2115
  </div>
2112
2116
  <div id="experiment-resize" class="resizer"></div>
@@ -2151,6 +2155,83 @@ NOTE: * and ? will be interpreted as wildcards"
2151
2155
  </div>
2152
2156
  </div>
2153
2157
 
2158
+ <!-- the DOWNLOAD dialog asks which outcomes to save, and how -->
2159
+ <div id="xp-download-modal" class="modal">
2160
+ <div id="xp-download-dlg" class="inp-dlg">
2161
+ <div class="dlg-title">
2162
+ Download results
2163
+ <img class="cancel-btn" src="images/cancel.png">
2164
+ <img class="ok-btn" src="images/ok.png">
2165
+ </div>
2166
+ <fieldset id="xp-download-variables">
2167
+ <legend>Variables:</legend>
2168
+ <div>
2169
+ <input type="radio" id="xp-download-selected-v"
2170
+ name="variables" value="selected">
2171
+ <label for="selected">Only the selected variable</label>
2172
+ </div>
2173
+ <div>
2174
+ <input type="radio" id="xp-download-all-v"
2175
+ name="variables" value="all">
2176
+ <label for="all">All variables (N =
2177
+ <span id="xp-download-var-count"></span>)
2178
+ </label>
2179
+ </div>
2180
+ </fieldset>
2181
+ <fieldset id="xp-download-runs">
2182
+ <legend>Runs:</legend>
2183
+ <div>
2184
+ <input type="radio" id="xp-download-selected-r"
2185
+ name="runs" value="selected">
2186
+ <label for="selected">Only the selected
2187
+ run<span id="xp-download-run-s">s</span>
2188
+ </label>
2189
+ </div>
2190
+ <div>
2191
+ <input type="radio" id="xp-download-all-r"
2192
+ name="runs" value="all">
2193
+ <label for="all">All runs (N =
2194
+ <span id="xp-download-run-count"></span>)
2195
+ </label>
2196
+ </div>
2197
+ </fieldset>
2198
+ <fieldset id="xp-download-data">
2199
+ <legend>Data:</legend>
2200
+ <div>
2201
+ <input type="checkbox" id="xp-download-statistics" value="1">
2202
+ <label for="stats">Statistics</label>
2203
+ </div>
2204
+ <div>
2205
+ <input type="checkbox" id="xp-download-series" value="1">
2206
+ <label for="series">Time series data</label>
2207
+ </div>
2208
+ <div>
2209
+ <input type="checkbox" id="xp-download-solver" value="1">
2210
+ <label for="series">Solver information</label>
2211
+ </div>
2212
+ </fieldset>
2213
+ <fieldset id="xp-download-format">
2214
+ <legend>Format:</legend>
2215
+ <div style="margin-left: 5px">
2216
+ <label for="xp-download-separator">Separator:</label>
2217
+ <select id="xp-download-separator">
2218
+ <option value="comma">Comma</option>
2219
+ <option value="semicolon">Semicolon</option>
2220
+ <option value="tab">Tab</option>
2221
+ </select>
2222
+ <label for="xp-download-quotes">String quotes:</label>
2223
+ <select id="xp-download-quotes">
2224
+ <option value="none">None</option>
2225
+ <option value="single">Single</option>
2226
+ <option value="double">Double</option>
2227
+ </select>
2228
+ <label for="xp-download-precision">Precision:</label>
2229
+ <input type="text" id="xp-download-precision"> digits
2230
+ </div>
2231
+ </fieldset>
2232
+ </div>
2233
+ </div>
2234
+
2154
2235
  <!-- the SETTINGS dialog permits editing the model settings dimensions -->
2155
2236
  <div id="xp-settings-modal" class="modal">
2156
2237
  <div id="xp-settings-dlg" class="inp-dlg">
@@ -2966,9 +2966,9 @@ td.sa-not-run {
2966
2966
  display: none;
2967
2967
  z-index: 20;
2968
2968
  margin: 0;
2969
- width: 400px;
2969
+ width: 425px;
2970
2970
  height: 275px;
2971
- min-width: 400px;
2971
+ min-width: 425px;
2972
2972
  min-height: 250px;
2973
2973
  max-height: 99vh;
2974
2974
  max-width: 99vw;
@@ -3403,7 +3403,8 @@ div.no-colors {
3403
3403
  #sa-copy-btn,
3404
3404
  #sa-copy-data-btn,
3405
3405
  #xv-copy-btn,
3406
- #xv-chart-btn {
3406
+ #xv-chart-btn,
3407
+ #xv-download-btn {
3407
3408
  display: inline-block;
3408
3409
  vertical-align: bottom;
3409
3410
  width: 19px;
@@ -3530,6 +3531,63 @@ span.sd-clear {
3530
3531
  margin-left: 5px;
3531
3532
  }
3532
3533
 
3534
+ /* the DOWNLOAD dialog prompts for selection of outcomes */
3535
+ #xp-download-dlg {
3536
+ width: 335px;
3537
+ height: 168px;
3538
+ }
3539
+
3540
+ #xp-download-variables {
3541
+ position: absolute;
3542
+ top: 22px;
3543
+ left: 1px;
3544
+ width: 163px;
3545
+ border: none;
3546
+ padding-left: 0px;
3547
+ }
3548
+
3549
+ #xp-download-runs {
3550
+ position: absolute;
3551
+ top: 22px;
3552
+ left: 180px;
3553
+ width: 146px;
3554
+ border: none;
3555
+ padding-left: 0px;
3556
+ }
3557
+
3558
+ #xp-download-data {
3559
+ position: absolute;
3560
+ top: 84px;
3561
+ left: 1px;
3562
+ width: 163px;
3563
+ border: none;
3564
+ padding-left: 0px;
3565
+ }
3566
+
3567
+ #xp-download-format {
3568
+ position: absolute;
3569
+ top: 84px;
3570
+ left: 180px;
3571
+ width: 146px;
3572
+ border: none;
3573
+ padding-left: 0px;
3574
+ }
3575
+
3576
+ #xp-download-separator,
3577
+ #xp-download-quotes {
3578
+ font-size: 11px;
3579
+ height: 19px;
3580
+ margin-bottom: 3px;
3581
+ }
3582
+
3583
+ #xp-download-precision {
3584
+ font-size: 12px;
3585
+ height: 15px !important;
3586
+ width: 20px;
3587
+ text-align: center;
3588
+ }
3589
+
3590
+
3533
3591
  /* the ACTOR DIMENSION dialog allows editing this dimension */
3534
3592
  #xp-actor-dimension-dlg {
3535
3593
  width: 150px;
@@ -738,8 +738,11 @@ class SensitivityAnalysis {
738
738
  oax = (obj ? obj.attributeExpression(vn[1]) : null);
739
739
  if(oax) {
740
740
  this.parameters.push(oax);
741
+ } else if(vn.length === 1 && obj instanceof Dataset) {
742
+ // Dataset without selector => push the dataset vector
743
+ this.parameters.push(obj.vector);
741
744
  } else {
742
- UI.alert(`Parameter ${p} is not an expression`);
745
+ UI.alert(`Parameter ${p} is not a dataset or expression`);
743
746
  }
744
747
  }
745
748
  this.chart = new Chart(this.chart_title);
@@ -841,6 +844,7 @@ class SensitivityAnalysis {
841
844
  VM.halt();
842
845
  this.readyButtons();
843
846
  this.showProgress('');
847
+ this.must_pause = false;
844
848
  }
845
849
 
846
850
  clearResults() {
@@ -2808,7 +2808,7 @@ class ModalDialog {
2808
2808
  }
2809
2809
 
2810
2810
  show(name=null) {
2811
- // Makes dialog visible and focuses on element with `focal`
2811
+ // Makes dialog visible and focuses on element with `focal`
2812
2812
  this.modal.style.display = 'block';
2813
2813
  if(name) this.element(name).focus();
2814
2814
  }
@@ -10513,7 +10513,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
10513
10513
  this.base_selectors.addEventListener(
10514
10514
  'blur', () => SENSITIVITY_ANALYSIS.setBaseSelectors());
10515
10515
 
10516
- this.delta = document.getElementById('sa-delta');
10516
+ this.delta = document.getElementById('sensitivity-delta');
10517
10517
  this.delta.addEventListener(
10518
10518
  'focus', () => SENSITIVITY_ANALYSIS.editDelta());
10519
10519
  this.delta.addEventListener(
@@ -10610,6 +10610,8 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
10610
10610
  }
10611
10611
 
10612
10612
  updateControlPanel() {
10613
+ // Shows the control panel, or when the analysis is running the
10614
+ // legend to the outcomes (also to prevent changes to parameters)
10613
10615
  this.base_selectors.value = MODEL.base_case_selectors;
10614
10616
  this.delta.value = VM.sig4Dig(MODEL.sensitivity_delta);
10615
10617
  const tr = [];
@@ -10900,6 +10902,8 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
10900
10902
  // NOTE: clusters have no suitable attributes, and equations are endogenous
10901
10903
  md.element('cluster').style.display = 'none';
10902
10904
  md.element('equation').style.display = 'none';
10905
+ // NOTE: update to ensure that valid attributes are selectable
10906
+ X_EDIT.updateVariableBar('add-sa-');
10903
10907
  md.show();
10904
10908
  }
10905
10909
 
@@ -10909,6 +10913,8 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
10909
10913
  md.element('type').innerText = 'outcome';
10910
10914
  md.element('cluster').style.display = 'block';
10911
10915
  md.element('equation').style.display = 'block';
10916
+ // NOTE: update to ensure that valid attributes are selectable
10917
+ X_EDIT.updateVariableBar('add-sa-');
10912
10918
  md.show();
10913
10919
  }
10914
10920
 
@@ -10977,9 +10983,14 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
10977
10983
  a = md.selectedOption('attr').text;
10978
10984
  let n = '';
10979
10985
  if(e === 'Equation' && a) {
10986
+ // For equations, the attribute denotes the name
10980
10987
  n = a;
10981
10988
  } else if(o && a) {
10989
+ // Most variables are defined by name + attribute ...
10982
10990
  n = o + UI.OA_SEPARATOR + a;
10991
+ } else if(e === 'Dataset' && o) {
10992
+ // ... but for datasets the selector is optional
10993
+ n = o;
10983
10994
  }
10984
10995
  if(n) {
10985
10996
  if(t === 'parameter' && MODEL.sensitivity_parameters.indexOf(n) < 0) {
@@ -11021,6 +11032,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
11021
11032
  this.start_btn.classList.add('off');
11022
11033
  this.pause_btn.classList.remove('off');
11023
11034
  this.stop_btn.classList.add('off');
11035
+ this.must_pause = false;
11024
11036
  return paused;
11025
11037
  }
11026
11038
 
@@ -11029,6 +11041,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
11029
11041
  this.pause_btn.classList.add('off');
11030
11042
  this.stop_btn.classList.add('off');
11031
11043
  this.start_btn.classList.remove('off', 'blink');
11044
+ this.must_pause = false;
11032
11045
  }
11033
11046
 
11034
11047
  pausedButtons(aci) {
@@ -11047,6 +11060,8 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
11047
11060
  this.readyButtons();
11048
11061
  this.reset_btn.classList.add('off');
11049
11062
  this.selected_run = -1;
11063
+ this.must_pause = false;
11064
+ this.progress.innerHTML = '';
11050
11065
  this.updateDialog();
11051
11066
  }
11052
11067
 
@@ -11293,6 +11308,8 @@ class GUIExperimentManager extends ExperimentManager {
11293
11308
  'click', () => EXPERIMENT_MANAGER.designMode());
11294
11309
  document.getElementById('xv-copy-btn').addEventListener(
11295
11310
  'click', () => EXPERIMENT_MANAGER.copyTableToClipboard());
11311
+ document.getElementById('xv-download-btn').addEventListener(
11312
+ 'click', () => EXPERIMENT_MANAGER.promptForDownload());
11296
11313
  // The viewer's drop-down selectors
11297
11314
  document.getElementById('viewer-variable').addEventListener(
11298
11315
  'change', () => EXPERIMENT_MANAGER.setVariable());
@@ -11391,6 +11408,12 @@ class GUIExperimentManager extends ExperimentManager {
11391
11408
  this.clusters_modal.element('delete-btn').addEventListener(
11392
11409
  'click', () => EXPERIMENT_MANAGER.deleteClusterFromIgnoreList());
11393
11410
 
11411
+ this.download_modal = new ModalDialog('xp-download');
11412
+ this.download_modal.ok.addEventListener(
11413
+ 'click', () => EXPERIMENT_MANAGER.downloadDataAsCSV());
11414
+ this.download_modal.cancel.addEventListener(
11415
+ 'click', () => EXPERIMENT_MANAGER.download_modal.hide());
11416
+
11394
11417
  // Initialize properties
11395
11418
  this.reset();
11396
11419
  }
@@ -11794,6 +11817,32 @@ class GUIExperimentManager extends ExperimentManager {
11794
11817
  }
11795
11818
  }
11796
11819
 
11820
+ toggleChartRow(r, n, shift) {
11821
+ // Toggle `n` consecutive rows, starting at row `r` (0 = top), to be
11822
+ // (no longer) part of the chart combination set
11823
+ const
11824
+ x = this.selected_experiment,
11825
+ // Let `n` be the number of the first run on row `r`
11826
+ nconf = r * this.nr_of_configurations;
11827
+ if(x && r < x.combinations.length / this.nr_of_configurations) {
11828
+ // NOTE: first cell of row determines ADD or REMOVE
11829
+ const add = x.chart_combinations.indexOf(n) < 0;
11830
+ for(let i = 0; i < this.nr_of_configurations; i++) {
11831
+ const ic = x.chart_combinations.indexOf(i);
11832
+ if(add) {
11833
+ if(ic < 0) x.chart_combinations.push(nconf + i);
11834
+ } else {
11835
+ if(!add) x.chart_combinations.splice(nconf + i, 1);
11836
+ }
11837
+ }
11838
+ this.updateData();
11839
+ }
11840
+ }
11841
+
11842
+ toggleChartColumn(c, shift) {
11843
+ // Toggle column `c` (0 = leftmost) to be part of the chart combination set
11844
+ }
11845
+
11797
11846
  toggleChartCombi(n, shift, alt) {
11798
11847
  // Set `n` to be the chart combination, or toggle if Shift-key is pressed,
11799
11848
  // or execute single run if Alt-key is pressed
@@ -12750,6 +12799,77 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
12750
12799
  UI.notify('Table copied to clipboard (as HTML)');
12751
12800
  }
12752
12801
 
12802
+ promptForDownload() {
12803
+ // Show the download modal
12804
+ const x = this.selected_experiment;
12805
+ if(!x) return;
12806
+ const
12807
+ md = this.download_modal,
12808
+ ds = x.download_settings,
12809
+ runs = x.runs.length,
12810
+ sruns = x.chart_combinations.length;
12811
+ if(!runs) {
12812
+ UI.notify('No experiment results');
12813
+ return;
12814
+ }
12815
+ md.element(ds.variables + '-v').checked = true;
12816
+ // Disable "selected runs" button when no runs have been selected
12817
+ if(sruns) {
12818
+ md.element('selected-r').disabled = false;
12819
+ md.element(ds.runs + '-r').checked = true;
12820
+ } else {
12821
+ md.element('selected-r').disabled = true;
12822
+ // Check "all runs" but do not change download setting
12823
+ md.element('all-r').checked = true;
12824
+ }
12825
+ this.download_modal.show();
12826
+ md.element('statistics').checked = ds.statistics;
12827
+ md.element('series').checked = ds.series;
12828
+ md.element('solver').checked = ds.solver;
12829
+ md.element('separator').value = ds.separator;
12830
+ md.element('quotes').value = ds.quotes;
12831
+ md.element('precision').value = ds.precision;
12832
+ md.element('var-count').innerText = x.runs[0].results.length;
12833
+ md.element('run-count').innerText = runs;
12834
+ md.element('run-s').innerText = (sruns === 1 ? '' : 's');
12835
+ }
12836
+
12837
+ downloadDataAsCSV() {
12838
+ // Push results to browser
12839
+ if(this.selected_experiment) {
12840
+ const md = this.download_modal;
12841
+ this.selected_experiment.download_settings = {
12842
+ variables: md.element('all-v').checked ? 'all' : 'selected',
12843
+ runs: md.element('all-r').checked ? 'all' : 'selected',
12844
+ statistics: md.element('statistics').checked,
12845
+ series: md.element('series').checked,
12846
+ solver: md.element('solver').checked,
12847
+ separator: md.element('separator').value,
12848
+ quotes: md.element('quotes').value,
12849
+ precision: safeStrToInt(md.element('precision').value,
12850
+ CONFIGURATION.results_precision),
12851
+ };
12852
+ md.hide();
12853
+ const data = this.selected_experiment.resultsAsCSV;
12854
+ if(data) {
12855
+ UI.setMessage('CSV file size: ' + UI.sizeInBytes(data.length));
12856
+ const el = document.getElementById('xml-saver');
12857
+ el.href = 'data:attachment/text,' + encodeURI(data);
12858
+ console.log('Encoded CSV file size:', el.href.length);
12859
+ el.download = 'results.csv';
12860
+ if(el.href.length > 25*1024*1024 &&
12861
+ navigator.userAgent.search('Chrome') <= 0) {
12862
+ UI.notify('CSV file size exceeds 25 MB. ' +
12863
+ 'If it does not download, select fewer runs');
12864
+ }
12865
+ el.click();
12866
+ UI.normalCursor();
12867
+ } else {
12868
+ UI.notify('No data');
12869
+ }
12870
+ }
12871
+ }
12872
+
12753
12873
  } // END of class GUIExperimentManager
12754
12874
 
12755
12875
 
@@ -10,7 +10,7 @@ Linny-R project.
10
10
  */
11
11
 
12
12
  /*
13
- Copyright (c) 2017-2022 Delft University of Technology
13
+ Copyright (c) 2017-2023 Delft University of Technology
14
14
 
15
15
  Permission is hereby granted, free of charge, to any person obtaining a copy
16
16
  of this software and associated documentation files (the "Software"), to deal
@@ -2485,12 +2485,15 @@ class LinnyRModel {
2485
2485
  const ds_dict = {};
2486
2486
  for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
2487
2487
  const ds = this.datasets[k];
2488
- for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
2489
- const s = ds.modifiers[m].selector;
2490
- if(s in ds_dict) {
2491
- ds_dict[s].push(ds);
2492
- } else {
2493
- ds_dict[s] = [ds];
2488
+ // NOTE: ignore selectors of the equations dataset
2489
+ if(ds !== this.equations_dataset) {
2490
+ for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
2491
+ const s = ds.modifiers[m].selector;
2492
+ if(s in ds_dict) {
2493
+ ds_dict[s].push(ds);
2494
+ } else {
2495
+ ds_dict[s] = [ds];
2496
+ }
2494
2497
  }
2495
2498
  }
2496
2499
  }
@@ -7482,71 +7485,7 @@ class Dataset {
7482
7485
  for(let k in this.modifiers) if(this.modifiers.hasOwnProperty(k)) {
7483
7486
  sl.push(this.modifiers[k].selector);
7484
7487
  }
7485
- return sl.sort((s1, s2) => {
7486
- // Dataset selectors comparison is case-insensitive, and puts wildcards
7487
- // last, where * comes later than ?
7488
- // NOTE: without wildcards, strings that are identical except for the
7489
- // digits they *end* on are sorted on this "end number" (so abc12 > abc2)
7490
- // NOTE: this also applies to percentages ("end number"+ %)
7491
- if(s1 === s2) return 0;
7492
- if(s1 === '*') return 1;
7493
- if(s2 === '*') return -1;
7494
- const
7495
- star1 = s1.indexOf('*'),
7496
- star2 = s2.indexOf('*');
7497
- if(star1 >= 0) {
7498
- if(star2 < 0) return 1;
7499
- return s1.localeCompare(s2);
7500
- }
7501
- if(star2 >= 0) return -1;
7502
- // Replace ? by | because | has a higher ASCII value than all other chars
7503
- let s_1 = s1.replace('?', '|').toLowerCase(),
7504
- s_2 = s2.replace('?', '|').toLowerCase(),
7505
- // NOTE: treat selectors ending on a number or percentage as special case
7506
- n_1 = endsWithDigits(s_1),
7507
- p_1 = (s1.endsWith('%') ? endsWithDigits(s1.slice(0, -1)) : '');
7508
- if(n_1) {
7509
- const
7510
- ss_1 = s1.slice(0, -n_1.length),
7511
- n_2 = endsWithDigits(s2);
7512
- if(n_2 && ss_1 === s2.slice(0, -n_2.length)) {
7513
- return parseInt(n_1) - parseInt(n_2);
7514
- }
7515
- } else if(p_1) {
7516
- const
7517
- ss_1 = s1.slice(0, -p_1.length - 1),
7518
- p_2 = (s2.endsWith('%') ? endsWithDigits(s2.slice(0, -1)) : '');
7519
- if(p_2 && ss_1 === s2.slice(0, -p_2.length - 1)) {
7520
- return parseInt(p_1) - parseInt(p_2);
7521
- }
7522
- }
7523
- // Also sort selectors ending on minuses lower than those ending on plusses,
7524
- // and such that X-- comes before X-, like X+ automatically comes before X++
7525
- // ASCII(+) = 43, ASCII(-) = 45, so replace trailing minuses by as many spaces
7526
- // (ASCII 32) and add a '!' (ASCII 33) -- this then "sorts things out"
7527
- let n = s_1.length,
7528
- i = n - 1;
7529
- while(i >= 0 && s_1[i] === '-') i--;
7530
- // If trailing minuses, replace by as many spaces and add an exclamation point
7531
- if(i < n - 1) {
7532
- s_1 = s_1.substr(0, i);
7533
- while(s_1.length < n) s_1 += ' ';
7534
- s_1 += '!';
7535
- }
7536
- // Do the same for the second "normalized" selector
7537
- n = s_2.length;
7538
- i = n - 1;
7539
- while(i >= 0 && s_2[i] === '-') i--;
7540
- if(i < n - 1) {
7541
- s_2 = s_2.substr(0, i);
7542
- while(s_2.length < n) s_2 += ' ';
7543
- s_2 += '!';
7544
- }
7545
- // Now compare the two "normalized" selectors
7546
- if(s_1 < s_2) return -1;
7547
- if(s_1 > s_2) return 1;
7548
- return 0;
7549
- });
7488
+ return sl.sort(compareSelectors);
7550
7489
  }
7551
7490
 
7552
7491
  get plainSelectors() {
@@ -7708,9 +7647,11 @@ class Dataset {
7708
7647
  attributeExpression(a) {
7709
7648
  // Returns expression for selector `a`, or NULL if no such selector exists
7710
7649
  // NOTE: selectors no longer are case-sensitive
7711
- a = UI.nameToID(a);
7712
- for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
7713
- if(m === a) return this.modifiers[m].expression;
7650
+ if(a) {
7651
+ a = UI.nameToID(a);
7652
+ for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
7653
+ if(m === a) return this.modifiers[m].expression;
7654
+ }
7714
7655
  }
7715
7656
  return null;
7716
7657
  }
@@ -7803,9 +7744,14 @@ class Dataset {
7803
7744
  p += ' outcome="1"';
7804
7745
  }
7805
7746
  if(this.black_box) p += ' black-box="1"';
7806
- const ml = [];
7747
+ const ml = [],
7748
+ sl = [];
7807
7749
  for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
7808
- ml.push(this.modifiers[m].asXML);
7750
+ sl.push(m);
7751
+ }
7752
+ sl.sort(compareSelectors);
7753
+ for(let i = 0; i < sl.length; i++) {
7754
+ ml.push(this.modifiers[sl[i]].asXML);
7809
7755
  }
7810
7756
  // NOTE: "black-boxed" datasets are stored anonymously without comments
7811
7757
  const id = UI.nameToID(n);
@@ -9285,7 +9231,12 @@ class ExperimentRunResult {
9285
9231
  obj = MODEL.objectByID(this.object_id),
9286
9232
  dn = obj.displayName;
9287
9233
  // NOTE: for equations dataset, only display the modifier selector
9288
- if(obj === MODEL.equations_dataset) return this.attribute;
9234
+ if(obj === MODEL.equations_dataset) {
9235
+ const m = obj.modifiers[this.attribute.toLowerCase()];
9236
+ if(m) return m.selector;
9237
+ console.log('WARNING: Run result of non-existent equation', this.attribute);
9238
+ return this.attribute;
9239
+ }
9289
9240
  return (this.attribute ? dn + '|' + this.attribute : dn);
9290
9241
  }
9291
9242
 
@@ -9651,6 +9602,16 @@ class Experiment {
9651
9602
  constructor(n) {
9652
9603
  this.title = n;
9653
9604
  this.comments = '';
9605
+ this.download_settings = {
9606
+ variables: 'selected',
9607
+ runs: 'selected',
9608
+ statistics: true,
9609
+ series: false,
9610
+ solver: false,
9611
+ separator: 'semicolon',
9612
+ quotes: 'none',
9613
+ precision: CONFIGURATION.results_precision
9614
+ };
9654
9615
  this.dimensions = [];
9655
9616
  this.charts = [];
9656
9617
  this.actual_dimensions = [];
@@ -9774,6 +9735,14 @@ class Experiment {
9774
9735
  (this.completed ? '" completed="1' : ''),
9775
9736
  '" started="', this.time_started,
9776
9737
  '" stopped="', this.time_stopped,
9738
+ '" variables="', this.download_settings.variables,
9739
+ '" runs="', this.download_settings.runs,
9740
+ '" statistics="', this.download_settings.statistics ? 1 : 0,
9741
+ '" series="', this.download_settings.series ? 1 : 0,
9742
+ '" solver="', this.download_settings.solver ? 1 : 0,
9743
+ '" separator="', this.download_settings.separator,
9744
+ '" quotes="', this.download_settings.quotes,
9745
+ '" precision="', this.download_settings.precision,
9777
9746
  '"><title>', xmlEncoded(this.title),
9778
9747
  '</title><notes>', xmlEncoded(this.comments),
9779
9748
  '</notes><dimensions>', d,
@@ -9796,6 +9765,18 @@ class Experiment {
9796
9765
  this.completed = nodeParameterValue(node, 'completed') === '1';
9797
9766
  this.time_started = safeStrToInt(nodeParameterValue(node, 'started'));
9798
9767
  this.time_stopped = safeStrToInt(nodeParameterValue(node, 'stopped'));
9768
+ // Restore last download dialog settings for this experiment
9769
+ this.download_settings = {
9770
+ variables: nodeParameterValue(node, 'variables') || 'selected',
9771
+ runs: nodeParameterValue(node, 'runs') || 'selected',
9772
+ statistics: nodeParameterValue(node, 'statistics') !== '0',
9773
+ series: nodeParameterValue(node, 'series') === '1',
9774
+ solver: nodeParameterValue(node, 'solver') === '1',
9775
+ separator: nodeParameterValue(node, 'separator') || 'semicolon',
9776
+ quotes: nodeParameterValue(node, 'quotes') || 'none',
9777
+ precision: safeStrToInt(nodeParameterValue(node, 'precision'),
9778
+ CONFIGURATION.results_precision)
9779
+ };
9799
9780
  this.title = xmlDecoded(nodeContentByTag(node, 'title'));
9800
9781
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
9801
9782
  let c, n = childNodeByTag(node, 'dimensions');
@@ -10021,6 +10002,135 @@ class Experiment {
10021
10002
  return null;
10022
10003
  }
10023
10004
 
10005
+ get resultsAsCSV() {
10006
+ // Return results as specfied by the download settings
10007
+ // NOTE: no runs => no results => return empty string
10008
+ if(this.runs.length === 0) return '';
10009
+ const
10010
+ // Local function to convert number to string
10011
+ numval = (v, p) => {
10012
+ // Return 0 as single digit
10013
+ if(Math.abs(v) < VM.NEAR_ZERO) return '0';
10014
+ // Return empty string for undefined or exceptional values
10015
+ if(!v || v < VM.MINUS_INFINITY || v > VM.PLUS_INFINITY) return '';
10016
+ // Return other values as float with specified precision
10017
+ return v.toPrecision(p);
10018
+ },
10019
+ prec = this.download_settings.precision,
10020
+ allruns = this.download_settings.runs === 'all',
10021
+ sep = (this.download_settings.separator === 'tab' ? '\t' :
10022
+ (this.download_settings.separator === 'comma' ? ',' : ';')),
10023
+ quo = (this.download_settings.quotes === 'single' ? "'" :
10024
+ (this.download_settings.quotes === 'double' ? '"' : '')),
10025
+ vars = [],
10026
+ data = {
10027
+ nr: `${quo}Run number${quo}${sep}`,
10028
+ combi: `${quo}Selectors${quo}${sep}`,
10029
+ rsecs: `${quo}Run duration${quo}${sep}`,
10030
+ ssecs: `${quo}Solver time${quo}${sep}`,
10031
+ warnings: `${quo}Warnings${quo}${sep}`,
10032
+ variable: `${quo}Variable${quo}${sep}`,
10033
+ N: `${quo}N${quo}${sep}`,
10034
+ sum: `${quo}Sum${quo}${sep}`,
10035
+ mean: `${quo}Mean${quo}${sep}`,
10036
+ variance: `${quo}Variance${quo}${sep}`,
10037
+ minimum: `${quo}Minimum${quo}${sep}`,
10038
+ maximum: `${quo}Maximum${quo}${sep}`,
10039
+ NZ: `${quo}Non-zero${quo}${sep}`,
10040
+ last: `${quo}Last${quo}${sep}`,
10041
+ exceptions: `${quo}Exceptions${quo}${sep}`,
10042
+ run: []
10043
+ };
10044
+ for(let i = 0; i < this.combinations.length; i++) {
10045
+ if(i < this.runs.length &&
10046
+ (allruns || this.chart_combinations.indexOf(i) >= 0)) {
10047
+ data.run.push(i);
10048
+ }
10049
+ }
10050
+ let series_length = 0,
10051
+ // By default, assume all variables to be output
10052
+ start = 0,
10053
+ stop = this.runs[0].results.length;
10054
+ if(this.download_settings.variables === 'selected') {
10055
+ // Only one variable
10056
+ start = this.resultIndex(this.selected_variable);
10057
+ stop = start + 1;
10058
+ }
10059
+ for(let i = 0; i < data.run.length; i++) {
10060
+ const
10061
+ rnr = data.run[i],
10062
+ r = this.runs[rnr];
10063
+ data.nr += r.number;
10064
+ data.combi += quo + this.combinations[rnr].join('|') + quo;
10065
+ // Run duration in seconds
10066
+ data.rsecs += numval((r.time_recorded - r.time_started) * 0.001, 4);
10067
+ data.ssecs += numval(r.solver_seconds, 4);
10068
+ data.warnings += r.warning_count;
10069
+ for(let j = start; j < stop; j++) {
10070
+ // Add empty cells for run attributes
10071
+ data.nr += sep;
10072
+ data.combi += sep;
10073
+ data.rsecs += sep;
10074
+ data.ssecs += sep;
10075
+ data.warnings += sep;
10076
+ const rr = r.results[j];
10077
+ if(rr) {
10078
+ data.variable += rr.displayName + sep;
10079
+ // Series may differ in length; the longest determines the
10080
+ // number of rows of series data to be added
10081
+ series_length = Math.max(series_length, rr.vector.length);
10082
+ if(this.download_settings.statistics) {
10083
+ data.N += rr.N + sep;
10084
+ data.sum += numval(rr.sum, prec) + sep;
10085
+ data.mean += numval(rr.mean, prec) + sep;
10086
+ data.variance += numval(rr.variance, prec) + sep;
10087
+ data.minimum += numval(rr.minimum, prec) + sep;
10088
+ data.maximum += numval(rr.maximum, prec) + sep;
10089
+ data.NZ += rr.non_zero_tally + sep;
10090
+ data.last += numval(rr.last, prec) + sep;
10091
+ data.exceptions += rr.exceptions + sep;
10092
+ }
10093
+ } else {
10094
+ console.log('No run results for ', this.variables[vars[j]].displayName);
10095
+ }
10096
+ }
10097
+ }
10098
+ const ds = [data.nr, data.combi];
10099
+ if(this.download_settings.solver) {
10100
+ ds.push(data.rsecs, data.ssecs, data.warnings);
10101
+ }
10102
+ // Always add the row with variable names
10103
+ ds.push(data.variable);
10104
+ if(this.download_settings.statistics) {
10105
+ ds.push(data.N, data.sum, data.mean, data.variance, data.minimum,
10106
+ data.maximum, data.NZ, data.last, data.exceptions);
10107
+ }
10108
+ if(this.download_settings.series) {
10109
+ ds.push('t');
10110
+ const row = [];
10111
+ for(let i = 0; i < series_length; i++) {
10112
+ row.length = 0;
10113
+ row.push(i);
10114
+ for(let j = 0; j < data.run.length; j++) {
10115
+ const rnr = data.run[j];
10116
+ for(let k = start; k < stop; k++) {
10117
+ const rr = this.runs[rnr].results[k];
10118
+ if(rr) {
10119
+ // NOTE: only experiment variables have vector data
10120
+ if(rr.x_variable && i <= rr.N) {
10121
+ row.push(numval(rr.vector[i], prec));
10122
+ } else {
10123
+ row.push('');
10124
+ }
10125
+ }
10126
+ }
10127
+ }
10128
+ ds.push(row.join(sep));
10129
+ }
10130
+ }
10131
+ return ds.join('\n');
10132
+ }
10133
+
10024
10134
  } // END of CLASS Experiment
10025
10135
 
10026
10136
 
@@ -9,7 +9,7 @@ This JavaScript file (linny-r-utils.js) defines a variety of "helper" functions
9
9
  that are used in other Linny-R modules.
10
10
  */
11
11
  /*
12
- Copyright (c) 2017-2022 Delft University of Technology
12
+ Copyright (c) 2017-2023 Delft University of Technology
13
13
 
14
14
  Permission is hereby granted, free of charge, to any person obtaining a copy
15
15
  of this software and associated documentation files (the "Software"), to deal
@@ -326,6 +326,72 @@ function escapeRegex(str) {
326
326
  return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
327
327
  }
328
328
 
329
+ function compareSelectors(s1, s2) {
330
+ // Dataset selectors comparison is case-insensitive, and puts wildcards
331
+ // last, where * comes later than ?
332
+ // NOTE: without wildcards, strings that are identical except for the
333
+ // digits they *end* on are sorted on this "end number" (so abc12 > abc2)
334
+ // NOTE: this also applies to percentages ("end number"+ %)
335
+ if(s1 === s2) return 0;
336
+ if(s1 === '*') return 1;
337
+ if(s2 === '*') return -1;
338
+ const
339
+ star1 = s1.indexOf('*'),
340
+ star2 = s2.indexOf('*');
341
+ if(star1 >= 0) {
342
+ if(star2 < 0) return 1;
343
+ return s1.localeCompare(s2);
344
+ }
345
+ if(star2 >= 0) return -1;
346
+ // Replace ? by | because | has a higher ASCII value than all other chars
347
+ let s_1 = s1.replace('?', '|').toLowerCase(),
348
+ s_2 = s2.replace('?', '|').toLowerCase(),
349
+ // NOTE: treat selectors ending on a number or percentage as special case
350
+ n_1 = endsWithDigits(s_1),
351
+ p_1 = (s1.endsWith('%') ? endsWithDigits(s1.slice(0, -1)) : '');
352
+ if(n_1) {
353
+ const
354
+ ss_1 = s1.slice(0, -n_1.length),
355
+ n_2 = endsWithDigits(s2);
356
+ if(n_2 && ss_1 === s2.slice(0, -n_2.length)) {
357
+ return parseInt(n_1) - parseInt(n_2);
358
+ }
359
+ } else if(p_1) {
360
+ const
361
+ ss_1 = s1.slice(0, -p_1.length - 1),
362
+ p_2 = (s2.endsWith('%') ? endsWithDigits(s2.slice(0, -1)) : '');
363
+ if(p_2 && ss_1 === s2.slice(0, -p_2.length - 1)) {
364
+ return parseInt(p_1) - parseInt(p_2);
365
+ }
366
+ }
367
+ // Also sort selectors ending on minuses lower than those ending on plusses,
368
+ // and such that X-- comes before X-, like X+ automatically comes before X++
369
+ // ASCII(+) = 43, ASCII(-) = 45, so replace trailing minuses by as many spaces
370
+ // (ASCII 32) and add a '!' (ASCII 33) -- this then "sorts things out"
371
+ let n = s_1.length,
372
+ i = n - 1;
373
+ while(i >= 0 && s_1[i] === '-') i--;
374
+ // If trailing minuses, replace by as many spaces and add an exclamation point
375
+ if(i < n - 1) {
376
+ s_1 = s_1.substr(0, i);
377
+ while(s_1.length < n) s_1 += ' ';
378
+ s_1 += '!';
379
+ }
380
+ // Do the same for the second "normalized" selector
381
+ n = s_2.length;
382
+ i = n - 1;
383
+ while(i >= 0 && s_2[i] === '-') i--;
384
+ if(i < n - 1) {
385
+ s_2 = s_2.substr(0, i);
386
+ while(s_2.length < n) s_2 += ' ';
387
+ s_2 += '!';
388
+ }
389
+ // Now compare the two "normalized" selectors
390
+ if(s_1 < s_2) return -1;
391
+ if(s_1 > s_2) return 1;
392
+ return 0;
393
+ }
394
+
329
395
  //
330
396
  // Functions that perform set-like operations on lists of string
331
397
  //
@@ -5182,7 +5182,16 @@ function VMI_push_var(x, args) {
5182
5182
  }
5183
5183
  if(Array.isArray(obj)) {
5184
5184
  // Object is a vector
5185
- x.push(t < obj.length ? obj[t] : VM.UNDEFINED);
5185
+ let v = t < obj.length ? obj[t] : VM.UNDEFINED;
5186
+ // NOTE: when the vector is the "active" parameter for sensitivity
5187
+ // analysis, the value is multiplied by 1 + delta %
5188
+ if(obj === MODEL.active_sensitivity_parameter) {
5189
+ // NOTE: do NOT scale exceptional values
5190
+ if(v > VM.MINUS_INFINITY && v < VM.PLUS_INFINITY) {
5191
+ v *= (1 + MODEL.sensitivity_delta * 0.01);
5192
+ }
5193
+ }
5194
+ x.push(v);
5186
5195
  } else if(xv) {
5187
5196
  // Variable references an earlier value computed for this expression `x`
5188
5197
  x.push(t >= 0 && t < x.vector.length ? x.vector[t] : obj.dv);
@@ -5315,8 +5324,10 @@ function VMI_push_dataset_modifier(x, args) {
5315
5324
  tot[1] + (tot[2] ? ':' + tot[2] : ''), ' value = ', VM.sig4Dig(v));
5316
5325
  console.log(' --', x.text, ' for owner ', x.object.displayName, x.attribute);
5317
5326
  }
5318
- // NOTE: unless error, push default value if exceptional ("undefined", etc.)
5319
- x.push(v < VM.PLUS_INFINITY ? v : ds.defaultValue);
5327
+ // NOTE: if value is exceptional ("undefined", etc.), use default value
5328
+ if(v >= VM.PLUS_INFINITY) v = ds.defaultValue;
5329
+ // Finally, push the value onto the expression stack
5330
+ x.push(v);
5320
5331
  }
5321
5332
 
5322
5333
 
@@ -6034,21 +6045,19 @@ function VMI_jump(x, index) {
6034
6045
  }
6035
6046
 
6036
6047
  function VMI_jump_if_false(x, index) {
6037
- // Pops the top number A from the stack, and if A is FALSE (zero or
6048
+ // Tests the top number A of the stack, and if A is FALSE (zero or
6038
6049
  // VM.UNDEFINED) sets the program counter of the VM to `index` minus 1,
6039
6050
  // as the counter is ALWAYS increased by 1 after calling a VMI function
6040
6051
  const r = x.top(true);
6041
- // NOTE: FALSE indicates a stack error => skip this operation
6042
- if(r !== false) {
6043
- if(DEBUGGING) console.log(`JUMP-IF-FALSE (${r}, ${index})`);
6044
- if(r === 0 || r === VM.UNDEFINED) {
6045
- // Only jump on FALSE, leaving the stack "as is", so that in case
6046
- // of no THEN the expression result equals the IF condition value
6047
- x.program_counter = index - 1;
6048
- } else {
6049
- // Remove the value from the stack
6050
- x.stack.pop();
6051
- }
6052
+ if(DEBUGGING) console.log(`JUMP-IF-FALSE (${r}, ${index})`);
6053
+ if(r === 0 || r === VM.UNDEFINED || r === false) {
6054
+ // Only jump on FALSE, leaving the stack "as is", so that in case
6055
+ // of no THEN the expression result equals the IF condition value
6056
+ // NOTE: Also do this on a stack error (r === false)
6057
+ x.program_counter = index - 1;
6058
+ } else {
6059
+ // Remove the value from the stack
6060
+ x.stack.pop();
6052
6061
  }
6053
6062
  }
6054
6063