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.
@@ -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
- pasteSelection() {
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
- const xml = window.localStorage.getItem('Linny-R-selection-XML');
5260
- if(xml) {
5261
- // @@ TO DO!
5262
- this.notify('Paste not implemented yet -- WORK IN PROGRESS!');
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
- if(!UI.validName(nn)) {
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
- let timeout = parseInt(sp.get('timeout'));
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 will output many warnings
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, options);
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 x_values = [];
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
- while(i < output.length && !output[i].startsWith('C1')) i++;
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
- let v = output[i].replace(/C\d+\s*/, '');
261
- // Remove variable names from result output
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
  }