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
|
@@ -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 <
|
|
317
|
+
if(mi < this.sd_count) {
|
|
318
318
|
path += sd.subdirs[mi].name;
|
|
319
319
|
} else {
|
|
320
|
-
path += sd.models[mi -
|
|
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 >=
|
|
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:
|
|
1227
|
-
//
|
|
1228
|
-
//
|
|
1229
|
-
//
|
|
1230
|
-
//
|
|
1231
|
-
|
|
1232
|
-
|
|
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(('
|
|
1239
|
-
('
|
|
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:
|
|
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
|
-
|
|
1535
|
-
|
|
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
|
-
|
|
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
|
|
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],
|
|
9105
|
-
addCashIn(gpv.down[i], (1 - lr[i]) *
|
|
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]) *
|
|
9120
|
-
addCashOut(gpv.down[i],
|
|
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.
|