linny-r 2.1.2 → 2.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -1397,18 +1397,22 @@ td.a-weight {
1397
1397
  }
1398
1398
 
1399
1399
  td.import,
1400
- td.export {
1400
+ td.export,
1401
+ div.import,
1402
+ div.export {
1401
1403
  font-weight: 600;
1402
1404
  text-decoration-line: underline;
1403
1405
  text-decoration-style: dotted;
1404
1406
  }
1405
1407
 
1406
- td.import {
1408
+ td.import,
1409
+ div.import {
1407
1410
  color: #b00000;
1408
1411
  }
1409
1412
 
1410
- td.export {
1411
- color: #00000b;
1413
+ td.export,
1414
+ div.export {
1415
+ color: #0000b0;
1412
1416
  }
1413
1417
 
1414
1418
  /* styles for the SERVER dialog */
@@ -175,11 +175,11 @@ class GUIDatasetManager extends DatasetManager {
175
175
  if(r) {
176
176
  const e = new Event('click');
177
177
  if(this.focal_table === this.dataset_table) {
178
- // Emulate Alt-click in the table to open the time series dialog
178
+ // Emulate Alt-click in the table to open the time series dialog.
179
179
  e.altKey = true;
180
180
  r.dispatchEvent(e);
181
181
  } else if(this.focal_table === this.modifier_table) {
182
- // Emulate a double-click on the second cell to edit the expression
182
+ // Emulate a double-click on the second cell to edit the expression.
183
183
  this.last_time_clicked = Date.now();
184
184
  r.cells[1].dispatchEvent(e);
185
185
  }
@@ -576,7 +576,9 @@ class GUIDatasetManager extends DatasetManager {
576
576
  this.editData();
577
577
  return;
578
578
  }
579
- this.updateDialog();
579
+ // NOTE: Updating entire dialog may be very time-consuming
580
+ // when model contains numerous prefixed datasets.
581
+ this.updatePanes();
580
582
  }
581
583
 
582
584
  selectModifier(event, id, x=true) {
@@ -1196,7 +1198,7 @@ class GUIDatasetManager extends DatasetManager {
1196
1198
  }
1197
1199
  }
1198
1200
  for(let i = 0; i < dsn.length; i++) {
1199
- const n = unquoteCSV(dsn[i].trim());
1201
+ const n = UI.cleanName(unquoteCSV(dsn[i].trim()));
1200
1202
  if(!UI.validName(n)) {
1201
1203
  UI.warn(`Invalid dataset name "${n}" in column ${i}`);
1202
1204
  return false;
@@ -1265,8 +1267,10 @@ class GUIDatasetManager extends DatasetManager {
1265
1267
  added++;
1266
1268
  }
1267
1269
  ds.computeStatistics();
1268
- } else {
1270
+ } else if(ds && ds.type) {
1269
1271
  UI.warn(`Name conflict: ${ds.type} "${ds.displayName}" already exists`);
1272
+ } else {
1273
+ UI.alert(`No dataset "${n}" added`);
1270
1274
  }
1271
1275
  }
1272
1276
  // Notify modeler of changes (if any).
@@ -3395,13 +3395,22 @@ class LinnyRModel {
3395
3395
  }
3396
3396
  }
3397
3397
  sl.push('_____Datasets');
3398
- for(obj in this.datasets) {
3399
- if(this.datasets.hasOwnProperty(obj) && !obj.startsWith(UI.BLACK_BOX) &&
3400
- obj !== UI.EQUATIONS_DATASET_ID) {
3401
- sl.push(this.datasets[obj].displayName, this.datasets[obj].comments);
3398
+ for(const k of Object.keys(this.datasets)) {
3399
+ if(!k.startsWith(UI.BLACK_BOX) && k !== UI.EQUATIONS_DATASET_ID) {
3400
+ const ds = this.datasets[k];
3401
+ sl.push(ds.displayName, ds.comments);
3402
+ const keys = Object.keys(ds.modifiers).sort(compareSelectors);
3403
+ if(keys.length) {
3404
+ for(const k of keys) {
3405
+ const m = ds.modifiers[k];
3406
+ // NOTE: The trailing arrow signals the Documentation manager that
3407
+ // this (name, documentation) pair is a dataset modifier.
3408
+ sl.push(`${m.selector}&nbsp;&rarr;` , m.expression.text);
3409
+ }
3410
+ }
3402
3411
  }
3403
3412
  }
3404
- const keys = Object.keys(this.equations_dataset.modifiers);
3413
+ const keys = Object.keys(this.equations_dataset.modifiers).sort();
3405
3414
  sl.push('_____Equations');
3406
3415
  for(const k of keys) {
3407
3416
  const m = this.equations_dataset.modifiers[k];
@@ -3486,6 +3495,7 @@ class LinnyRModel {
3486
3495
  this.cleanVector(p.level, p.initial_level.result(1));
3487
3496
  this.cleanVector(p.cost_price, VM.UNDEFINED);
3488
3497
  this.cleanVector(p.cash_flow, 0, 0);
3498
+ this.cleanVector(p.marginal_cash_flow, 0, 0);
3489
3499
  this.cleanVector(p.cash_in, 0, 0);
3490
3500
  this.cleanVector(p.cash_out, 0, 0);
3491
3501
  // NOTE: `start_ups` is a list of time steps when start-up occurred.
@@ -4407,7 +4417,17 @@ class IOBinding {
4407
4417
  this.actual_name = '';
4408
4418
  }
4409
4419
  }
4410
-
4420
+
4421
+ get copy() {
4422
+ // Return a copy of this binding.
4423
+ const copy = new IOBinding(this.io_type, this.entity_type,
4424
+ this.is_data, this.name_in_module);
4425
+ copy.id = this.id;
4426
+ copy.actual_name = this.actual_name;
4427
+ copy.actual_id = this.actual_id;
4428
+ return copy;
4429
+ }
4430
+
4411
4431
  bind(an) {
4412
4432
  // Establish a binding with actual name `an` if this entity is known to be
4413
4433
  // of the correct type (and for products also a matching data property)
@@ -4561,7 +4581,7 @@ class IOContext {
4561
4581
  // Return a deep copy of the bindings object.
4562
4582
  const copy = {};
4563
4583
  for(const k of Object.keys(this.bindings)) {
4564
- copy[k] = Object.assign({}, this.bindings[k]);
4584
+ copy[k] = this.bindings[k].copy;
4565
4585
  }
4566
4586
  return copy;
4567
4587
  }
@@ -5458,7 +5478,7 @@ class Note extends ObjectWithXYWH {
5458
5478
  from_unit = '1';
5459
5479
  }
5460
5480
  }
5461
- } else if(attr === 'CI' || attr === 'CO' || attr === 'CF') {
5481
+ } else if(['CI', 'CO', 'CF', 'MCF'].indexOf(attr) >= 0) {
5462
5482
  from_unit = MODEL.currency_unit;
5463
5483
  }
5464
5484
  // If still no value, `attr` may be an expression-type attribute.
@@ -5672,6 +5692,20 @@ class NodeBox extends ObjectWithXYWH {
5672
5692
  return this.name;
5673
5693
  }
5674
5694
 
5695
+ get bindingsAsString() {
5696
+ if(!this.module) return '';
5697
+ const bk = Object.keys(this.module.bindings);
5698
+ if(bk.length) {
5699
+ const list = [pluralS(bk.length, 'binding') + ':'];
5700
+ for(const k of bk) {
5701
+ const b = this.module.bindings[k];
5702
+ list.push(`&#8227;&nbsp;${b.name_in_module}&nbsp;&rarrlp;&nbsp;${b.actual_name}`);
5703
+ }
5704
+ return list.join('\n');
5705
+ }
5706
+ return '(no bindings)';
5707
+ }
5708
+
5675
5709
  get infoLineName() {
5676
5710
  // Return display name plus VM variable indices when debugging.
5677
5711
  let n = this.displayName;
@@ -5685,7 +5719,8 @@ class NodeBox extends ObjectWithXYWH {
5685
5719
  dl.push(pluralS(this.all_processes.length, 'process').toLowerCase());
5686
5720
  dl.push(pluralS(this.all_products.length, 'product').toLowerCase());
5687
5721
  }
5688
- if(this.module) dl.push(`included from <span class="mod-name">${this.module.name}</span>`);
5722
+ if(this.module) dl.push('included from <span class="mod-name" title="' +
5723
+ `${this.bindingsAsString}">${this.module.name}</span>`);
5689
5724
  if(dl.length) n += `<span class="node-details">${dl.join(', ')}</span>`;
5690
5725
  }
5691
5726
  if(!MODEL.solved) return n;
@@ -7842,8 +7877,9 @@ class Process extends Node {
7842
7877
  this.power_grid = null;
7843
7878
  this.length_in_km = 0;
7844
7879
  this.reactance = 0;
7845
- // Processes have 3 more result attributes: CP, CF, CI and CO
7880
+ // Processes have 4 more result attributes: CF, MCF, CI and CO
7846
7881
  this.cash_flow = [];
7882
+ this.marginal_cash_flow = [];
7847
7883
  this.cash_in = [];
7848
7884
  this.cash_out = [];
7849
7885
  // Production level changing from 0 to positive counts as "start up",
@@ -7898,6 +7934,7 @@ class Process extends Node {
7898
7934
  const t = MODEL.t;
7899
7935
  a.L = this.level[t];
7900
7936
  a.CF = this.cash_flow[t];
7937
+ a.MCF = this.marginal_cash_flow[t];
7901
7938
  a.CI = this.cash_in[t];
7902
7939
  a.CO = this.cash_out[t];
7903
7940
  if(MODEL.infer_cost_prices) a.CP = this.cost_price[t];
@@ -8034,6 +8071,7 @@ class Process extends Node {
8034
8071
  // For processes, these are all vectors.
8035
8072
  if(a === 'L') return this.level;
8036
8073
  if(a === 'CF') return this.cash_flow;
8074
+ if(a === 'MCF') return this.marginal_cash_flow;
8037
8075
  if(a === 'CI') return this.cash_in;
8038
8076
  if(a === 'CO') return this.cash_out;
8039
8077
  if(a === 'CP') return this.cost_price;
@@ -2359,6 +2359,7 @@ class VirtualMachine {
2359
2359
  'CP': 'cost price',
2360
2360
  'HCP': 'highest cost price',
2361
2361
  'CF': 'cash flow',
2362
+ 'MCF': 'marginal cash flow',
2362
2363
  'CI': 'cash in',
2363
2364
  'CO': 'cash out',
2364
2365
  'W': 'weight',
@@ -2371,7 +2372,7 @@ class VirtualMachine {
2371
2372
  // NOTE: Defaults are level (L), link flow (F), cluster cash flow (CF),
2372
2373
  // actor cash flow (CF); dataset value (no attribute).
2373
2374
  // NOTE: Exogenous properties first, then the computed properties.
2374
- this.process_attr = ['LB', 'UB', 'IL', 'LCF', 'L', 'CI', 'CO', 'CF', 'CP'];
2375
+ this.process_attr = ['LB', 'UB', 'IL', 'LCF', 'L', 'CI', 'CO', 'CF', 'MCF', 'CP'];
2375
2376
  this.product_attr = ['LB', 'UB', 'IL', 'P', 'L', 'CP', 'HCP'];
2376
2377
  this.cluster_attr = ['CI', 'CO', 'CF'];
2377
2378
  this.link_attr = ['R', 'D', 'SOC', 'F'];
@@ -2397,7 +2398,7 @@ class VirtualMachine {
2397
2398
  for(const a of ac) this.entity_attribute_names[el].push(a);
2398
2399
  }
2399
2400
  // Level-based attributes are computed only AFTER optimization.
2400
- this.level_based_attr = ['L', 'CP', 'HCP', 'CF', 'CI', 'CO', 'F', 'A'];
2401
+ this.level_based_attr = ['L', 'CP', 'HCP', 'CF', 'MCF', 'CI', 'CO', 'F', 'A'];
2401
2402
  this.object_types = ['Process', 'Product', 'Cluster', 'Link', 'Constraint',
2402
2403
  'Actor', 'Dataset', 'Equation'];
2403
2404
  this.type_attributes = [this.process_attr, this.product_attr,
@@ -5139,13 +5140,18 @@ class VirtualMachine {
5139
5140
  // Cash flows of process p are now known.
5140
5141
  p.cash_in[b] = ci;
5141
5142
  p.cash_out[b] = co;
5142
- p.cash_flow[b] = ci - co;
5143
+ const
5144
+ cf = ci - co,
5145
+ apl = Math.abs(p.level[b]);
5146
+ p.cash_flow[b] = cf;
5147
+ // Marginal cash flow is considered 0 when process level = 0.
5148
+ p.marginal_cash_flow[b] = (apl < VM.NEAR_ZERO ? 0 : cf / apl);
5143
5149
  // Also add these flows to all parent clusters of the process.
5144
5150
  let c = p.cluster;
5145
5151
  while(c) {
5146
5152
  c.cash_in[b] += ci;
5147
5153
  c.cash_out[b] += co;
5148
- c.cash_flow[b] += ci - co;
5154
+ c.cash_flow[b] += cf;
5149
5155
  c = c.cluster;
5150
5156
  }
5151
5157
  }
@@ -7388,14 +7394,13 @@ function VMI_push_statistic(x, args) {
7388
7394
  // If so, trim the 'NZ'
7389
7395
  if(nz) stat = stat.slice(0, -2);
7390
7396
  // Now t1 ... t2 is the range of time steps to iterate over for each variable
7391
- let obj,
7392
- vlist = [];
7397
+ const vlist = [];
7393
7398
  for(let t = t1; t <= t2; t++) {
7394
7399
  // Get the list of values.
7395
7400
  // NOTE: Variables may be vectors or expressions.
7396
7401
  for(const obj of list) {
7397
7402
  if(Array.isArray(obj)) {
7398
- // Object is a vector
7403
+ // Object is a vector.
7399
7404
  if(t < obj.length) {
7400
7405
  v = obj[t];
7401
7406
  } else {
@@ -7701,7 +7706,7 @@ function VMI_sub(x) {
7701
7706
  } else if(d[0] === VM.MINUS_INFINITY || d[1] === VM.PLUS_INFINITY) {
7702
7707
  x.retop(VM.MINUS_INFINITY);
7703
7708
  } else {
7704
- x.retop(d[0] + d[1]);
7709
+ x.retop(d[0] - d[1]);
7705
7710
  }
7706
7711
  }
7707
7712
  }