linny-r 1.6.8 → 1.7.1

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.
@@ -260,7 +260,7 @@ class Controller {
260
260
  return VM.sig2Dig(n) + ' ' + 'kMGTP'.charAt(m) + 'B';
261
261
  }
262
262
 
263
- // Shapes are only used to draw model diagrams
263
+ // Shapes are only used to draw model diagrams.
264
264
 
265
265
  createShape(mdl) {
266
266
  if(this.paper) return new Shape(mdl);
@@ -275,7 +275,7 @@ class Controller {
275
275
  if(shape) shape.removeFromDOM();
276
276
  }
277
277
 
278
- // Methods to ensure proper naming of entities
278
+ // Methods to ensure proper naming of entities.
279
279
 
280
280
  cleanName(name) {
281
281
  // Returns `name` without the object-attribute separator |, backslashes,
@@ -429,7 +429,7 @@ class Controller {
429
429
 
430
430
 
431
431
  nameToID(name) {
432
- // Returns a name in lower case with link arrow replaced by three
432
+ // Return a name in lower case with link arrow replaced by three
433
433
  // underscores, constraint link arrow by four underscores, and spaces
434
434
  // converted to underscores; in this way, IDs will always be valid
435
435
  // JavaScript object properties.
@@ -442,7 +442,7 @@ class Controller {
442
442
  // Empty string signals failure.
443
443
  return '';
444
444
  }
445
- // NOTE: replace single quotes by Unicode apostrophe so that they
445
+ // NOTE: Replace single quotes by Unicode apostrophe so that they
446
446
  // cannot interfere with JavaScript strings delimited by single quotes.
447
447
  return name.toLowerCase().replace(/\s/g, '_').replace("'", '\u2019');
448
448
  }
@@ -485,8 +485,8 @@ class Controller {
485
485
  // Methods to notify modeler
486
486
 
487
487
  setMessage(msg, type, cause=null) {
488
- // Only log errors and warnings on the browser console
489
- // NOTE: optionally, the JavaScript error can be passed via `cause`
488
+ // Only log errors and warnings on the browser console.
489
+ // NOTE: Optionally, the JavaScript error can be passed via `cause`.
490
490
  if(type === 'error' || type === 'warning') {
491
491
  // Add type unless message already starts with it
492
492
  type = type.toUpperCase() + ':';
@@ -547,8 +547,8 @@ class Controller {
547
547
  }
548
548
 
549
549
  postResponseOK(text, notify=false) {
550
- // Checks whether server reponse text is warning or error, and notifies
551
- // the modeler if second argument is TRUE
550
+ // Check whether server reponse text is warning or error, and notify
551
+ // the modeler if second argument is TRUE.
552
552
  let mtype = 'notification';
553
553
  if(text.startsWith('ERROR:')) {
554
554
  mtype = 'error';
@@ -563,20 +563,20 @@ class Controller {
563
563
  }
564
564
 
565
565
  loginPrompt() {
566
- // The VM needs credentials - his should only occur for the GUI
566
+ // The VM needs credentials - his should only occur for the GUI.
567
567
  console.log('WARNING: VM needs credentials, but GUI not active');
568
568
  }
569
569
 
570
570
  resetModel() {
571
- // Resets the Virtual Machine (clears solution)
571
+ // Reset the Virtual Machine (clears solution).
572
572
  VM.reset();
573
- // Redraw model in the browser (GUI only)
573
+ // Redraw model in the browser (GUI only).
574
574
  MODEL.clearSelection();
575
575
  this.drawDiagram(MODEL);
576
576
  }
577
577
 
578
578
  stopSolving() {
579
- // Notify user only if VM was halted
579
+ // Notify user only if VM was halted.
580
580
  if(VM.halted) {
581
581
  this.notify('Solver HALTED');
582
582
  } else {
@@ -585,8 +585,9 @@ class Controller {
585
585
  }
586
586
 
587
587
  // NOTE: The following UI functions are implemented as "dummy" methods
588
- // because they are called by the Virtual Machine and/or by other controllers
589
- // while they can only be meaningfully performed by the GUI controller
588
+ // because they are called by the Virtual Machine and/or by other
589
+ // controllers while they can only be meaningfully performed by the
590
+ // GUI controller.
590
591
  addListeners() {}
591
592
  readyToReset() {}
592
593
  updateScaleUnitList() {}
@@ -616,27 +617,27 @@ class RepositoryBrowser {
616
617
  this.repositories = [];
617
618
  this.repository_index = -1;
618
619
  this.module_index = -1;
619
- // Get the repository list from the server
620
+ // Get the repository list from the server.
620
621
  this.getRepositories();
621
622
  this.reset();
622
623
  }
623
624
 
624
625
  reset() {
625
626
  this.visible = false;
626
- // NOTE: do NOT reset repository list or module index, because:
627
- // (1) they are properties of the local host, and hence model-independent
627
+ // NOTE: Do NOT reset repository list or module index, because:
628
+ // (1) they are properties of the local host, and hence model-independent;
628
629
  // (2) they must be known when loading a module as model, whereas the
629
- // loadingModel method hides and resets all stay-on-top dialogs
630
+ // loadingModel method hides and resets all stay-on-top dialogs.
630
631
  }
631
632
 
632
633
  get isLocalHost() {
633
- // Returns TRUE if first repository on the list is 'local host'
634
+ // Return TRUE if first repository on the list is 'local host'.
634
635
  return this.repositories.length > 0 &&
635
636
  this.repositories[0].name === 'local host';
636
637
  }
637
638
 
638
639
  getRepositories() {
639
- // Gets the list of repository names from the server
640
+ // Get the list of repository names from the server.
640
641
  this.repositories.length = 0;
641
642
  fetch('repo/', postData({action: 'list'}))
642
643
  .then((response) => {
@@ -647,14 +648,14 @@ class RepositoryBrowser {
647
648
  })
648
649
  .then((data) => {
649
650
  if(UI.postResponseOK(data)) {
650
- // NOTE: trim to prevent empty name strings
651
+ // NOTE: Trim to prevent empty name strings.
651
652
  const rl = data.trim().split('\n');
652
653
  for(let i = 0; i < rl.length; i++) {
653
654
  this.addRepository(rl[i].trim());
654
655
  }
655
656
  }
656
- // NOTE: set index to first repository on list (typically local host)
657
- // unless the list is empty
657
+ // NOTE: Set index to first repository on list (typically the
658
+ // local host repository) unless the list is empty.
658
659
  this.repository_index = Math.min(0, this.repositories.length - 1);
659
660
  this.updateDialog();
660
661
  })
@@ -662,7 +663,7 @@ class RepositoryBrowser {
662
663
  }
663
664
 
664
665
  repositoryByName(n) {
665
- // Returns the repository having name `n` if already known, otherwise NULL
666
+ // Return the repository having name `n` if already known, or NULL.
666
667
  for(let i = 0; i < this.repositories.length; i++) {
667
668
  if(this.repositories[i].name === n) {
668
669
  return this.repositories[i];
@@ -672,8 +673,8 @@ class RepositoryBrowser {
672
673
  }
673
674
 
674
675
  asFileName(s) {
675
- // Returns string `s` with whitespace converted to a single dash, and
676
- // special characters converted to underscores
676
+ // Return string `s` with whitespace converted to a single dash, and
677
+ // special characters converted to underscores.
677
678
  return s.normalize('NFKD').trim()
678
679
  .replace(/[\s\-]+/g, '-')
679
680
  .replace(/[^A-Za-z0-9_\-]/g, '_')
@@ -681,12 +682,13 @@ class RepositoryBrowser {
681
682
  }
682
683
 
683
684
  loadModuleAsModel() {
684
- // Loads selected module as model
685
+ // Load selected module as model.
685
686
  if(this.repository_index >= 0 && this.module_index >= 0) {
686
- // NOTE: when loading new model, the stay-on-top dialogs must be reset
687
+ // NOTE: When loading new model, the stay-on-top dialogs must be
688
+ // reset (GUI only; for console this is a "dummy" method).
687
689
  UI.hideStayOnTopDialogs();
688
690
  const r = this.repositories[this.repository_index];
689
- // NOTE: pass FALSE to indicate "no inclusion; load XML as model"
691
+ // NOTE: pass FALSE to indicate "no inclusion; load XML as model".
690
692
  r.loadModule(this.module_index, false);
691
693
  }
692
694
  }
@@ -1289,6 +1291,8 @@ class ExperimentManager {
1289
1291
  // Only now compute the simulation run time (number of time steps)
1290
1292
  xr.time_steps = MODEL.end_period - MODEL.start_period + 1;
1291
1293
  VM.callback = this.callback;
1294
+ // NOTE: Asynchronous call. All follow-up actions must be performed
1295
+ // by the callback function.
1292
1296
  VM.solveModel();
1293
1297
  }
1294
1298
  }
@@ -1315,11 +1319,11 @@ class ExperimentManager {
1315
1319
  }
1316
1320
 
1317
1321
  processRestOfRun() {
1318
- // Performs post-processing after run results have been added.
1322
+ // Perform post-processing after run results have been added.
1319
1323
  const x = MODEL.running_experiment;
1320
1324
  if(!x) return;
1321
1325
  const aci = x.active_combination_index;
1322
- // Always add solver messages
1326
+ // Always add solver messages.
1323
1327
  x.runs[aci].addMessages();
1324
1328
  const n = x.combinations.length;
1325
1329
  if(!VM.halted && aci < n - 1 && aci != x.single_run) {
@@ -1330,8 +1334,8 @@ class ExperimentManager {
1330
1334
  } else {
1331
1335
  x.active_combination_index++;
1332
1336
  let delay = 5;
1333
- // NOTE: when executing a remote command, wait for 1 second to
1334
- // allow enough time for report writing
1337
+ // NOTE: When executing a remote command, wait for 1 second to
1338
+ // allow enough time for report writing.
1335
1339
  if(RECEIVER.active && RECEIVER.experiment) {
1336
1340
  UI.setMessage('Reporting run #' + (x.active_combination_index - 1));
1337
1341
  delay = 1000;
@@ -1352,8 +1356,8 @@ class ExperimentManager {
1352
1356
  `Experiment <em>${x.title}</em> terminated during run #${aci}`);
1353
1357
  RECEIVER.deactivate();
1354
1358
  }
1355
- // No more runs => stop experiment, and perform call-back
1356
- // NOTE: if call-back is successful, the receiver will resume listening
1359
+ // No more runs => stop experiment, and perform call-back.
1360
+ // NOTE: If call-back is successful, the receiver will resume listening.
1357
1361
  if(RECEIVER.active) {
1358
1362
  RECEIVER.experiment = '';
1359
1363
  RECEIVER.callBack();
@@ -1363,34 +1367,35 @@ class ExperimentManager {
1363
1367
  MODEL.parseSettings(x.original_model_settings);
1364
1368
  MODEL.round_sequence = x.original_round_sequence;
1365
1369
  // Reset the Virtual Machine so t=0 at the status line,
1366
- // and ALL expressions are reset as well
1370
+ // and ALL expressions are reset as well.
1367
1371
  VM.reset();
1368
1372
  this.readyButtons();
1369
1373
  }
1370
1374
  this.drawTable();
1371
- // Reset the model, as results of last run will be showing still
1375
+ // Reset the model, as results of last run will be showing still.
1372
1376
  UI.resetModel();
1373
1377
  CHART_MANAGER.resetChartVectors();
1374
- // NOTE: clear chart only when done (charts do not update during experiment)
1378
+ // NOTE: Clear chart only when done; charts do not update when an
1379
+ // experiment is running.
1375
1380
  if(!MODEL.running_experiment) CHART_MANAGER.updateDialog();
1376
1381
  }
1377
1382
 
1378
1383
  stopExperiment() {
1379
- // Interrupt solver but retain data on server (and no resume)
1384
+ // Interrupt solver but retain data on server (and no resume).
1380
1385
  VM.halt();
1381
1386
  }
1382
1387
 
1383
1388
  showProgress(ci, p, n) {
1384
- // Report progress on the console
1389
+ // Report progress on the console.
1385
1390
  console.log('\nRun', ci, `(${p}% of ${n})`);
1386
1391
  }
1387
1392
 
1388
1393
  resumeButtons() {
1389
- // Console experiments cannot be paused, and hence not resumed
1394
+ // Console experiments cannot be paused, and hence not resumed.
1390
1395
  return false;
1391
1396
  }
1392
1397
 
1393
- // Dummy methods: actions that are meaningful only for the graphical UI
1398
+ // Dummy methods: actions that are meaningful only for the graphical UI.
1394
1399
  drawTable() {}
1395
1400
  readyButtons() {}
1396
1401
  pausedButtons() {}
@@ -449,7 +449,7 @@ class GUIController extends Controller {
449
449
  // not to other dialog objects.
450
450
  const main_modals = ['logon', 'model', 'load', 'password', 'settings',
451
451
  'actors', 'add-process', 'add-product', 'move', 'note', 'clone',
452
- 'replace', 'expression'];
452
+ 'replace', 'expression', 'server', 'solver'];
453
453
  for(let i = 0; i < main_modals.length; i++) {
454
454
  this.modals[main_modals[i]] = new ModalDialog(main_modals[i]);
455
455
  }
@@ -581,8 +581,10 @@ class GUIController extends Controller {
581
581
  // NOTE: When user name is specified, solver is not on local host.
582
582
  const hl = document.getElementById('host-logo');
583
583
  hl.classList.add('local-server');
584
- hl.addEventListener('click', () => UI.shutDownServer());
584
+ hl.addEventListener('click', () => UI.showServerModal());
585
585
  }
586
+ this.server_modal = new ModalDialog('server');
587
+
586
588
 
587
589
  // Vertical tool bar buttons:
588
590
  this.buttons.clone.addEventListener('click',
@@ -704,10 +706,27 @@ class GUIController extends Controller {
704
706
  // Ensure that model documentation can no longer be edited.
705
707
  DOCUMENTATION_MANAGER.clearEntity([MODEL]);
706
708
  });
707
- // 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.
708
712
  this.modals.settings.element('scale-units-btn').addEventListener('click',
709
- // Open the scale units modal dialog on top of the settings dialog.
710
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());
711
730
 
712
731
  // Modals related to vertical toolbar buttons.
713
732
  this.modals['add-process'].ok.addEventListener('click',
@@ -796,7 +815,7 @@ class GUIController extends Controller {
796
815
 
797
816
  // The REPLACE dialog appears when a product is Ctrl-clicked.
798
817
  this.modals.replace.ok.addEventListener('click',
799
- () => UI.replaceProduct());
818
+ () => UI.replaceProduct());
800
819
  this.modals.replace.cancel.addEventListener('click',
801
820
  () => UI.modals.replace.hide());
802
821
 
@@ -871,21 +890,25 @@ class GUIController extends Controller {
871
890
  }
872
891
 
873
892
  loadModelFromXML(xml) {
874
- // Parses `xml` and updates the GUI
893
+ // Parse `xml` and update the GUI.
875
894
  const loaded = MODEL.parseXML(xml);
876
- // 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.
877
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
+ }
878
901
  this.updateScaleUnitList();
879
902
  this.drawDiagram(MODEL);
880
- // Cursor may have been set to `waiting` when decrypting
903
+ // Cursor may have been set to `waiting` when decrypting.
881
904
  this.normalCursor();
882
905
  this.setMessage('');
883
906
  this.updateButtons();
884
907
  // Undoable operations no longer apply!
885
908
  UNDO_STACK.clear();
886
- // Autosaving should start anew
909
+ // Autosaving should start anew.
887
910
  AUTO_SAVE.setAutoSaveInterval();
888
- // Signal success or failure
911
+ // Signal success or failure.
889
912
  return loaded;
890
913
  }
891
914
 
@@ -899,15 +922,15 @@ class GUIController extends Controller {
899
922
  MODEL.clearSelection();
900
923
  this.paper.drawModel(MODEL);
901
924
  this.updateButtons();
902
- // NOTE: when "moving up" in the cluster hierarchy, bring the former focal
903
- // cluster into view
925
+ // NOTE: When "moving up" in the cluster hierarchy, bring the former
926
+ // focal cluster into view.
904
927
  if(fc.cluster == MODEL.focal_cluster) {
905
928
  this.scrollIntoView(fc.shape.element.childNodes[0]);
906
929
  }
907
930
  }
908
931
 
909
932
  drawDiagram(mdl) {
910
- // "Queue" a draw request (to avoid redrawing too often)
933
+ // "Queue" a draw request (to avoid redrawing too often).
911
934
  if(this.busy_drawing) {
912
935
  this.draw_requests += 1;
913
936
  } else {
@@ -953,19 +976,81 @@ class GUIController extends Controller {
953
976
  if(a.links.indexOf(link) >= 0) this.paper.drawArrow(a);
954
977
  }
955
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
+ }
956
1039
 
957
1040
  shutDownServer() {
958
1041
  // This terminates the local host server script and display a plain
959
1042
  // HTML message in the browser with a restart button.
1043
+ this.modals.server.hide();
960
1044
  if(!SOLVER.user_id) window.open('./shutdown', '_self');
961
1045
  }
962
1046
 
963
1047
  shutDownToUpdate() {
964
- // Sisgnal server that an update is required. This will close the
1048
+ // Signal server that an update is required. This will close the
965
1049
  // local host Linny-R server. If this server was started by the
966
1050
  // standard OS batch script, this script will proceed to update
967
1051
  // Linny-R via npm and then restart the server again. If not, the
968
1052
  // fetch request will time out, anf the user will be warned.
1053
+ this.modals.server.hide();
969
1054
  if(SOLVER.user_id) return;
970
1055
  fetch('update/')
971
1056
  .then((response) => {
@@ -1023,6 +1108,8 @@ class GUIController extends Controller {
1023
1108
  `Linny-R version ${m[1]} has been installed.`,
1024
1109
  'To continue, you must reload this page, and',
1025
1110
  'confirm when prompted by your browser.');
1111
+ // Hide "update" button in server dialog.
1112
+ UI.modals.server.element('update').style.display = 'none';
1026
1113
  } else {
1027
1114
  // Inform user that install appears to have failed.
1028
1115
  msg.push(
@@ -1047,6 +1134,10 @@ class GUIController extends Controller {
1047
1134
  preventUpdate() {
1048
1135
  // Signal server that no update is required.
1049
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';
1050
1141
  fetch('no-update/')
1051
1142
  .then((response) => {
1052
1143
  if(!response.ok) {
@@ -2591,7 +2682,7 @@ class GUIController extends Controller {
2591
2682
  }
2592
2683
  // Display text only if previous message has "timed out" or was less
2593
2684
  // urgent than this one.
2594
- if(mti > lmti || dt >= this.message_display_time) {
2685
+ if(lmti < 0 || mti > lmti || dt >= this.message_display_time) {
2595
2686
  this.time_last_message = t;
2596
2687
  this.last_message_type = type;
2597
2688
  if(type) SOUNDS[type].play().catch(() => {
@@ -3521,6 +3612,53 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3521
3612
  if(redraw) this.drawDiagram(model);
3522
3613
  }
3523
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
+
3524
3662
  // Note modal
3525
3663
 
3526
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