linny-r 1.6.7 → 1.7.0

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.
@@ -443,12 +443,13 @@ class GUIController extends Controller {
443
443
  // Keep track of time since last message displayed on the infoline.
444
444
  this.time_last_message = new Date('01 Jan 2001 00:00:00 GMT');
445
445
  this.message_display_time = 3000;
446
+ this.last_message_type = '';
446
447
 
447
448
  // Initialize "main" modals, i.e., those that relate to the controller,
448
449
  // not to other dialog objects.
449
450
  const main_modals = ['logon', 'model', 'load', 'password', 'settings',
450
451
  'actors', 'add-process', 'add-product', 'move', 'note', 'clone',
451
- 'replace', 'expression'];
452
+ 'replace', 'expression', 'server', 'solver'];
452
453
  for(let i = 0; i < main_modals.length; i++) {
453
454
  this.modals[main_modals[i]] = new ModalDialog(main_modals[i]);
454
455
  }
@@ -580,8 +581,10 @@ class GUIController extends Controller {
580
581
  // NOTE: When user name is specified, solver is not on local host.
581
582
  const hl = document.getElementById('host-logo');
582
583
  hl.classList.add('local-server');
583
- hl.addEventListener('click', () => UI.shutDownServer());
584
+ hl.addEventListener('click', () => UI.showServerModal());
584
585
  }
586
+ this.server_modal = new ModalDialog('server');
587
+
585
588
 
586
589
  // Vertical tool bar buttons:
587
590
  this.buttons.clone.addEventListener('click',
@@ -703,10 +706,27 @@ class GUIController extends Controller {
703
706
  // Ensure that model documentation can no longer be edited.
704
707
  DOCUMENTATION_MANAGER.clearEntity([MODEL]);
705
708
  });
706
- // Make the scale units button of the settings dialog responsive.
709
+ // Make the scale units and solver preferences buttons of the settings
710
+ // dialog responsive. Clicking will open these dialogs on top of the
711
+ // settings modal dialog.
707
712
  this.modals.settings.element('scale-units-btn').addEventListener('click',
708
- // Open the scale units modal dialog on top of the settings dialog.
709
713
  () => SCALE_UNIT_MANAGER.show());
714
+ this.modals.settings.element('solver-prefs-btn').addEventListener('click',
715
+ () => UI.showSolverPreferencesDialog());
716
+ // Make solver modal elements responsive.
717
+ this.modals.solver.ok.addEventListener('click',
718
+ () => UI.updateSolverPreferences());
719
+ this.modals.solver.cancel.addEventListener('click',
720
+ () => UI.modals.solver.hide());
721
+ // Make server modal elements responsive.
722
+ this.modals.server.ok.addEventListener('click',
723
+ () => UI.changeSolver(UI.modals.server.element('solver').value));
724
+ this.modals.server.cancel.addEventListener('click',
725
+ () => UI.modals.server.hide());
726
+ this.modals.server.element('update').addEventListener('click',
727
+ () => UI.shutDownToUpdate());
728
+ this.modals.server.element('shut-down').addEventListener('click',
729
+ () => UI.shutDownServer());
710
730
 
711
731
  // Modals related to vertical toolbar buttons.
