linny-r 1.4.0 → 1.4.2

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.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/server.js CHANGED
@@ -910,12 +910,12 @@ function loadData(res, url) {
910
910
  // the call-back Python script specified for the channel
911
911
 
912
912
  function receiver(res, sp) {
913
- //This function processes all receiver actions
913
+ // This function processes all receiver actions.
914
914
  let
915
915
  rpath = anyOSpath(sp.get('path') || ''),
916
916
  rfile = anyOSpath(sp.get('file') || '');
917
- // Assume that path is relative to channel directory unless it starts with
918
- // a (back)slash or specifiess drive or volume
917
+ // Assume that path is relative to working directory unless it starts
918
+ // with a (back)slash or specifies drive or volume.
919
919
  if(!(rpath.startsWith(path.sep) || rpath.indexOf(':') >= 0 ||
920
920
  rpath.startsWith(WORKING_DIRECTORY))) {
921
921
  rpath = path.join(WORKING_DIRECTORY, rpath);
@@ -1038,8 +1038,49 @@ function rcvrAbort(res, rpath, rfile, log) {
1038
1038
  }
1039
1039
 
1040
1040
  function rcvrReport(res, rpath, rfile, run, data, stats, log) {
1041
+ // Purge reports older than 24 hours.
1041
1042
  try {
1042
- let fp = path.join(rpath, rfile + run + '-data.txt');
1043
+ const
1044
+ now = new Date(),
1045
+ flist = fs.readdirSync(WORKSPACE.reports);
1046
+ let n = 0;
1047
+ for(let i = 0; i < flist.length; i++) {
1048
+ const
1049
+ pp = path.parse(flist[i]),
1050
+ fp = path.join(WORKSPACE.reports, flist[i]);
1051
+ // NOTE: Only consider text files (extension .txt)
1052
+ if(pp.ext === '.txt') {
1053
+ // Delete only if file is older than 24 hours.
1054
+ const fstat = fs.statSync(fp);
1055
+ if(now - fstat.mtimeMs > 24 * 3600000) {
1056
+ // Delete text file
1057
+ try {
1058
+ fs.unlinkSync(fp);
1059
+ n++;
1060
+ } catch(err) {
1061
+ console.log('WARNING: Failed to delete', fp);
1062
+ console.log(err);
1063
+ }
1064
+ }
1065
+ }
1066
+ }
1067
+ if(n) console.log(n + 'report file' + (n > 1 ? 's' : '') + 'purged');
1068
+ } catch(err) {
1069
+ // Log error, but do not abort.
1070
+ console.log(err);
1071
+ }
1072
+ // Now save the reports.
1073
+ // NOTE: The optional @ indicates where the run number must be inserted.
1074
+ // If not specified, append run number to the base report file name.
1075
+ if(rfile.indexOf('@') < 0) {
1076
+ rfile += run;
1077
+ } else {
1078
+ rfile = rfile.replace('@', run);
1079
+ }
1080
+ const base = path.join(rpath, rfile);
1081
+ let fp;
1082
+ try {
1083
+ fp = path.join(base + '-data.txt');
1043
1084
  fs.writeFileSync(fp, data);
1044
1085
  } catch(err) {
1045
1086
  console.log(err);
@@ -1048,7 +1089,7 @@ function rcvrReport(res, rpath, rfile, run, data, stats, log) {
1048
1089
  return;
1049
1090
  }
1050
1091
  try {
1051
- fp = path.join(rpath, rfile + run + '-stats.txt');
1092
+ fp = path.join(base + '-stats.txt');
1052
1093
  fs.writeFileSync(fp, stats);
1053
1094
  } catch(err) {
1054
1095
  console.log(err);
@@ -1057,7 +1098,7 @@ function rcvrReport(res, rpath, rfile, run, data, stats, log) {
1057
1098
  return;
1058
1099
  }
1059
1100
  try {
1060
- fp = path.join(rpath, rfile + run + '-log.txt');
1101
+ fp = path.join(base + '-log.txt');
1061
1102
  fs.writeFileSync(fp, log);
1062
1103
  } catch(err) {
1063
1104
  console.log(err);
@@ -1630,6 +1671,7 @@ function createWorkspace() {
1630
1671
  data: path.join(SETTINGS.user_dir, 'data'),
1631
1672
  diagrams: path.join(SETTINGS.user_dir, 'diagrams'),
1632
1673
  modules: path.join(SETTINGS.user_dir, 'modules'),
1674
+ reports: path.join(SETTINGS.user_dir, 'reports'),
1633
1675
  solver_output: path.join(SETTINGS.user_dir, 'solver')
1634
1676
  };
1635
1677
  // Create these sub-directories if not aready there
package/static/index.html CHANGED
@@ -165,8 +165,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
165
165
  // Inform user that newer version exists
166
166
  UI.check_update_modal.element('msg').innerHTML = [
167
167
  '<a href="', GITHUB_REPOSITORY,
168
- '/wiki/Linny-R-version-history#version-',
169
- info[1].replaceAll('.', ''), '" ',
168
+ '/releases/tag/v', info[1], '" ',
170
169
  'title="Click to view version release notes" ',
171
170
  'target="_blank">Version <strong>',
172
171
  info[1], '</strong></a> released on ',
@@ -601,6 +600,14 @@ and move the cursor over the status bar">
601
600
  </td>
602
601
  <td style="padding-bottom:4px">Infer and display cost prices</td>
603
602
  </tr>
603
+ <tr title="Reports will be saved in user/reports/, and removed after 24 h">
604
+ <td style="padding:0px">
605
+ <div id="settings-report-results" class="box clear"></div>
606
+ </td>
607
+ <td style="padding-bottom:4px">
608
+ Report results after each run
609
+ </td>
610
+ </tr>
604
611
  <tr>
605
612
  <td style="padding:0px">
606
613
  <div id="settings-block-arrows" class="box clear"></div>
@@ -1280,9 +1287,6 @@ NOTE: Unit symbols are case-sensitive, so BTU &ne; Btu">
1280
1287
  <div id="process-dlg" class="inp-dlg">
1281
1288
  <div class="dlg-title">
1282
1289
  Process properties
1283
- <div class="simbtn">
1284
- <img id="process-sim-btn" class="btn sim enab" src="images/process.png">
1285
- </div>
1286
1290
  <img class="cancel-btn" src="images/cancel.png">
1287
1291
  <img class="ok-btn" src="images/ok.png">
1288
1292
  </div>
@@ -346,20 +346,6 @@ div.contbtn {
346
346
  cursor: pointer;
347
347
  }
348
348
 
349
- div.simbtn {
350
- display: none;
351
- margin-left: 5px;
352
- }
353
-
354
- img.sim {
355
- height: 14px;
356
- width: 14px;
357
- }
358
-
359
- img.sim.enab:hover {
360
- background-color: #9e96e5;
361
- }
362
-
363
349
  input {
364
350
  vertical-align: baseline;
365
351
  }
@@ -302,14 +302,20 @@ class Controller {
302
302
  (name.startsWith(this.BLACK_BOX) || name[0].match(/[\w]/));
303
303
  }
304
304
 
305
- prefixesAndName(name) {
305
+ prefixesAndName(name, key=false) {
306
306
  // Returns name split exclusively at '[non-space]: [non-space]'
307
+ let sep = this.PREFIXER,
308
+ space = ' ';
309
+ if(key) {
310
+ sep = ':_';
311
+ space = '_';
312
+ }
307
313
  const
308
- s = name.split(this.PREFIXER),
314
+ s = name.split(sep),
309
315
  pan = [s[0]];
310
316
  for(let i = 1; i < s.length; i++) {
311
317
  const j = pan.length - 1;
312
- if(s[i].startsWith(' ') || (i > 0 && pan[j].endsWith(' '))) {
318
+ if(s[i].startsWith(space) || (i > 0 && pan[j].endsWith(space))) {
313
319
  pan[j] += s[i];
314
320
  } else {
315
321
  pan.push(s[i]);
@@ -350,11 +356,70 @@ class Controller {
350
356
  this.LINK_ARROW : this.CONSTRAINT_ARROW),
351
357
  nodes = name.split(arrow);
352
358
  for(let i = 0; i < nodes.length; i++) {
353
- nodes[i] = nodes[i].replace(/^:\s*/, prefix);
359
+ nodes[i] = nodes[i].replace(/^:\s*/, prefix)
360
+ // NOTE: An embedded double prefix, e.g., "xxx: : yyy" indicates
361
+ // that the second colon+space should be replaced by the prefix.
362
+ // This "double prefix" may occur only once in an entity name,
363
+ // hence no global regexp.
364
+ .replace(/(\w+):\s+:\s+(\w+)/, `$1: ${prefix}$2`);
354
365
  }
355
366
  return nodes.join(arrow);
356
367
  }
357
368
 
369
+ tailNumber(name) {
370
+ // Returns the string of digits at the end of `name`. If not there,
371
+ // check prefixes (if any) *from right to left* for a tail number.
372
+ // Thus, the number that is "closest" to the name part is returned.
373
+ const pan = UI.prefixesAndName(name);
374
+ let n = endsWithDigits(pan.pop());
375
+ while(!n && pan.length > 0) {
376
+ n = endsWithDigits(pan.pop());
377
+ }
378
+ return n;
379
+ }
380
+
381
+ compareFullNames(n1, n2, key=false) {
382
+ // Compare full names, considering prefixes in *left-to-right* order
383
+ // while taking into account the tailnumber for each part so that
384
+ // "xx: yy2: nnn" comes before "xx: yy10: nnn".
385
+ if(n1 === n2) return 0;
386
+ if(key) {
387
+ // NOTE: Replacing link and constraint arrows by two prefixers
388
+ // ensures that sort wil be first on FROM node, and then on TO node.
389
+ const p2 = UI.PREFIXER + UI.PREFIXER;
390
+ // Keys for links and constraints are not based on their names,
391
+ // so look up their names before comparing.
392
+ if(n1.indexOf('____') > 0 && MODEL.constraints[n1]) {
393
+ n1 = MODEL.constraints[n1].displayName
394
+ .replace(UI.CONSTRAINT_ARROW, p2);
395
+ } else if(n1.indexOf('___') > 0 && MODEL.links[n1]) {
396
+ n1 = MODEL.links[n1].displayName
397
+ .replace(UI.LINK_ARROW, p2);
398
+ }
399
+ if(n2.indexOf('____') > 0 && MODEL.constraints[n2]) {
400
+ n2 = MODEL.constraints[n2].displayName.
401
+ replace(UI.CONSTRAINT_ARROW, p2);
402
+ } else if(n2.indexOf('___') > 0 && MODEL.links[n2]) {
403
+ n2 = MODEL.links[n2].displayName
404
+ .replace(UI.LINK_ARROW, p2);
405
+ }
406
+ n1 = n1.toLowerCase().replaceAll(' ', '_');
407
+ n2 = n2.toLowerCase().replaceAll(' ', '_');
408
+ }
409
+ const
410
+ pan1 = UI.prefixesAndName(n1, key),
411
+ pan2 = UI.prefixesAndName(n2, key),
412
+ sl = Math.min(pan1.length, pan2.length);
413
+ let i = 0;
414
+ while(i < sl) {
415
+ const c = compareWithTailNumbers(pan1[i], pan2[i]);
416
+ if(c !== 0) return c;
417
+ i++;
418
+ }
419
+ return pan1.length - pan2.length;
420
+ }
421
+
422
+
358
423
  nameToID(name) {
359
424
  // Returns a name in lower case with link arrow replaced by three
360
425
  // underscores, constraint link arrow by four underscores, and spaces
@@ -4901,7 +4901,7 @@ class GUIController extends Controller {
4901
4901
  // Logs MB's of used heap memory to console (to detect memory leaks)
4902
4902
  // NOTE: this feature is supported only by Chrome
4903
4903
  if(msg) msg += ' -- ';
4904
- if(typeof performance.memory !== 'undefined') {
4904
+ if(performance.memory !== undefined) {
4905
4905
  console.log(msg + 'Allocated memory: ' + Math.round(
4906
4906
  performance.memory.usedJSHeapSize/1048576.0).toFixed(1) + ' MB');
4907
4907
  }
@@ -5701,6 +5701,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
5701
5701
  this.setBox('settings-decimal-comma', model.decimal_comma);
5702
5702
  this.setBox('settings-align-to-grid', model.align_to_grid);
5703
5703
  this.setBox('settings-cost-prices', model.infer_cost_prices);
5704
+ this.setBox('settings-report-results', model.report_results);
5704
5705
  this.setBox('settings-block-arrows', model.show_block_arrows);
5705
5706
  md.show('name');
5706
5707
  }
@@ -5753,6 +5754,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
5753
5754
  if(!model.scale_units.hasOwnProperty(dsu)) model.addScaleUnit(dsu);
5754
5755
  model.default_unit = dsu;
5755
5756
  model.currency_unit = md.element('currency-unit').value.trim();
5757
+ model.report_results = UI.boxChecked('settings-report-results');
5756
5758
  model.encrypt = UI.boxChecked('settings-encrypt');
5757
5759
  model.decimal_comma = UI.boxChecked('settings-decimal-comma');
5758
5760
  // Some changes may necessitate redrawing the diagram
@@ -7354,7 +7356,8 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
7354
7356
  // is passed to differentiate between the DOM elements to be used
7355
7357
  const
7356
7358
  type = document.getElementById(prefix + 'variable-obj').value,
7357
- n_list = this.namesByType(VM.object_types[type]).sort(ciCompare),
7359
+ n_list = this.namesByType(VM.object_types[type]).sort(
7360
+ (a, b) => UI.compareFullNames(a, b)),
7358
7361
  vn = document.getElementById(prefix + 'variable-name'),
7359
7362
  options = [];
7360
7363
  // Add "empty" as first and initial option, but disable it.
@@ -7396,7 +7399,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
7396
7399
  slist.push(d.modifiers[m].selector);
7397
7400
  }
7398
7401
  // Sort to present equations in alphabetical order
7399
- slist.sort(ciCompare);
7402
+ slist.sort((a, b) => UI.compareFullNames(a, b));
7400
7403
  for(let i = 0; i < slist.length; i++) {
7401
7404
  options.push(`<option value="${slist[i]}">${slist[i]}</option>`);
7402
7405
  }
@@ -9898,13 +9901,7 @@ class GUIDatasetManager extends DatasetManager {
9898
9901
  dl = [],
9899
9902
  dnl = [],
9900
9903
  sd = this.selected_dataset,
9901
- ioclass = ['', 'import', 'export'],
9902
- ciPrefixCompare = (a, b) => {
9903
- const
9904
- pa = a.split(':_').join(' '),
9905
- pb = b.split(':_').join(' ');
9906
- return ciCompare(pa, pb);
9907
- };
9904
+ ioclass = ['', 'import', 'export'];
9908
9905
  for(let d in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(d) &&
9909
9906
  // NOTE: do not list "black-boxed" entities
9910
9907
  !d.startsWith(UI.BLACK_BOX) &&
@@ -9915,7 +9912,7 @@ class GUIDatasetManager extends DatasetManager {
9915
9912
  dnl.push(d);
9916
9913
  }
9917
9914
  }
9918
- dnl.sort(ciPrefixCompare);
9915
+ dnl.sort((a, b) => UI.compareFullNames(a, b, true));
9919
9916
  // First determine indentation levels, prefixes and names
9920
9917
  const
9921
9918
  indent = [],
@@ -10676,7 +10673,7 @@ class GUIDatasetManager extends DatasetManager {
10676
10673
  const
10677
10674
  ln = document.getElementById('series-line-number'),
10678
10675
  lc = document.getElementById('series-line-count');
10679
- ln.innerHTML = this.series_data.value.substr(0,
10676
+ ln.innerHTML = this.series_data.value.substring(0,
10680
10677
  this.series_data.selectionStart).split('\n').length;
10681
10678
  lc.innerHTML = this.series_data.value.split('\n').length;
10682
10679
  }
@@ -12188,7 +12185,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
12188
12185
  const
12189
12186
  ds_dict = MODEL.listOfAllSelectors,
12190
12187
  html = [],
12191
- sl = Object.keys(ds_dict).sort(ciCompare);
12188
+ sl = Object.keys(ds_dict).sort((a, b) => UI.compareFullNames(a, b, true));
12192
12189
  for(let i = 0; i < sl.length; i++) {
12193
12190
  const
12194
12191
  s = sl[i],
@@ -13173,7 +13170,7 @@ class GUIExperimentManager extends ExperimentManager {
13173
13170
  for(let i = 0; i < x.variables.length; i++) {
13174
13171
  addDistinct(x.variables[i].displayName, vl);
13175
13172
  }
13176
- vl.sort(ciCompare);
13173
+ vl.sort((a, b) => UI.compareFullNames(a, b));
13177
13174
  for(let i = 0; i < vl.length; i++) {
13178
13175
  ol.push(['<option value="', vl[i], '"',
13179
13176
  (vl[i] == x.selected_variable ? ' selected="selected"' : ''),
@@ -15522,7 +15519,7 @@ class Finder {
15522
15519
  }
15523
15520
  }
15524
15521
  }
15525
- enl.sort(ciCompare);
15522
+ enl.sort((a, b) => UI.compareFullNames(a, b, true));
15526
15523
  }
15527
15524
  document.getElementById('finder-entity-imgs').innerHTML = imgs;
15528
15525
  let seid = 'etr';
@@ -15919,7 +15916,7 @@ class Finder {
15919
15916
  // No cost price calculation => trim associated attributes from header
15920
15917
  let p = ah.indexOf('\tCost price');
15921
15918
  if(p > 0) {
15922
- ah = ah.substr(0, p);
15919
+ ah = ah.substring(0, p);
15923
15920
  } else {
15924
15921
  // SOC is exogenous, and hence comes before F in header => replace
15925
15922
  ah = ah.replace('\tShare of cost', '');
@@ -15991,8 +15988,8 @@ class GUIReceiver {
15991
15988
  }
15992
15989
 
15993
15990
  log(msg) {
15994
- // Logs a message displayed on the status line while solving
15995
- if(this.active) {
15991
+ // Logs a message displayed on the status line while solving.
15992
+ if(this.active || MODEL.report_results) {
15996
15993
  if(!msg.startsWith('[')) {
15997
15994
  const
15998
15995
  d = new Date(),
@@ -16135,21 +16132,34 @@ class GUIReceiver {
16135
16132
  report() {
16136
16133
  // Posts the run results to the local server, or signals an error
16137
16134
  let form,
16138
- run = '';
16135
+ run = '',
16136
+ path = this.channel,
16137
+ file = this.file_name;
16139
16138
  // NOTE: Always set `solving` to FALSE
16140
16139
  this.solving = false;
16141
- if(this.experiment){
16140
+ // NOTE: When reporting receiver while is not active, report the
16141
+ // results of the running experiment.
16142
+ if(this.experiment || !this.active) {
16142
16143
  if(MODEL.running_experiment) {
16143
16144
  run = MODEL.running_experiment.active_combination_index;
16144
16145
  this.log(`Reporting: ${this.file_name} (run #${run})`);
16145
16146
  }
16146
16147
  }
16148
+ // NOTE: If receiver is not active, path and file must be set.
16149
+ if(!this.active) {
16150
+ path = 'user/reports';
16151
+ // NOTE: The @ will be replaced by the run number, so that that
16152
+ // number precedes the clock time. The @ will be unique because
16153
+ // `asFileName()` replaces special characters by underscores.
16154
+ file = REPOSITORY_BROWSER.asFileName(MODEL.name || 'model') +
16155
+ '@-' + compactClockTime();
16156
+ }
16147
16157
  if(MODEL.solved && !VM.halted) {
16148
16158
  // Normal execution termination => report results
16149
16159
  const od = MODEL.outputData;
16150
16160
  form = {
16151
- path: this.channel,
16152
- file: this.file_name,
16161
+ path: path,
16162
+ file: file,
16153
16163
  action: 'report',
16154
16164
  run: run,
16155
16165
  data: od[0],
@@ -16176,12 +16186,13 @@ class GUIReceiver {
16176
16186
  return response.text();
16177
16187
  })
16178
16188
  .then((data) => {
16179
- // For experiments, only display server response if warning or error
16189
+ // For experiments, only display server response if warning or error.
16180
16190
  UI.postResponseOK(data, !RECEIVER.experiment);
16181
- // If execution completed, perform the call-back action
16191
+ // If execution completed, perform the call-back action if the
16192
+ // receiver is active (so not when auto-reporting a run).
16182
16193
  // NOTE: for experiments, call-back is performed upon completion by
16183
- // the Experiment Manager
16184
- if(!RECEIVER.experiment) RECEIVER.callBack();
16194
+ // the Experiment Manager.
16195
+ if(RECEIVER.active && !RECEIVER.experiment) RECEIVER.callBack();
16185
16196
  })
16186
16197
  .catch(() => UI.warn(UI.WARNING.NO_CONNECTION, err));
16187
16198
  }