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.
@@ -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: ['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 = (this.solver_name === 'lp_solve' ?
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 = (this.solver_name === 'lp_solve' ?
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
- 'minus', ncv, 'singular variables is not a multiple of # columns',
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
- writeLpSolveFormat() {
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
- const abl = this.actualBlockLength;
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
- this.lines = '/* Objective function */\nmax:\n';
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
- if(c === -1) {
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 + ';\n';
4334
+ this.lines += line + EOL;
4307
4335
  line = '';
4308
4336
  // Add the row constraints
4309
- this.lines += '\n/* Constraints */\n';
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
- if(c === -1) {
4320
- line += ' -C' + p;
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 + ';\n';
4360
+ this.constraint_symbols[this.constraint_types[r]] + ' ' + c + EOL;
4337
4361
  line = '';
4338
4362
  }
4339
4363
  // Add the variable bounds
4340
- this.lines += '\n/* Variable bounds */\n';
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 = 'C' + p + ' = ' + lb;
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
- if(lb !== null && lb !== 0) line = lb + ' <= ' + line;
4367
- if(ub !== null) line += ' <= ' + ub;
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 + ';\n';
4409
+ if(line) this.lines += line + EOL;
4370
4410
  }
4371
4411
  // Add the special variable types
4372
- const v_set = [];
4373
- // NOTE: for binary variables, add the constraint <= 1
4374
- for(let i in this.is_binary) if(Number(i)) {
4375
- this.lines += 'C' + i + ' <= 1;\n';
4376
- v_set.push('C' + i);
4377
- }
4378
- for(let i in this.is_integer) if(Number(i)) v_set.push('C' + i);
4379
- if(v_set.length > 0) this.lines += 'int ' + v_set.join(', ') + ';\n';
4380
- // Clear the INT variable list
4381
- v_set.length = 0;
4382
- // Add the semi-continuous variables
4383
- for(let i in this.is_semi_continuous) if(Number(i)) v_set.push('C' + i);
4384
- if(v_set.length > 0) this.lines += 'sec ' + v_set.join(', ') + ';\n';
4385
- // Add the SOS section
4386
- if(this.sos_var_indices.length > 0) {
4387
- this.lines += 'sos\n';
4388
- let sos = 1;
4389
- for(let j = 0; j < abl; j++) {
4390
- for(let i = 0; i < this.sos_var_indices.length; i++) {
4391
- v_set.length = 0;
4392
- let vi = this.sos_var_indices[i][0] + j * this.cols;
4393
- const n = this.sos_var_indices[i][1];
4394
- for(let j = 1; j <= n; j++) {
4395
- v_set.push('C' + vi);
4396
- vi++;
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
- this.lines += col_lbl + cols[c].join('\n' + col_lbl) + '\n';
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 sindicate that solver did not return
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 === 'lp_solve') {
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
  }