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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.6.8",
3
+ "version": "1.7.1",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/server.js CHANGED
@@ -137,13 +137,13 @@ const MILPSolver = require('./static/scripts/linny-r-milp.js');
137
137
  // - workspace=[path] will create workspace in [path] instead of (main)/user
138
138
  const SETTINGS = commandLineSettings();
139
139
 
140
- // The workspace defines the paths to directories where Linny-R can write files
140
+ // The workspace defines the paths to directories where Linny-R can write files.
141
141
  const WORKSPACE = createWorkspace();
142
142
 
143
- // Initialize the solver
144
- const SOLVER = new MILPSolver(SETTINGS, WORKSPACE);
143
+ // Initialize the solver.
144
+ const SOLVER = new MILPSolver(SETTINGS.preferred_solver, WORKSPACE);
145
145
 
146
- // Create launch script
146
+ // Create launch script.
147
147
  createLaunchScript();
148
148
 
149
149
  // Create the HTTP server.
@@ -979,7 +979,7 @@ function receiver(res, sp) {
979
979
  rcvrAbort(res, rpath, rfile, sp.get('log') || 'NO EVENT LOG');
980
980
  } else if(action === 'report') {
981
981
  let run = sp.get('run');
982
- // Zero-pad run number to permit sorting run report file names in sequence
982
+ // Zero-pad run number to permit sorting run report file names in sequence.
983
983
  run = (run ? '-' + run.padStart(3, '0') : '');
984
984
  let data = sp.get('data') || '',
985
985
  stats = sp.get('stats') || '',
@@ -1262,11 +1262,24 @@ function processRequest(req, res, cmd, data) {
1262
1262
  // NOTE: On remote servers, solver actions require authentication.
1263
1263
  if(action === 'logon') {
1264
1264
  // No authentication -- simply return the passed token, "local host"
1265
- // as server name, and the identifier of the solver.
1266
- serveJSON(res,
1267
- {token: 'local host', server: 'local host', solver: SOLVER.id});
1265
+ // as server name, the name of the solver, the list of installed
1266
+ // solvers, and the Linny-R directory.
1267
+ serveJSON(res, {
1268
+ token: 'local host',
1269
+ server: 'local host',
1270
+ solver: SOLVER.id,
1271
+ solver_list: Object.keys(SOLVER.solver_list),
1272
+ path: WORKING_DIRECTORY
1273
+ });
1268
1274
  } else if(action === 'png') {
1269
1275
  convertSVGtoPNG(req, res, sp);
1276
+ } else if(action === 'change') {
1277
+ const sid = sp.get('solver');
1278
+ if(SOLVER.changeDefault(sid)) {
1279
+ servePlainText(res, 'Default solver set to ' + SOLVER.name);
1280
+ } else {
1281
+ servePlainText(res, 'WARNING: Failed to change solver to ' + sid);
1282
+ }
1270
1283
  } else if(action === 'solve') {
1271
1284
  serveJSON(res, SOLVER.solveBlock(sp));
1272
1285
  } else {
@@ -1282,7 +1295,6 @@ function processRequest(req, res, cmd, data) {
1282
1295
  serveHTML(res, SHUTDOWN_MESSAGE);
1283
1296
  SERVER.close();
1284
1297
  } else if(cmd === 'version') {
1285
- logAction('HERE version = ' + VERSION_INFO.current);
1286
1298
  servePlainText(res, 'Current version is ' + VERSION_INFO.current);
1287
1299
  } else if(cmd === 'update') {
1288
1300
  // Shut down this server silently. When the server was started from
@@ -1529,8 +1541,6 @@ function commandLineSettings() {
1529
1541
  launch: false,
1530
1542
  port: 5050,
1531
1543
  preferred_solver: '',
1532
- solver: '',
1533
- solver_path: '',
1534
1544
  user_dir: path.join(WORKING_DIRECTORY, 'user')
1535
1545
  };
1536
1546
  const
@@ -1600,142 +1610,36 @@ Possible options are:
1600
1610
  }
1601
1611
  }
1602
1612
  }
1603
- // Check whether MILP solver(s) and Inkscape have been installed
1613
+ // Check whether Inkscape has been installed.
1604
1614
  const path_list = process.env.PATH.split(path.delimiter);
1605
- let gurobi_path = '',
1606
- scip_path = '',
1607
- cplex_path = '',
1608
- match,
1609
- max_v = -1;
1610
1615
  for(let i = 0; i < path_list.length; i++) {
1611
- match = path_list[i].match(/gurobi(\d+)/i);
1612
- if(match && parseInt(match[1]) > max_v) {
1613
- gurobi_path = path_list[i];
1614
- max_v = parseInt(match[1]);
1615
- }
1616
- match = path_list[i].match(/[\/\\]cplex[\/\\]bin/i);
1617
- if(match) {
1618
- cplex_path = path_list[i];
1619
- } else {
1620
- // NOTE: CPLEX may create its own environment variable for its paths
1621
- match = path_list[i].match(/%(.*cplex.*)%/i);
1622
- if(match) {
1623
- const cpl = process.env[match[1]].split(path.delimiter);
1624
- for(let i = 0; i < cpl.length; i++) {
1625
- match = cpl[i].match(/[\/\\]cplex[\/\\]bin/i);
1626
- if(match) {
1627
- cplex_path = cpl[i];
1628
- break;
1629
- }
1630
- }
1631
- }
1632
- }
1633
- match = path_list[i].match(/[\/\\]scip[^\/\\]+[\/\\]bin/i);
1634
- if(match) scip_path = path_list[i];
1616
+ // Check whether it is an Inkscape path.
1635
1617
  match = path_list[i].match(/inkscape/i);
1618
+ // If so, use it (so the *last* matching path will be used).
1636
1619
  if(match) settings.inkscape = path_list[i];
1637
1620
  }
1638
- if(!gurobi_path && !PLATFORM.startsWith('win')) {
1639
- console.log('Looking for Gurobi in /usr/local/bin');
1640
- try {
1641
- // On macOS and Unix, Gurobi is in the user's local binaries
1642
- const gp = '/usr/local/bin';
1643
- fs.accessSync(gp + '/gurobi_cl');
1644
- gurobi_path = gp;
1645
- } catch(err) {
1646
- // No real error, so no action needed
1647
- }
1648
- }
1649
- if(gurobi_path) {
1650
- console.log('Path to Gurobi:', gurobi_path);
1651
- // Check if command line version is executable
1652
- const sp = path.join(gurobi_path,
1653
- 'gurobi_cl' + (PLATFORM.startsWith('win') ? '.exe' : ''));
1654
- try {
1655
- fs.accessSync(sp, fs.constants.X_OK);
1656
- if(settings.solver !== 'gurobi')
1657
- settings.solver = 'gurobi';
1658
- settings.solver_path = sp;
1659
- } catch(err) {
1660
- console.log(err.message);
1661
- console.log(
1662
- 'WARNING: Failed to access the Gurobi command line application');
1663
- }
1664
- }
1665
- // Check if cplex(.exe) exists in its directory
1666
- let sp = path.join(cplex_path, 'cplex' + (PLATFORM.startsWith('win') ? '.exe' : ''));
1667
- const need_cplex = !settings.solver || settings.preferred_solver === 'cplex';
1668
- try {
1669
- fs.accessSync(sp, fs.constants.X_OK);
1670
- console.log('Path to CPLEX:', sp);
1671
- if(need_cplex) {
1672
- settings.solver = 'cplex';
1673
- settings.solver_path = sp;
1674
- }
1675
- } catch(err) {
1676
- // Only report error if CPLEX is needed
1677
- if(need_cplex) {
1678
- console.log(err.message);
1679
- console.log('WARNING: CPLEX application not found in', sp);
1680
- }
1681
- }
1682
- // Check if scip(.exe) exists in its directory
1683
- sp = path.join(scip_path, 'scip' + (PLATFORM.startsWith('win') ? '.exe' : ''));
1684
- const need_scip = !settings.solver || settings.preferred_solver === 'scip';
1685
- try {
1686
- fs.accessSync(sp, fs.constants.X_OK);
1687
- console.log('Path to SCIP:', sp);
1688
- if(need_scip) {
1689
- settings.solver = 'scip';
1690
- settings.solver_path = sp;
1691
- }
1692
- } catch(err) {
1693
- // Only report error if SCIP is needed
1694
- if(need_scip) {
1695
- console.log(err.message);
1696
- console.log('WARNING: SCIP application not found in', sp);
1697
- }
1698
- }
1699
- // Check if lp_solve(.exe) exists in working directory
1700
- sp = path.join(WORKING_DIRECTORY,
1701
- 'lp_solve' + (PLATFORM.startsWith('win') ? '.exe' : ''));
1702
- const need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
1703
- try {
1704
- fs.accessSync(sp, fs.constants.X_OK);
1705
- console.log('Path to LP_solve:', sp);
1706
- if(need_lps) {
1707
- settings.solver = 'lp_solve';
1708
- settings.solver_path = sp;
1709
- }
1710
- } catch(err) {
1711
- // Only report error if LP_solve is needed
1712
- if(need_lps) {
1713
- console.log(err.message);
1714
- console.log('WARNING: LP_solve application not found in', sp);
1715
- }
1716
- }
1717
- // On macOS, Inkscape is not added to the PATH environment variable
1621
+ // NOTE: On macOS, Inkscape is not added to the PATH environment variable.
1718
1622
  if(!settings.inkscape && PLATFORM === 'darwin') {
1719
1623
  console.log('Looking for Inkscape in Applications...');
1720
1624
  try {
1721
- // Look in the default directory
1625
+ // Look in the default directory.
1722
1626
  const ip = '/Applications/Inkscape.app/Contents/MacOS';
1723
1627
  fs.accessSync(ip);
1724
1628
  settings.inkscape = ip;
1725
1629
  } catch(err) {
1726
- // No real error, so no action needed
1630
+ // No real error, so no action needed.
1727
1631
  }
1728
1632
  }
1729
- // Verify that Inkscape is installed
1633
+ // Verify that Inkscape is installed.
1730
1634
  if(settings.inkscape) {
1731
- // NOTE: On Windows, the command line version is a .com file
1635
+ // NOTE: On Windows, the command line version is a .com file.
1732
1636
  let ip = path.join(settings.inkscape,
1733
1637
  'inkscape' + (PLATFORM.startsWith('win') ? '.com' : ''));
1734
1638
  try {
1735
1639
  fs.accessSync(ip, fs.constants.X_OK);
1736
1640
  } catch(err) {
1737
1641
  // NOTE: As of Inkscape version 1.3, the command line executable
1738
- // is called inkscapecom(.com)
1642
+ // is called inkscapecom(.com).
1739
1643
  ip = path.join(settings.inkscape,
1740
1644
  'inkscapecom' + (PLATFORM.startsWith('win') ? '.com' : ''));
1741
1645
  }
@@ -1759,14 +1663,14 @@ Possible options are:
1759
1663
  }
1760
1664
 
1761
1665
  function createWorkspace() {
1762
- // Verifies that Linny-R has write access to the user workspace, defines
1763
- // paths to sub-directories, and creates them if necessary
1666
+ // Verify that Linny-R has write access to the user workspace, define
1667
+ // paths to sub-directories, and create them if necessary.
1764
1668
  try {
1765
- // See whether the user directory already exists
1669
+ // See whether the user directory already exists.
1766
1670
  try {
1767
1671
  fs.accessSync(SETTINGS.user_dir, fs.constants.R_OK | fs.constants.W_O);
1768
1672
  } catch(err) {
1769
- // If not, try to create it
1673
+ // If not, try to create it.
1770
1674
  fs.mkdirSync(SETTINGS.user_dir);
1771
1675
  console.log('Created user directory:', SETTINGS.user_dir);
1772
1676
  }
@@ -1776,7 +1680,7 @@ function createWorkspace() {
1776
1680
  SETTINGS.user_dir);
1777
1681
  process.exit();
1778
1682
  }
1779
- // Define the sub-directory paths
1683
+ // Define the sub-directory paths.
1780
1684
  const ws = {
1781
1685
  autosave: path.join(SETTINGS.user_dir, 'autosave'),
1782
1686
  channel: path.join(SETTINGS.user_dir, 'channel'),
@@ -1787,7 +1691,7 @@ function createWorkspace() {
1787
1691
  reports: path.join(SETTINGS.user_dir, 'reports'),
1788
1692
  solver_output: path.join(SETTINGS.user_dir, 'solver')
1789
1693
  };
1790
- // Create these sub-directories if not aready there
1694
+ // Create these sub-directories if not aready there.
1791
1695
  try {
1792
1696
  for(let p in ws) if(ws.hasOwnProperty(p)) {
1793
1697
  try {
@@ -1801,9 +1705,11 @@ function createWorkspace() {
1801
1705
  console.log(err.message);
1802
1706
  console.log('WARNING: No access to workspace directory');
1803
1707
  }
1804
- // The file containing name, URL and access token for remote repositories
1708
+ // The file containing name, URL and access token for remote repositories.
1805
1709
  ws.repositories = path.join(SETTINGS.user_dir, 'repositories.cfg');
1806
- // Return the updated workspace object
1710
+ // For completeness, add path to Linny-R directory.
1711
+ ws.working_directory = WORKING_DIRECTORY;
1712
+ // Return the updated workspace object.
1807
1713
  return ws;
1808
1714
  }
1809
1715
 
package/static/index.html CHANGED
@@ -162,16 +162,17 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
162
162
  if(info.length > 1) {
163
163
  LINNY_R_VERSION = info[0];
164
164
  const v = 'Version ' + LINNY_R_VERSION;
165
- // Update the "home page" of the documentation manager
165
+ // Update the "home page" of the documentation manager.
166
166
  DOCUMENTATION_MANAGER.about_linny_r =
167
167
  DOCUMENTATION_MANAGER.about_linny_r.replace(
168
168
  '[LINNY_R_VERSION]', v);
169
- // Update the version number in the browser's upper left corner
169
+ // Update the version number in the browser's upper left corner.
170
170
  document.getElementById('linny-r-version-number').innerHTML = v;
171
- // NOTE: server detects "version 0" when npmjs website was
172
- // not reached; if so, do not suggest a new version exists
171
+ // NOTE: Server detects "version 0" when npmjs website was
172
+ // not reached; if so, do not suggest a new version exists.
173
173
  if(info[1] !== 'up-to-date' && info[1] !== '0') {
174
- // Inform user that newer version exists
174
+ // Inform user that newer version exists.
175
+ UI.newer_version = info[1];
175
176
  UI.check_update_modal.element('msg').innerHTML = [
176
177
  '<a href="', GITHUB_REPOSITORY,
177
178
  '/releases/tag/v', info[1], '" ',
@@ -423,6 +424,28 @@ and move the cursor over the status bar">
423
424
  </div>
424
425
  </div>
425
426
 
427
+ <!-- the SERVER dialog permits solver selection, update and shut-down -->
428
+ <div id="server-modal" class="modal">
429
+ <div id="server-dlg" class="inp-dlg">
430
+ <div class="dlg-title">
431
+ Server and solver
432
+ <img class="cancel-btn" src="images/cancel.png">
433
+ <img class="ok-btn" src="images/ok.png">
434
+ </div>
435
+ <div id="server-host-div" title="Linny-R directory">Server on local host</div>
436
+ <div id="server-no-solver-div">No solver detected</div>
437
+ <div id="server-solver-div" hidden="hidden">
438
+ <div id="server-sel">
439
+ <label>Default solver:</label>
440
+ <select id="server-solver">
441
+ </select>
442
+ </div>
443
+ </div>
444
+ <button id="server-update" hidden="hidden">Update Linny-R to new version</button>
445
+ <button id="server-shut-down">Shut down server</button>
446
+ </div>
447
+ </div>
448
+
426
449
  <!-- the MODEL dialog prompts for (optional) model name and author name -->
427
450
  <div id="model-modal" class="modal">
428
451
  <div id="model-dlg" class="inp-dlg">
@@ -605,6 +628,8 @@ and move the cursor over the status bar">
605
628
  <td colspan="2">Solver time limit:&nbsp;
606
629
  <input id="settings-time-limit" type="text" autocomplete="off"
607
630
  style="width:35px;text-align:center"> seconds per block
631
+ <img id="settings-solver-prefs-btn" class="sbtn" src="images/settings.png"
632
+ title="Set solver preferences">
608
633
  </td>
609
634
  </tr>
610
635
  <tr>
@@ -688,6 +713,40 @@ NOTE: Unit symbols are case-sensitive, so BTU &ne; Btu">
688
713
  </div>
689
714
  </div>
690
715
 
716
+ <!-- the SOLVER dialog permits specifying solver preference and tolerances -->
717
+ <div id="solver-modal" class="modal">
718
+ <div id="solver-dlg" class="inp-dlg">
719
+ <div class="dlg-title">
720
+ Solver preferences
721
+ <img class="cancel-btn" src="images/cancel.png">
722
+ <img class="ok-btn" src="images/ok.png">
723
+ </div>
724
+ <table style="width: 100%">
725
+ <tr title="This solver will be used if it is installed">
726
+ <td>
727
+ <label>Preferred solver:</label>
728
+ <select id="solver-preference">
729
+ </select>
730
+ </td>
731
+ </tr>
732
+ <tr title="Tolerance may range from 1e-9 to 0.1">
733
+ <td>
734
+ <label>Integer feasibility tolerance:</label>
735
+ <input id="solver-int-feasibility" style="width: 55px"
736
+ placeholder="5e-7" type="text" autocomplete="off">
737
+ </td>
738
+ </tr>
739
+ <tr title="Relative gap may range from 0 to 0.5">
740
+ <td>
741
+ <label>Relative MIP gap:</label>
742
+ <input id="solver-mip-gap" style="width: 55px"
743
+ placeholder="1e-4" type="text" autocomplete="off">
744
+ </td>
745
+ </tr>
746
+ </table>
747
+ </div>
748
+ </div>
749
+
691
750
  <!-- the REPOSITORY BROWSER dialog allows interaction with repositories -->
692
751
  <div id="repository-dlg" class="inp-dlg">
693
752
  <div id="repository-hdr" class="dragger dlg-title">Repository browser
@@ -944,6 +944,7 @@ img.inline-cancel-btn {
944
944
  height: min-content;
945
945
  }
946
946
 
947
+ #settings-solver-prefs-btn,
947
948
  #settings-scale-units-btn {
948
949
  margin-left: 0px;
949
950
  margin-right: 7px;
@@ -952,6 +953,7 @@ img.inline-cancel-btn {
952
953
  filter: brightness(85%);
953
954
  }
954
955
 
956
+ #settings-solver-prefs-btn:hover,
955
957
  #settings-scale-units-btn:hover {
956
958
  filter: brightness(200%);
957
959
  }
@@ -994,6 +996,16 @@ input.pws-5 {
994
996
  background-color: #c0ffc0;
995
997
  }
996
998
 
999
+ /* styles for the SOLVER PREFERENCES dialog */
1000
+ #solver-modal {
1001
+ z-index: 110; /* on top of the SETTINGS dialog */
1002
+ }
1003
+
1004
+ #solver-dlg {
1005
+ width: 212px;
1006
+ height: min-content;
1007
+ }
1008
+
997
1009
  /* styles for the ACTOR DIALOG
998
1010
  NOTE: the z-index is very high for this dialog, as it should appear
999
1011
  on top of the ACTOR LIST DIALOG
@@ -1190,6 +1202,35 @@ td.export {
1190
1202
  color: #0000b0;
1191
1203
  }
1192
1204
 
1205
+ /* styles for the SERVER dialog */
1206
+ #server-dlg {
1207
+ width: 200px;
1208
+ height: min-content;
1209
+ }
1210
+ #server-host-div,
1211
+ #server-solver-div {
1212
+ margin: 3px;
1213
+ }
1214
+
1215
+ #server-no-solver-div {
1216
+ margin: 3px;
1217
+ padding-top: 2px;
1218
+ padding-bottom: 2px;
1219
+ font-style: italic;
1220
+ margin-bottom: 2px;
1221
+ }
1222
+
1223
+ #server-solver {
1224
+ font-size: 12px;
1225
+ margin-bottom: 3px;
1226
+ }
1227
+
1228
+ #server-update,
1229
+ #server-shut-down {
1230
+ margin: 3px;
1231
+ font-size: 12px;
1232
+ }
1233
+
1193
1234
  /* styles for the MODEL dialog */
1194
1235
  #model-dlg {
1195
1236
  width: 270px;