linny-r 1.3.2 → 1.3.4

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/console.js CHANGED
@@ -87,7 +87,7 @@ console.log('Module directory:', MODULE_DIRECTORY);
87
87
  console.log('Working directory:', WORKING_DIRECTORY);
88
88
 
89
89
  // Currently, these external solvers are supported:
90
- const SUPPORTED_SOLVERS = ['gurobi', 'scip', 'lp_solve'];
90
+ const SUPPORTED_SOLVERS = ['gurobi', 'cplex', 'scip', 'lp_solve'];
91
91
 
92
92
  const
93
93
  // Load the MILP solver (dependent on Node.js: `fs`, `os` and `path`)
@@ -126,7 +126,7 @@ Possible options are:
126
126
  [name]-stats.txt in (workspace)/reports
127
127
  run will run the loaded model
128
128
  solver=[name] will select solver [name], or warn if not found
129
- (name choices: Gurobi, SCIP or LP_solve)
129
+ (name choices: Gurobi, CPLEX, SCIP or LP_solve)
130
130
  user=[identifier] user ID will be used to log onto remote servers
131
131
  verbose will output solver messages to the console
132
132
  workspace=[path] will create workspace in [path] instead of (main)/user
@@ -932,6 +932,23 @@ function commandLineSettings() {
932
932
  gurobi_path = path_list[i];
933
933
  max_v = parseInt(match[1]);
934
934
  }
935
+ match = path_list[i].match(/[\/\\]cplex[\/\\]bin/i);
936
+ if(match) {
937
+ cplex_path = path_list[i];
938
+ } else {
939
+ // NOTE: CPLEX may create its own environment variable for its paths
940
+ match = path_list[i].match(/%(.*cplex.*)%/i);
941
+ if(match) {
942
+ const cpl = process.env[match[1]].split(path.delimiter);
943
+ for(let i = 0; i < cpl.length; i++) {
944
+ match = cpl[i].match(/[\/\\]cplex[\/\\]bin/i);
945
+ if(match) {
946
+ cplex_path = cpl[i];
947
+ break;
948
+ }
949
+ }
950
+ }
951
+ }
935
952
  match = path_list[i].match(/[\/\\]scip[^\/\\]+[\/\\]bin/i);
936
953
  if(match) scip_path = path_list[i];
937
954
  match = path_list[i].match(/inkscape/i);
@@ -964,8 +981,25 @@ function commandLineSettings() {
964
981
  'WARNING: Failed to access the Gurobi command line application');
965
982
  }
966
983
  }
984
+ // Check if cplex(.exe) exists in its directory
985
+ let sp = path.join(cplex_path, 'cplex' + (PLATFORM.startsWith('win') ? '.exe' : ''));
986
+ const need_cplex = !settings.solver || settings.preferred_solver === 'cplex';
987
+ try {
988
+ fs.accessSync(sp, fs.constants.X_OK);
989
+ console.log('Path to CPLEX:', sp);
990
+ if(need_cplex) {
991
+ settings.solver = 'cplex';
992
+ settings.solver_path = sp;
993
+ }
994
+ } catch(err) {
995
+ // Only report error if CPLEX is needed
996
+ if(need_cplex) {
997
+ console.log(err.message);
998
+ console.log('WARNING: CPLEX application not found in', sp);
999
+ }
1000
+ }
967
1001
  // Check if scip(.exe) exists in its directory
968
- let sp = path.join(scip_path, 'scip' + (PLATFORM.startsWith('win') ? '.exe' : ''));
1002
+ sp = path.join(scip_path, 'scip' + (PLATFORM.startsWith('win') ? '.exe' : ''));
969
1003
  const need_scip = !settings.solver || settings.preferred_solver === 'scip';
970
1004
  try {
971
1005
  fs.accessSync(sp, fs.constants.X_OK);
@@ -982,10 +1016,9 @@ function commandLineSettings() {
982
1016
  }
983
1017
  }
984
1018
  // Check if lp_solve(.exe) exists in main directory
