linny-r 3.0.7 → 3.0.8

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.8",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -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;
@@ -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
  }
@@ -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.