712
732
  this.modals['add-process'].ok.addEventListener('click',
@@ -795,7 +815,7 @@ class GUIController extends Controller {
795
815
 
796
816
  // The REPLACE dialog appears when a product is Ctrl-clicked.
797
817
  this.modals.replace.ok.addEventListener('click',
798
- () => UI.replaceProduct());
818
+ () => UI.replaceProduct());
799
819
  this.modals.replace.cancel.addEventListener('click',
800
820
  () => UI.modals.replace.hide());
801
821
 
@@ -870,21 +890,25 @@ class GUIController extends Controller {
870
890
  }
871
891
 
872
892
  loadModelFromXML(xml) {
873
- // Parses `xml` and updates the GUI
893
+ // Parse `xml` and update the GUI.
874
894
  const loaded = MODEL.parseXML(xml);
875
- // If not a valid Linny-R model, ensure that the current model is clean
895
+ // If not a valid Linny-R model, ensure that the current model is clean.
876
896
  if(!loaded) MODEL = new LinnyRModel();
897
+ // If model specifies a preferred solver, immediately try to switch.
898
+ if(MODEL.preferred_solver !== VM.solver_name) {
899
+ UI.changeSolver(MODEL.preferred_solver);
900
+ }
877
901
  this.updateScaleUnitList();
878
902
  this.drawDiagram(MODEL);
879
- // Cursor may have been set to `waiting` when decrypting
903
+ // Cursor may have been set to `waiting` when decrypting.
880
904
  this.normalCursor();
881
905
  this.setMessage('');
882
906
  this.updateButtons();
883
907
  // Undoable operations no longer apply!
884
908
  UNDO_STACK.clear();
885
- // Autosaving should start anew
909
+ // Autosaving should start anew.
886
910
  AUTO_SAVE.setAutoSaveInterval();
887
- // Signal success or failure
911
+ // Signal success or failure.
888
912
  return loaded;
889
913
  }
890
914
 
@@ -898,15 +922,15 @@ class GUIController extends Controller {
898
922
  MODEL.clearSelection();
899
923
  this.paper.drawModel(MODEL);
900
924
  this.updateButtons();
901
- // NOTE: when "moving up" in the cluster hierarchy, bring the former focal
902
- // cluster into view
925
+ // NOTE: When "moving up" in the cluster hierarchy, bring the former
926
+ // focal cluster into view.
903
927
  if(fc.cluster == MODEL.focal_cluster) {
904
928
  this.scrollIntoView(fc.shape.element.childNodes[0]);
905
929
  }
906
930
  }
907
931
 
908
932
  drawDiagram(mdl) {
909
- // "Queue" a draw request (to avoid redrawing too often)
933
+ // "Queue" a draw request (to avoid redrawing too often).
910
934
  if(this.busy_drawing) {
911
935
  this.draw_requests += 1;
912
936
  } else {
@@ -952,19 +976,81 @@ class GUIController extends Controller {
952
976
  if(a.links.indexOf(link) >= 0) this.paper.drawArrow(a);
953
977
  }
954
978
  }
979
+
980
+ showServerModal() {
981
+ // Prepare and show the server modal dialog.
982
+ const
983
+ md = this.modals.server,
984
+ host = md.element('host-div'),
985
+ sd = md.element('solver-div'),
986
+ nsd = md.element('no-solver-div'),
987
+ html = [];
988
+ host.innerText = 'Server on ' + VM.server;
989
+ if(VM.server === 'local host') {
990
+ host.title = 'Linny-R directory is ' + VM.working_directory;
991
+ }
992
+ for(let i = 0; i < VM.solver_list.length; i++) {
993
+ const s = VM.solver_list[i];
994
+ html.push(['<option value="', s,
995
+ (s === VM.solver_name ? '"selected="selected' : ''),
996
+ '">', VM.solver_names[s], '</option>'].join(''));
997
+ }
998
+ md.element('solver').innerHTML = html.join('');
999
+ if(html.length) {
1000
+ sd.style.display = 'block';
1001
+ nsd.style.display = 'none';
1002
+ } else {
1003
+ sd.style.display = 'none';
1004
+ nsd.style.display = 'block';
1005
+ }
1006
+ md.show();
1007
+ }
1008
+
1009
+ changeSolver(sid) {
1010
+ // Change preferred solver to `sid` if specified.
1011
+ if(!sid) return;
1012
+ const
1013
+ md = this.modals.server,
1014
+ mps = MODEL.preferred_solver;
1015
+ md.hide();
1016
+ if(mps && mps !== sid) {
1017
+ UI.warn('Model setttings designate ' + VM.solver_names[mps] +
1018
+ ' as preferred solver');
1019
+ return;
1020
+ }
1021
+ const pd = postData({action: 'change', solver: sid});
1022
+ fetch('solver/', pd)
1023
+ .then((response) => {
1024
+ if(!response.ok) {
1025
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
1026
+ }
1027
+ return response.text();
1028
+ })
1029
+ .then((data) => {
1030
+ if(UI.postResponseOK(data, true)) {
1031
+ VM.solver_name = sid;
1032
+ UI.modals.server.hide();
1033
+ }
1034
+ })
1035
+ .catch((err) => {
1036
+ UI.warn(UI.WARNING.NO_CONNECTION, err);
1037
+ });
1038
+ }
955
1039
 
956
1040
  shutDownServer() {
957
1041
  // This terminates the local host server script and display a plain
958
1042
  // HTML message in the browser with a restart button.
1043
+ this.modals.server.hide();
959
1044
  if(!SOLVER.user_id) window.open('./shutdown', '_self');
960
1045
  }
961
1046
 
962
1047
  shutDownToUpdate() {
963
- // Sisgnal server that an update is required. This will close the
1048
+ // Signal server that an update is required. This will close the
964
1049
  // local host Linny-R server. If this server was started by the
965
1050
  // standard OS batch script, this script will proceed to update
966
1051
  // Linny-R via npm and then restart the server again. If not, the
967
1052
  // fetch request will time out, anf the user will be warned.
1053
+ this.modals.server.hide();
968
1054
  if(SOLVER.user_id) return;
969
1055
  fetch('update/')
970
1056
  .then((response) => {
@@ -1022,6 +1108,8 @@ class GUIController extends Controller {
1022
1108
  `Linny-R version ${m[1]} has been installed.`,
1023
1109
  'To continue, you must reload this page, and',
1024
1110
  'confirm when prompted by your browser.');
1111
+ // Hide "update" button in server dialog.
1112
+ UI.modals.server.element('update').style.display = 'none';
1025
1113
  } else {
1026
1114
  // Inform user that install appears to have failed.
1027
1115
  msg.push(
@@ -1046,6 +1134,10 @@ class GUIController extends Controller {
1046
1134
  preventUpdate() {
1047
1135
  // Signal server that no update is required.
1048
1136
  if(SOLVER.user_id) return;
1137
+ // Show "update" button in server dialog to permit updating later.
1138
+ const btn = this.modals.server.element('update');
1139
+ btn.innerText = 'Update Linny-R to version ' + this.newer_version;
1140
+ btn.style.display = 'block';
1049
1141
  fetch('no-update/')
1050
1142
  .then((response) => {
1051
1143
  if(!response.ok) {
@@ -2572,28 +2664,32 @@ class GUIController extends Controller {
2572
2664
  // Displays message on infoline unless no type (= plain text) and some
2573
2665
  // info, warning or error message is already displayed
2574
2666
  super.setMessage(msg, type);
2667
+ const types = ['notification', 'warning', 'error'];
2575
2668
  let d = new Date(),
2576
2669
  t = d.getTime(),
2577
- dt = t - this.time_last_message;
2670
+ dt = t - this.time_last_message,
2671
+ mti = types.indexOf(type),
2672
+ lmti = types.indexOf(this.last_message_type);
2578
2673
  if(type) {
2579
- // Update global variable (and force display) only for "real" messages
2580
- this.time_last_message = t;
2581
- dt = this.message_display_time;
2582
- SOUNDS[type].play().catch(() => {
2583
- console.log('NOTICE: Sounds will only play after first user action');
2584
- });
2674
+ // Only log "real" messages.
2585
2675
  const
2586
2676
  now = [d.getHours(), d.getMinutes().toString().padStart(2, '0'),
2587
2677
  d.getSeconds().toString().padStart(2, '0')].join(':'),
2588
2678
  im = {time: now, text: msg, status: type};
2589
2679
  DOCUMENTATION_MANAGER.addMessage(im);
2590
- // When receiver is active, add message to its log
2680
+ // When receiver is active, add message to its log.
2591
2681
  if(RECEIVER.active) RECEIVER.log(`[${now}] ${msg}`);
2592
2682
  }
2593
- // Display text only if previous message has "timed out" or was plain text
2594
- if(dt >= this.message_display_time) {
2683
+ // Display text only if previous message has "timed out" or was less
2684
+ // urgent than this one.
2685
+ if(lmti < 0 || mti > lmti || dt >= this.message_display_time) {
2686
+ this.time_last_message = t;
2687
+ this.last_message_type = type;
2688
+ if(type) SOUNDS[type].play().catch(() => {
2689
+ console.log('NOTICE: Sounds will only play after first user action');
2690
+ });
2595
2691
  const il = document.getElementById('info-line');
2596
- il.classList.remove('error', 'warning', 'notification');
2692
+ il.classList.remove(...types);
2597
2693
  il.classList.add(type);
2598
2694
  il.innerHTML = msg;
2599
2695
  }
@@ -3516,6 +3612,53 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3516
3612
  if(redraw) this.drawDiagram(model);
3517
3613
  }
3518
3614
 
3615
+ // Solver preferences modal
3616
+
3617
+ showSolverPreferencesDialog() {
3618
+ const
3619
+ md = this.modals.solver,
3620
+ html = ['<option value="">(default)</option>'];
3621
+ for(let i = 0; i < VM.solver_list.length; i++) {
3622
+ const s = VM.solver_list[i];
3623
+ html.push(['<option value="', s,
3624
+ (s === MODEL.preferred_solver ? '"selected="selected' : ''),
3625
+ '">', VM.solver_names[s], '</option>'].join(''));
3626
+ }
3627
+ md.element('preference').innerHTML = html.join('');
3628
+ md.element('int-feasibility').value = MODEL.integer_tolerance;
3629
+ md.element('mip-gap').value = MODEL.MIP_gap;
3630
+ md.show();
3631
+ }
3632
+
3633
+ updateSolverPreferences() {
3634
+ // Set values for solver preferences.
3635
+ const
3636
+ md = this.modals.solver,
3637
+ it = md.element('int-feasibility'),
3638
+ mg = md.element('mip-gap');
3639
+ let itol = 5e-7,
3640
+ mgap = 1e-4;
3641
+ // Validate input, assuming default values for empty fields.
3642
+ if(it.value.trim()) itol = UI.validNumericInput('solver-int-feasibility',
3643
+ 'integer feasibility tolerance');
3644
+ if(itol === false) return false;
3645
+ if(mg.value.trim()) mgap = UI.validNumericInput('solver-mip-gap',
3646
+ 'relative MIP gap');
3647
+ if(mgap === false) return false;
3648
+ // Modify solver preferences for the current model.
3649
+ const ps = md.element('preference').value;
3650
+ if(ps !== MODEL.preferred_solver) {
3651
+ MODEL.preferred_solver = ps;
3652
+ // Immediately try to change to the preferred solver, as this is
3653
+ // an asynchronous call that may take time to complete.
3654
+ UI.changeSolver(ps);
3655
+ }
3656
+ MODEL.integer_tolerance = Math.max(1e-9, Math.min(0.1, itol));
3657
+ MODEL.MIP_gap = Math.max(0, Math.min(0.5, mgap));
3658
+ // Close the dialog.
3659
+ md.hide();
3660
+ }
3661
+
3519
3662
  // Note modal
3520
3663
 
3521
3664
  showNotePropertiesDialog(n=null) {
@@ -326,13 +326,13 @@ class GUIMonitor {
326
326
  } else if(jsr.server) {
327
327
  VM.solver_token = jsr.token;
328
328
  VM.solver_name = jsr.solver;
329
- // Remote solver may indicate user-specific solver time limit
329
+ // Remote solver may indicate user-specific solver time limit.
330
330
  let utl = '';
331
331
  if(jsr.time_limit) {
332
332
  VM.max_solver_time = jsr.time_limit;
333
333
  utl = ` -- ${VM.solver_name} solver: ` +
334
334
  `max. ${VM.max_solver_time} seconds per block`;
335
- // If user has a set time limit, no restrictions on tableau size
335
+ // If user has a set time limit, no restrictions on tableau size.
336
336
  VM.max_tableau_size = 0;
337
337
  }
338
338
  UI.notify('Logged on to ' + jsr.server + utl);
@@ -346,12 +346,15 @@ class GUIMonitor {
346
346
  }
347
347
 
348
348
  connectToServer() {
349
- // Prompts for credentials if not connected yet
350
- // NOTE: no authentication prompt if SOLVER.user_id in `linny-r-config.js`
351
- // is left blank
349
+ // Prompts for credentials if not connected yet.
350
+ // NOTE: No authentication prompt if SOLVER.user_id in `linny-r-config.js`
351
+ // is left blank.
352
352
  if(!VM.solver_user) {
353
+ VM.connected = false;
353
354
  VM.solver_token = 'local host';
354
- fetch('solver/', postData({action: 'logon'}))
355
+ fetch('solver/', postData({
356
+ action: 'logon',
357
+ solver: MODEL.preferred_solver || VM.solver_name}))
355
358
  .then((response) => {
356
359
  if(!response.ok) {
357
360
  UI.alert(`ERROR ${response.status}: ${response.statusText}`);
@@ -362,10 +365,15 @@ class GUIMonitor {
362
365
  try {
363
366
  const
364
367
  jsr = JSON.parse(data),
365
- svr = `Solver on ${jsr.server} is ${jsr.solver}`;
366
- if(jsr.solver !== VM.solver_name) UI.notify(svr);
368
+ sname = VM.solver_names[jsr.solver] || 'unknown',
369
+ svr = `Solver on ${jsr.server} is ${sname}`;
370
+ if(jsr.solver !== VM.solver_name) UI.notify(svr);
371
+ VM.server = jsr.server;
372
+ VM.working_directory = jsr.path;
367
373
  VM.solver_name = jsr.solver;
374
+ VM.solver_list = jsr.solver_list;
368
375
  document.getElementById('host-logo').title = svr;
376
+ VM.connected = true;
369
377
  } catch(err) {
370
378
  console.log(err, data);
371
379
  UI.alert('ERROR: Unexpected data from server: ' +
@@ -381,6 +389,7 @@ class GUIMonitor {
381
389
  }
382
390
 
383
391
  submitBlockToSolver() {
392
+ // Post MILP model plus relevant metadata to the server.
384
393
  let top = MODEL.timeout_period;
385
394
  if(VM.max_solver_time && top > VM.max_solver_time) {
386
395
  top = VM.max_solver_time;
@@ -398,7 +407,10 @@ UI.logHeapSize(`BEFORE creating post data`);
398
407
  round: VM.round_sequence[VM.current_round],
399
408
  columns: VM.columnsInBlock,
400
409
  data: VM.lines,
401
- timeout: top
410
+ solver: MODEL.preferred_solver,
411
+ timeout: top,
412
+ inttol: MODEL.integer_tolerance,
413
+ mipgap: MODEL.MIP_gap
402
414
  });
403
415
  UI.logHeapSize(`AFTER creating post data`);
404
416
  // Immediately free the memory taken up by VM.lines