linny-r 1.3.1 → 1.3.2
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 +25 -2
- package/package.json +1 -1
- package/server.js +26 -5
- package/static/index.html +59 -1
- package/static/linny-r.css +56 -0
- package/static/scripts/linny-r-ctrl.js +16 -1
- package/static/scripts/linny-r-gui.js +335 -7
- package/static/scripts/linny-r-milp.js +144 -13
- package/static/scripts/linny-r-model.js +109 -21
- package/static/scripts/linny-r-utils.js +10 -2
- package/static/scripts/linny-r-vm.js +202 -87
@@ -3237,6 +3237,14 @@ class GUIController extends Controller {
|
|
3237
3237
|
this.modals.replace.cancel.addEventListener('click',
|
3238
3238
|
() => UI.modals.replace.hide());
|
3239
3239
|
|
3240
|
+
// The PASTE dialog appears when name conflicts must be resolved
|
3241
|
+
this.paste_modal = new ModalDialog('paste');
|
3242
|
+
this.paste_modal.ok.addEventListener('click',
|
3243
|
+
() => UI.setPasteMapping());
|
3244
|
+
this.paste_modal.cancel.addEventListener('click',
|
3245
|
+
() => UI.paste_modal.hide());
|
3246
|
+
|
3247
|
+
// The CHECK UPDATE dialog appears when a new version is detected
|
3240
3248
|
this.check_update_modal = new ModalDialog('check-update');
|
3241
3249
|
this.check_update_modal.ok.addEventListener('click',
|
3242
3250
|
() => UI.shutDownServer());
|
@@ -5231,7 +5239,6 @@ class GUIController extends Controller {
|
|
5231
5239
|
copySelection() {
|
5232
5240
|
// Save selection as XML in local storage of the browser
|
5233
5241
|
const xml = MODEL.selectionAsXML;
|
5234
|
-
//console.log('HERE copy xml', xml);
|
5235
5242
|
if(xml) {
|
5236
5243
|
window.localStorage.setItem('Linny-R-selection-XML', xml);
|
5237
5244
|
this.updateButtons();
|
@@ -5252,15 +5259,333 @@ class GUIController extends Controller {
|
|
5252
5259
|
return false;
|
5253
5260
|
}
|
5254
5261
|
|
5255
|
-
|
5262
|
+
promptForMapping(mapping) {
|
5263
|
+
// Prompt user to specify name conflict resolution strategy
|
5264
|
+
console.log('HERE prompt for mapping', mapping);
|
5265
|
+
const md = this.paste_modal;
|
5266
|
+
md.mapping = mapping;
|
5267
|
+
md.element('from-prefix').innerText = mapping.from_prefix || '';
|
5268
|
+
md.element('to-prefix').innerText = mapping.to_prefix || '';
|
5269
|
+
md.element('ftp').style.display = (mapping.from_prefix ? 'block' : 'none');
|
5270
|
+
md.element('from-actor').innerText = mapping.from_actor || '';
|
5271
|
+
md.element('to-actor').innerText = mapping.to_actor || '';
|
5272
|
+
md.element('fta').style.display = (mapping.from_actor ? 'block' : 'none');
|
5273
|
+
md.element('actor').value = mapping.actor || '';
|
5274
|
+
md.element('prefix').value = mapping.prefix || '';
|
5275
|
+
const
|
5276
|
+
ft = Object.keys(mapping.from_to).sort(ciCompare),
|
5277
|
+
sl = [];
|
5278
|
+
if(ft.length) {
|
5279
|
+
// Add selectors for unresolved FROM/TO nodes
|
5280
|
+
for(let i = 0; i < ft.length; i++) {
|
5281
|
+
const ti = mapping.from_to[ft[i]];
|
5282
|
+
if(ft[i] === ti) {
|
5283
|
+
sl.push('<div class="paste-option"><span>', ft[i], '</span> ',
|
5284
|
+
'<div class="paste-select"><select id="paste-ft-', i,
|
5285
|
+
'" style="font-size: 12px"><option value="', ti, '">', ti,
|
5286
|
+
'</option></select></div></div>');
|
5287
|
+
}
|
5288
|
+
}
|
5289
|
+
md.element('scroll-area').innerHTML = sl.join('');
|
5290
|
+
}
|
5291
|
+
// Open dialog, which will call pasteSelection(...) on OK
|
5292
|
+
this.paste_modal.show();
|
5293
|
+
}
|
5294
|
+
|
5295
|
+
setPasteMapping() {
|
5296
|
+
// Updates the paste mapping as specified by the modeler and then
|
5297
|
+
// proceeds to paste
|
5298
|
+
const
|
5299
|
+
md = this.paste_modal,
|
5300
|
+
mapping = Object.assign(md.mapping, {});
|
5301
|
+
mapping.actor = md.element('actor').value;
|
5302
|
+
mapping.prefix = md.element('prefix').value.trim();
|
5303
|
+
mapping.increment = true;
|
5304
|
+
this.pasteSelection(mapping);
|
5305
|
+
}
|
5306
|
+
|
5307
|
+
pasteSelection(mapping={}) {
|
5256
5308
|
// If selection has been saved as XML in local storage, test to
|
5257
5309
|
// see whether PASTE would result in name conflicts, and if so,
|
5258
5310
|
// open the name conflict resolution window
|
5259
|
-
|
5260
|
-
|
5261
|
-
|
5262
|
-
|
5311
|
+
let xml = window.localStorage.getItem('Linny-R-selection-XML');
|
5312
|
+
console.log('HERE xml', xml);
|
5313
|
+
try {
|
5314
|
+
xml = parseXML(xml);
|
5315
|
+
} catch(e) {
|
5316
|
+
console.log(e);
|
5317
|
+
this.alert('Paste failed due to invalid XML');
|
5318
|
+
return;
|
5263
5319
|
}
|
5320
|
+
|
5321
|
+
// For now, while still implementing:
|
5322
|
+
this.notify('Paste not fully implemented yet -- WORK IN PROGRESS!');
|
5323
|
+
|
5324
|
+
const
|
5325
|
+
entities_node = childNodeByTag(xml, 'entities'),
|
5326
|
+
from_tos_node = childNodeByTag(xml, 'from-tos'),
|
5327
|
+
extras_node = childNodeByTag(xml, 'extras'),
|
5328
|
+
selection_node = childNodeByTag(xml, 'selection'),
|
5329
|
+
actor_names = [],
|
5330
|
+
new_entities = [],
|
5331
|
+
name_map = {},
|
5332
|
+
name_conflicts = [];
|
5333
|
+
|
5334
|
+
// AUXILIARY FUNCTIONS
|
5335
|
+
|
5336
|
+
function fullName(node) {
|
5337
|
+
// Returns full entity name inferred from XML node data
|
5338
|
+
if(node.nodeName === 'from-to') {
|
5339
|
+
const
|
5340
|
+
n = xmlDecoded(nodeParameterValue(node, 'name')),
|
5341
|
+
an = xmlDecoded(nodeParameterValue(node, 'actor-name'));
|
5342
|
+
if(an && an !== UI.NO_ACTOR) {
|
5343
|
+
addDistinct(an, actor_names);
|
5344
|
+
return `${n} (${an})`;
|
5345
|
+
}
|
5346
|
+
return n;
|
5347
|
+
}
|
5348
|
+
if(node.nodeName !== 'link' && node.nodeName !== 'constraint') {
|
5349
|
+
const
|
5350
|
+
n = xmlDecoded(nodeContentByTag(node, 'name')),
|
5351
|
+
an = xmlDecoded(nodeContentByTag(node, 'actor-name'));
|
5352
|
+
if(an && an !== UI.NO_ACTOR) {
|
5353
|
+
addDistinct(an, actor_names);
|
5354
|
+
return `${n} (${an})`;
|
5355
|
+
}
|
5356
|
+
return n;
|
5357
|
+
} else {
|
5358
|
+
let fn = xmlDecoded(nodeContentByTag(node, 'from-name')),
|
5359
|
+
fa = xmlDecoded(nodeContentByTag(node, 'from-owner')),
|
5360
|
+
tn = xmlDecoded(nodeContentByTag(node, 'to-name')),
|
5361
|
+
ta = xmlDecoded(nodeContentByTag(node, 'to-owner')),
|
5362
|
+
arrow = (node.nodeName === 'link' ? UI.LINK_ARROW : UI.CONSTRAINT_ARROW);
|
5363
|
+
if(fa && fa !== UI.NO_ACTOR) {
|
5364
|
+
addDistinct(fa, actor_names);
|
5365
|
+
fn = `${fn} (${fa})`;
|
5366
|
+
}
|
5367
|
+
if(ta && ta !== UI.NO_ACTOR) {
|
5368
|
+
addDistinct(ta, actor_names);
|
5369
|
+
tn = `${tn} (${ta})`;
|
5370
|
+
}
|
5371
|
+
return `${fn}${arrow}${tn}`;
|
5372
|
+
}
|
5373
|
+
}
|
5374
|
+
|
5375
|
+
function nameAndActor(name) {
|
5376
|
+
// Returns tuple [entity name, actor name] if `name` ends with
|
5377
|
+
// a parenthesized string that identifies an actor in the selection
|
5378
|
+
const ai = name.lastIndexOf(' (');
|
5379
|
+
if(ai < 0) return [name, ''];
|
5380
|
+
let actor = name.slice(ai + 2, -1);
|
5381
|
+
// Test whether parenthesized string denotes an actor
|
5382
|
+
if(actor_names.indexOf(actor) >= 0 || actor === mapping.actor ||
|
5383
|
+
actor === mapping.from_actor || actor === mapping.to_actor) {
|
5384
|
+
name = name.substring(0, ai);
|
5385
|
+
} else {
|
5386
|
+
actor = '';
|
5387
|
+
}
|
5388
|
+
return [name, actor];
|
5389
|
+
}
|
5390
|
+
|
5391
|
+
function mappedName(n) {
|
5392
|
+
// Returns full name `n` modified according to the mapping
|
5393
|
+
// NOTE: links and constraints require two mappings (recursion!)
|
5394
|
+
if(n.indexOf(UI.LINK_ARROW) > 0) {
|
5395
|
+
const ft = n.split(UI.LINK_ARROW);
|
5396
|
+
return mappedName(ft[0]) + UI.LINK_ARROW + mappedName(ft[1]);
|
5397
|
+
}
|
5398
|
+
if(n.indexOf(UI.CONSTRAINT_ARROW) > 0) {
|
5399
|
+
const ft = n.split(UI.CONSTRAINT_ARROW);
|
5400
|
+
return mappedName(ft[0]) + UI.CONSTRAINT_ARROW + mappedName(ft[1]);
|
5401
|
+
}
|
5402
|
+
// Mapping precedence order:
|
5403
|
+
// (1) prefix inherited from cluster
|
5404
|
+
// (2) actor name inherited from cluster
|
5405
|
+
// (3) actor name specified by modeler
|
5406
|
+
// (4) prefix specified by modeler
|
5407
|
+
// (5) auto-increment tail number
|
5408
|
+
// (6) nearest eligible node
|
5409
|
+
if(mapping.from_prefix && n.startsWith(mapping.from_prefix)) {
|
5410
|
+
return n.replace(mapping.from_prefix, mapping.to_prefix);
|
5411
|
+
}
|
5412
|
+
if(mapping.from_actor) {
|
5413
|
+
const ai = n.lastIndexOf(mapping.from_actor);
|
5414
|
+
if(ai > 0) return n.substring(0, ai) + mapping.to_actor;
|
5415
|
+
}
|
5416
|
+
// NOTE: specified actor cannot override existing actor
|
5417
|
+
if(mapping.actor && !nameAndActor(n)[1]) {
|
5418
|
+
return `${n} (${mapping.actor})`;
|
5419
|
+
}
|
5420
|
+
if(mapping.prefix) {
|
5421
|
+
return mapping.prefix + UI.PREFIXER + n;
|
5422
|
+
}
|
5423
|
+
let nr = endsWithDigits(n);
|
5424
|
+
if(mapping.increment && nr) {
|
5425
|
+
return n.replace(new RegExp(nr + '$'), parseInt(nr) + 1);
|
5426
|
+
}
|
5427
|
+
// No mapping => return original name
|
5428
|
+
return n;
|
5429
|
+
}
|
5430
|
+
|
5431
|
+
function nameConflicts(node) {
|
5432
|
+
// Maps names of entities defined by the child nodes of `node`
|
5433
|
+
// while detecting name conflicts
|
5434
|
+
for(let i = 0; i < node.childNodes.length; i++) {
|
5435
|
+
const c = node.childNodes[i];
|
5436
|
+
if(c.nodeName !== 'link' && c.nodeName !== 'constraint') {
|
5437
|
+
const
|
5438
|
+
fn = fullName(c),
|
5439
|
+
mn = mappedName(fn);
|
5440
|
+
// Name conflict occurs when the mapped name is already in use
|
5441
|
+
// in the target model, or when the original name is mapped onto
|
5442
|
+
// different names (this might occur due to modeler input)
|
5443
|
+
if(MODEL.objectByName(mn) || (name_map[fn] && name_map[fn] !== mn)) {
|
5444
|
+
addDistinct(fn, name_conflicts);
|
5445
|
+
} else {
|
5446
|
+
name_map[fn] = mn;
|
5447
|
+
}
|
5448
|
+
}
|
5449
|
+
}
|
5450
|
+
}
|
5451
|
+
|
5452
|
+
function addEntityFromNode(node) {
|
5453
|
+
// Adds entity to model based on XML node data and mapping
|
5454
|
+
// NOTE: do not add if an entity having this type and mapped name
|
5455
|
+
// already exists; name conflicts accross entity types may occur
|
5456
|
+
// and result in error messages
|
5457
|
+
const
|
5458
|
+
et = node.nodeName,
|
5459
|
+
fn = fullName(node),
|
5460
|
+
mn = mappedName(fn);
|
5461
|
+
let obj;
|
5462
|
+
if(et === 'process' && !MODEL.processByID(UI.nameToID(mn))) {
|
5463
|
+
const
|
5464
|
+
na = nameAndActor(mn),
|
5465
|
+
new_actor = !MODEL.actorByID(UI.nameToID(na[1]));
|
5466
|
+
obj = MODEL.addProcess(na[0], na[1], node);
|
5467
|
+
if(obj) {
|
5468
|
+
obj.code = '';
|
5469
|
+
obj.setCode();
|
5470
|
+
if(new_actor) new_entities.push(obj.actor);
|
5471
|
+
new_entities.push(obj);
|
5472
|
+
}
|
5473
|
+
} else if(et === 'product' && !MODEL.productByID(UI.nameToID(mn))) {
|
5474
|
+
obj = MODEL.addProduct(mn, node);
|
5475
|
+
if(obj) {
|
5476
|
+
obj.code = '';
|
5477
|
+
obj.setCode();
|
5478
|
+
new_entities.push(obj);
|
5479
|
+
}
|
5480
|
+
} else if(et === 'cluster' && !MODEL.clusterByID(UI.nameToID(mn))) {
|
5481
|
+
const
|
5482
|
+
na = nameAndActor(mn),
|
5483
|
+
new_actor = !MODEL.actorByID(UI.nameToID(na[1]));
|
5484
|
+
obj = MODEL.addCluster(na[0], na[1], node);
|
5485
|
+
if(obj) {
|
5486
|
+
if(new_actor) new_entities.push(obj.actor);
|
5487
|
+
new_entities.push(obj);
|
5488
|
+
}
|
5489
|
+
} else if(et === 'dataset' && !MODEL.datasetByID(UI.nameToID(mn))) {
|
5490
|
+
obj = MODEL.addDataset(mn, node);
|
5491
|
+
if(obj) new_entities.push(obj);
|
5492
|
+
} else if(et === 'link' || et === 'constraint') {
|
5493
|
+
const
|
5494
|
+
ft = mn.split(et === 'link' ? UI.LINK_ARROW : UI.CONSTRAINT_ARROW),
|
5495
|
+
fl = MODEL.objectByName(ft[0]),
|
5496
|
+
tl = MODEL.objectByName(ft[1]);
|
5497
|
+
if(fl && tl) {
|
5498
|
+
obj = (et === 'link' ?
|
5499
|
+
MODEL.addLink(fl, tl, node) :
|
5500
|
+
MODEL.addConstraint(fl, tl, node));
|
5501
|
+
if(obj) new_entities.push(obj);
|
5502
|
+
} else {
|
5503
|
+
UI.alert(`Failed to paste ${et} ${fn} as ${mn}`);
|
5504
|
+
}
|
5505
|
+
}
|
5506
|
+
}
|
5507
|
+
|
5508
|
+
const
|
5509
|
+
mts = nodeParameterValue(xml, 'model-timestamp'),
|
5510
|
+
cn = nodeParameterValue(xml, 'cluster-name'),
|
5511
|
+
ca = nodeParameterValue(xml, 'cluster-actor'),
|
5512
|
+
fc = MODEL.focal_cluster,
|
5513
|
+
fcn = fc.name,
|
5514
|
+
fca = fc.actor.name,
|
5515
|
+
sp = this.sharedPrefix(cn, fcn),
|
5516
|
+
fpn = (cn === UI.TOP_CLUSTER_NAME ? '' : cn.replace(sp, '')),
|
5517
|
+
tpn = (fcn === UI.TOP_CLUSTER_NAME ? '' : fcn.replace(sp, ''));
|
5518
|
+
// Infer mapping from XML data and focal cluster name & actor name
|
5519
|
+
mapping.shared_prefix = sp;
|
5520
|
+
mapping.from_prefix = (fpn ? sp + fpn + UI.PREFIXER : sp);
|
5521
|
+
mapping.to_prefix = (tpn ? sp + tpn + UI.PREFIXER : sp);
|
5522
|
+
mapping.from_actor = (ca === UI.NO_ACTOR ? '' : ca);
|
5523
|
+
mapping.to_actor = (fca === UI.NO_ACTOR ? '' : fca);
|
5524
|
+
// Prompt for mapping when pasting to the same model and cluster
|
5525
|
+
if(parseInt(mts) === MODEL.time_created.getTime() &&
|
5526
|
+
ca === fca && mapping.from_prefix === mapping.to_prefix &&
|
5527
|
+
!(mapping.prefix || mapping.actor || mapping.increment)) {
|
5528
|
+
this.promptForMapping(mapping);
|
5529
|
+
return;
|
5530
|
+
}
|
5531
|
+
// Also prompt if FROM and/or TO nodes are not selected, and map to
|
5532
|
+
// existing entities
|
5533
|
+
if(from_tos_node.childNodes.length && !mapping.from_to) {
|
5534
|
+
const ft_map = {};
|
5535
|
+
for(let i = 0; i < from_tos_node.childNodes.length; i++) {
|
5536
|
+
const
|
5537
|
+
c = from_tos_node.childNodes[i],
|
5538
|
+
fn = fullName(c),
|
5539
|
+
mn = mappedName(fn);
|
5540
|
+
if(MODEL.objectByName(mn)) ft_map[fn] = mn;
|
5541
|
+
}
|
5542
|
+
// Prompt only for FROM/TO nodes that map to existing nodes
|
5543
|
+
if(Object.keys(ft_map).length) {
|
5544
|
+
mapping.from_to = ft_map;
|
5545
|
+
this.promptForMapping(mapping);
|
5546
|
+
return;
|
5547
|
+
}
|
5548
|
+
}
|
5549
|
+
|
5550
|
+
// Only check for selected entities and from-to's; extra's should be
|
5551
|
+
// used if they exist, or should be created when copying to a different
|
5552
|
+
// model
|
5553
|
+
name_map.length = 0;
|
5554
|
+
nameConflicts(entities_node);
|
5555
|
+
nameConflicts(from_tos_node);
|
5556
|
+
if(name_conflicts.length) {
|
5557
|
+
UI.notify(pluralS(name_conflicts.length, 'name conflict'));
|
5558
|
+
console.log('HERE name conflicts', name_conflicts);
|
5559
|
+
return;
|
5560
|
+
}
|
5561
|
+
|
5562
|
+
// No conflicts => add all
|
5563
|
+
console.log('HERE name map', name_map);
|
5564
|
+
for(let i = 0; i < extras_node.childNodes.length; i++) {
|
5565
|
+
addEntityFromNode(extras_node.childNodes[i]);
|
5566
|
+
}
|
5567
|
+
for(let i = 0; i < from_tos_node.childNodes.length; i++) {
|
5568
|
+
addEntityFromNode(from_tos_node.childNodes[i]);
|
5569
|
+
}
|
5570
|
+
for(let i = 0; i < entities_node.childNodes.length; i++) {
|
5571
|
+
addEntityFromNode(entities_node.childNodes[i]);
|
5572
|
+
}
|
5573
|
+
// Update diagram, showing newly added nodes as selection
|
5574
|
+
MODEL.clearSelection();
|
5575
|
+
for(let i = 0; i < selection_node.childNodes.length; i++) {
|
5576
|
+
const
|
5577
|
+
n = xmlDecoded(nodeContent(selection_node.childNodes[i])),
|
5578
|
+
obj = MODEL.objectByName(mappedName(n));
|
5579
|
+
if(obj) {
|
5580
|
+
// NOTE: selected products must be positioned
|
5581
|
+
if(obj instanceof Product) MODEL.focal_cluster.addProductPosition(obj);
|
5582
|
+
MODEL.select(obj);
|
5583
|
+
}
|
5584
|
+
}
|
5585
|
+
// Force redrawing the selection to ensure that links to positioned
|
5586
|
+
// products are displayed as arrows instead of block arrows
|
5587
|
+
fc.clearAllProcesses();
|
5588
|
+
UI.drawDiagram(MODEL);
|
5264
5589
|
}
|
5265
5590
|
|
5266
5591
|
//
|
@@ -6235,6 +6560,7 @@ UI.logHeapSize(`BEFORE creating post data`);
|
|
6235
6560
|
token: VM.solver_token,
|
6236
6561
|
block: VM.block_count,
|
6237
6562
|
round: VM.round_sequence[VM.current_round],
|
6563
|
+
columns: VM.columnsInBlock,
|
6238
6564
|
data: VM.lines,
|
6239
6565
|
timeout: top
|
6240
6566
|
});
|
@@ -7687,7 +8013,9 @@ class ActorManager {
|
|
7687
8013
|
xp = new ExpressionParser(x);
|
7688
8014
|
if(n !== UI.NO_ACTOR) {
|
7689
8015
|
nn = this.actor_name.value.trim();
|
7690
|
-
|
8016
|
+
// NOTE: prohibit colons in actor names to avoid confusion with
|
8017
|
+
// prefixed entities
|
8018
|
+
if(!UI.validName(nn) || nn.indexOf(':') >= 0) {
|
7691
8019
|
UI.warn(UI.WARNING.INVALID_ACTOR_NAME);
|
7692
8020
|
return false;
|
7693
8021
|
}
|
@@ -82,6 +82,42 @@ 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 === 'scip') {
|
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
|
+
this.log = path.join(workspace.solver_output, 'model.log');
|
91
|
+
// NOTE: SCIP command line accepts space separated commands ...
|
92
|
+
this.args = [
|
93
|
+
'read', this.user_model,
|
94
|
+
'write problem', this.solver_model,
|
95
|
+
'set limit time', 300,
|
96
|
+
'optimize',
|
97
|
+
'write solution', this.solution,
|
98
|
+
'quit'
|
99
|
+
];
|
100
|
+
// ... when SCIP is called with -c option; then commands must be
|
101
|
+
// enclosed in double quotes; SCIP outputs its messages to the
|
102
|
+
// terminal, so these must be caputured in a log file
|
103
|
+
this.solve_cmd = 'scip -c "' + this.args.join(' ') + '" >' + this.log;
|
104
|
+
this.errors = {
|
105
|
+
1: 'User interrupt',
|
106
|
+
2: 'Node limit reached',
|
107
|
+
3: 'Total node limit reached',
|
108
|
+
4: 'Stalling node limit reached',
|
109
|
+
5: 'Time limit reached',
|
110
|
+
6: 'Memory limit reached',
|
111
|
+
7: 'Gap limit reached',
|
112
|
+
8: 'Solution limit reached',
|
113
|
+
9: 'Solution improvement limit reached',
|
114
|
+
10: 'Restart limit reached',
|
115
|
+
11: 'Optimal solution found',
|
116
|
+
12: 'Problem is infeasible',
|
117
|
+
13: 'Problem is unbounded',
|
118
|
+
14: 'Problem is either infeasible or unbounded',
|
119
|
+
15: 'Solver terminated by user'
|
120
|
+
};
|
85
121
|
} else if(this.id === 'lp_solve') {
|
86
122
|
// Execute file commands differ across platforms
|
87
123
|
if(os.platform().startsWith('win')) {
|
@@ -129,8 +165,12 @@ module.exports = class MILPSolver {
|
|
129
165
|
error: '',
|
130
166
|
messages: []
|
131
167
|
};
|
132
|
-
|
168
|
+
// Number of columns (= decision variables) is passed to ensure
|
169
|
+
// that solution vector is complete and its values are placed in
|
170
|
+
// the correct order
|
171
|
+
result.columns = parseInt(sp.get('columns')) || 0;
|
133
172
|
// Default timeout per block is 30 seconds
|
173
|
+
let timeout = parseInt(sp.get('timeout'));
|
134
174
|
if(isNaN(timeout)) timeout = 30;
|
135
175
|
if(!this.id) {
|
136
176
|
result.status = -999;
|
@@ -165,19 +205,24 @@ module.exports = class MILPSolver {
|
|
165
205
|
// (2) the shell option must be set to TRUE (so the command is
|
166
206
|
// executed within an OS shell script) or LP_solve will interpret
|
167
207
|
// the first argument as the model file, and complain
|
168
|
-
// (3) output must be ignored, as LP_solve
|
169
|
-
// about 0-value coefficients, and these would otherwise also
|
208
|
+
// (3) output must be ignored, as LP_solve warnings should not also
|
170
209
|
// appear on the console
|
171
210
|
// (4) prevent Windows opening a visible sub-process shell window
|
172
211
|
const
|
173
212
|
cmd = this.solve_cmd + ' ' + this.args.join(' '),
|
174
213
|
options = {shell: true, stdio: 'ignore', windowsHide: true};
|
175
|
-
spawn = child_process.spawnSync(cmd,
|
176
|
-
} else {
|
214
|
+
spawn = child_process.spawnSync(cmd, options);
|
215
|
+
} else if(this.id === 'gurobi') {
|
216
|
+
// When using Gurobi, the standard way with arguments works well
|
177
217
|
this.args[0] = 'TimeLimit=' + timeout;
|
178
|
-
// When using Gurobi, the standard way works well
|
179
218
|
const options = {windowsHide: true};
|
180
219
|
spawn = child_process.spawnSync(this.solver_path, this.args, options);
|
220
|
+
} else if(this.id === 'scip') {
|
221
|
+
// When using SCIP, take the LP_solve approach
|
222
|
+
const
|
223
|
+
cmd = this.solve_cmd.replace(/limit time \d+/, `limit time ${timeout}`),
|
224
|
+
options = {shell: true, stdio: 'ignore', windowsHide: true};
|
225
|
+
spawn = child_process.spawnSync(cmd, options);
|
181
226
|
}
|
182
227
|
status = spawn.status;
|
183
228
|
} catch(err) {
|
@@ -200,7 +245,37 @@ module.exports = class MILPSolver {
|
|
200
245
|
|
201
246
|
processSolverOutput(result) {
|
202
247
|
// Read solver output files and return solution (or error)
|
203
|
-
const
|
248
|
+
const
|
249
|
+
x_values = [],
|
250
|
+
x_dict = {},
|
251
|
+
getValuesFromDict = () => {
|
252
|
+
// Returns a result vector for as many real numbers (as strings!)
|
253
|
+
// as there are columns (0 if not reported by the solver)
|
254
|
+
// (1) Sort on variable name
|
255
|
+
const vlist = Object.keys(x_dict).sort();
|
256
|
+
// Start with column 1
|
257
|
+
let col = 1;
|
258
|
+
for(let i = 0; i < vlist.length; i++) {
|
259
|
+
const
|
260
|
+
v = vlist[i],
|
261
|
+
// Variable names have zero-padded column numbers, e.g. "X001"
|
262
|
+
vnr = parseInt(v.substring(1));
|
263
|
+
// Add zeros for unreported variables until column number matches
|
264
|
+
while(col < vnr) {
|
265
|
+
x_values.push('0');
|
266
|
+
col++;
|
267
|
+
}
|
268
|
+
x_values.push(x_dict[v]);
|
269
|
+
col++;
|
270
|
+
}
|
271
|
+
// Add zeros to vector for remaining columns
|
272
|
+
while(col <= result.columns) {
|
273
|
+
x_values.push('0');
|
274
|
+
col++;
|
275
|
+
}
|
276
|
+
// No return value; function operates on x_values
|
277
|
+
};
|
278
|
+
|
204
279
|
if(this.id === 'gurobi') {
|
205
280
|
// `messages` must be an array of strings
|
206
281
|
result.messages = fs.readFileSync(this.log, 'utf8').split(os.EOL);
|
@@ -236,6 +311,60 @@ module.exports = class MILPSolver {
|
|
236
311
|
result.error = 'No solution found';
|
237
312
|
}
|
238
313
|
}
|
314
|
+
} else if(this.id === 'scip') {
|
315
|
+
result.seconds = 0;
|
316
|
+
// `messages` must be an array of strings
|
317
|
+
result.messages = fs.readFileSync(this.log, 'utf8').split(os.EOL);
|
318
|
+
let solved = false,
|
319
|
+
output = [];
|
320
|
+
if(result.status !== 0) {
|
321
|
+
// Non-zero solver exit code indicates serious trouble
|
322
|
+
result.error = 'SCIP solver terminated with error';
|
323
|
+
} else {
|
324
|
+
try {
|
325
|
+
output = fs.readFileSync(
|
326
|
+
this.solution, 'utf8').trim().split(os.EOL);
|
327
|
+
} catch(err) {
|
328
|
+
console.log('No SCIP solution file');
|
329
|
+
}
|
330
|
+
// Look in messages for solver status and solving time
|
331
|
+
for(let i = 0; i < result.messages.length; i++) {
|
332
|
+
const m = result.messages[i];
|
333
|
+
if(m.startsWith('SCIP Status')) {
|
334
|
+
if(m.indexOf('problem is solved') >= 0) {
|
335
|
+
solved = true;
|
336
|
+
} else if(m.indexOf('interrupted') >= 0) {
|
337
|
+
if(m.indexOf('time limit') >= 0) {
|
338
|
+
result.status = 5;
|
339
|
+
} else if(m.indexOf('memory limit') >= 0) {
|
340
|
+
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
|
+
}
|
346
|
+
result.error = this.errors[result.status];
|
347
|
+
}
|
348
|
+
} else if (m.startsWith('Solving Time')) {
|
349
|
+
result.seconds = parseFloat(m.split(':')[1]);
|
350
|
+
}
|
351
|
+
}
|
352
|
+
}
|
353
|
+
if(solved) {
|
354
|
+
// Look for line with first variable
|
355
|
+
let i = 0;
|
356
|
+
while(i < output.length && !output[i].startsWith('X')) i++;
|
357
|
+
// Fill dictionary with variable name: value entries
|
358
|
+
while(i < output.length) {
|
359
|
+
const v = output[i].split(/\s+/);
|
360
|
+
x_dict[v[0]] = v[1];
|
361
|
+
i++;
|
362
|
+
}
|
363
|
+
// Fill the solution vector, adding 0 for missing columns
|
364
|
+
getValuesFromDict();
|
365
|
+
} else {
|
366
|
+
console.log('No solution found');
|
367
|
+
}
|
239
368
|
} else if(this.id === 'lp_solve') {
|
240
369
|
// Read solver messages from file
|
241
370
|
// NOTE: Linny-R client expects a list of strings
|
@@ -246,7 +375,7 @@ module.exports = class MILPSolver {
|
|
246
375
|
result.seconds = 0;
|
247
376
|
let i = 0,
|
248
377
|
solved = false;
|
249
|
-
while(i< output.length && !solved) {
|
378
|
+
while(i < output.length && !solved) {
|
250
379
|
msgs.push(output[i]);
|
251
380
|
const m = output[i].match(/in total (\d+\.\d+) seconds/);
|
252
381
|
if(m && m.length > 1) result.seconds = parseFloat(m[1]);
|
@@ -255,14 +384,16 @@ module.exports = class MILPSolver {
|
|
255
384
|
}
|
256
385
|
result.messages = msgs;
|
257
386
|
if(solved) {
|
258
|
-
|
387
|
+
// Look for line with first variable
|
388
|
+
while(i < output.length && !output[i].startsWith('X')) i++;
|
389
|
+
// Fill dictionary with variable name: value entries
|
259
390
|
while(i < output.length) {
|
260
|
-
|
261
|
-
|
262
|
-
v = parseFloat(v);
|
263
|
-
x_values.push(v);
|
391
|
+
const v = output[i].split(/\s+/);
|
392
|
+
x_dict[v[0]] = v[1];
|
264
393
|
i++;
|
265
394
|
}
|
395
|
+
// Fill the solution vector, adding 0 for missing columns
|
396
|
+
getValuesFromDict();
|
266
397
|
} else {
|
267
398
|
console.log('No solution found');
|
268
399
|
}
|