linny-r 1.3.0 → 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 +162 -7
- package/package.json +1 -1
- package/server.js +41 -16
- package/static/images/paperclip.png +0 -0
- package/static/index.html +64 -3
- package/static/linny-r.css +65 -3
- package/static/scripts/linny-r-ctrl.js +16 -1
- package/static/scripts/linny-r-gui.js +421 -84
- 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 +205 -89
@@ -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
|
}
|
@@ -277,6 +277,21 @@ class LinnyRModel {
|
|
277
277
|
return null;
|
278
278
|
}
|
279
279
|
|
280
|
+
productByID(id) {
|
281
|
+
if(this.products.hasOwnProperty(id)) return this.products[id];
|
282
|
+
return null;
|
283
|
+
}
|
284
|
+
|
285
|
+
processByID(id) {
|
286
|
+
if(this.processes.hasOwnProperty(id)) return this.processes[id];
|
287
|
+
return null;
|
288
|
+
}
|
289
|
+
|
290
|
+
clusterByID(id) {
|
291
|
+
if(this.clusters.hasOwnProperty(id)) return this.clusters[id];
|
292
|
+
return null;
|
293
|
+
}
|
294
|
+
|
280
295
|
nodeBoxByID(id) {
|
281
296
|
if(this.products.hasOwnProperty(id)) return this.products[id];
|
282
297
|
if(this.processes.hasOwnProperty(id)) return this.processes[id];
|
@@ -1572,7 +1587,7 @@ class LinnyRModel {
|
|
1572
1587
|
}
|
1573
1588
|
return true;
|
1574
1589
|
}
|
1575
|
-
|
1590
|
+
|
1576
1591
|
get selectionAsXML() {
|
1577
1592
|
// Returns XML for the selected entities, and also for the entities
|
1578
1593
|
// referenced by expressions for their attributes.
|
@@ -1595,34 +1610,92 @@ class LinnyRModel {
|
|
1595
1610
|
from_tos = [],
|
1596
1611
|
xml = [],
|
1597
1612
|
ft_xml = [],
|
1598
|
-
extra_xml = []
|
1613
|
+
extra_xml = [],
|
1614
|
+
selected_xml = [];
|
1599
1615
|
for(let i = 0; i < this.selection.length; i++) {
|
1600
1616
|
const obj = this.selection[i];
|
1601
1617
|
entities[obj.type].push(obj);
|
1618
|
+
selected_xml.push('<sel>' + xmlEncoded(obj.displayName) + '</sel>');
|
1602
1619
|
}
|
1620
|
+
// Expand clusters by adding all its model entities to their respective
|
1621
|
+
// lists
|
1622
|
+
for(let i = 0; i < entities.Cluster.length; i++) {
|
1623
|
+
const c = entities.Cluster[i];
|
1624
|
+
c.clearAllProcesses();
|
1625
|
+
c.categorizeEntities();
|
1626
|
+
// All processes and products in (sub)clusters must be copied
|
1627
|
+
mergeDistinct(c.all_processes, entities.Process);
|
1628
|
+
mergeDistinct(c.all_products, entities.Product);
|
1629
|
+
// Likewise for all related links and constraints
|
1630
|
+
mergeDistinct(c.related_links, entities.Link);
|
1631
|
+
mergeDistinct(c.related_constraints, entities.Constraint);
|
1632
|
+
// NOTE: add entities referenced by notes within selected clusters
|
1633
|
+
// to `extras`, but not the XML for these notes, as this is already
|
1634
|
+
// part of the clusters' XML
|
1635
|
+
const an = c.allNotes;
|
1636
|
+
// Add selected notes as these must also be checked for "extras"
|
1637
|
+
mergeDistinct(entities.Note, an);
|
1638
|
+
for(let i = 0; i < an.length; i++) {
|
1639
|
+
const n = an[i];
|
1640
|
+
mergeDistinct(n.color.referencedEntities, extras);
|
1641
|
+
for(let i = 0; i < n.fields.length; i++) {
|
1642
|
+
addDistinct(n.object, extras);
|
1643
|
+
}
|
1644
|
+
}
|
1645
|
+
}
|
1646
|
+
// Only add the XML for notes in the selection
|
1603
1647
|
for(let i = 0; i < entities.Note.length; i++) {
|
1604
|
-
const n = entities.Note[i];
|
1605
1648
|
xml.push(n.asXML);
|
1606
1649
|
}
|
1607
1650
|
for(let i = 0; i < entities.Product.length; i++) {
|
1608
|
-
|
1651
|
+
const p = entities.Product[i];
|
1652
|
+
mergeDistinct(p.lower_bound.referencedEntities, extras);
|
1653
|
+
mergeDistinct(p.upper_bound.referencedEntities, extras);
|
1654
|
+
mergeDistinct(p.initial_level.referencedEntities, extras);
|
1655
|
+
mergeDistinct(p.price.referencedEntities, extras);
|
1656
|
+
xml.push(p.asXML);
|
1609
1657
|
}
|
1610
1658
|
for(let i = 0; i < entities.Process.length; i++) {
|
1611
|
-
|
1612
|
-
|
1659
|
+
const p = entities.Process[i];
|
1660
|
+
mergeDistinct(p.lower_bound.referencedEntities, extras);
|
1661
|
+
mergeDistinct(p.upper_bound.referencedEntities, extras);
|
1662
|
+
mergeDistinct(p.initial_level.referencedEntities, extras);
|
1663
|
+
mergeDistinct(p.pace_expression.referencedEntities, extras);
|
1664
|
+
xml.push(p.asXML);
|
1665
|
+
}
|
1666
|
+
// Only now add the XML for the selected clusters
|
1613
1667
|
for(let i = 0; i < entities.Cluster.length; i++) {
|
1614
1668
|
xml.push(entities.Cluster[i].asXML);
|
1615
1669
|
}
|
1670
|
+
// Add all links that have (implicitly via clusters) been selected
|
1616
1671
|
for(let i = 0; i < entities.Link.length; i++) {
|
1617
1672
|
const l = entities.Link[i];
|
1618
|
-
|
1619
|
-
|
1673
|
+
// NOTE: the FROM and/or TO node need not be selected; if not, put
|
1674
|
+
// them in a separate list
|
1675
|
+
if(entities.Process.indexOf(l.from_node) < 0 &&
|
1676
|
+
entities.Product.indexOf(l.from_node) < 0) {
|
1677
|
+
addDistinct(l.from_node, from_tos);
|
1678
|
+
}
|
1679
|
+
if(entities.Process.indexOf(l.to_node) < 0 &&
|
1680
|
+
entities.Product.indexOf(l.to_node) < 0) {
|
1681
|
+
addDistinct(l.to_node, from_tos);
|
1682
|
+
}
|
1683
|
+
mergeDistinct(l.relative_rate.referencedEntities, extras);
|
1684
|
+
mergeDistinct(l.flow_delay.referencedEntities, extras);
|
1620
1685
|
xml.push(l.asXML);
|
1621
1686
|
}
|
1622
1687
|
for(let i = 0; i < entities.Constraint.length; i++) {
|
1623
1688
|
const c = entities.Constraint[i];
|
1624
|
-
|
1625
|
-
|
1689
|
+
// NOTE: the FROM and/or TO node need not be selected; if not, put
|
1690
|
+
// them in a separate list
|
1691
|
+
if(entities.Process.indexOf(c.from_node) < 0 &&
|
1692
|
+
entities.Product.indexOf(c.from_node) < 0) {
|
1693
|
+
addDistinct(c.from_node, from_tos);
|
1694
|
+
}
|
1695
|
+
if(entities.Process.indexOf(c.to_node) < 0 &&
|
1696
|
+
entities.Product.indexOf(c.to_node) < 0) {
|
1697
|
+
addDistinct(c.to_node, from_tos);
|
1698
|
+
}
|
1626
1699
|
xml.push(c.asXML);
|
1627
1700
|
}
|
1628
1701
|
for(let i = 0; i < from_tos.length; i++) {
|
@@ -1631,7 +1704,9 @@ class LinnyRModel {
|
|
1631
1704
|
if(p instanceof Process) ft_xml.push('" actor-name="', xmlEncoded(p.actor.name));
|
1632
1705
|
ft_xml.push('"></from-to>');
|
1633
1706
|
}
|
1634
|
-
for(let i = 0; i < extras.length; i++)
|
1707
|
+
for(let i = 0; i < extras.length; i++) {
|
1708
|
+
extra_xml.push(extras[i].asXML);
|
1709
|
+
}
|
1635
1710
|
return ['<copy timestamp="', Date.now(),
|
1636
1711
|
'" model-timestamp="', this.time_created.getTime(),
|
1637
1712
|
'" cluster-name="', xmlEncoded(fc_name),
|
@@ -1639,7 +1714,8 @@ class LinnyRModel {
|
|
1639
1714
|
'"><entities>', xml.join(''),
|
1640
1715
|
'</entities><from-tos>', ft_xml.join(''),
|
1641
1716
|
'</from-tos><extras>', extra_xml.join(''),
|
1642
|
-
'</extras
|
1717
|
+
'</extras><selection>', selected_xml.join(''),
|
1718
|
+
'</selection></copy>'].join('');
|
1643
1719
|
}
|
1644
1720
|
|
1645
1721
|
dropSelectionIntoCluster(c) {
|
@@ -4375,7 +4451,10 @@ class Actor {
|
|
4375
4451
|
|
4376
4452
|
rename(name) {
|
4377
4453
|
// Change the name of this actor
|
4378
|
-
|
4454
|
+
// NOTE: since version 1.3.2, colons are prohibited in actor names to
|
4455
|
+
// avoid confusion with prefixed entities; they are silently removed
|
4456
|
+
// to avoid model compatibility issues
|
4457
|
+
name = UI.cleanName(name).replace(':', '');
|
4379
4458
|
if(!UI.validName(name)) {
|
4380
4459
|
UI.warn(UI.WARNING.INVALID_ACTOR_NAME);
|
4381
4460
|
return null;
|
@@ -5414,6 +5493,7 @@ class Cluster extends NodeBox {
|
|
5414
5493
|
get asXML() {
|
5415
5494
|
let xml;
|
5416
5495
|
const
|
5496
|
+
cmnts = xmlEncoded(this.comments),
|
5417
5497
|
flags = (this.collapsed ? ' collapsed="1"' : '') +
|
5418
5498
|
(this.ignore ? ' ignore="1"' : '') +
|
5419
5499
|
(this.black_box ? ' black-box="1"' : '') +
|
@@ -5422,7 +5502,8 @@ class Cluster extends NodeBox {
|
|
5422
5502
|
'</name><owner>', xmlEncoded(this.actor.name),
|
5423
5503
|
'</owner><x-coord>', this.x,
|
5424
5504
|
'</x-coord><y-coord>', this.y,
|
5425
|
-
'</y-coord><
|
5505
|
+
'</y-coord><comments>', cmnts,
|
5506
|
+
'</comments><process-set>'].join('');
|
5426
5507
|
for(let i = 0; i < this.processes.length; i++) {
|
5427
5508
|
let n = this.processes[i].displayName;
|
5428
5509
|
const id = UI.nameToID(n);
|
@@ -5458,6 +5539,7 @@ class Cluster extends NodeBox {
|
|
5458
5539
|
initFromXML(node) {
|
5459
5540
|
this.x = safeStrToInt(nodeContentByTag(node, 'x-coord'));
|
5460
5541
|
this.y = safeStrToInt(nodeContentByTag(node, 'y-coord'));
|
5542
|
+
this.comments = xmlDecoded(nodeContentByTag(node, 'comments'));
|
5461
5543
|
this.collapsed = nodeParameterValue(node, 'collapsed') === '1';
|
5462
5544
|
this.ignore = nodeParameterValue(node, 'ignore') === '1';
|
5463
5545
|
this.black_box = nodeParameterValue(node, 'black-box') === '1';
|
@@ -5468,8 +5550,8 @@ class Cluster extends NodeBox {
|
|
5468
5550
|
name,
|
5469
5551
|
actor;
|
5470
5552
|
|
5471
|
-
// NOTE: to compensate for shameful bug in earlier version, look
|
5472
|
-
// "product-positions" node and for "notes" node in the process-set,
|
5553
|
+
// NOTE: to compensate for a shameful bug in an earlier version, look
|
5554
|
+
// for "product-positions" node and for "notes" node in the process-set,
|
5473
5555
|
// as it may have been put there instead of in the cluster node itself
|
5474
5556
|
const
|
5475
5557
|
hidden_pp = childNodeByTag(n, 'product-positions'),
|
@@ -5649,13 +5731,19 @@ class Cluster extends NodeBox {
|
|
5649
5731
|
addDistinct(this.product_positions[i].product, prods);
|
5650
5732
|
}
|
5651
5733
|
for(let i = 0; i < this.sub_clusters.length; i++) {
|
5652
|
-
|
5653
|
-
for(let j = 0; j < ap.length; j++) {
|
5654
|
-
addDistinct(ap[j], prods);
|
5655
|
-
}
|
5734
|
+
mergeDistinct(this.sub_clusters[i].allProducts, prods); // recursion!
|
5656
5735
|
}
|
5657
5736
|
return prods;
|
5658
5737
|
}
|
5738
|
+
|
5739
|
+
get allNotes() {
|
5740
|
+
// Return the set of all notes in this cluster and its subclusters
|
5741
|
+
let notes = this.notes.slice();
|
5742
|
+
for(let i = 0; i < this.sub_clusters.length; i++) {
|
5743
|
+
notes = notes.concat(this.sub_clusters[i].allNotes); // recursion!
|
5744
|
+
}
|
5745
|
+
return notes;
|
5746
|
+
}
|
5659
5747
|
|
5660
5748
|
clearAllProcesses() {
|
5661
5749
|
// Clear `all_processes` property of this cluster AND of all its parent clusters
|
@@ -6897,7 +6985,7 @@ class Process extends Node {
|
|
6897
6985
|
// The production level in time T thus corresponds to decision variable
|
6898
6986
|
// X[Math.floor((T-1) / PACE + 1]
|
6899
6987
|
this.pace = 1;
|
6900
|
-
this.pace_expression = new Expression(this, '
|
6988
|
+
this.pace_expression = new Expression(this, 'LCF', '1');
|
6901
6989
|
// NOTE: processes have NO input attributes other than LB, UB and IL
|
6902
6990
|
// for processes, the default bounds are [0, +INF]
|
6903
6991
|
this.equal_bounds = false;
|
@@ -256,8 +256,9 @@ function markFirstDifference(s1, s2) {
|
|
256
256
|
//
|
257
257
|
|
258
258
|
function ciCompare(a, b) {
|
259
|
-
// Performs case-insensitive comparison
|
260
|
-
|
259
|
+
// Performs case-insensitive comparison that does differentiate
|
260
|
+
// between accented characters (as this differentiates between identifiers)
|
261
|
+
return a.localeCompare(b, undefined, {sensitivity: 'accent'});
|
261
262
|
}
|
262
263
|
|
263
264
|
function endsWithDigits(str) {
|
@@ -439,6 +440,13 @@ function addDistinct(e, list) {
|
|
439
440
|
if(list.indexOf(e) < 0) list.push(e);
|
440
441
|
}
|
441
442
|
|
443
|
+
function mergeDistinct(list, into) {
|
444
|
+
// Adds elements of `list` to `into` if not already in `into`
|
445
|
+
for(let i = 0; i < list.length; i++) {
|
446
|
+
addDistinct(list[i], into);
|
447
|
+
}
|
448
|
+
}
|
449
|
+
|
442
450
|
function setString(sl) {
|
443
451
|
// Returns elements of stringlist `sl` in set notation
|
444
452
|
return '{' + sl.join(', ') + '}';
|