985
- const
986
- sp = path.join(WORKING_DIRECTORY,
987
- 'lp_solve' + (PLATFORM.startsWith('win') ? '.exe' : '')),
988
- need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
1019
+ sp = path.join(WORKING_DIRECTORY,
1020
+ 'lp_solve' + (PLATFORM.startsWith('win') ? '.exe' : ''));
1021
+ const need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
989
1022
  try {
990
1023
  fs.accessSync(sp, fs.constants.X_OK);
991
1024
  console.log('Path to LP_solve:', sp);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/server.js CHANGED
@@ -123,7 +123,7 @@ function checkNodeModule(name) {
123
123
  }
124
124
 
125
125
  // Currently, these external solvers are supported:
126
- const SUPPORTED_SOLVERS = ['gurobi', 'scip', 'lp_solve'];
126
+ const SUPPORTED_SOLVERS = ['gurobi', 'cplex', 'scip', 'lp_solve'];
127
127
 
128
128
  // Load class MILPSolver
129
129
  const MILPSolver = require('./static/scripts/linny-r-milp.js');
@@ -1458,6 +1458,7 @@ function commandLineSettings() {
1458
1458
  const path_list = process.env.PATH.split(path.delimiter);
1459
1459
  let gurobi_path = '',
1460
1460
  scip_path = '',
1461
+ cplex_path = '',
1461
1462
  match,
1462
1463
  max_v = -1;
1463
1464
  for(let i = 0; i < path_list.length; i++) {
@@ -1466,6 +1467,23 @@ function commandLineSettings() {
1466
1467
  gurobi_path = path_list[i];
1467
1468
  max_v = parseInt(match[1]);
1468
1469
  }
1470
+ match = path_list[i].match(/[\/\\]cplex[\/\\]bin/i);
1471
+ if(match) {
1472
+ cplex_path = path_list[i];
1473
+ } else {
1474
+ // NOTE: CPLEX may create its own environment variable for its paths
1475
+ match = path_list[i].match(/%(.*cplex.*)%/i);
1476
+ if(match) {
1477
+ const cpl = process.env[match[1]].split(path.delimiter);
1478
+ for(let i = 0; i < cpl.length; i++) {
1479
+ match = cpl[i].match(/[\/\\]cplex[\/\\]bin/i);
1480
+ if(match) {
1481
+ cplex_path = cpl[i];
1482
+ break;
1483
+ }
1484
+ }
1485
+ }
1486
+ }
1469
1487
  match = path_list[i].match(/[\/\\]scip[^\/\\]+[\/\\]bin/i);
1470
1488
  if(match) scip_path = path_list[i];
1471
1489
  match = path_list[i].match(/inkscape/i);
@@ -1498,8 +1516,25 @@ function commandLineSettings() {
1498
1516
  'WARNING: Failed to access the Gurobi command line application');
1499
1517
  }
1500
1518
  }
1519
+ // Check if cplex(.exe) exists in its directory
1520
+ let sp = path.join(cplex_path, 'cplex' + (PLATFORM.startsWith('win') ? '.exe' : ''));
1521
+ const need_cplex = !settings.solver || settings.preferred_solver === 'cplex';
1522
+ try {
1523
+ fs.accessSync(sp, fs.constants.X_OK);
1524
+ console.log('Path to CPLEX:', sp);
1525
+ if(need_cplex) {
1526
+ settings.solver = 'cplex';
1527
+ settings.solver_path = sp;
1528
+ }
1529
+ } catch(err) {
1530
+ // Only report error if CPLEX is needed
1531
+ if(need_cplex) {
1532
+ console.log(err.message);
1533
+ console.log('WARNING: CPLEX application not found in', sp);
1534
+ }
1535
+ }
1501
1536
  // Check if scip(.exe) exists in its directory
1502
- let sp = path.join(scip_path, 'scip' + (PLATFORM.startsWith('win') ? '.exe' : ''));
1537
+ sp = path.join(scip_path, 'scip' + (PLATFORM.startsWith('win') ? '.exe' : ''));
1503
1538
  const need_scip = !settings.solver || settings.preferred_solver === 'scip';
1504
1539
  try {
1505
1540
  fs.accessSync(sp, fs.constants.X_OK);
@@ -1595,7 +1630,7 @@ function createWorkspace() {
1595
1630
  data: path.join(SETTINGS.user_dir, 'data'),
1596
1631
  diagrams: path.join(SETTINGS.user_dir, 'diagrams'),
1597
1632
  modules: path.join(SETTINGS.user_dir, 'modules'),
1598
- solver_output: path.join(SETTINGS.user_dir, 'solver'),
1633
+ solver_output: path.join(SETTINGS.user_dir, 'solver')
1599
1634
  };
1600
1635
  // Create these sub-directories if not aready there
1601
1636
  try {
@@ -5273,9 +5273,30 @@ class GUIController extends Controller {
5273
5273
  md.element('actor').value = mapping.actor || '';
5274
5274
  md.element('prefix').value = mapping.prefix || '';
5275
5275
  const
5276
- ft = Object.keys(mapping.from_to).sort(ciCompare),
5276
+ tc = (mapping.top_clusters ?
5277
+ Object.keys(mapping.top_clusters).sort(ciCompare) : []),
5278
+ ft = (mapping.from_to ?
5279
+ Object.keys(mapping.from_to).sort(ciCompare) : []),
5277
5280
  sl = [];
5281
+ if(tc.length) {
5282
+ sl.push('<div style="font-weight: bold; margin:4px 2px 2px 2px">',
5283
+ 'Names for top-level clusters:</div>');
5284
+ // Add text inputs for selected cluster nodes
5285
+ for(let i = 0; i < tc.length; i++) {
5286
+ const
5287
+ ti = mapping.top_clusters[tc[i]],
5288
+ state = (ti === tc[i] ? 'color: #e09000; ' :
5289
+ this.validName(ti) ? 'color: #0000c0; ' :
5290
+ 'font-style: italic; color: red; ');
5291
+ sl.push('<div class="paste-option"><span>', tc[i], '</span> ',
5292
+ '<div class="paste-select"><input id="paste-selc-', i,
5293
+ '" type="text" style="', state, 'font-size: 12px" value="',
5294
+ ti, '"></div></div>');
5295
+ }
5296
+ }
5278
5297
  if(ft.length) {
5298
+ sl.push('<div style="font-weight: bold; margin:4px 2px 2px 2px">',
5299
+ 'Mapping of nodes to link from/to:</div>');
5279
5300
  // Add selectors for unresolved FROM/TO nodes
5280
5301
  for(let i = 0; i < ft.length; i++) {
5281
5302
  const ti = mapping.from_to[ft[i]];
@@ -5286,8 +5307,8 @@ class GUIController extends Controller {
5286
5307
  '</option></select></div></div>');
5287
5308
  }
5288
5309
  }
5289
- md.element('scroll-area').innerHTML = sl.join('');
5290
5310
  }
5311
+ md.element('scroll-area').innerHTML = sl.join('');
5291
5312
  // Open dialog, which will call pasteSelection(...) on OK
5292
5313
  this.paste_modal.show();
5293
5314
  }
@@ -5297,10 +5318,18 @@ class GUIController extends Controller {
5297
5318
  // proceeds to paste
5298
5319
  const
5299
5320
  md = this.paste_modal,
5300
- mapping = Object.assign(md.mapping, {});
5321
+ mapping = Object.assign(md.mapping, {}),
5322
+ tc = (mapping.top_clusters ?
5323
+ Object.keys(mapping.top_clusters).sort(ciCompare) : []),
5324
+ ft = (mapping.from_to ?
5325
+ Object.keys(mapping.from_to).sort(ciCompare) : []);
5301
5326
  mapping.actor = md.element('actor').value;
5302
5327
  mapping.prefix = md.element('prefix').value.trim();
5303
5328
  mapping.increment = true;
5329
+ for(let i = 0; i < tc.length; i++) {
5330
+ const cn = md.element('selc-' + i).value.trim();
5331
+ if(this.validName(cn)) mapping.top_clusters[tc[i]] = cn;
5332
+ }
5304
5333
  this.pasteSelection(mapping);
5305
5334
  }
5306
5335
 
@@ -5325,6 +5354,7 @@ console.log('HERE xml', xml);
5325
5354
  entities_node = childNodeByTag(xml, 'entities'),
5326
5355
  from_tos_node = childNodeByTag(xml, 'from-tos'),
5327
5356
  extras_node = childNodeByTag(xml, 'extras'),
5357
+ selc_node = childNodeByTag(xml, 'selected-clusters'),
5328
5358
  selection_node = childNodeByTag(xml, 'selection'),
5329
5359
  actor_names = [],
5330
5360
  new_entities = [],
@@ -5335,7 +5365,7 @@ console.log('HERE xml', xml);
5335
5365
 
5336
5366
  function fullName(node) {
5337
5367
  // Returns full entity name inferred from XML node data
5338
- if(node.nodeName === 'from-to') {
5368
+ if(node.nodeName === 'from-to' || node.nodeName === 'selc') {
5339
5369
  const
5340
5370
  n = xmlDecoded(nodeParameterValue(node, 'name')),
5341
5371
  an = xmlDecoded(nodeParameterValue(node, 'actor-name'));
@@ -5525,6 +5555,17 @@ console.log('HERE xml', xml);
5525
5555
  if(parseInt(mts) === MODEL.time_created.getTime() &&
5526
5556
  ca === fca && mapping.from_prefix === mapping.to_prefix &&
5527
5557
  !(mapping.prefix || mapping.actor || mapping.increment)) {
5558
+ // Prompt for names of selected cluster nodes
5559
+ if(selc_node.childNodes.length && !mapping.prefix) {
5560
+ mapping.top_clusters = {};
5561
+ for(let i = 0; i < selc_node.childNodes.length; i++) {
5562
+ const
5563
+ c = selc_node.childNodes[i],
5564
+ fn = fullName(c),
5565
+ mn = mappedName(fn);
5566
+ mapping.top_clusters[fn] = mn;
5567
+ }
5568
+ }
5528
5569
  this.promptForMapping(mapping);
5529
5570
  return;
5530
5571
  }
@@ -6525,11 +6566,12 @@ class GUIMonitor {
6525
6566
  })
6526
6567
  .then((data) => {
6527
6568
  try {
6528
- const jsr = JSON.parse(data);
6529
- if(jsr.solver !== VM.solver_name) {
6530
- UI.notify(`Solver on ${jsr.server} is ${jsr.solver}`);
6531
- }
6569
+ const
6570
+ jsr = JSON.parse(data),
6571
+ svr = `Solver on ${jsr.server} is ${jsr.solver}`;
6572
+ if(jsr.solver !== VM.solver_name) UI.notify(svr);
6532
6573
  VM.solver_name = jsr.solver;
6574
+ document.getElementById('host-logo').title = svr;
6533
6575
  } catch(err) {
6534
6576
  console.log(err, data);
6535
6577
  UI.alert('ERROR: Unexpected data from server: ' +
@@ -82,6 +82,27 @@ module.exports = class MILPSolver {
82
82
  14: 'Optimization still in progress',
83
83
  15: 'User-specified objective limit has been reached'
84
84
  };
85
+ } else if(this.id === 'cplex') {
86
+ this.ext = '.lp';
87
+ this.user_model = path.join(workspace.solver_output, 'usr_model.lp');
88
+ this.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
89
+ this.solution = path.join(workspace.solver_output, 'model.sol');
90
+ // NOTE: CPLEX log file is located in the Linny-R working directory
91
+ this.log = path.join(workspace.solver_output, 'cplex.log');
92
+ // NOTE: CPLEX command line accepts space separated commands ...
93
+ this.args = [
94
+ 'read ' + this.user_model,
95
+ 'write ' + this.solver_model + ' lp',
96
+ 'set timelimit 300',
97
+ 'optimize',
98
+ 'write ' + this.solution + ' 0',
99
+ 'quit'
100
+ ];
101
+ // ... when CPLEX is called with -c option; then each command must be
102
+ // enclosed in double quotes; SCIP outputs its messages to a log file
103
+ // terminal, so these must be caputured in a log file
104
+ this.solve_cmd = 'cplex -c "' + this.args.join('" "') + '"';
105
+ this.errors = {};
85
106
  } else if(this.id === 'scip') {
86
107
  this.ext = '.lp';
87
108
  this.user_model = path.join(workspace.solver_output, 'usr_model.lp');
@@ -217,6 +238,18 @@ module.exports = class MILPSolver {
217
238
  this.args[0] = 'TimeLimit=' + timeout;
218
239
  const options = {windowsHide: true};
219
240
  spawn = child_process.spawnSync(this.solver_path, this.args, options);
241
+ } else if(this.id === 'cplex') {
242
+ // Delete previous solver model file (if any)
243
+ try {
244
+ if(this.solver_model) fs.unlinkSync(this.solver_model);
245
+ } catch(err) {
246
+ // Ignore error
247
+ }
248
+ // Spawn using the LP_solve approach
249
+ const
250
+ cmd = this.solve_cmd.replace(/timelimit \d+/, `timelimit ${timeout}`),
251
+ options = {shell: true, cwd: 'user/solver', stdio: 'ignore', windowsHide: true};
252
+ spawn = child_process.spawnSync(cmd, options);
220
253
  } else if(this.id === 'scip') {
221
254
  // When using SCIP, take the LP_solve approach
222
255
  const
@@ -311,6 +344,64 @@ module.exports = class MILPSolver {
311
344
  result.error = 'No solution found';
312
345
  }
313
346
  }
347
+ } else if(this.id === 'cplex') {
348
+ result.seconds = 0;
349
+ const
350
+ msg = fs.readFileSync(this.log, 'utf8'),
351
+ no_license = (msg.indexOf('No license found') >= 0);
352
+ // `messages` must be an array of strings
353
+ result.messages = msg.split(os.EOL);
354
+ let solved = false,
355
+ output = [];
356
+ if(no_license) {
357
+ result.error = 'Too many variables for unlicensed CPLEX solver';
358
+ result.status = -13;
359
+ } else if(result.status !== 0) {
360
+ // Non-zero solver exit code indicates serious trouble
361
+ result.error = 'CPLEX solver terminated with error';
362
+ result.status = -13;
363
+ } else {
364
+ try {
365
+ output = fs.readFileSync(this.solution, 'utf8').trim();
366
+ if(output.indexOf('CPLEXSolution') >= 0) {
367
+ solved = true;
368
+ output = output.split(os.EOL);
369
+ }
370
+ } catch(err) {
371
+ console.log('No CPLEX solution file');
372
+ }
373
+ }
374
+ if(solved) {
375
+ // CPLEX saves solution as XML, but for now just extract the
376
+ // status and then the variables
377
+ let i = 0;
378
+ while(i < output.length) {
379
+ const s = output[i].split('"');
380
+ if(s[0].indexOf('objectiveValue') >= 0) {
381
+ result.obj = s[1];
382
+ } else if(s[0].indexOf('solutionStatusValue') >= 0) {
383
+ result.status = s[1];
384
+ } else if(s[0].indexOf('solutionStatusString') >= 0) {
385
+ result.error = s[1];
386
+ break;
387
+ }
388
+ i++;
389
+ }
390
+ if(['1', '101', '102'].indexOf(result.status) >= 0) {
391
+ result.status = 0;
392
+ result.error = '';
393
+ }
394
+ // Fill dictionary with variable name: value entries
395
+ while(i < output.length) {
396
+ const m = output[i].match(/^.*name="(X[^"]+)".*value="([^"]+)"/);
397
+ if(m !== null) x_dict[m[1]] = m[2];
398
+ i++;
399
+ }
400
+ // Fill the solution vector, adding 0 for missing columns
401
+ getValuesFromDict();
402
+ } else {
403
+ console.log('No solution found');
404
+ }
314
405
  } else if(this.id === 'scip') {
315
406
  result.seconds = 0;
316
407
  // `messages` must be an array of strings
@@ -332,19 +423,21 @@ module.exports = class MILPSolver {
332
423
  const m = result.messages[i];
333
424
  if(m.startsWith('SCIP Status')) {
334
425
  if(m.indexOf('problem is solved') >= 0) {
335
- solved = true;
426
+ if(m.indexOf('infeasible') >= 0) {
427
+ result.status = (m.indexOf('unbounded') >= 0 ? 14 : 12);
428
+ } else if(m.indexOf('unbounded') >= 0) {
429
+ result.status = 13;
430
+ } else {
431
+ solved = true;
432
+ }
336
433
  } else if(m.indexOf('interrupted') >= 0) {
337
434
  if(m.indexOf('time limit') >= 0) {
338
435
  result.status = 5;
339
436
  } else if(m.indexOf('memory limit') >= 0) {
340
437
  result.status = 6;
341
- } else if(m.indexOf('infeasible') >= 0) {
342
- result.status = (m.indexOf('unbounded') >= 0 ? 14 : 12);
343
- } else if(m.indexOf('unbounded') >= 0) {
344
- result.status = 13;
345
438
  }
346
- result.error = this.errors[result.status];
347
439
  }
440
+ if(result.status) result.error = this.errors[result.status];
348
441
  } else if (m.startsWith('Solving Time')) {
349
442
  result.seconds = parseFloat(m.split(':')[1]);
350
443
  }
@@ -1609,16 +1609,20 @@ class LinnyRModel {
1609
1609
  extras = [],
1610
1610
  from_tos = [],
1611
1611
  xml = [],
1612
- ft_xml = [],
1613
1612
  extra_xml = [],
1613
+ ft_xml = [],
1614
+ selc_xml = [],
1614
1615
  selected_xml = [];
1615
1616
  for(let i = 0; i < this.selection.length; i++) {
1616
1617
  const obj = this.selection[i];
1617
1618
  entities[obj.type].push(obj);
1618
- selected_xml.push('<sel>' + xmlEncoded(obj.displayName) + '</sel>');
1619
+ if(obj instanceof Cluster) selc_xml.push(
1620
+ '<selc name="', xmlEncoded(obj.name),
1621
+ '" actor-name="', xmlEncoded(obj.actor.name), '"></selc>');
1622
+ selected_xml.push(`<sel>${xmlEncoded(obj.displayName)}</sel>`);
1619
1623
  }
1620
- // Expand clusters by adding all its model entities to their respective
1621
- // lists
1624
+ // Expand (sub)clusters by adding all their model entities to their
1625
+ // respective lists
1622
1626
  for(let i = 0; i < entities.Cluster.length; i++) {
1623
1627
  const c = entities.Cluster[i];
1624
1628
  c.clearAllProcesses();
@@ -1714,7 +1718,8 @@ class LinnyRModel {
1714
1718
  '"><entities>', xml.join(''),
1715
1719
  '</entities><from-tos>', ft_xml.join(''),
1716
1720
  '</from-tos><extras>', extra_xml.join(''),
1717
- '</extras><selection>', selected_xml.join(''),
1721
+ '</extras><selected-clusters>', selc_xml.join(''),
1722
+ '</selected-clusters><selection>', selected_xml.join(''),
1718
1723
  '</selection></copy>'].join('');
1719
1724
  }
1720
1725
 
@@ -3236,6 +3241,7 @@ class LinnyRModel {
3236
3241
  for(let i = 0; i < constraints.length; i++) {
3237
3242
  const
3238
3243
  c = constraints[i],
3244
+ // NOTE: constraints in list have levels greater than near-zero
3239
3245
  fl = c.from_node.actualLevel(t),
3240
3246
  tl = c.to_node.actualLevel(t);
3241
3247
  let tcp;
@@ -3306,10 +3312,10 @@ class LinnyRModel {
3306
3312
  if(af > VM.NEAR_ZERO) {
3307
3313
  // Prevent division by zero
3308
3314
  // NOTE: level can be zero even if actual flow > 0!
3309
- const al = p.actualLevel(dt);
3315
+ const al = p.nonZeroLevel(dt);
3310
3316
  // NOTE: scale to level only when level > 1, or fixed
3311
3317
  // costs for start-up or first commit will be amplified
3312
- if(al > VM.ON_OFF_THRESHOLD) cp -= pr * af / Math.max(al, 1);
3318
+ if(al > VM.NEAR_ZERO) cp -= pr * af / Math.max(al, 1);
3313
3319
  }
3314
3320
  }
3315
3321
  }
@@ -3428,8 +3434,8 @@ class LinnyRModel {
3428
3434
  p.cost_price[t] = cp;
3429
3435
  // For stocks, the CP includes stock price on t-1
3430
3436
  if(p.is_buffer) {
3431
- const prevl = p.actualLevel(t-1);
3432
- if(prevl > 0) {
3437
+ const prevl = p.nonZeroLevel(t-1);
3438
+ if(prevl > VM.NEAR_ZERO) {
3433
3439
  cp = (cnp + prevl * p.stockPrice(t-1)) / (qnp + prevl);
3434
3440
  }
3435
3441
  p.stock_price[t] = cp;
@@ -3486,7 +3492,7 @@ class LinnyRModel {
3486
3492
  // Then (also) look for links having AF = 0 ...
3487
3493
  for(let i = links.length-1; i >= 0; i--) {
3488
3494
  const af = links[i].actualFlow(t);
3489
- if(Math.abs(af) < VM.SIG_DIF_FROM_ZERO) {
3495
+ if(Math.abs(af) < VM.NEAR_ZERO) {
3490
3496
  // ... and set their UCP to 0
3491
3497
  links[i].unit_cost_price = 0;
3492
3498
  links.splice(i, 1);
@@ -3522,7 +3528,7 @@ class LinnyRModel {
3522
3528
  let hcp = VM.MINUS_INFINITY;
3523
3529
  for(let i = 0; i < p.inputs.length; i++) {
3524
3530
  const l = p.inputs[i];
3525
- if(l.from_node instanceof Process && l.actualFlow(t) > 0) {
3531
+ if(l.from_node instanceof Process && l.actualFlow(t) > VM.NEAR_ZERO) {
3526
3532
  const ld = l.actualDelay(t);
3527
3533
  // NOTE: only consider the allocated share of cost
3528
3534
  let cp = l.from_node.costPrice(t - ld) * l.share_of_cost;
@@ -2249,7 +2249,7 @@ class VirtualMachine {
2249
2249
  }
2250
2250
  // NOTES:
2251
2251
  // (1) Processes have NO slack variables, because sufficient slack is
2252
- // provided by addinng slack variables to products; these slack
2252
+ // provided by adding slack variables to products; these slack
2253
2253
  // variables will have high cost penalty values in the objective
2254
2254
  // function, to serve as "last resort" to still obtain a solution when
2255
2255
  // the "real" product levels are overconstrained
@@ -2352,7 +2352,7 @@ class VirtualMachine {
2352
2352
  }
2353
2353
  }
2354
2354
  // Likewise get the upper bound
2355
- if(p.equal_bounds) {
2355
+ if(p.equal_bounds && p.lower_bound.defined) {
2356
2356
  u = l;
2357
2357
  } else if(p.upper_bound.defined) {
2358
2358
  if(p.upper_bound.isStatic) {
@@ -2575,9 +2575,9 @@ class VirtualMachine {
2575
2575
  // efficient, while cash flows are inferred properties and will not
2576
2576
  // result in an "unbounded problem" error message from the solver
2577
2577
  this.code.push(
2578
- [VMI_set_const_bounds, [a.cash_in_var_index,
2578
+ [VMI_set_bounds, [a.cash_in_var_index,
2579
2579
  VM.MINUS_INFINITY, VM.PLUS_INFINITY, true]],
2580
- [VMI_set_const_bounds, [a.cash_out_var_index,
2580
+ [VMI_set_bounds, [a.cash_out_var_index,
2581
2581
  VM.MINUS_INFINITY, VM.PLUS_INFINITY, true]]
2582
2582
  );
2583
2583
  }
@@ -2590,18 +2590,17 @@ class VirtualMachine {
2590
2590
  if(!MODEL.ignored_entities[k]) {
2591
2591
  p = MODEL.processes[k];
2592
2592
  lbx = p.lower_bound;
2593
- ubx = (p.equal_bounds ? lbx : p.upper_bound);
2593
+ // NOTE: if UB = LB, set UB to LB only if LB is defined,
2594
+ // because LB expressions default to -INF while UB expressions
2595
+ // default to + INF
2596
+ ubx = (p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
2597
+ if(lbx.isStatic) lbx = lbx.result(0);
2598
+ if(ubx.isStatic) ubx = ubx.result(0);
2594
2599
  // NOTE: pass TRUE as fourth parameter to indicate that +INF
2595
2600
  // and -INF can be coded as the infinity values used by the
2596
2601
  // solver, rather than the Linny-R values used to detect
2597
2602
  // unbounded problems
2598
- if(lbx.isStatic && ubx.isStatic) {
2599
- this.code.push([VMI_set_const_bounds,
2600
- [p.level_var_index, lbx.result(0), ubx.result(0), true]]);
2601
- } else {
2602
- this.code.push([VMI_set_var_bounds,
2603
- [p.level_var_index, lbx, ubx, true]]);
2604
- }
2603
+ this.code.push([VMI_set_bounds, [p.level_var_index, lbx, ubx, true]]);
2605
2604
  // Add level variable index to "fixed" list for specified rounds
2606
2605
  const rf = p.actor.round_flags;
2607
2606
  if(rf != 0) {
@@ -2629,17 +2628,17 @@ class VirtualMachine {
2629
2628
  // If no slack, the bound constraints can be set on the
2630
2629
  // variables themselves
2631
2630
  lbx = p.lower_bound;
2632
- ubx = (p.equal_bounds ? lbx : p.upper_bound);
2633
- if(lbx.isStatic && ubx.isStatic) {
2634
- this.code.push([VMI_set_const_bounds,
2635
- [vi, lbx.result(0), ubx.result(0)]]);
2636
- } else {
2637
- this.code.push([VMI_set_var_bounds, [vi, lbx, ubx]]);
2638
- }
2631
+ // NOTE: if UB = LB, set UB to LB only if LB is defined,
2632
+ // because LB expressions default to -INF while UB expressions
2633
+ // default to + INF
2634
+ ubx = (p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
2635
+ if(lbx.isStatic) lbx = lbx.result(0);
2636
+ if(ubx.isStatic) ubx = ubx.result(0);
2637
+ this.code.push([VMI_set_bounds, [vi, lbx, ubx]]);
2639
2638
  } else {
2640
2639
  // Otherwise, set bounds of stock variable to -INF and +INF,
2641
2640
  // as product constraints will be added later on
2642
- this.code.push([VMI_set_const_bounds,
2641
+ this.code.push([VMI_set_bounds,
2643
2642
  [vi, VM.MINUS_INFINITY, VM.PLUS_INFINITY]]);
2644
2643
  }
2645
2644
  }
@@ -3224,7 +3223,7 @@ class VirtualMachine {
3224
3223
  // To deal with this, the default equations will NOT be set when UB <= 0,
3225
3224
  // while the "exceptional" equations (q.v.) will NOT be set when UB > 0.
3226
3225
  // This can be realized using a special VM instruction:
3227
- ubx = (p.equal_bounds && p.lower_bound.text ? p.lower_bound : p.upper_bound);
3226
+ ubx = (p.equal_bounds && p.lower_bound.defined ? p.lower_bound : p.upper_bound);
3228
3227
  this.code.push([VMI_set_add_constraints_flag, [ubx, '>', 0]]);
3229
3228
 
3230
3229
  // NOTE: if UB <= 0 the following constraints will be prepared but NOT added
@@ -3256,19 +3255,22 @@ class VirtualMachine {
3256
3255
  if(ubx.isStatic) {
3257
3256
  // If UB is very high (typically: undefined, so +INF), try to infer
3258
3257
  // a lower value for UB to use for the ON/OFF binary
3259
- let ub = ubx.result(0);
3258
+ let ub = ubx.result(0),
3259
+ hub = ub;
3260
3260
  if(ub > VM.MEGA_UPPER_BOUND) {
3261
- ub = p.highestUpperBound([]);
3261
+ hub = p.highestUpperBound([]);
3262
3262
  // If UB still very high, warn modeler on infoline and in monitor
3263
- if(ub > VM.MEGA_UPPER_BOUND) {
3264
- const msg = 'High upper bound (' + this.sig4Dig(ub) +
3263
+ if(hub > VM.MEGA_UPPER_BOUND) {
3264
+ const msg = 'High upper bound (' + this.sig4Dig(hub) +
3265
3265
  ') for <strong>' + p.displayName + '</strong>' +
3266
3266
  ' will compromise computation of its binary variables';
3267
3267
  UI.warn(msg);
3268
3268
  this.logMessage(this.block_count,
3269
3269
  'WARNING: ' + msg.replace(/<\/?strong>/g, '"'));
3270
3270
  }
3271
- } else {
3271
+ }
3272
+ if(hub !== ub) {
3273
+ ub = hub;
3272
3274
  this.logMessage(this.block_count,
3273
3275
  `Inferred upper bound for ${p.displayName}: ${this.sig4Dig(ub)}`);
3274
3276
  }
@@ -3772,7 +3774,7 @@ class VirtualMachine {
3772
3774
  for(let k = 0; k < l; k++) {
3773
3775
  const
3774
3776
  vi = svl[k],
3775
- slack = x[vi + j],
3777
+ slack = parseFloat(x[vi + j]),
3776
3778
  absl = Math.abs(slack);
3777
3779
  if(absl > VM.NEAR_ZERO) {
3778
3780
  const v = this.variables[vi - 1];
@@ -4731,8 +4733,6 @@ class VirtualMachine {
4731
4733
  return [-2, 2, 6];
4732
4734
  } else if(this.solver_name === 'gurobi') {
4733
4735
  return [1, 3, 4, 6, 11, 12, 14];
4734
- } else if(this.solver_name === 'scip') {
4735
- return [];
4736
4736
  } else {
4737
4737
  return [];
4738
4738
  }
@@ -6446,8 +6446,8 @@ from) the k'th coefficient.
6446
6446
 
6447
6447
  */
6448
6448
 
6449
- function VMI_set_const_bounds(args) {
6450
- // `args`: [var_index, number, number]
6449
+ function VMI_set_bounds(args) {
6450
+ // `args`: [var_index, number or expression, number or expression]
6451
6451
  const
6452
6452
  vi = args[0],
6453
6453
  vbl = VM.variables[vi - 1][1],
@@ -6465,7 +6465,8 @@ function VMI_set_const_bounds(args) {
6465
6465
  // if this is the first round
6466
6466
  if(VM.current_round) {
6467
6467
  l = vbl.actualLevel(VM.t);
6468
- //PATCH!!
6468
+ // QUICK PATCH! should resolve that small non-zero process levels
6469
+ // computed in prior round make problem infeasible
6469
6470
  if(l < 0.0005) l = 0;
6470
6471
  } else {
6471
6472
  l = 0;
@@ -6474,17 +6475,22 @@ function VMI_set_const_bounds(args) {
6474
6475
  fixed = ' (FIXED ' + vbl.displayName + ')';
6475
6476
  } else {
6476
6477
  // Set bounds as specified by the two arguments
6477
- l = (args[1] === VM.UNDEFINED ? 0 : args[1]),
6478
- u = Math.min(args[2], inf_val);
6478
+ l = args[1];
6479
+ if(l instanceof Expression) l = l.result(VM.t);
6480
+ if(l === VM.UNDEFINED) l = 0;
6481
+ u = args[2];
6482
+ if(u instanceof Expression) u = u.result(VM.t);
6483
+ u = Math.min(u, VM.PLUS_INFINITY);
6479
6484
  if(solver_inf) {
6480
6485
  if(l === VM.MINUS_INFINITY) l = -inf_val;
6481
6486
  if(u === VM.PLUS_INFINITY) u = inf_val;
6482
6487
  }
6483
6488
  fixed = '';
6484
6489
  }
6485
- // NOTE: to check, add this to the condition below: fixed !== ''
6490
+ // NOTE: to see in the console whether fixing across rounds works, insert
6491
+ // "fixed !== '' || " before DEBUGGING below
6486
6492
  if(DEBUGGING) {
6487
- console.log(['set_const_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
6493
+ console.log(['set_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
6488
6494
  ' LB = ', VM.sig4Dig(l), ', UB = ', VM.sig4Dig(u), fixed].join(''));
6489
6495
  }
6490
6496
  // NOTE: since the VM vectors for lower bounds and upper bounds are
@@ -6511,61 +6517,6 @@ function VMI_set_const_bounds(args) {
6511
6517
  }
6512
6518
  }
6513
6519
 
6514
- function VMI_set_var_bounds(args) {
6515
- // `args`: [var_index, expression, expression]
6516
- const
6517
- vi = args[0],
6518
- vbl = VM.variables[vi - 1][1],
6519
- k = VM.offset + vi,
6520
- r = VM.round_letters.indexOf(VM.round_sequence[VM.current_round]),
6521
- // Optional fourth parameter indicates whether the solver's
6522
- // infinity values should be used
6523
- solver_inf = args.length > 3 && args[3],
6524
- inf_val = (solver_inf ? VM.SOLVER_PLUS_INFINITY : VM.PLUS_INFINITY);
6525
- let l,
6526
- u,
6527
- fixed = (vi in VM.fixed_var_indices[r - 1]);
6528
- if(fixed) {
6529
- // Set both bounds equal to the level set in the previous round, or to 0
6530
- // if this is the first round
6531
- if(VM.current_round) {
6532
- l = vbl.actualLevel(VM.t);
6533
- } else {
6534
- l = 0;
6535
- }
6536
- u = l;
6537
- fixed = ' (FIXED ' + vbl.displayName + ')';
6538
- } else {
6539
- l = args[1].result(VM.t);
6540
- if(l === VM.UNDEFINED) l = 0;
6541
- u = Math.min(args[2].result(VM.t), inf_val);
6542
- if(solver_inf) {
6543
- if(l === VM.MINUS_INFINITY) l = -inf_val;
6544
- if(u === VM.PLUS_INFINITY) u = inf_val;
6545
- }
6546
- fixed = '';
6547
- }
6548
- // Here, too, no need to set default values
6549
- if(Math.abs(l) > VM.NEAR_ZERO || u !== inf_val) {
6550
- VM.lower_bounds[k] = l;
6551
- VM.upper_bounds[k] = u;
6552
- // Check for peak increase -- see comments in VMI_set_const_bound
6553
- if(vbl.peak_inc_var_index >= 0) {
6554
- u = Math.max(0, u - vbl.b_peak[VM.block_count - 1]);
6555
- const
6556
- cvi = VM.chunk_offset + vbl.peak_inc_var_index,
6557
- piub = VM.upper_bounds[cvi];
6558
- if(piub) u = Math.max(piub, u);
6559
- VM.upper_bounds[cvi] = u;
6560
- VM.upper_bounds[cvi + 1] = u;
6561
- }
6562
- }
6563
- if(DEBUGGING) {
6564
- console.log(['set_var_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
6565
- ' LB = ', l, ', UB = ', u, fixed].join(''));
6566
- }
6567
- }
6568
-
6569
6520
  function VMI_clear_coefficients(empty) {
6570
6521
  if(DEBUGGING) console.log('clear_coefficients');
6571
6522
  VM.coefficients = {};