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
@@ -77,6 +77,24 @@ class Expression {
|
|
77
77
|
return MODEL.timeStepDuration;
|
78
78
|
}
|
79
79
|
|
80
|
+
get referencedEntities() {
|
81
|
+
// Returns a list of entities referenced in this expression
|
82
|
+
if(this.text.indexOf('[') < 0) return [];
|
83
|
+
const
|
84
|
+
el = [],
|
85
|
+
ml = [...this.text.matchAll(/\[(\{[^\}]+\}){0,1}([^\]]+)\]/g)];
|
86
|
+
for(let i = 0; i < ml.length; i++) {
|
87
|
+
const n = ml[i][2].trim();
|
88
|
+
let sep = n.lastIndexOf('|');
|
89
|
+
if(sep < 0) sep = n.lastIndexOf('@');
|
90
|
+
const
|
91
|
+
en = (sep < 0 ? n : n.substring(0, sep)),
|
92
|
+
e = MODEL.objectByName(en.trim());
|
93
|
+
if(e) addDistinct(e, el);
|
94
|
+
}
|
95
|
+
return el;
|
96
|
+
}
|
97
|
+
|
80
98
|
update(parser) {
|
81
99
|
// Must be called after successful compilation by the expression parser
|
82
100
|
this.text = parser.expr;
|
@@ -216,7 +234,7 @@ class Expression {
|
|
216
234
|
if(!this.compiled) this.compile();
|
217
235
|
return this.is_static;
|
218
236
|
}
|
219
|
-
|
237
|
+
|
220
238
|
trace(action) {
|
221
239
|
// Adds step stack (if any) and action to the trace.
|
222
240
|
if(DEBUGGING) {
|
@@ -343,7 +361,7 @@ class Expression {
|
|
343
361
|
}
|
344
362
|
return v[t];
|
345
363
|
}
|
346
|
-
|
364
|
+
|
347
365
|
get asAttribute() {
|
348
366
|
// Returns the result for the current time step if the model has been solved
|
349
367
|
// (special values as human-readable string), or the expression as text
|
@@ -1604,8 +1622,9 @@ class VirtualMachine {
|
|
1604
1622
|
this.attribute_names = {
|
1605
1623
|
'LB': 'lower bound',
|
1606
1624
|
'UB': 'upper bound',
|
1607
|
-
'L': 'level',
|
1608
1625
|
'IL': 'initial level',
|
1626
|
+
'LCF': 'level change frequency',
|
1627
|
+
'L': 'level',
|
1609
1628
|
'P': 'price',
|
1610
1629
|
'CP': 'cost price',
|
1611
1630
|
'HCP': 'highest cost price',
|
@@ -1622,21 +1641,21 @@ class VirtualMachine {
|
|
1622
1641
|
// NOTE: defaults are level (L), link flow (F), cluster cash flow (CF),
|
1623
1642
|
// actor cash flow (CF); dataset value (no attribute)
|
1624
1643
|
// NOTE: exogenous properties first, then the computed properties
|
1625
|
-
this.process_attr = ['LB', 'UB', 'IL', 'L', 'CI', 'CO', 'CF', 'CP'];
|
1644
|
+
this.process_attr = ['LB', 'UB', 'IL', 'LCF', 'L', 'CI', 'CO', 'CF', 'CP'];
|
1626
1645
|
this.product_attr = ['LB', 'UB', 'IL', 'P', 'L', 'CP', 'HCP'];
|
1627
1646
|
this.cluster_attr = ['CI', 'CO', 'CF'];
|
1628
1647
|
this.link_attr = ['R', 'D', 'SOC', 'F'];
|
1629
1648
|
this.constraint_attr = ['SOC', 'A'];
|
1630
1649
|
this.actor_attr = ['W', 'CI', 'CO', 'CF'];
|
1631
1650
|
// Only expression attributes can be used for sensitivity analysis
|
1632
|
-
this.expression_attr = ['LB', 'UB', 'IL', 'P', 'R', 'W'];
|
1651
|
+
this.expression_attr = ['LB', 'UB', 'IL', 'LCF', 'P', 'R', 'D', 'W'];
|
1633
1652
|
// Attributes per entity type letter
|
1634
1653
|
this.attribute_codes = {
|
1635
1654
|
A: this.actor_attr,
|
1636
1655
|
B: this.constraint_attr,
|
1637
1656
|
C: this.cluster_attr,
|
1638
|
-
D: ['
|
1639
|
-
E: ['X'],
|
1657
|
+
D: ['DSM'], // ("dataset modifier" -- placeholder value, not used)
|
1658
|
+
E: ['X'], // ("expression" -- placeholder value, not used)
|
1640
1659
|
L: this.link_attr,
|
1641
1660
|
P: this.process_attr,
|
1642
1661
|
Q: this.product_attr
|
@@ -2282,8 +2301,7 @@ class VirtualMachine {
|
|
2282
2301
|
let l = '';
|
2283
2302
|
for(let i = 0; i < vcnt; i++) {
|
2284
2303
|
const obj = this.variables[i][1];
|
2285
|
-
let v = (
|
2286
|
-
'C' + (i+1) : 'X' + (i+1).toString().padStart(z, '0'));
|
2304
|
+
let v = 'X' + (i+1).toString().padStart(z, '0');
|
2287
2305
|
v += ' '.slice(v.length) + obj.displayName;
|
2288
2306
|
const p = (obj instanceof Process && obj.pace > 1 ? ' 1/' + obj.pace : '');
|
2289
2307
|
l += v + ' [' + this.variables[i][0] + p + ']\n';
|
@@ -2297,8 +2315,7 @@ class VirtualMachine {
|
|
2297
2315
|
obj = this.chunk_variables[i][1],
|
2298
2316
|
// NOTE: chunk offset takes into account that indices are 0-based
|
2299
2317
|
cvi = chof + i;
|
2300
|
-
let v = (
|
2301
|
-
'C' + cvi : 'X' + cvi.toString().padStart(z, '0'));
|
2318
|
+
let v = 'X' + cvi.toString().padStart(z, '0');
|
2302
2319
|
v += ' '.slice(v.length) + obj.displayName;
|
2303
2320
|
l += v + ' [' + this.chunk_variables[i][0] + ']\n';
|
2304
2321
|
}
|
@@ -3607,11 +3624,11 @@ class VirtualMachine {
|
|
3607
3624
|
// simulation end time)
|
3608
3625
|
const
|
3609
3626
|
ncv = this.chunk_variables.length,
|
3627
|
+
ncv_msg = (ncv ? ' minus ' + pluralS(ncv, 'singular variable') : ''),
|
3610
3628
|
xratio = (x.length - ncv) / this.cols,
|
3611
3629
|
xbl = Math.floor(xratio);
|
3612
3630
|
if(xbl < xratio) console.log('ANOMALY: solution vector length', x.length,
|
3613
|
-
|
3614
|
-
this.cols);
|
3631
|
+
ncv_msg + ' is not a multiple of # columns', this.cols);
|
3615
3632
|
if(xbl < abl) {
|
3616
3633
|
console.log('Cropping actual block length', abl,
|
3617
3634
|
'to solved block length', xbl);
|
@@ -4257,13 +4274,42 @@ class VirtualMachine {
|
|
4257
4274
|
(this.block_count - 1) * MODEL.block_length;
|
4258
4275
|
}
|
4259
4276
|
|
4260
|
-
|
4277
|
+
get columnsInBlock() {
|
4278
|
+
// Returns the actual block length plus the number of chunk variables
|
4279
|
+
return this.actualBlockLength * this.cols + this.chunk_variables.length;
|
4280
|
+
}
|
4281
|
+
|
4282
|
+
writeLpFormat(cplex=false) {
|
4261
4283
|
// NOTE: actual block length `abl` of last block is likely to be
|
4262
4284
|
// shorter than the standard, as it should not go beyond the end time
|
4263
|
-
|
4285
|
+
|
4286
|
+
|
4287
|
+
const
|
4288
|
+
abl = this.actualBlockLength,
|
4289
|
+
// Get the number digits for variable names
|
4290
|
+
z = this.columnsInBlock.toString().length,
|
4291
|
+
// LP_solve uses semicolon as separator between equations
|
4292
|
+
EOL = (cplex ? '\n' : ';\n'),
|
4293
|
+
// Local function that returns variable symbol (e.g. X001) with
|
4294
|
+
// its coefficient if specified (e.g., -0.123 X001) in the
|
4295
|
+
// most compact notation
|
4296
|
+
vbl = (index, c=false) => {
|
4297
|
+
const v = 'X' + index.toString().padStart(z, '0');
|
4298
|
+
if(c === false) return v; // Only the symbol
|
4299
|
+
if(c === -1) return ` -${v}`; // No coefficient needed
|
4300
|
+
if(c < 0) return ` ${c} ${v}`; // Number had minus sign
|
4301
|
+
if(c === 1) return ` +${v}`; // No coefficient needed
|
4302
|
+
return ` +${c} ${v}`; // Prefix coefficient with +
|
4303
|
+
// NOTE: this may return +0 X001
|
4304
|
+
};
|
4305
|
+
|
4264
4306
|
this.numeric_issue = '';
|
4265
4307
|
// First add the objective (always MAXimize)
|
4266
|
-
|
4308
|
+
if(cplex) {
|
4309
|
+
this.lines = 'Maximize\n';
|
4310
|
+
} else {
|
4311
|
+
this.lines = '/* Objective function */\nmax:\n';
|
4312
|
+
}
|
4267
4313
|
let c,
|
4268
4314
|
p,
|
4269
4315
|
line = '';
|
@@ -4277,25 +4323,7 @@ class VirtualMachine {
|
|
4277
4323
|
this.setNumericIssue(c, p, 'objective function coefficient');
|
4278
4324
|
break;
|
4279
4325
|
}
|
4280
|
-
|
4281
|
-
// No coefficient needed
|
4282
|
-
line += ' -C' + p;
|
4283
|
-
} else if(c < 0) {
|
4284
|
-
// Minus sign already included in c
|
4285
|
-
line += ' ' + c + ' C' + p;
|
4286
|
-
} else if (c === 1) {
|
4287
|
-
// No coefficient needed
|
4288
|
-
line += ' +C' + p;
|
4289
|
-
} else {
|
4290
|
-
// Prefix coefficient with + sign
|
4291
|
-
// NOTE: do NOT check for near-zero -- see note below!
|
4292
|
-
line += ' +' + c + ' C' + p;
|
4293
|
-
}
|
4294
|
-
} else {
|
4295
|
-
// Add variable with coefficient 0 to the objective
|
4296
|
-
// NOTE: This may result in warnings by the solver; however, this is
|
4297
|
-
// needed to maintain the variables in their order, so do not modify!
|
4298
|
-
line += ' +0 C' + p;
|
4326
|
+
line += vbl(p, c);
|
4299
4327
|
}
|
4300
4328
|
// Keep lines under approx. 110 chars
|
4301
4329
|
if(line.length >= 100) {
|
@@ -4303,10 +4331,14 @@ class VirtualMachine {
|
|
4303
4331
|
line = '';
|
4304
4332
|
}
|
4305
4333
|
}
|
4306
|
-
this.lines += line +
|
4334
|
+
this.lines += line + EOL;
|
4307
4335
|
line = '';
|
4308
4336
|
// Add the row constraints
|
4309
|
-
|
4337
|
+
if(cplex) {
|
4338
|
+
this.lines += '\nSubject To\n';
|
4339
|
+
} else {
|
4340
|
+
this.lines += '\n/* Constraints */\n';
|
4341
|
+
}
|
4310
4342
|
n = this.matrix.length;
|
4311
4343
|
for(let r = 0; r < n; r++) {
|
4312
4344
|
const row = this.matrix[r];
|
@@ -4316,16 +4348,8 @@ class VirtualMachine {
|
|
4316
4348
|
this.setNumericIssue(c, p, 'constraint coefficient');
|
4317
4349
|
break;
|
4318
4350
|
}
|
4319
|
-
|
4320
|
-
|
4321
|
-
} else if(c < 0) {
|
4322
|
-
line += ' ' + c + ' C' + p;
|
4323
|
-
} else if (c === 1) {
|
4324
|
-
line += ' +C' + p;
|
4325
|
-
} else {
|
4326
|
-
line += ' +' + c + ' C' + p;
|
4327
|
-
}
|
4328
|
-
// Keep lines under approx. 80 chars
|
4351
|
+
line += vbl(p, c);
|
4352
|
+
// Keep lines under approx. 110 chars
|
4329
4353
|
if(line.length >= 100) {
|
4330
4354
|
this.lines += line + '\n';
|
4331
4355
|
line = '';
|
@@ -4333,11 +4357,15 @@ class VirtualMachine {
|
|
4333
4357
|
}
|
4334
4358
|
c = this.right_hand_side[r];
|
4335
4359
|
this.lines += line + ' ' +
|
4336
|
-
this.constraint_symbols[this.constraint_types[r]] + ' ' + c +
|
4360
|
+
this.constraint_symbols[this.constraint_types[r]] + ' ' + c + EOL;
|
4337
4361
|
line = '';
|
4338
4362
|
}
|
4339
4363
|
// Add the variable bounds
|
4340
|
-
|
4364
|
+
if(cplex) {
|
4365
|
+
this.lines += '\nBounds\n';
|
4366
|
+
} else {
|
4367
|
+
this.lines += '\n/* Variable bounds */\n';
|
4368
|
+
}
|
4341
4369
|
n = abl * this.cols;
|
4342
4370
|
for(p = 1; p <= n; p++) {
|
4343
4371
|
let lb = null,
|
@@ -4359,44 +4387,115 @@ class VirtualMachine {
|
|
4359
4387
|
}
|
4360
4388
|
line = '';
|
4361
4389
|
if(lb === ub) {
|
4362
|
-
if(lb !== null) line =
|
4390
|
+
if(lb !== null) line = ` ${vbl(p)} = ${lb}`;
|
4363
4391
|
} else {
|
4364
|
-
line = 'C' + p;
|
4365
4392
|
// NOTE: by default, lower bound of variables is 0
|
4366
|
-
|
4367
|
-
if(
|
4393
|
+
line = ` ${vbl(p)}`;
|
4394
|
+
if(cplex) {
|
4395
|
+
// Explicitly denote free variables
|
4396
|
+
if(lb === null && ub === null && !this.is_binary[p]) {
|
4397
|
+
line += ' free';
|
4398
|
+
} else {
|
4399
|
+
// Separate lines for LB and UB if specified
|
4400
|
+
if(ub !== null) line += ' <= ' + ub;
|
4401
|
+
if(lb !== null && lb !== 0) line += `\n ${vbl(p)} >= ${lb}`;
|
4402
|
+
}
|
4403
|
+
} else {
|
4404
|
+
// Bounds can be specified on a single line: lb <= X001 <= ub
|
4405
|
+
if(lb !== null && lb !== 0) line = lb + ' <= ' + line;
|
4406
|
+
if(ub !== null) line += ' <= ' + ub;
|
4407
|
+
}
|
4368
4408
|
}
|
4369
|
-
if(line) this.lines += line +
|
4409
|
+
if(line) this.lines += line + EOL;
|
4370
4410
|
}
|
4371
4411
|
// Add the special variable types
|
4372
|
-
|
4373
|
-
|
4374
|
-
|
4375
|
-
|
4376
|
-
|
4377
|
-
|
4378
|
-
|
4379
|
-
|
4380
|
-
|
4381
|
-
|
4382
|
-
|
4383
|
-
|
4384
|
-
|
4385
|
-
|
4386
|
-
|
4387
|
-
|
4388
|
-
|
4389
|
-
|
4390
|
-
|
4391
|
-
|
4392
|
-
|
4393
|
-
|
4394
|
-
|
4395
|
-
|
4396
|
-
|
4412
|
+
if(cplex) {
|
4413
|
+
line = '';
|
4414
|
+
let scv = 0;
|
4415
|
+
for(let i in this.is_binary) if(Number(i)) {
|
4416
|
+
line += ' ' + vbl(i);
|
4417
|
+
scv++;
|
4418
|
+
// Max. 10 variables per line
|
4419
|
+
if(scv >= 10) line += '\n';
|
4420
|
+
}
|
4421
|
+
if(scv) {
|
4422
|
+
this.lines += `Binary\n${line}\n`;
|
4423
|
+
line = '';
|
4424
|
+
scv = 0;
|
4425
|
+
}
|
4426
|
+
for(let i in this.is_integer) if(Number(i)) {
|
4427
|
+
line += ' ' + vbl(i);
|
4428
|
+
scv++;
|
4429
|
+
// Max. 10 variables per line
|
4430
|
+
if(scv >= 10) line += '\n';
|
4431
|
+
}
|
4432
|
+
if(scv) {
|
4433
|
+
this.lines += `General\n${line}\n`;
|
4434
|
+
line = '';
|
4435
|
+
scv = 0;
|
4436
|
+
}
|
4437
|
+
for(let i in this.is_semi_continuous) if(Number(i)) {
|
4438
|
+
line += ' '+ vbl(i);
|
4439
|
+
scv++;
|
4440
|
+
// Max. 10 variables per line
|
4441
|
+
if(scv >= 10) line += '\n';
|
4442
|
+
}
|
4443
|
+
if(scv) {
|
4444
|
+
this.lines += `Semi-continuous\n${line}\n`;
|
4445
|
+
line = '';
|
4446
|
+
scv = 0;
|
4447
|
+
}
|
4448
|
+
if(this.sos_var_indices.length > 0) {
|
4449
|
+
this.lines += 'SOS\n';
|
4450
|
+
let sos = 0;
|
4451
|
+
const v_set = [];
|
4452
|
+
for(let j = 0; j < abl; j++) {
|
4453
|
+
for(let i = 0; i < this.sos_var_indices.length; i++) {
|
4454
|
+
v_set.length = 0;
|
4455
|
+
let vi = this.sos_var_indices[i][0] + j * this.cols;
|
4456
|
+
const n = this.sos_var_indices[i][1];
|
4457
|
+
for(let j = 1; j <= n; j++) {
|
4458
|
+
v_set.push(`${vbl(vi)}:${j}`);
|
4459
|
+
vi++;
|
4460
|
+
}
|
4461
|
+
this.lines += ` s${sos}: S2:: ${v_set.join(' ')}\n`;
|
4462
|
+
sos++;
|
4463
|
+
}
|
4464
|
+
}
|
4465
|
+
}
|
4466
|
+
this.lines += 'End';
|
4467
|
+
} else {
|
4468
|
+
// NOTE: LP_solve does not differentiate between binary and integer,
|
4469
|
+
// so for binary variables, the constraint <= 1 must be added
|
4470
|
+
const v_set = [];
|
4471
|
+
for(let i in this.is_binary) if(Number(i)) {
|
4472
|
+
const v = vbl(i);
|
4473
|
+
this.lines += `${v} <= 1;\n`;
|
4474
|
+
v_set.push(v);
|
4475
|
+
}
|
4476
|
+
for(let i in this.is_integer) if(Number(i)) v_set.push(vbl(i));
|
4477
|
+
if(v_set.length > 0) this.lines += 'int ' + v_set.join(', ') + ';\n';
|
4478
|
+
// Clear the INT variable list
|
4479
|
+
v_set.length = 0;
|
4480
|
+
// Add the semi-continuous variables
|
4481
|
+
for(let i in this.is_semi_continuous) if(Number(i)) v_set.push(vbl(i));
|
4482
|
+
if(v_set.length > 0) this.lines += 'sec ' + v_set.join(', ') + ';\n';
|
4483
|
+
// Add the SOS section
|
4484
|
+
if(this.sos_var_indices.length > 0) {
|
4485
|
+
this.lines += 'sos\n';
|
4486
|
+
let sos = 1;
|
4487
|
+
for(let j = 0; j < abl; j++) {
|
4488
|
+
for(let i = 0; i < this.sos_var_indices.length; i++) {
|
4489
|
+
v_set.length = 0;
|
4490
|
+
let vi = this.sos_var_indices[i][0] + j * this.cols;
|
4491
|
+
const n = this.sos_var_indices[i][1];
|
4492
|
+
for(let j = 1; j <= n; j++) {
|
4493
|
+
v_set.push(vbl(vi));
|
4494
|
+
vi++;
|
4495
|
+
}
|
4496
|
+
this.lines += `SOS${sos}: ${v_set.join(',')} <= 2;\n`;
|
4497
|
+
sos++;
|
4397
4498
|
}
|
4398
|
-
this.lines += `SOS${sos}: ${v_set.join(',')} <= 2;\n`;
|
4399
|
-
sos++;
|
4400
4499
|
}
|
4401
4500
|
}
|
4402
4501
|
}
|
@@ -4485,7 +4584,16 @@ class VirtualMachine {
|
|
4485
4584
|
this.lines += 'COLUMNS\n';
|
4486
4585
|
for(c = 1; c <= ncol; c++) {
|
4487
4586
|
const col_lbl = ' X' + c.toString().padStart(this.decimals, '0') + ' ';
|
4488
|
-
|
4587
|
+
// NOTE: if processes have no in- or outgoing links their decision
|
4588
|
+
// variable does not occur in any constraint, and this may cause
|
4589
|
+
// problems for solvers that cannot handle columns having a blank
|
4590
|
+
// row name (e.g., CPLEX). To prevent errors, these columns are
|
4591
|
+
// given coefficient 0 in the OBJ row
|
4592
|
+
if(cols[c].length) {
|
4593
|
+
this.lines += col_lbl + cols[c].join('\n' + col_lbl) + '\n';
|
4594
|
+
} else {
|
4595
|
+
this.lines += col_lbl + ' OBJ 0\n';
|
4596
|
+
}
|
4489
4597
|
}
|
4490
4598
|
// Free up memory
|
4491
4599
|
cols.length = 0;
|
@@ -4537,7 +4645,7 @@ class VirtualMachine {
|
|
4537
4645
|
}
|
4538
4646
|
}
|
4539
4647
|
bnd = ' BND X' + p.toString().padStart(this.decimals, '0') + ' ';
|
4540
|
-
/* MPS format bound types:
|
4648
|
+
/* Gurobi uses these MPS format bound types:
|
4541
4649
|
LO lower bound
|
4542
4650
|
UP upper bound
|
4543
4651
|
FX variable is fixed at the specified value
|
@@ -4617,12 +4725,14 @@ class VirtualMachine {
|
|
4617
4725
|
}
|
4618
4726
|
|
4619
4727
|
get noSolutionStatus() {
|
4620
|
-
// Returns set of status codes that
|
4728
|
+
// Returns set of status codes that indicate that solver did not return
|
4621
4729
|
// a solution (so look-ahead should be conserved)
|
4622
4730
|
if(this.solver_name === 'lp_solve') {
|
4623
4731
|
return [-2, 2, 6];
|
4624
4732
|
} else if(this.solver_name === 'gurobi') {
|
4625
4733
|
return [1, 3, 4, 6, 11, 12, 14];
|
4734
|
+
} else if(this.solver_name === 'scip') {
|
4735
|
+
return [];
|
4626
4736
|
} else {
|
4627
4737
|
return [];
|
4628
4738
|
}
|
@@ -4780,10 +4890,15 @@ Solver status = ${json.status}`);
|
|
4780
4890
|
this.show_progress = false;
|
4781
4891
|
}
|
4782
4892
|
// Generate lines of code in format that should be accepted by solver
|
4783
|
-
if(this.solver_name === '
|
4784
|
-
this.writeLpSolveFormat();
|
4785
|
-
} else if(this.solver_name === 'gurobi') {
|
4893
|
+
if(this.solver_name === 'gurobi') {
|
4786
4894
|
this.writeMPSFormat();
|
4895
|
+
} else if(this.solver_name === 'scip' || this.solver_name === 'cplex') {
|
4896
|
+
// NOTE: the CPLEX LP format that is also used by SCIP differs from
|
4897
|
+
// the LP_solve format that was used by the first versions of Linny-R;
|
4898
|
+
// TRUE indicates "CPLEX format"
|
4899
|
+
this.writeLpFormat(true);
|
4900
|
+
} else if(this.solver_name === 'lp_solve') {
|
4901
|
+
this.writeLpFormat(false);
|
4787
4902
|
} else {
|
4788
4903
|
this.numeric_issue = 'solver name: ' + this.solver_name;
|
4789
4904
|
}
|
@@ -4856,11 +4971,12 @@ Solver status = ${json.status}`);
|
|
4856
4971
|
return;
|
4857
4972
|
} else {
|
4858
4973
|
// Wait no longer, but warn user that data may be incomplete
|
4859
|
-
dsl = [];
|
4974
|
+
const dsl = [];
|
4860
4975
|
for(let i = 0; i < MODEL.loading_datasets.length; i++) {
|
4861
4976
|
dsl.push(MODEL.loading_datasets[i].displayName);
|
4862
4977
|
}
|
4863
|
-
UI.warn(
|
4978
|
+
UI.warn('Loading of ' + pluralS(dsl.length, 'dataset') + ' (' +
|
4979
|
+
dsl.join(', ') + ') takes too long');
|
4864
4980
|
}
|
4865
4981
|
}
|
4866
4982
|
if(MONITOR.connectToServer()) {
|