linny-r 3.0.7 → 3.0.9

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": "3.0.7",
3
+ "version": "3.0.9",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -8,7 +8,7 @@
8
8
  "test": "echo \"Error: no test specified\" && exit 1"
9
9
  },
10
10
  "dependencies": {
11
- "@xmldom/xmldom": ">=0.8.2"
11
+ "@xmldom/xmldom": ">=0.9.10"
12
12
  },
13
13
  "repository": {
14
14
  "type": "git",
@@ -1480,8 +1480,9 @@ class PowerGridManager {
1480
1480
  // to add constraints that enforce Kirchhoff's voltage law.
1481
1481
  this.nodes = {};
1482
1482
  this.edges = {};
1483
- this.spanning_tree = [];
1484
1483
  this.tree_incidence = {};
1484
+ this.node_sets = [];
1485
+ this.spanning_edges = [];
1485
1486
  this.cycle_edges = [];
1486
1487
  this.cycle_basis = [];
1487
1488
  this.min_length = 0;
@@ -1599,33 +1600,68 @@ class PowerGridManager {
1599
1600
  grid.join(', ').toLowerCase());
1600
1601
  }
1601
1602
 
1602
- inferSpanningTree() {
1603
+ nodeSetIndex(n) {
1604
+ // Return index of node set containing `n`, or -1 if no such set exists.
1605
+ for(let i = 0; i < this.node_sets.length; i++) {
1606
+ if(this.node_sets[i][n]) return i;
1607
+ }
1608
+ return -1;
1609
+ }
1610
+
1611
+ inferSpanningTrees() {
1603
1612
  // Use Kruksal's algorithm to build spanning tree.
1604
1613
  // NOTE: Tree needs not be minimal, so edges are not sorted.
1605
- this.spanning_tree.length = 0;
1614
+ this.spanning_edges.length = 0;
1606
1615
  this.cycle_edges.length = 0;
1607
1616
  this.tree_incidence = {};
1608
- const node_set = {};
1617
+ this.node_sets = [];
1609
1618
  for(let k in this.edges) if(this.edges.hasOwnProperty(k)) {
1610
1619
  const
1611
1620
  edge = this.edges[k],
1612
1621
  efn = edge.from_node,
1613
1622
  etn = edge.to_node,
1614
1623
  kvl = edge.process.grid.kirchhoff,
1615
- fn_in_tree = node_set.hasOwnProperty(efn),
1616
- tn_in_tree = node_set.hasOwnProperty(etn);
1624
+ fn_set = this.nodeSetIndex(efn),
1625
+ tn_set = this.nodeSetIndex(etn);
1617
1626
  // Only add edges of grids for which Kirchhoff's voltage law
1618
1627
  // has to be enforced.
1619
1628
  if(kvl) {
1620
- if(fn_in_tree && tn_in_tree) {
1621
- // Edge forms a cycle, so add it to the cycle edge list.
1622
- this.cycle_edges.push(edge);
1629
+ if(fn_set === tn_set) {
1630
+ if(fn_set < 0) {
1631
+ // Edge disconnected from tree => add nodes as a new set.
1632
+ const new_set = {};
1633
+ new_set[efn] = true;
1634
+ new_set[etn] = true;
1635
+ this.node_sets.push(new_set);
1636
+ this.spanning_edges.push(edge);
1637
+ } else {
1638
+ // Edge forms a cycle, so add it to the cycle edge list.
1639
+ this.cycle_edges.push(edge);
1640
+ }
1623
1641
  } else {
1624
- // Edge is not incident with *two* nodes already in the tree, so
1625
- // add it to the tree.
1626
- this.spanning_tree.push(edge);
1627
- node_set[efn] = true;
1628
- node_set[etn] = true;
1642
+ // Edge is not incident with *two* nodes in same node set, so
1643
+ // add it to the tree...
1644
+ this.spanning_edges.push(edge);
1645
+ if(fn_set < 0) {
1646
+ // Add FROM node to the TO node set.
1647
+ this.node_sets[tn_set][efn] = true;
1648
+ } else if(tn_set < 0) {
1649
+ // Add TO node to the FROM node set.
1650
+ this.node_sets[fn_set][etn] = true;
1651
+ } else {
1652
+ // If both nodes are "known" but in diferent sets, then merge
1653
+ // the sets into the one having the lowest index...
1654
+ let
1655
+ target = fn_set,
1656
+ other = tn_set;
1657
+ if(fn_set > tn_set) {
1658
+ target = tn_set;
1659
+ other = fn_set;
1660
+ }
1661
+ Object.assign(this.node_sets[target], this.node_sets[other]);
1662
+ // ... and delete the other set.
1663
+ this.node_sets.splice(other, 1);
1664
+ }
1629
1665
  }
1630
1666
  const ti = this.tree_incidence;
1631
1667
  // Always record that both its nodes are incident with it.
@@ -1643,19 +1679,27 @@ class PowerGridManager {
1643
1679
  }
1644
1680
  }
1645
1681
 
1646
- pathInSpanningTree(fn, tn, path) {
1682
+ pathInSpanningTree(fn, tn, path, eop) {
1647
1683
  // Recursively constructs `path` as the list of edges forming the path
1648
1684
  // from `fn` to `tn` in the spanning tree of this grid.
1649
1685
  // If edge connects path with TO node, `path` is complete.
1650
1686
  if(fn === tn) return true;
1687
+ // Consider all edges that connect with the FROM node.
1651
1688
  for(const e of this.tree_incidence[fn]) {
1652
1689
  // Ignore edges already in the path.
1653
- if(path.indexOf(e) < 0) {
1690
+ if(eop.indexOf(e) < 0) {
1654
1691
  // NOTE: Edges are directed, but should not be considered as such.
1655
- const nn = (e.from_node === fn ? e.to_node : e.from_node);
1656
- path.push(e);
1657
- if(this.pathInSpanningTree(nn, tn, path)) return true;
1692
+ let nn = e.to_node,
1693
+ sign = 1;
1694
+ if(e.from_node !== fn) {
1695
+ nn = e.from_node;
1696
+ sign = -1;
1697
+ }
1698
+ path.push({process: e.process, orientation: sign});
1699
+ eop.push(e);
1700
+ if(this.pathInSpanningTree(nn, tn, path, eop)) return true;
1658
1701
  path.pop();
1702
+ eop.pop();
1659
1703
  }
1660
1704
  }
1661
1705
  return false;
@@ -1666,28 +1710,19 @@ class PowerGridManager {
1666
1710
  this.cycle_basis.length = 0;
1667
1711
  if(!(MODEL.with_power_flow && MODEL.powerGridsWithKVL.length)) return;
1668
1712
  this.inferNodesAndEdges();
1669
- this.inferSpanningTree();
1713
+ this.inferSpanningTrees();
1670
1714
  for(const edge of this.cycle_edges) {
1671
- const path = [];
1672
- if(this.pathInSpanningTree(edge.from_node, edge.to_node, path)) {
1673
- // Add flags that indicate whether the edge on the path is reversed.
1674
- // The closing edge determines the orientation.
1675
- const cycle = [{process: edge.process, orientation: 1}];
1676
- let node = edge.to_node;
1677
- for(let i = path.length - 1; i >= 0; i--) {
1678
- const
1679
- pe = path[i],
1680
- ce = {process: pe.process};
1681
- if(pe.from_node === node) {
1682
- ce.orientation = 1;
1683
- node = pe.to_node;
1684
- } else {
1685
- ce.orientation = -1;
1686
- node = pe.from_node;
1687
- }
1688
- cycle.push(ce);
1689
- }
1690
- this.cycle_basis.push(cycle);
1715
+ const
1716
+ path = [],
1717
+ // Path cannot include the closing edge.
1718
+ eop = [edge];
1719
+ if(this.pathInSpanningTree(edge.to_node, edge.from_node, path, eop)) {
1720
+ // The closing edge has orientation +1.
1721
+ path.push({process: edge.process, orientation: 1});
1722
+ this.cycle_basis.push(path);
1723
+ } else {
1724
+ // Log that edge did not close a cycle.
1725
+ console.log('ANOMALY: No path for edge', edge);
1691
1726
  }
1692
1727
  }
1693
1728
  }
@@ -314,10 +314,10 @@ class GUIFileManager {
314
314
  const mi = this.model_index;
315
315
  if(mi >= 0) {
316
316
  path += this.separator;
317
- if(mi < sd.sdcount) {
317
+ if(mi < this.sd_count) {
318
318
  path += sd.subdirs[mi].name;
319
319
  } else {
320
- path += sd.models[mi - sd.sdcount].name + '.lnr';
320
+ path += sd.models[mi - this.sd_count].name + '.lnr';
321
321
  this.new_window_btn.title =
322
322
  'Open selected model in new Linny-R tab in browser';
323
323
  }
@@ -1260,7 +1260,7 @@ class GUIFileManager {
1260
1260
  const
1261
1261
  mi = this.model_index,
1262
1262
  sd = this.selected_dir;
1263
- if(mi >= sd.sdcount) {
1263
+ if(mi >= this.sd_count) {
1264
1264
  const mdl = sd.models[mi - this.sd_count];
1265
1265
  if(mdl) {
1266
1266
  window.localStorage.setItem('linny-r-model-file',
@@ -167,6 +167,8 @@ class Finder {
167
167
  enl = [],
168
168
  et = this.entity_types,
169
169
  fp = this.filter_pattern && this.filter_pattern.length > 0;
170
+ // Position "orphan" products (if any) in focal cluster and notify modeler.
171
+ MODEL.revealOrphans();
170
172
  let imgs = '';
171
173
  this.entities.length = 0;
172
174
  this.filtered_types.length = 0;
@@ -293,4 +293,4 @@ class GUIPowerGridManager extends PowerGridManager {
293
293
  return flows.join('\n');
294
294
  }
295
295
 
296
- } // END of class GUIPowerGridManager
296
+ } // END of class GUIPowerGridManager
@@ -1102,6 +1102,8 @@ class LinnyRModel {
1102
1102
  canLink(from, to) {
1103
1103
  // Return TRUE iff FROM-node can feature a "straight" link (i.e., a
1104
1104
  // product flow) to TO-node.
1105
+ // FROM and TO *must* be different nodes.
1106
+ if(from === to) return false;
1105
1107
  if(from.type === to.type) {
1106
1108
  // No "straight" link between nodes of same type (see canConstrain
1107
1109
  // for "curved" links) UNLESS TO-node is a data product.
@@ -1135,6 +1137,37 @@ class LinnyRModel {
1135
1137
  return this.end_period - this.start_period + 1 + this.look_ahead;
1136
1138
  }
1137
1139
 
1140
+ revealOrphans() {
1141
+ // Find all products that are *not* positioned in some cluster,
1142
+ // position them in the focal cluster, and notify the user.
1143
+ const orphans = [];
1144
+ for(const k in this.products) if(this.products.hasOwnProperty(k)) {
1145
+ const p = this.products[k];
1146
+ if(p.isOrphan) orphans.push(p);
1147
+ }
1148
+ if(orphans.length) {
1149
+ let x = 100,
1150
+ y = 70,
1151
+ n = 0;
1152
+ const fc = this.focal_cluster;
1153
+ for(const p of orphans) {
1154
+ const pp = fc.addProductPosition(p, x, y);
1155
+ if(pp) {
1156
+ x += 90;
1157
+ y += 45;
1158
+ n++;
1159
+ }
1160
+ }
1161
+ // Prepare focal cluster for redrawing.
1162
+ fc.clearAllProcesses();
1163
+ // Select the added product positions and redraw.
1164
+ this.selectList(orphans);
1165
+ UI.drawDiagram(this);
1166
+ // Finally, notify the modeler.
1167
+ UI.warn(pluralS(n, '"orphaned" product') + ' added to the focal cluster');
1168
+ }
1169
+ }
1170
+
1138
1171
  processSelectorList(sl) {
1139
1172
  // Check whether selector list `sl` constitutes a new dimension.
1140
1173
  // Ignore lists of fewer than 2 "plain" selectors.
@@ -1675,6 +1708,12 @@ class LinnyRModel {
1675
1708
  if(node) l.initFromXML(node);
1676
1709
  return l;
1677
1710
  }
1711
+ // FROM and TO nodes must be different. The UI should not permit
1712
+ // drawing such nodes, but check nonetheless.
1713
+ if(from === to) {
1714
+ UI.warn(`${from.type} "${from.displayName}" cannot be linked to itself`);
1715
+ return null;
1716
+ }
1678
1717
  l = new Link(from, to);
1679
1718
  if(node) l.initFromXML(node);
1680
1719
  this.links[l.identifier] = l;
@@ -1705,6 +1744,12 @@ class LinnyRModel {
1705
1744
  if(node) c.initFromXML(node);
1706
1745
  return c;
1707
1746
  }
1747
+ // FROM and TO nodes must be different. The UI should not permit
1748
+ // drawing such nodes, but check nonetheless.
1749
+ if(from === to) {
1750
+ UI.warn(`${from.type} "${from.displayName}" cannot constrain itself`);
1751
+ return null;
1752
+ }
1708
1753
  c = new Constraint(from, to);
1709
1754
  if(node) c.initFromXML(node);
1710
1755
  // New constraint => prepare for redraw.
@@ -2829,15 +2874,12 @@ class LinnyRModel {
2829
2874
  updateChartVariables(e) {
2830
2875
  // Ensure that all chart variable names based on entity `e` will be
2831
2876
  // displayed correctly the next time they are drawn.
2832
- console.log('HERE e', e.displayName);
2833
2877
  const sc = this.charts[CHART_MANAGER.chart_index];
2834
2878
  let ucm = false;
2835
2879
  for(const c of this.charts) {
2836
2880
  for(const v of c.variables) {
2837
- console.log('HERE v', v.displayName);
2838
2881
  if(v.object === e) {
2839
2882
  v.display_name = '';
2840
- console.log('HERE v new', v.displayName);
2841
2883
  ucm = ucm || c === sc;
2842
2884
  }
2843
2885
  }
@@ -4083,8 +4125,8 @@ console.log('HERE e', e.displayName);
4083
4125
  }
4084
4126
  // All potential inflows known => CP proxy can be calculated.
4085
4127
  if(count === p.inputs.length) {
4086
- // Also consider output links to products having price < 0.
4087
- for(const l of p.outputs) {
4128
+ // Also consider regular output links to products having price < 0.
4129
+ for(const l of p.outputs) if(l.multiplier === VM.LM_LEVEL) {
4088
4130
  // NOTE: Here, a delay may apply.
4089
4131
  const
4090
4132
  d = l.actualDelay(t),
@@ -8826,6 +8868,11 @@ class Product extends Node {
8826
8868
  return ppc;
8827
8869
  }
8828
8870
 
8871
+ get isOrphan() {
8872
+ // Return TRUE if this product has no position in any cluster.
8873
+ return this.productPositionClusters.length <= 0;
8874
+ }
8875
+
8829
8876
  get toBeBlackBoxed() {
8830
8877
  // Return TRUE if this product occurs only in "black box" clusters.
8831
8878
  for(const c of this.productPositionClusters) if(!c.blackBoxed) return false;
@@ -173,9 +173,6 @@ class Expression {
173
173
  this.wildcard_vectors = {};
174
174
  this.wildcard_vector_index = false;
175
175
  this.method_object_list.length = 0;
176
- if(!isEmpty(this.cache)) {
177
- console.log('HERE Clearing cache', this.text, '\n', Object.keys(this.cache));
178
- }
179
176
  this.cache = {};
180
177
  this.compile(); // if(!this.compiled) REMOVED to ensure correct isStatic!!
181
178
  // Static expressions only need a vector with one element (having index 0)
@@ -1223,26 +1220,24 @@ class ExpressionParser {
1223
1220
  if(!anchor1) anchor1 = 't';
1224
1221
  if(!anchor2) anchor2 = 't';
1225
1222
  }
1226
- // First handle this special case: no name or attribute. This is valid
1227
- // only for dataset modifier expressions (and hence also equations).
1228
- // Variables like [@t-1] are interpreted as a self-reference. This is
1229
- // meaningful when a *negative* offset is specified to denote "use the
1230
- // value of this expression for some earlier time step".
1231
- // NOTES:
1232
- // (1) This makes the expression dynamic.
1233
- // (2) It does not apply to array-type datasets, as these have no
1234
- // time dimension.
1235
- if(!name && !attr && this.dataset && !this.dataset.array) {
1223
+ // First handle this special case: an equation self-reference.
1224
+ // Variables like [@t-1] are interpreted as an implicit self-reference.
1225
+ // Self-references are meaningful when a *negative* offset is specified
1226
+ // to denote "use the value of this expression for some earlier time step".
1227
+ // NOTE: This makes the expression dynamic.
1228
+ if(!attr && this.dataset === MODEL.equations_dataset &&
1229
+ (!name || UI.nameToID(name) === UI.nameToID(this.attribute))) {
1236
1230
  this.is_static = false;
1237
1231
  this.log('dynamic because of self-reference');
1238
- if(('cips'.indexOf(anchor1) >= 0 || anchor1 === 't' && offset1 < 0) &&
1239
- ('cips'.indexOf(anchor2) >= 0 ||anchor2 === 't' && offset2 < 0)) {
1232
+ if(('cfps'.indexOf(anchor1) >= 0 || anchor1 === 't' && offset1 < 0) &&
1233
+ ('cfps'.indexOf(anchor2) >= 0 ||anchor2 === 't' && offset2 < 0)) {
1240
1234
  if(this.TRACE) console.log('TRACE: Variable is a self-reference.');
1241
1235
  // The `xv` attribute will be recognized by VMI_push_var to denote
1242
1236
  // "use the vector of the expression for which this VMI is code".
1243
- return [{xv: true, dv: this.dataset.defaultValue},
1237
+ return [{xv: true, dv: VM.UNDEFINED},
1244
1238
  anchor1, offset1, anchor2, offset2];
1245
1239
  }
1240
+ // Fall-through: invalid offset => warning.
1246
1241
  msg = 'Expression can reference only previous values of itself';
1247
1242
  }
1248
1243
  // A leading "!" denotes: pass variable reference instead of its value.
@@ -1531,8 +1526,12 @@ class ExpressionParser {
1531
1526
  let sel = '',
1532
1527
  xtype = '';
1533
1528
  if(obj instanceof DatasetModifier) {
1534
- sel = obj.selector;
1535
- xtype = 'Equation';
1529
+ if(attr) {
1530
+ msg = 'Equations have no attributes';
1531
+ } else {
1532
+ sel = obj.selector;
1533
+ xtype = 'Equation';
1534
+ }
1536
1535
  } else if(obj instanceof Dataset) {
1537
1536
  sel = attr;
1538
1537
  xtype = 'Dataset modifier expression';
@@ -3394,7 +3393,7 @@ class VirtualMachine {
3394
3393
  // If not a sink, UB is set to 0.
3395
3394
  if(notsnk) u = 0;
3396
3395
  }
3397
-
3396
+
3398
3397
  // NOTE: Stock constraints must take into account extra inflows
3399
3398
  // (source) or outflows (sink).
3400
3399
  // Check for special case of equal bounds, as then one EQ constraint
@@ -3434,7 +3433,7 @@ class VirtualMachine {
3434
3433
  this.code.push(
3435
3434
  [l instanceof Expression? VMI_set_var_rhs : VMI_set_const_rhs, l],
3436
3435
  [VMI_add_constraint, VM.GE]
3437
- );
3436
+ );
3438
3437
  }
3439
3438
  // Add upper bound (LE) constraint unless product is a sink node
3440
3439
  if(notsnk) {
@@ -3448,7 +3447,7 @@ class VirtualMachine {
3448
3447
  this.code.push(
3449
3448
  [u instanceof Expression ? VMI_set_var_rhs : VMI_set_const_rhs, u],
3450
3449
  [VMI_add_constraint, VM.LE]
3451
- );
3450
+ );
3452
3451
  }
3453
3452
  }
3454
3453
  }
@@ -5079,7 +5078,7 @@ class VirtualMachine {
5079
5078
  MODEL.calculateCostPrices(b);
5080
5079
  }
5081
5080
  }
5082
-
5081
+ /*
5083
5082
  // THEN: Reset all datasets that are outcomes or serve as "formulas".
5084
5083
  for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
5085
5084
  const ds = MODEL.datasets[k];
@@ -5092,7 +5091,7 @@ class VirtualMachine {
5092
5091
  }
5093
5092
  }
5094
5093
  }
5095
-
5094
+ */
5096
5095
  // THEN: Reset the vectors of all chart variables.
5097
5096
  for(const c of MODEL.charts) c.resetVectors();
5098
5097
 
@@ -6743,7 +6742,12 @@ function VMI_push_var(x, args) {
6743
6742
  x.push(v);
6744
6743
  } else if(xv) {
6745
6744
  // Variable references an earlier value computed for this expression `x`.
6746
- x.push(t >= 0 && t < x.vector.length ? x.vector[t] : obj.dv);
6745
+ // NOTE: When this value has NOT been computed yet, use the specified default.
6746
+ if(t >= 0 && t < x.vector.length && x.vector[t] !== VM.NOT_COMPUTED) {
6747
+ x.push(x.vector[t]);
6748
+ } else {
6749
+ x.push(obj.dv);
6750
+ }
6747
6751
  } else if(obj.hasOwnProperty('c') && obj.hasOwnProperty('u')) {
6748
6752
  // Object holds link lists for cluster balance computation.
6749
6753
  x.push(MODEL.flowBalance(obj, t));
@@ -8375,7 +8379,7 @@ function VMI_set_bounds(args) {
8375
8379
  VM.variables[vi - 1][0],'] t = ', VM.t, ' LB = ', VM.sig4Dig(l),
8376
8380
  ', UB = ', VM.sig4Dig(u), fixed].join(''), l, u, inf_val, 'args:', args);
8377
8381
  console.log(p);
8378
- throw "STOP";
8382
+ if(!DEBUGGING) throw "STOP";
8379
8383
  } else if(u < l) {
8380
8384
  // Check the difference, as this may be negligible.
8381
8385
  if(u - l < VM.SIG_DIF_FROM_ZERO) {
@@ -9075,6 +9079,8 @@ function VMI_update_grid_process_cash_coefficients(p) {
9075
9079
  // VMI_update_cash_coefficient).
9076
9080
  let fn = null,
9077
9081
  tn = null;
9082
+ // NOTE: Grid processes are assumed to connect exactly *two* products by
9083
+ // regular links, so it suffices to find the *first* ingoing...
9078
9084
  for(const l of p.inputs) {
9079
9085
  if(l.multiplier === VM.LM_LEVEL &&
9080
9086
  !MODEL.ignored_entities[l.identifier]) {
@@ -9082,6 +9088,7 @@ function VMI_update_grid_process_cash_coefficients(p) {
9082
9088
  break;
9083
9089
  }
9084
9090
  }
9091
+ // ... and the first outgoing regular link.
9085
9092
  for(const l of p.outputs) {
9086
9093
  if(l.multiplier === VM.LM_LEVEL &&
9087
9094
  !MODEL.ignored_entities[l.identifier]) {
@@ -9092,7 +9099,7 @@ function VMI_update_grid_process_cash_coefficients(p) {
9092
9099
  const
9093
9100
  fp = (fn && fn.price.defined ? fn.price.result(VM.t) : 0),
9094
9101
  tp = (tn && tn.price.defined ? tn.price.result(VM.t) : 0);
9095
- // Only proceed if process links to a product with a non-zero price.
9102
+ // Only proceed if process links to at least one product with a non-zero price.
9096
9103
  if(fp || tp) {
9097
9104
  const
9098
9105
  gpv = VM.gridProcessVarIndices(p, VM.offset),
@@ -9101,30 +9108,30 @@ function VMI_update_grid_process_cash_coefficients(p) {
9101
9108
  // If FROM node has price > 0, then all UP flows generate cash OUT
9102
9109
  // *without* loss while all DOWN flows generate cash IN *with* loss.
9103
9110
  for(let i = 0; i < gpv.slopes; i++) {
9104
- addCashOut(gpv.up[i], -fp);
9105
- addCashIn(gpv.down[i], (1 - lr[i]) * -fp);
9111
+ addCashOut(gpv.up[i], fp);
9112
+ addCashIn(gpv.down[i], (1 - lr[i]) * fp);
9106
9113
  }
9107
9114
  } else if(fp < 0) {
9108
9115
  // If FROM node has price < 0, then all UP flows generate cash IN
9109
9116
  // *without* loss while all DOWN flows generate cash OUT *with* loss.
9110
9117
  for(let i = 0; i < gpv.slopes; i++) {
9111
- addCashIn(gpv.up[i], fp);
9112
- addCashOut(gpv.down[i], (1 - lr[i]) * fp);
9118
+ addCashIn(gpv.up[i], -fp);
9119
+ addCashOut(gpv.down[i], (1 - lr[i]) * -fp);
9113
9120
  }
9114
9121
  }
9115
9122
  if(tp > 0) {
9116
9123
  // If TO node has price > 0, then all UP flows generate cash IN *with*
9117
9124
  // loss while all DOWN flows generate cash OUT *without* loss.
9118
9125
  for(let i = 0; i < gpv.slopes; i++) {
9119
- addCashIn(gpv.up[i], (1 - lr[i]) * -tp);
9120
- addCashOut(gpv.down[i], -tp);
9126
+ addCashIn(gpv.up[i], (1 - lr[i]) * tp);
9127
+ addCashOut(gpv.down[i], tp);
9121
9128
  }
9122
9129
  } else if(tp < 0) {
9123
9130
  // If TO node has price < 0, then all UP flows generate cash OUT
9124
9131
  // *with* loss while all DOWN flows generate cash IN *without* loss.
9125
9132
  for(let i = 0; i < gpv.slopes; i++) {
9126
- addCashOut(gpv.up[i], (1 - lr[i]) * tp);
9127
- addCashIn(gpv.down[i], tp);
9133
+ addCashOut(gpv.up[i], (1 - lr[i]) * -tp);
9134
+ addCashIn(gpv.down[i], -tp);
9128
9135
  }
9129
9136
  }
9130
9137
  }
@@ -9179,15 +9186,14 @@ function VMI_add_constraint(ct) {
9179
9186
  for(let i in VM.coefficients) if(Number(i)) {
9180
9187
  // Do not add (near)zero coefficients to the matrix.
9181
9188
  const c = VM.coefficients[i];
9182
- if(Math.abs(c) >= VM.NEAR_ZERO) {
9183
- row[i] = c;
9184
- }
9189
+ if(Math.abs(c) >= VM.NEAR_ZERO) row[i] = c;
9185
9190
  }
9186
9191
  // Special case:
9187
9192
  if(ct === VM.ACTOR_CASH) {
9188
9193
  VM.actor_cash_constraints.push(VM.matrix.length);
9189
9194
  ct = VM.EQ;
9190
9195
  }
9196
+
9191
9197
  let rhs = VM.rhs;
9192
9198
  // Check for <= (near) +infinity and >= (near) -infinity: such
9193
9199
  // constraints should not be added to the model.