linny-r 1.6.1 → 1.6.3

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.6.1",
3
+ "version": "1.6.3",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/server.js CHANGED
@@ -1521,7 +1521,7 @@ function connectionErrorText(msg) {
1521
1521
  //
1522
1522
 
1523
1523
  function commandLineSettings() {
1524
- // Sets default settings, and then checks the command line arguments
1524
+ // Sets default settings, and then checks the command line arguments.
1525
1525
  const settings = {
1526
1526
  cli_name: (PLATFORM.startsWith('win') ? 'Command Prompt' : 'Terminal'),
1527
1527
  inkscape: '',
@@ -1533,6 +1533,22 @@ function commandLineSettings() {
1533
1533
  solver_path: '',
1534
1534
  user_dir: path.join(WORKING_DIRECTORY, 'user')
1535
1535
  };
1536
+ const
1537
+ cmd = process.argv[0],
1538
+ app = (cmd.endsWith('node.exe') ? 'node' : 'linny-r'),
1539
+ usage = `Usage: ${app} server [options]
1540
+
1541
+ Possible options are:
1542
+ dpi=[number] will make InkScape render SVGs in the specified resolution
1543
+ help will display these command line options
1544
+ launch will open the Linny-R GUI in a browser window
1545
+ port=[number] will listen at the specified port number
1546
+ (default is 5050; number must be unique for each server)
1547
+ solver=[name] will select solver [name], or warn if not found
1548
+ (name choices: Gurobi, CPLEX, SCIP or LP_solve)
1549
+ verbose will output solver messages to the console
1550
+ workspace=[path] will create workspace in [path] instead of (Linny-R)/user
1551
+ `;
1536
1552
  for(let i = 2; i < process.argv.length; i++) {
1537
1553
  const lca = process.argv[i].toLowerCase();
1538
1554
  if(lca === 'launch') {
@@ -1541,7 +1557,7 @@ function commandLineSettings() {
1541
1557
  const av = lca.split('=');
1542
1558
  if(av.length === 1) av.push('');
1543
1559
  if(av[0] === 'port') {
1544
- // Accept any number greater than or equal to 1024
1560
+ // Accept any number greater than or equal to 1024.
1545
1561
  const n = parseInt(av[1]);
1546
1562
  if(isNaN(n) || n < 1024) {
1547
1563
  console.log(`WARNING: Invalid port number ${av[1]}`);
@@ -1555,7 +1571,7 @@ function commandLineSettings() {
1555
1571
  settings.preferred_solver = av[1];
1556
1572
  }
1557
1573
  } else if(av[0] === 'dpi') {
1558
- // Accept any number greater than or equal to 1024
1574
+ // Accept any number greater than or equal to 1024.
1559
1575
  const n = parseInt(av[1]);
1560
1576
  if(isNaN(n) || n > 1200) {
1561
1577
  console.log(`WARNING: Invalid resolution ${av[1]} (max. 1200 dpi)`);
@@ -1563,7 +1579,7 @@ function commandLineSettings() {
1563
1579
  settings.dpi = n;
1564
1580
  }
1565
1581
  } else if(av[0] === 'workspace') {
1566
- // User directory must be READ/WRITE-accessible
1582
+ // User directory must be READ/WRITE-accessible.
1567
1583
  try {
1568
1584
  fs.accessSync(av[1], fs.constants.R_OK | fs.constants.W_O);
1569
1585
  } catch(err) {
@@ -1571,10 +1587,15 @@ function commandLineSettings() {
1571
1587
  process.exit();
1572
1588
  }
1573
1589
  settings.user_dir = av[1];
1590
+ } else if(av[0] === 'help') {
1591
+ // Print command line options.
1592
+ console.log(usage);
1593
+ process.exit();
1574
1594
  } else {
1575
- // Terminate script
1595
+ // Terminate script.
1576
1596
  console.log(
1577
- `ERROR: Invalid command line argument "${process.argv[i]}"`);
1597
+ `ERROR: Invalid command line argument "${process.argv[i]}"\n`);
1598
+ console.log(usage);
1578
1599
  process.exit();
1579
1600
  }
1580
1601
  }
package/static/index.html CHANGED
@@ -1765,6 +1765,8 @@ NOTE: * and ? will be interpreted as wildcards"
1765
1765
  <img id="eq-delete-btn" class="btn disab" src="images/delete.png"
1766
1766
  title="Delete selected equation">
1767
1767
  </div>
1768
+ <img id="equation-outcome" class="not-selected" src="images/outcome.png"
1769
+ title="Click to consider/ignore selected equation as experiment outcome">
1768
1770
  <div id="equation-scroll-area">
1769
1771
  <table id="equation-table">
1770
1772
  </table>
@@ -2011,6 +2013,22 @@ NOTE: * and ? will be interpreted as wildcards"
2011
2013
  </div>
2012
2014
  </div>
2013
2015
 
2016
+ <!-- the ADD WILDCARD VARIABLES dialog allows selecting wildcard
2017
+ variables from a list to be added to the current chart -->
2018
+ <div id="add-wildcard-variables-modal" class="modal">
2019
+ <div id="add-wildcard-variables-dlg" class="inp-dlg">
2020
+ <div class="dlg-title">
2021
+ Add wildcard matches
2022
+ <img class="cancel-btn" src="images/cancel.png">
2023
+ <img class="ok-btn" src="images/ok.png">
2024
+ </div>
2025
+ <div id="add-wildcard-variables-scroll-area">
2026
+ <table id="add-wildcard-variables-table">
2027
+ </table>
2028
+ </div>
2029
+ </div>
2030
+ </div>
2031
+
2014
2032
  <!-- the RENAME CHART dialog prompts for the new title for a chart -->
2015
2033
  <div id="rename-chart-modal" class="modal">
2016
2034
  <div id="rename-chart-dlg" class="inp-dlg">
@@ -2161,7 +2161,8 @@ div.io-box {
2161
2161
  cursor: pointer;
2162
2162
  }
2163
2163
 
2164
- #dataset-outcome.not-selected {
2164
+ #dataset-outcome.not-selected,
2165
+ #equation-outcome.not-selected {
2165
2166
  filter: saturate(0) contrast(50%) brightness(170%);
2166
2167
  }
2167
2168
 
@@ -2273,10 +2274,12 @@ div.modif::before {
2273
2274
  margin-right: 2px;
2274
2275
  }
2275
2276
 
2276
- div.outcome::before {
2277
+ div.outcome::before,
2278
+ span.outcome::before {
2277
2279
  content: ' \25C8';
2278
2280
  color: #b00080;
2279
2281
  margin-right: 1px;
2282
+ font-style: normal;
2280
2283
  }
2281
2284
 
2282
2285
  div.array::before {
@@ -2449,6 +2452,15 @@ td.equation-expression {
2449
2452
  border-top: 1px solid Silver;
2450
2453
  }
2451
2454
 
2455
+ #equation-outcome {
2456
+ position: absolute;
2457
+ top: 25px;
2458
+ right: 4px;
2459
+ width: 16px;
2460
+ height: 16px;
2461
+ cursor: pointer;
2462
+ }
2463
+
2452
2464
  /* NOTE: Rename equation modal must be above Edit variable modal */
2453
2465
  #rename-equation-modal {
2454
2466
  z-index: 110;
@@ -3098,6 +3110,31 @@ img.v-disab {
3098
3110
  width: calc(100% - 6px);
3099
3111
  }
3100
3112
 
3113
+ #add-wildcard-variables-dlg {
3114
+ width: 250px;
3115
+ min-height: 42px;
3116
+ max-height: 50vh;
3117
+ }
3118
+
3119
+ #add-wildcard-variables-scroll-area {
3120
+ position: absolute;
3121
+ top: 22px;
3122
+ left: 2px;
3123
+ width: calc(100% - 4px);
3124
+ height: calc(100% - 26px);
3125
+ overflow-y: auto;
3126
+ border-top: 1px solid Silver;
3127
+ }
3128
+
3129
+ #add-wildcard-variables-table {
3130
+ width: 100%;
3131
+ border-collapse: collapse;
3132
+ line-height: 16px;
3133
+ background-color: white;
3134
+ border: 1px solid Silver;
3135
+ border-top: none;
3136
+ }
3137
+
3101
3138
  /* the SENSITIVITY DIALOG displays the sensitivity analysis */
3102
3139
  #sensitivity-dlg {
3103
3140
  display: none;
@@ -834,12 +834,19 @@ class ChartManager {
834
834
  }
835
835
 
836
836
  resetChartVectors() {
837
- // Reset vectors of all charts
837
+ // Reset vectors of all charts.
838
838
  for(let i = 0; i < MODEL.charts.length; i++) {
839
839
  MODEL.charts[i].resetVectors();
840
840
  }
841
841
  }
842
842
 
843
+ promptForWildcardIndices(chart, dsm) {
844
+ // No GUI dialog, so add *all* vectors for wildcard dataset modifier
845
+ // `dsm` as variables to `chart`.
846
+ const indices = Object.keys(dsm.expression.wildcard_vectors);
847
+ chart.addWildcardVariables(dsm, indices);
848
+ }
849
+
843
850
  setRunsChart(show) {
844
851
  // Indicate whether the chart manager should display a run result chart.
845
852
  this.runs_chart = show;
@@ -131,7 +131,7 @@ class GUIChartManager extends ChartManager {
131
131
  document.getElementById('chart-narrow-btn').addEventListener(
132
132
  'click', () => CHART_MANAGER.stretchChart(-1));
133
133
 
134
- // The Add variable modal
134
+ // The Add variable modal.
135
135
  this.add_variable_modal = new ModalDialog('add-variable');
136
136
  this.add_variable_modal.ok.addEventListener(
137
137
  'click', () => CHART_MANAGER.addVariable());
@@ -143,7 +143,7 @@ class GUIChartManager extends ChartManager {
143
143
  this.add_variable_modal.element('name').addEventListener(
144
144
  'change', () => X_EDIT.updateAttributeSelector('add-'));
145
145
 
146
- // The Edit variable modal
146
+ // The Edit variable modal.
147
147
  this.variable_modal = new ModalDialog('variable');
148
148
  this.variable_modal.ok.addEventListener(
149
149
  'click', () => CHART_MANAGER.modifyVariable());
@@ -160,7 +160,7 @@ class GUIChartManager extends ChartManager {
160
160
  'mouseleave', () => CHART_MANAGER.hidePasteColor());
161
161
  document.getElementById('variable-color').addEventListener(
162
162
  'click', (event) => CHART_MANAGER.copyPasteColor(event));
163
- // NOTE: uses the color picker developed by James Daniel
163
+ // NOTE: Uses the color picker developed by James Daniel.
164
164
  this.color_picker = new iro.ColorPicker("#color-picker", {
165
165
  width: 92,
166
166
  height: 92,
@@ -179,13 +179,20 @@ class GUIChartManager extends ChartManager {
179
179
  CHART_MANAGER.color_picker.color.hexString;
180
180
  });
181
181
 
182
- // The Rename chart modal
182
+ // The Rename chart modal.
183
183
  this.rename_chart_modal = new ModalDialog('rename-chart');
184
184
  this.rename_chart_modal.ok.addEventListener(
185
185
  'click', () => CHART_MANAGER.renameChart());
186
186
  this.rename_chart_modal.cancel.addEventListener(
187
187
  'click', () => CHART_MANAGER.rename_chart_modal.hide());
188
188
 
189
+ // The Add wildcard variables modal.
190
+ this.add_wildcard_modal = new ModalDialog('add-wildcard-variables');
191
+ this.add_wildcard_modal.ok.addEventListener(
192
+ 'click', () => CHART_MANAGER.addSelectedWildcardVariables());
193
+ this.add_wildcard_modal.cancel.addEventListener(
194
+ 'click', () => CHART_MANAGER.add_wildcard_modal.hide());
195
+
189
196
  // Do not display the time step until cursor moves over chart
190
197
  this.time_step.style.display = 'none';
191
198
  document.getElementById('table-only-buttons').style.display = 'none';
@@ -326,16 +333,25 @@ class GUIChartManager extends ChartManager {
326
333
  }
327
334
 
328
335
  updateSelector() {
329
- // Adds one option to the selector for each chart defined for the model
330
- // NOTE: add the "new chart" option if still absent
336
+ // Adds one option to the selector for each chart defined for the model.
337
+ // NOTE: Add the "new chart" option if it is not in the list.
331
338
  MODEL.addChart(this.new_chart_title);
332
339
  if(this.chart_index < 0) this.chart_index = 0;
333
340
  const ol = [];
334
341
  for(let i = 0; i < MODEL.charts.length; i++) {
335
- ol.push('<option value="', i,
336
- (i == this.chart_index ? '"selected="selected' : ''),
337
- '">', MODEL.charts[i].title , '</option>');
342
+ const t = MODEL.charts[i].title;
343
+ ol.push(['<option value="', i,
344
+ (i == this.chart_index ? '" selected="selected' : ''),
345
+ '">', t , '</option>'].join(''));
338
346
  }
347
+ // Sort option list by chart title.
348
+ ol.sort((a, b) => {
349
+ const
350
+ re = /<option value="\d+"( selected="selected")?>(.+)<\/option>/,
351
+ ta = a.replace(re, '$2'),
352
+ tb = b.replace(re, '$2');
353
+ return UI.compareFullNames(ta, tb);
354
+ });
339
355
  this.chart_selector.innerHTML = ol.join('');
340
356
  }
341
357
 
@@ -379,6 +395,11 @@ class GUIChartManager extends ChartManager {
379
395
  } else {
380
396
  this.variable_index = -1;
381
397
  }
398
+ // Just in case variable index has not been adjusted after some
399
+ // variables have been deleted
400
+ if(this.variable_index >= c.variables.length) {
401
+ this.variable_index = -1;
402
+ }
382
403
  // Set the image of the sort type button.
383
404
  if(this.variable_index >= 0) {
384
405
  const
@@ -392,11 +413,6 @@ class GUIChartManager extends ChartManager {
392
413
  u_btn = 'chart-variable-up ',
393
414
  d_btn = 'chart-variable-down ',
394
415
  ed_btns = 'chart-edit-variable chart-sort-variable chart-delete-variable ';
395
- // Just in case variable index has not been adjusted after some
396
- // variables have been deleted
397
- if(this.variable_index >= c.variables.length) {
398
- this.variable_index = -1;
399
- }
400
416
  if(this.variable_index < 0) {
401
417
  UI.disableButtons(ed_btns + u_btn + d_btn);
402
418
  } else {
@@ -713,15 +729,63 @@ class GUIChartManager extends ChartManager {
713
729
  }
714
730
  }
715
731
 
732
+ promptForWildcardIndices(chart, dsm) {
733
+ // Prompt modeler with list of vectors for wildcard dataset modifier
734
+ // `dsm` as variables to `chart`.
735
+ const
736
+ md = this.add_wildcard_modal,
737
+ indices = Object.keys(dsm.expression.wildcard_vectors);
738
+ // First hide the "Add variable" modal.
739
+ this.add_variable_modal.hide();
740
+ // Do not prompt for selection if there is only 1 match.
741
+ if(indices.length < 2) chart.addWildcardVariables(dsm, indices);
742
+ md.chart = chart;
743
+ md.modifier = dsm;
744
+ md.indices = indices;
745
+ const
746
+ tr = [],
747
+ dn = dsm.displayName,
748
+ tbl = md.element('table');
749
+ for(let i = 0; i < indices.length; i++) {
750
+ tr.push('<tr><td class="v-box"><div id="wcv-box-', indices[i],
751
+ '" class="box clear" onclick="UI.toggleBox(event);"></td>',
752
+ '<td class="vname">', dn.replace('??', indices[i]),
753
+ '</td></tr>');
754
+ tbl.innerHTML = tr.join('');
755
+ }
756
+ md.dialog.style.height = (22 + indices.length * 16) + 'px';
757
+ md.show();
758
+ }
759
+
760
+ addSelectedWildcardVariables() {
761
+ // Let the chart add selected wildcard matches (if any) as chart
762
+ // variables.
763
+ const
764
+ md = this.add_wildcard_modal,
765
+ c = md.chart,
766
+ dsm = md.modifier,
767
+ il = md.indices,
768
+ indices = [];
769
+ if(c && dsm && il) {
770
+ for(let i = 0; i < il.length; i++) {
771
+ if(UI.boxChecked('wcv-box-'+ il[i])) indices.push(il[i]);
772
+ }
773
+ }
774
+ if(indices.length) c.addWildcardVariables(dsm, indices);
775
+ // Always hide the dialog.
776
+ md.hide();
777
+ this.updateDialog();
778
+ }
779
+
716
780
  selectVariable(vi) {
717
- // Select variable, and edit it when double-clicked
781
+ // Select variable, and edit it when double-clicked.
718
782
  const
719
783
  now = Date.now(),
720
784
  dt = now - this.last_time_selected;
721
785
  if(vi >= 0 && this.chart_index >= 0) {
722
786
  this.last_time_selected = now;
723
787
  if(vi === this.variable_index) {
724
- // Consider click to be "double" if it occurred less than 300 ms ago
788
+ // Consider click to be "double" if it occurred less than 300 ms ago.
725
789
  if(dt < 300) {
726
790
  this.last_time_selected = 0;
727
791
  this.editVariable();
@@ -109,7 +109,7 @@ class GroupPropertiesDialog extends ModalDialog {
109
109
  // input fields this means `onkeydown` events.
110
110
  const fnc = (event) => {
111
111
  const id = event.target.id.split('-').shift();
112
- // NOTE: add a short delay to permit checkboxes to update their
112
+ // NOTE: Add a short delay to permit checkboxes to update their
113
113
  // status first, before checking for change.
114
114
  setTimeout(() => UI.modals[id].highlightModifiedFields(), 100);
115
115
  };
@@ -819,8 +819,8 @@ class GUIController extends Controller {
819
819
 
820
820
  // Add all draggable stay-on-top dialogs as controller properties.
821
821
 
822
- // Make checkboxes respond to click
823
- // NOTE: checkbox-specific events must be bound AFTER this general setting
822
+ // Make checkboxes respond to click.
823
+ // NOTE: Checkbox-specific events must be bound AFTER this general setting.
824
824
  const
825
825
  cbs = document.getElementsByClassName('box'),
826
826
  cbf = (event) => UI.toggleBox(event);
@@ -54,6 +54,9 @@ class EquationManager {
54
54
  'click', () => EQUATION_MANAGER.editEquation());
55
55
  document.getElementById('eq-delete-btn').addEventListener(
56
56
  'click', () => EQUATION_MANAGER.deleteEquation());
57
+ this.outcome_btn = document.getElementById('equation-outcome');
58
+ this.outcome_btn.addEventListener(
59
+ 'click', () => EQUATION_MANAGER.toggleOutcome());
57
60
 
58
61
  // Create modal dialogs
59
62
  this.new_modal = new ModalDialog('new-equation');
@@ -134,6 +137,11 @@ class EquationManager {
134
137
  ml = [],
135
138
  msl = ed.selectorList,
136
139
  sm = this.selected_modifier;
140
+ if(sm && sm.outcome_equation) {
141
+ this.outcome_btn.classList.remove('not-selected');
142
+ } else {
143
+ this.outcome_btn.classList.add('not-selected');
144
+ }
137
145
  let smid = 'eqmtr';
138
146
  for(let i = 0; i < msl.length; i++) {
139
147
  const
@@ -155,6 +163,7 @@ class EquationManager {
155
163
  (m.expression.noMethodObject ? ' no-object' : ''),
156
164
  (m.expression.isStatic ? '' : ' it'), issue,
157
165
  (wild ? ' wildcard' : ''), clk, ', false);"', mover, '>',
166
+ (m.outcome_equation ? '<span class="outcome"></span>' : ''),
158
167
  (wild ? wildcardFormat(m.selector) : m.selector),
159
168
  '</td><td class="equation-expression', issue,
160
169
  (issue ? '"title="' +
@@ -201,6 +210,16 @@ class EquationManager {
201
210
  this.updateDialog();
202
211
  }
203
212
 
213
+ toggleOutcome() {
214
+ const m = this.selected_modifier;
215
+ // NOTE: Methods cannot be outcomes.
216
+ if(m && !m.selector.startsWith(':')) {
217
+ m.outcome_equation = !m.outcome_equation;
218
+ this.updateDialog();
219
+ if(!UI.hidden('experiment-dlg')) EXPERIMENT_MANAGER.updateDialog();
220
+ }
221
+ }
222
+
204
223
  promptForEquation(add=false) {
205
224
  this.add_to_chart = add;
206
225
  this.new_modal.element('name').value = '';
@@ -356,7 +356,11 @@ class GUIExperimentManager extends ExperimentManager {
356
356
  dim_count.innerHTML = pluralS(x.available_dimensions.length,
357
357
  'more dimension');
358
358
  x.inferActualDimensions();
359
+ for(let i = 0; i < x.actual_dimensions.length; i++) {
360
+ x.actual_dimensions[i].sort(compareSelectors);
361
+ }
359
362
  x.inferCombinations();
363
+ //x.combinations.sort(compareCombinations);
360
364
  combi_count.innerHTML = pluralS(x.combinations.length, 'combination');
361
365
  if(x.combinations.length === 0) canview = false;
362
366
  header.innerHTML = x.title;
@@ -850,6 +854,9 @@ class GUIExperimentManager extends ExperimentManager {
850
854
  }
851
855
  // Get the selected statistic for each run so as to get an array of numbers
852
856
  const data = [];
857
+ // Set reference column indices so that values for the reference|
858
+ // configuration can be displayed in orange.
859
+ const ref_conf_indices = [];
853
860
  for(let i = 0; i < x.runs.length; i++) {
854
861
  const
855
862
  r = x.runs[i],
@@ -878,7 +885,7 @@ class GUIExperimentManager extends ExperimentManager {
878
885
  data.push(rr.last);
879
886
  }
880
887
  }
881
- // Scale data as selected
888
+ // Scale data as selected.
882
889
  const scaled = data.slice();
883
890
  // NOTE: scale only after the experiment has been completed AND
884
891
  // configurations have been defined (otherwise comparison is pointless)
@@ -896,7 +903,9 @@ class GUIExperimentManager extends ExperimentManager {
896
903
  }
897
904
  // Set difference for reference configuration itself to 0
898
905
  for(let i = 0; i < n; i++) {
899
- scaled[rc * n + i] = 0;
906
+ const index = rc * n + i;
907
+ scaled[index] = 0;
908
+ ref_conf_indices.push(index);
900
909
  }
901
910
  } else if(x.selected_scale === 'reg') {
902
911
  // Compute regret: current config - high value config in same scenario
@@ -934,13 +943,15 @@ class GUIExperimentManager extends ExperimentManager {
934
943
  formatted.push(VM.sig4Dig(scaled[i]));
935
944
  }
936
945
  uniformDecimals(formatted);
937
- // Display formatted data in cells
946
+ // Display formatted data in cells.
938
947
  for(let i = 0; i < x.combinations.length; i++) {
939
948
  const cell = document.getElementById('xr' + i);
940
949
  if(i < x.runs.length) {
941
950
  cell.innerHTML = formatted[i];
942
951
  cell.classList.remove('not-run');
943
952
  cell.style.backgroundColor = this.color_scale.rgb(normalized[i]);
953
+ cell.style.color = (ref_conf_indices.indexOf(i) >= 0 ?
954
+ 'orange' : 'black');
944
955
  const
945
956
  r = x.runs[i],
946
957
  rr = r.results[rri],
@@ -377,7 +377,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
377
377
  }
378
378
  va.innerHTML = options.join('');
379
379
  // NOTE: Chart Manager variable dialog is 60px wider
380
- va.style.width = (prefix ? 'calc(100% - 82px)' : 'calc(100% - 142px)');
380
+ va.style.width = (prefix ? 'calc(100% - 84px)' : 'calc(100% - 142px)');
381
381
  return;
382
382
  }
383
383
  // Add "empty" as first and initial option, as it denotes "use default"
@@ -280,6 +280,7 @@ class GUIMonitor {
280
280
 
281
281
  hideCallStack() {
282
282
  document.getElementById('call-stack-modal').style.display = 'none';
283
+ this.call_stack_shown = false;
283
284
  }
284
285
 
285
286
  logMessage(block, msg) {