linny-r 1.1.20 → 1.1.22

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/README.md CHANGED
@@ -25,7 +25,7 @@ Technical documentation will be developed on GitHub: https://github.com/pwgbots/
25
25
  Linny-R is developed as a JavaScript package, and requires that **Node.js** is installed on your computer.
26
26
  This software can be downloaded from <a href="https://nodejs.org" target="_blank">https://nodejs.org</a>.
27
27
  Make sure that you choose the correct installer for your computer.
28
- Linny-R is developed using the _current_ release. Presently (October 2022) this is 19.0.0.
28
+ Linny-R is developed using the _current_ release. Presently (February 2023) this is 19.6.0.
29
29
 
30
30
  Run the installer and accept the default settings.
31
31
  There is **no** need to install the optional _Tools for Native Modules_.
@@ -36,7 +36,7 @@ Verify the installation by typing:
36
36
 
37
37
  ``node --version``
38
38
 
39
- The response should be the version number of Node.js, for example: v19.0.0.
39
+ The response should be the version number of Node.js, for example: v19.6.0.
40
40
 
41
41
  ## Installing Linny-R
42
42
  It is advisable to install Linny-R in a directory on your computer, not in a cloud.
@@ -169,8 +169,8 @@ Open the Command Line Interface (CLI) of your computer, change to your `WORKING_
169
169
  This response should be something similar to:
170
170
 
171
171
  <pre>
172
- Node.js server for Linny-R version 1.1.14
173
- Node.js version: v19.0.0
172
+ Node.js server for Linny-R version 1.1.21
173
+ Node.js version: v19.6.0
174
174
  ... etc.
175
175
  </pre>
176
176
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.1.20",
3
+ "version": "1.1.22",
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,
@@ -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;
@@ -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
  }
@@ -6739,9 +6739,14 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
6739
6739
  options.push('<option selected disabled value="-1"></option>');
6740
6740
  const d = MODEL.equations_dataset;
6741
6741
  if(d) {
6742
+ const slist = [];
6742
6743
  for(let m in d.modifiers) if(d.modifiers.hasOwnProperty(m)) {
6743
- const s = d.modifiers[m].selector;
6744
- options.push(`<option value="${s}">${s}</option>`);
6744
+ slist.push(d.modifiers[m].selector);
6745
+ }
6746
+ // Sort to present equations in alphabetical order
6747
+ slist.sort();
6748
+ for(let i = 0; i < slist.length; i++) {
6749
+ options.push(`<option value="${slist[i]}">${slist[i]}</option>`);
6745
6750
  }
6746
6751
  }
6747
6752
  va.innerHTML = options.join('');
@@ -9819,6 +9824,12 @@ class GUIChartManager extends ChartManager {
9819
9824
  } else if(this.chart_index >= 0) {
9820
9825
  // Only accept when all conditions are met
9821
9826
  ev.preventDefault();
9827
+ if(obj instanceof DatasetModifier) {
9828
+ // Equations can be added directly as chart variable
9829
+ this.addVariable(obj.name);
9830
+ return;
9831
+ }
9832
+ // For other entities, the attribute must be specified
9822
9833
  this.add_variable_modal.show();
9823
9834
  const
9824
9835
  tn = VM.object_types.indexOf(obj.type),
@@ -11282,6 +11293,8 @@ class GUIExperimentManager extends ExperimentManager {
11282
11293
  'click', () => EXPERIMENT_MANAGER.designMode());
11283
11294
  document.getElementById('xv-copy-btn').addEventListener(
11284
11295
  'click', () => EXPERIMENT_MANAGER.copyTableToClipboard());
11296
+ document.getElementById('xv-download-btn').addEventListener(
11297
+ 'click', () => EXPERIMENT_MANAGER.promptForDownload());
11285
11298
  // The viewer's drop-down selectors
11286
11299
  document.getElementById('viewer-variable').addEventListener(
11287
11300
  'change', () => EXPERIMENT_MANAGER.setVariable());
@@ -11380,6 +11393,12 @@ class GUIExperimentManager extends ExperimentManager {
11380
11393
  this.clusters_modal.element('delete-btn').addEventListener(
11381
11394
  'click', () => EXPERIMENT_MANAGER.deleteClusterFromIgnoreList());
11382
11395
 
11396
+ this.download_modal = new ModalDialog('xp-download');
11397
+ this.download_modal.ok.addEventListener(
11398
+ 'click', () => EXPERIMENT_MANAGER.downloadDataAsCSV());
11399
+ this.download_modal.cancel.addEventListener(
11400
+ 'click', () => EXPERIMENT_MANAGER.download_modal.hide());
11401
+
11383
11402
  // Initialize properties
11384
11403
  this.reset();
11385
11404
  }
@@ -12739,6 +12758,77 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
12739
12758
  UI.notify('Table copied to clipboard (as HTML)');
12740
12759
  }
12741
12760
 
12761
+ promptForDownload() {
12762
+ // Show the download modal
12763
+ const x = this.selected_experiment;
12764
+ if(!x) return;
12765
+ const
12766
+ md = this.download_modal,
12767
+ ds = x.download_settings,
12768
+ runs = x.runs.length,
12769
+ sruns = x.chart_combinations.length;
12770
+ if(!runs) {
12771
+ UI.notify('No experiment results');
12772
+ return;
12773
+ }
12774
+ md.element(ds.variables + '-v').checked = true;
12775
+ // Disable "selected runs" button when no runs have been selected
12776
+ if(sruns) {
12777
+ md.element('selected-r').disabled = false;
12778
+ md.element(ds.runs + '-r').checked = true;
12779
+ } else {
12780
+ md.element('selected-r').disabled = true;
12781
+ // Check "all runs" but do not change download setting
12782
+ md.element('all-r').checked = true;
12783
+ }
12784
+ this.download_modal.show();
12785
+ md.element('statistics').checked = ds.statistics;
12786
+ md.element('series').checked = ds.series;
12787
+ md.element('solver').checked = ds.solver;
12788
+ md.element('separator').value = ds.separator;
12789
+ md.element('quotes').value = ds.quotes;
12790
+ md.element('precision').value = ds.precision;
12791
+ md.element('var-count').innerText = x.variables.length;
12792
+ md.element('run-count').innerText = runs;
12793
+ md.element('run-s').innerText = (sruns === 1 ? '' : 's');
12794
+ }
12795
+
12796
+ downloadDataAsCSV() {
12797
+ // Push results to browser
12798
+ if(this.selected_experiment) {
12799
+ const md = this.download_modal;
12800
+ this.selected_experiment.download_settings = {
12801
+ variables: md.element('all-v').checked ? 'all' : 'selected',
12802
+ runs: md.element('all-r').checked ? 'all' : 'selected',
12803
+ statistics: md.element('statistics').checked,
12804
+ series: md.element('series').checked,
12805
+ solver: md.element('solver').checked,
12806
+ separator: md.element('separator').value,
12807
+ quotes: md.element('quotes').value,
12808
+ precision: safeStrToInt(md.element('precision').value,
12809
+ CONFIGURATION.results_precision),
12810
+ };
12811
+ md.hide();
12812
+ const data = this.selected_experiment.resultsAsCSV;
12813
+ if(data) {
12814
+ UI.setMessage('CSV file size: ' + UI.sizeInBytes(data.length));
12815
+ const el = document.getElementById('xml-saver');
12816
+ el.href = 'data:attachment/text,' + encodeURI(data);
12817
+ console.log('Encoded CSV file size:', el.href.length);
12818
+ el.download = 'results.csv';
12819
+ if(el.href.length > 25*1024*1024 &&
12820
+ navigator.userAgent.search('Chrome') <= 0) {
12821
+ UI.notify('CSV file size exceeds 25 MB. ' +
12822
+ 'If it does not download, select fewer runs');
12823
+ }
12824
+ el.click();
12825
+ UI.normalCursor();
12826
+ } else {
12827
+ UI.notify('No data');
12828
+ }
12829
+ }
12830
+ }
12831
+
12742
12832
  } // END of class GUIExperimentManager
12743
12833
 
12744
12834
 
@@ -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
@@ -7482,71 +7482,7 @@ class Dataset {
7482
7482
  for(let k in this.modifiers) if(this.modifiers.hasOwnProperty(k)) {
7483
7483
  sl.push(this.modifiers[k].selector);
7484
7484
  }
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
- });
7485
+ return sl.sort(compareSelectors);
7550
7486
  }
7551
7487
 
7552
7488
  get plainSelectors() {
@@ -7803,9 +7739,14 @@ class Dataset {
7803
7739
  p += ' outcome="1"';
7804
7740
  }
7805
7741
  if(this.black_box) p += ' black-box="1"';
7806
- const ml = [];
7742
+ const ml = [],
7743
+ sl = [];
7807
7744
  for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
7808
- ml.push(this.modifiers[m].asXML);
7745
+ sl.push(m);
7746
+ }
7747
+ sl.sort(compareSelectors);
7748
+ for(let i = 0; i < sl.length; i++) {
7749
+ ml.push(this.modifiers[sl[i]].asXML);
7809
7750
  }
7810
7751
  // NOTE: "black-boxed" datasets are stored anonymously without comments
7811
7752
  const id = UI.nameToID(n);
@@ -9651,6 +9592,16 @@ class Experiment {
9651
9592
  constructor(n) {
9652
9593
  this.title = n;
9653
9594
  this.comments = '';
9595
+ this.download_settings = {
9596
+ variables: 'selected',
9597
+ runs: 'selected',
9598
+ statistics: true,
9599
+ series: false,
9600
+ solver: false,
9601
+ separator: 'semicolon',
9602
+ quotes: 'none',
9603
+ precision: CONFIGURATION.results_precision
9604
+ };
9654
9605
  this.dimensions = [];
9655
9606
  this.charts = [];
9656
9607
  this.actual_dimensions = [];
@@ -9774,6 +9725,14 @@ class Experiment {
9774
9725
  (this.completed ? '" completed="1' : ''),
9775
9726
  '" started="', this.time_started,
9776
9727
  '" stopped="', this.time_stopped,
9728
+ '" variables="', this.download_settings.variables,
9729
+ '" runs="', this.download_settings.runs,
9730
+ '" statistics="', this.download_settings.statistics ? 1 : 0,
9731
+ '" series="', this.download_settings.series ? 1 : 0,
9732
+ '" solver="', this.download_settings.solver ? 1 : 0,
9733
+ '" separator="', this.download_settings.separator,
9734
+ '" quotes="', this.download_settings.quotes,
9735
+ '" precision="', this.download_settings.precision,
9777
9736
  '"><title>', xmlEncoded(this.title),
9778
9737
  '</title><notes>', xmlEncoded(this.comments),
9779
9738
  '</notes><dimensions>', d,
@@ -9796,6 +9755,18 @@ class Experiment {
9796
9755
  this.completed = nodeParameterValue(node, 'completed') === '1';
9797
9756
  this.time_started = safeStrToInt(nodeParameterValue(node, 'started'));
9798
9757
  this.time_stopped = safeStrToInt(nodeParameterValue(node, 'stopped'));
9758
+ // Restore last download dialog settings for this experiment
9759
+ this.download_settings = {
9760
+ variables: nodeParameterValue(node, 'variables') || 'selected',
9761
+ runs: nodeParameterValue(node, 'runs') || 'selected',
9762
+ statistics: nodeParameterValue(node, 'statistics') !== '0',
9763
+ series: nodeParameterValue(node, 'series') === '1',
9764
+ solver: nodeParameterValue(node, 'solver') === '1',
9765
+ separator: nodeParameterValue(node, 'separator') || 'semicolon',
9766
+ quotes: nodeParameterValue(node, 'quotes') || 'none',
9767
+ precision: safeStrToInt(nodeParameterValue(node, 'precision'),
9768
+ CONFIGURATION.results_precision)
9769
+ };
9799
9770
  this.title = xmlDecoded(nodeContentByTag(node, 'title'));
9800
9771
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
9801
9772
  let c, n = childNodeByTag(node, 'dimensions');
@@ -10021,6 +9992,124 @@ class Experiment {
10021
9992
  return null;
10022
9993
  }
10023
9994
 
9995
+ get resultsAsCSV() {
9996
+ // Return results as specfied by the download settings
9997
+ const
9998
+ allruns = this.download_settings.runs === 'all',
9999
+ sep = (this.download_settings.separator === 'tab' ? '\t' :
10000
+ (this.download_settings.separator === 'comma' ? ',' : ';')),
10001
+ quo = (this.download_settings.quotes === 'single' ? "'" :
10002
+ (this.download_settings.quotes === 'double' ? '"' : '')),
10003
+ vars = [],
10004
+ data = {
10005
+ nr: `${quo}Run number${quo}${sep}`,
10006
+ combi: `${quo}Selectors${quo}${sep}`,
10007
+ rsecs: `${quo}Run duration${quo}${sep}`,
10008
+ ssecs: `${quo}Solver time${quo}${sep}`,
10009
+ warnings: `${quo}Warnings${quo}${sep}`,
10010
+ variable: `${quo}Variable${quo}${sep}`,
10011
+ N: `${quo}N${quo}${sep}`,
10012
+ sum: `${quo}Sum${quo}${sep}`,
10013
+ mean: `${quo}Mean${quo}${sep}`,
10014
+ variance: `${quo}Variance${quo}${sep}`,
10015
+ minimum: `${quo}Minimum${quo}${sep}`,
10016
+ maximum: `${quo}Maximum${quo}${sep}`,
10017
+ NZ: `${quo}Non-zero${quo}${sep}`,
10018
+ last: `${quo}Last${quo}${sep}`,
10019
+ exceptions: `${quo}Exceptions${quo}${sep}`,
10020
+ run: []
10021
+ };
10022
+ // Make list of indices of variables to include
10023
+ if(this.download_settings.variables === 'selected') {
10024
+ // Only one variable
10025
+ vars.push(this.resultIndex(this.selected_variable));
10026
+ } else {
10027
+ // All variables
10028
+ for(let i = 0; i < this.variables.length; i++) {
10029
+ vars.push(i);
10030
+ }
10031
+ }
10032
+ const nvars = vars.length;
10033
+ for(let i = 0; i < this.combinations.length; i++) {
10034
+ if(i < this.runs.length &&
10035
+ (allruns || this.chart_combinations.indexOf(i) >= 0)) {
10036
+ data.run.push(i);
10037
+ }
10038
+ }
10039
+ let series_length = 0;
10040
+ for(let i = 0; i < data.run.length; i++) {
10041
+ const
10042
+ rnr = data.run[i],
10043
+ r = this.runs[rnr];
10044
+ data.nr += r.number;
10045
+ data.combi += quo + this.combinations[rnr].join('|') + quo;
10046
+ // Run duration in seconds
10047
+ data.rsecs += VM.sig2Dig((r.time_recorded - r.time_started) * 0.001);
10048
+ data.ssecs += VM.sig2Dig(r.solver_seconds);
10049
+ data.warnings += r.warning_count;
10050
+ for(let j = 0; j < nvars; j++) {
10051
+ // Add empty cells for run attributes
10052
+ data.nr += sep;
10053
+ data.combi += sep;
10054
+ data.rsecs += sep;
10055
+ data.ssecs += sep;
10056
+ data.warnings += sep;
10057
+ const rr = r.results[vars[j]];
10058
+ data.variable += rr.displayName + sep;
10059
+ // Series may differ in length; the longest determines the
10060
+ // number of rows of series data to be added
10061
+ series_length = Math.max(series_length, rr.vector.length);
10062
+ if(this.download_settings.statistics) {
10063
+ data.N += rr.N + sep;
10064
+ data.sum += rr.sum + sep;
10065
+ data.mean += rr.mean + sep;
10066
+ data.variance += rr.variance + sep;
10067
+ data.minimum += rr.minimum + sep;
10068
+ data.maximum += rr.maximum + sep;
10069
+ data.NZ += rr.non_zero_tally + sep;
10070
+ data.last += rr.last + sep;
10071
+ data.exceptions += rr.exceptions + sep;
10072
+ }
10073
+ }
10074
+ }
10075
+ const ds = [data.nr, data.combi];
10076
+ if(this.download_settings.solver) {
10077
+ ds.push(data.rsecs, data.ssecs, data.warnings);
10078
+ }
10079
+ // Always add the row with variable names
10080
+ ds.push(data.variable);
10081
+ if(this.download_settings.statistics) {
10082
+ ds.push(data.N, data.sum, data.mean, data.variance, data.minimum,
10083
+ data.maximum, data.NZ, data.last, data.exceptions);
10084
+ }
10085
+ if(this.download_settings.series) {
10086
+ ds.push('t');
10087
+ const
10088
+ prec = this.download_settings.precision,
10089
+ row = [];
10090
+ for(let i = 0; i < series_length; i++) {
10091
+ row.length = 0;
10092
+ row.push(i);
10093
+ for(let j = 0; j < data.run.length; j++) {
10094
+ const rnr = data.run[j];
10095
+ for(let k = 0; k < vars.length; k++) {
10096
+ const rr = this.runs[rnr].results[vars[k]];
10097
+ if(i < rr.vector.length) {
10098
+ const v = rr.vector[i];
10099
+ if(v >= VM.MINUS_INFINITY && v <= VM.PLUS_INFINITY) {
10100
+ row.push(v.toPrecision(prec));
10101
+ } else {
10102
+ row.push('');
10103
+ }
10104
+ }
10105
+ }
10106
+ }
10107
+ ds.push(row.join(sep));
10108
+ }
10109
+ }
10110
+ return ds.join('\n');
10111
+ }
10112
+
10024
10113
  } // END of CLASS Experiment
10025
10114
 
10026
10115
 
@@ -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
  //
@@ -305,6 +305,7 @@ class Expression {
305
305
  // Pop the time step
306
306
  this.step.pop();
307
307
  this.trace('--STOP: ' + this.variableName);
308
+ DEBUGGING = false;
308
309
  // Clear context for #
309
310
  this.wildcard_number = false;
310
311
  // If error, display the call stack (only once)
@@ -1047,7 +1048,7 @@ class ExpressionParser {
1047
1048
  // NOTE: + and - operators are special case, since they may also
1048
1049
  // be part of a floating point number, hence the more elaborate check
1049
1050
  while(pl <= this.eot && (SEPARATOR_CHARS.indexOf(cpl) < 0 ||
1050
- ('+-'.indexOf(cpl) >= 0 && digs && pcpl === 'e'))) {
1051
+ ('+-'.indexOf(cpl) >= 0 && digs && pcpl.toLowerCase() === 'e'))) {
1051
1052
  digs = digs || '0123456789'.indexOf(cpl) >= 0;
1052
1053
  this.los++;
1053
1054
  pl++;
@@ -6034,21 +6035,19 @@ function VMI_jump(x, index) {
6034
6035
  }
6035
6036
 
6036
6037
  function VMI_jump_if_false(x, index) {
6037
- // Pops the top number A from the stack, and if A is FALSE (zero or
6038
+ // Tests the top number A of the stack, and if A is FALSE (zero or
6038
6039
  // VM.UNDEFINED) sets the program counter of the VM to `index` minus 1,
6039
6040
  // as the counter is ALWAYS increased by 1 after calling a VMI function
6040
6041
  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
- }
6042
+ if(DEBUGGING) console.log(`JUMP-IF-FALSE (${r}, ${index})`);
6043
+ if(r === 0 || r === VM.UNDEFINED || r === false) {
6044
+ // Only jump on FALSE, leaving the stack "as is", so that in case
6045
+ // of no THEN the expression result equals the IF condition value
6046
+ // NOTE: Also do this on a stack error (r === false)
6047
+ x.program_counter = index - 1;
6048
+ } else {
6049
+ // Remove the value from the stack
6050
+ x.stack.pop();
6052
6051
  }
6053
6052
  }
6054
6053
 
@@ -6249,6 +6248,8 @@ function VMI_set_const_bounds(args) {
6249
6248
  // if this is the first round
6250
6249
  if(VM.current_round) {
6251
6250
  l = vbl.actualLevel(VM.t);
6251
+ //PATCH!!
6252
+ if(l < 0.0005) l = 0;
6252
6253
  } else {
6253
6254
  l = 0;
6254
6255
  }