linny-r 1.6.1 → 1.6.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.
- package/package.json +1 -1
- package/server.js +27 -6
- package/static/scripts/linny-r-gui-experiment-manager.js +14 -3
- package/static/scripts/linny-r-gui-paper.js +25 -23
- package/static/scripts/linny-r-model.js +38 -32
- package/static/scripts/linny-r-utils.js +15 -1
- package/static/scripts/linny-r-vm.js +239 -50
package/package.json
CHANGED
package/server.js
CHANGED
@@ -1521,7 +1521,7 @@ function connectionErrorText(msg) {
|
|
1521
1521
|
//
|
1522
1522
|
|
1523
1523
|
function commandLineSettings() {
|
1524
|
-
// Sets default settings, and then checks the command line arguments
|
1524
|
+
// Sets default settings, and then checks the command line arguments.
|
1525
1525
|
const settings = {
|
1526
1526
|
cli_name: (PLATFORM.startsWith('win') ? 'Command Prompt' : 'Terminal'),
|
1527
1527
|
inkscape: '',
|
@@ -1533,6 +1533,22 @@ function commandLineSettings() {
|
|
1533
1533
|
solver_path: '',
|
1534
1534
|
user_dir: path.join(WORKING_DIRECTORY, 'user')
|
1535
1535
|
};
|
1536
|
+
const
|
1537
|
+
cmd = process.argv[0],
|
1538
|
+
app = (cmd.endsWith('node.exe') ? 'node' : 'linny-r'),
|
1539
|
+
usage = `Usage: ${app} server [options]
|
1540
|
+
|
1541
|
+
Possible options are:
|
1542
|
+
dpi=[number] will make InkScape render SVGs in the specified resolution
|
1543
|
+
help will display these command line options
|
1544
|
+
launch will open the Linny-R GUI in a browser window
|
1545
|
+
port=[number] will listen at the specified port number
|
1546
|
+
(default is 5050; number must be unique for each server)
|
1547
|
+
solver=[name] will select solver [name], or warn if not found
|
1548
|
+
(name choices: Gurobi, CPLEX, SCIP or LP_solve)
|
1549
|
+
verbose will output solver messages to the console
|
1550
|
+
workspace=[path] will create workspace in [path] instead of (Linny-R)/user
|
1551
|
+
`;
|
1536
1552
|
for(let i = 2; i < process.argv.length; i++) {
|
1537
1553
|
const lca = process.argv[i].toLowerCase();
|
1538
1554
|
if(lca === 'launch') {
|
@@ -1541,7 +1557,7 @@ function commandLineSettings() {
|
|
1541
1557
|
const av = lca.split('=');
|
1542
1558
|
if(av.length === 1) av.push('');
|
1543
1559
|
if(av[0] === 'port') {
|
1544
|
-
// Accept any number greater than or equal to 1024
|
1560
|
+
// Accept any number greater than or equal to 1024.
|
1545
1561
|
const n = parseInt(av[1]);
|
1546
1562
|
if(isNaN(n) || n < 1024) {
|
1547
1563
|
console.log(`WARNING: Invalid port number ${av[1]}`);
|
@@ -1555,7 +1571,7 @@ function commandLineSettings() {
|
|
1555
1571
|
settings.preferred_solver = av[1];
|
1556
1572
|
}
|
1557
1573
|
} else if(av[0] === 'dpi') {
|
1558
|
-
// Accept any number greater than or equal to 1024
|
1574
|
+
// Accept any number greater than or equal to 1024.
|
1559
1575
|
const n = parseInt(av[1]);
|
1560
1576
|
if(isNaN(n) || n > 1200) {
|
1561
1577
|
console.log(`WARNING: Invalid resolution ${av[1]} (max. 1200 dpi)`);
|
@@ -1563,7 +1579,7 @@ function commandLineSettings() {
|
|
1563
1579
|
settings.dpi = n;
|
1564
1580
|
}
|
1565
1581
|
} else if(av[0] === 'workspace') {
|
1566
|
-
// User directory must be READ/WRITE-accessible
|
1582
|
+
// User directory must be READ/WRITE-accessible.
|
1567
1583
|
try {
|
1568
1584
|
fs.accessSync(av[1], fs.constants.R_OK | fs.constants.W_O);
|
1569
1585
|
} catch(err) {
|
@@ -1571,10 +1587,15 @@ function commandLineSettings() {
|
|
1571
1587
|
process.exit();
|
1572
1588
|
}
|
1573
1589
|
settings.user_dir = av[1];
|
1590
|
+
} else if(av[0] === 'help') {
|
1591
|
+
// Print command line options.
|
1592
|
+
console.log(usage);
|
1593
|
+
process.exit();
|
1574
1594
|
} else {
|
1575
|
-
// Terminate script
|
1595
|
+
// Terminate script.
|
1576
1596
|
console.log(
|
1577
|
-
`ERROR: Invalid command line argument "${process.argv[i]}"`);
|
1597
|
+
`ERROR: Invalid command line argument "${process.argv[i]}"\n`);
|
1598
|
+
console.log(usage);
|
1578
1599
|
process.exit();
|
1579
1600
|
}
|
1580
1601
|
}
|
@@ -356,7 +356,11 @@ class GUIExperimentManager extends ExperimentManager {
|
|
356
356
|
dim_count.innerHTML = pluralS(x.available_dimensions.length,
|
357
357
|
'more dimension');
|
358
358
|
x.inferActualDimensions();
|
359
|
+
for(let i = 0; i < x.actual_dimensions.length; i++) {
|
360
|
+
x.actual_dimensions[i].sort(compareSelectors);
|
361
|
+
}
|
359
362
|
x.inferCombinations();
|
363
|
+
//x.combinations.sort(compareCombinations);
|
360
364
|
combi_count.innerHTML = pluralS(x.combinations.length, 'combination');
|
361
365
|
if(x.combinations.length === 0) canview = false;
|
362
366
|
header.innerHTML = x.title;
|
@@ -850,6 +854,9 @@ class GUIExperimentManager extends ExperimentManager {
|
|
850
854
|
}
|
851
855
|
// Get the selected statistic for each run so as to get an array of numbers
|
852
856
|
const data = [];
|
857
|
+
// Set reference column indices so that values for the reference|
|
858
|
+
// configuration can be displayed in orange.
|
859
|
+
const ref_conf_indices = [];
|
853
860
|
for(let i = 0; i < x.runs.length; i++) {
|
854
861
|
const
|
855
862
|
r = x.runs[i],
|
@@ -878,7 +885,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
878
885
|
data.push(rr.last);
|
879
886
|
}
|
880
887
|
}
|
881
|
-
// Scale data as selected
|
888
|
+
// Scale data as selected.
|
882
889
|
const scaled = data.slice();
|
883
890
|
// NOTE: scale only after the experiment has been completed AND
|
884
891
|
// configurations have been defined (otherwise comparison is pointless)
|
@@ -896,7 +903,9 @@ class GUIExperimentManager extends ExperimentManager {
|
|
896
903
|
}
|
897
904
|
// Set difference for reference configuration itself to 0
|
898
905
|
for(let i = 0; i < n; i++) {
|
899
|
-
|
906
|
+
const index = rc * n + i;
|
907
|
+
scaled[index] = 0;
|
908
|
+
ref_conf_indices.push(index);
|
900
909
|
}
|
901
910
|
} else if(x.selected_scale === 'reg') {
|
902
911
|
// Compute regret: current config - high value config in same scenario
|
@@ -934,13 +943,15 @@ class GUIExperimentManager extends ExperimentManager {
|
|
934
943
|
formatted.push(VM.sig4Dig(scaled[i]));
|
935
944
|
}
|
936
945
|
uniformDecimals(formatted);
|
937
|
-
// Display formatted data in cells
|
946
|
+
// Display formatted data in cells.
|
938
947
|
for(let i = 0; i < x.combinations.length; i++) {
|
939
948
|
const cell = document.getElementById('xr' + i);
|
940
949
|
if(i < x.runs.length) {
|
941
950
|
cell.innerHTML = formatted[i];
|
942
951
|
cell.classList.remove('not-run');
|
943
952
|
cell.style.backgroundColor = this.color_scale.rgb(normalized[i]);
|
953
|
+
cell.style.color = (ref_conf_indices.indexOf(i) >= 0 ?
|
954
|
+
'orange' : 'black');
|
944
955
|
const
|
945
956
|
r = x.runs[i],
|
946
957
|
rr = r.results[rri],
|
@@ -2312,17 +2312,18 @@ class Paper {
|
|
2312
2312
|
}
|
2313
2313
|
if(prod.hasBounds) {
|
2314
2314
|
font_color = 'black';
|
2315
|
-
// By default, "plain" factors having bounds are filled in silver
|
2315
|
+
// By default, "plain" factors having bounds are filled in silver.
|
2316
2316
|
fill_color = this.palette.has_bounds;
|
2317
2317
|
// Use relative distance to bounds so that 100000.1 is not shown
|
2318
|
-
// as overflow, but 100.1 is
|
2318
|
+
// as overflow, but 100.1 is.
|
2319
2319
|
let udif = this.relDif(l, ub),
|
2320
2320
|
ldif = this.relDif(lb, l);
|
2321
|
-
// Special case: for LB = 0, use the ON/OFF threshold
|
2321
|
+
// Special case: for LB = 0, use the ON/OFF threshold.
|
2322
2322
|
if(Math.abs(lb) <= VM.SIG_DIF_LIMIT &&
|
2323
2323
|
Math.abs(l) <= VM.ON_OFF_THRESHOLD) ldif = 0;
|
2324
2324
|
if(MODEL.solved) {
|
2325
|
-
// NOTE:
|
2325
|
+
// NOTE: Use bright red and blue colors in case of "stock level
|
2326
|
+
// out of bounds".
|
2326
2327
|
if(ub < VM.PLUS_INFINITY && l < VM.UNDEFINED && udif > VM.SIG_DIF_LIMIT) {
|
2327
2328
|
fill_color = this.palette.above_upper_bound;
|
2328
2329
|
font_color = 'blue';
|
@@ -2332,51 +2333,52 @@ class Paper {
|
|
2332
2333
|
} else if(l < VM.ERROR || l > VM.EXCEPTION) {
|
2333
2334
|
font_color = this.palette.VM_error;
|
2334
2335
|
} else if(l < VM.UNDEFINED) {
|
2335
|
-
// Shades of green reflect whether level within bounds, where
|
2336
|
+
// Shades of green reflect whether level is within bounds, where
|
2336
2337
|
// "sources" (negative level) and "sinks" (positive level) are
|
2337
|
-
// shown as more reddish / bluish shades of green
|
2338
|
+
// shown as more reddish / bluish shades of green.
|
2338
2339
|
if(l < -VM.ON_OFF_THRESHOLD) {
|
2339
2340
|
fill_color = this.palette.neg_within_bounds;
|
2340
2341
|
} else if(l > VM.ON_OFF_THRESHOLD) {
|
2341
2342
|
fill_color = this.palette.pos_within_bounds;
|
2342
2343
|
} else {
|
2343
|
-
|
2344
|
+
fill_color = this.palette.zero_within_bounds;
|
2344
2345
|
}
|
2345
2346
|
if(ub - lb < VM.NEAR_ZERO) {
|
2347
|
+
// When LB = UB, fill completely in the color, but ...
|
2346
2348
|
if(prod.isConstant && Math.abs(l) > VM.NEAR_ZERO) {
|
2347
|
-
//
|
2349
|
+
// ... non-zero constants have less saturated shades.
|
2348
2350
|
fill_color = (l < 0 ? this.palette.neg_constant :
|
2349
2351
|
this.palette.pos_constant);
|
2350
2352
|
}
|
2351
2353
|
} else if(ub - l < VM.SIG_DIF_LIMIT) {
|
2352
|
-
// Black font and darker fill color indicate "at upper bound"
|
2354
|
+
// Black font and darker fill color indicate "at upper bound".
|
2353
2355
|
font_color = 'black';
|
2354
2356
|
fill_color = (ub > 0 ? this.palette.at_pos_ub_fill :
|
2355
2357
|
(ub < 0 ? this.palette.at_neg_ub_fill :
|
2356
2358
|
this.palette.at_zero_ub_fill));
|
2357
2359
|
at_bound = true;
|
2358
2360
|
} else if (l - lb < VM.SIG_DIF_LIMIT) {
|
2359
|
-
// Font and rim color indicate "at
|
2361
|
+
// Font and rim color indicate "at lower bound".
|
2360
2362
|
font_color = 'black';
|
2361
2363
|
fill_color = (lb > 0 ? this.palette.at_pos_lb_fill :
|
2362
2364
|
(lb < 0 ? this.palette.at_neg_lb_fill :
|
2363
2365
|
this.palette.at_zero_lb_fill));
|
2364
2366
|
at_bound = true;
|
2365
2367
|
} else {
|
2366
|
-
//
|
2368
|
+
// Set "partial fill" flag if not at lower bound and UB < INF.
|
2367
2369
|
pf = ub < VM.PLUS_INFINITY;
|
2368
2370
|
font_color = this.palette.within_bounds_font;
|
2369
2371
|
}
|
2370
2372
|
}
|
2371
2373
|
} else if(ub - lb < VM.NEAR_ZERO) {
|
2372
|
-
// Not solved but equal bounds => probably constants
|
2374
|
+
// Not solved but equal bounds => probably constants.
|
2373
2375
|
if(prod.isConstant && Math.abs(ub) > VM.NEAR_ZERO) {
|
2374
|
-
// Non-zero constants have less saturated shades
|
2376
|
+
// Non-zero constants have less saturated shades.
|
2375
2377
|
fill_color = (ub < 0 ? this.palette.neg_constant :
|
2376
2378
|
this.palette.pos_constant);
|
2377
2379
|
}
|
2378
2380
|
} else if(l < VM.UNDEFINED) {
|
2379
|
-
// Different bounds and initial level set => partial fill
|
2381
|
+
// Different bounds and initial level set => partial fill.
|
2380
2382
|
fill_color = this.palette.src_snk;
|
2381
2383
|
pf = true;
|
2382
2384
|
if(ub - l < VM.SIG_DIF_LIMIT || l - lb < VM.SIG_DIF_LIMIT) {
|
@@ -2418,8 +2420,8 @@ class Paper {
|
|
2418
2420
|
let npfbg = 'white';
|
2419
2421
|
if(fill_color === this.palette.above_upper_bound ||
|
2420
2422
|
fill_color === this.palette.below_lower_bound ||
|
2421
|
-
// NOTE:
|
2422
|
-
(at_bound && l > VM.ON_OFF_THRESHOLD)) {
|
2423
|
+
// NOTE: Empty buffers should be entirely white.
|
2424
|
+
(at_bound && l > lb + VM.ON_OFF_THRESHOLD)) {
|
2423
2425
|
npfbg = fill_color;
|
2424
2426
|
pf = false;
|
2425
2427
|
}
|
@@ -2434,26 +2436,26 @@ class Paper {
|
|
2434
2436
|
{fill: fill_color, stroke: stroke_color, 'stroke-width': stroke_width,
|
2435
2437
|
'stroke-dasharray': sda, 'stroke-linecap': 'round',
|
2436
2438
|
'rx': hh, 'ry': hh});
|
2437
|
-
// NOTE:
|
2439
|
+
// NOTE: Set fill color to darker shade for partial fill.
|
2438
2440
|
fill_color = (!MODEL.solved ? this.palette.src_snk :
|
2439
2441
|
(l > VM.NEAR_ZERO ? this.palette.above_zero_fill :
|
2440
2442
|
(l < -VM.NEAR_ZERO ? this.palette.below_zero_fill :
|
2441
2443
|
this.palette.at_zero_fill)));
|
2442
2444
|
}
|
2443
|
-
// Add partial fill if appropriate
|
2445
|
+
// Add partial fill if appropriate.
|
2444
2446
|
if(pf && l > lb && l < VM.UNDEFINED) {
|
2445
2447
|
// Calculate used part of range (1 = 100%)
|
2446
2448
|
let part,
|
2447
2449
|
range = ub - lb;
|
2448
2450
|
if(l >= VM.PLUS_INFINITY) {
|
2449
|
-
// Show exceptions and +INF as "overflow"
|
2451
|
+
// Show exceptions and +INF as "overflow".
|
2450
2452
|
part = 1;
|
2451
2453
|
fill_color = this.palette.above_upper_bound;
|
2452
2454
|
} else {
|
2453
2455
|
part = (range > 0 ? (l - lb) / range : 1);
|
2454
2456
|
}
|
2455
2457
|
if(part > 0 && l >= lb) {
|
2456
|
-
// Only fill the portion of used range with the fill color
|
2458
|
+
// Only fill the portion of used range with the fill color.
|
2457
2459
|
const rad = Math.asin(1 - 2*part);
|
2458
2460
|
prod.shape.addPath(['m', x + hw - hh + (hh - 1.5) * Math.cos(rad),
|
2459
2461
|
',', y + (hh - 1.5) * Math.sin(rad),
|
@@ -2467,7 +2469,7 @@ class Paper {
|
|
2467
2469
|
stroke_color = 'none';
|
2468
2470
|
stroke_width = 0;
|
2469
2471
|
// Sources have a triangle pointing up from the bottom
|
2470
|
-
// (in outline if *implicit* source)
|
2472
|
+
// (in outline if *implicit* source).
|
2471
2473
|
if(prod.isSourceNode) {
|
2472
2474
|
if(!prod.is_source) {
|
2473
2475
|
fill_color = 'none';
|
@@ -2479,7 +2481,7 @@ class Paper {
|
|
2479
2481
|
'stroke-width': stroke_width});
|
2480
2482
|
}
|
2481
2483
|
// Sinks have a triangle pointing down from the top
|
2482
|
-
// (in outline if implicit sink)
|
2484
|
+
// (in outline if implicit sink).
|
2483
2485
|
if(prod.isSinkNode) {
|
2484
2486
|
if(!prod.is_sink) {
|
2485
2487
|
fill_color = 'none';
|
@@ -2491,7 +2493,7 @@ class Paper {
|
|
2491
2493
|
'stroke-width': stroke_width});
|
2492
2494
|
}
|
2493
2495
|
// Integer level is denoted by enclosing name in large [ and ]
|
2494
|
-
// to denote "floor" as well as "ceiling"
|
2496
|
+
// to denote "floor" as well as "ceiling".
|
2495
2497
|
if(prod.integer_level) {
|
2496
2498
|
const
|
2497
2499
|
brh = prod.name_lines.split('\n').length * this.font_heights[8] + 4,
|
@@ -1574,7 +1574,7 @@ class LinnyRModel {
|
|
1574
1574
|
const
|
1575
1575
|
note = fc.notes[i],
|
1576
1576
|
nbn = note.nearbyNode;
|
1577
|
-
note.nearby_pos = (nbn ? {node: nbn, oldx: x, oldy: y} : null);
|
1577
|
+
note.nearby_pos = (nbn ? {node: nbn, oldx: nbn.x, oldy: nbn.y} : null);
|
1578
1578
|
}
|
1579
1579
|
for(let i = 0; i < fc.processes.length; i++) {
|
1580
1580
|
move = fc.processes[i].alignToGrid() || move;
|
@@ -5158,7 +5158,7 @@ class Note extends ObjectWithXYWH {
|
|
5158
5158
|
// If attribute omitted, use default attribute of entity type.
|
5159
5159
|
const attr = (ena.length > 1 ? ena[1].trim() : obj.defaultAttribute);
|
5160
5160
|
let val = null;
|
5161
|
-
// NOTE:
|
5161
|
+
// NOTE: For datasets, use the active modifier if no attribute.
|
5162
5162
|
if(!attr && obj instanceof Dataset) {
|
5163
5163
|
val = obj.activeModifierExpression;
|
5164
5164
|
} else {
|
@@ -7894,13 +7894,11 @@ class Product extends Node {
|
|
7894
7894
|
}
|
7895
7895
|
|
7896
7896
|
get isConstant() {
|
7897
|
-
// Return TRUE if this product is data,
|
7898
|
-
//
|
7897
|
+
// Return TRUE if this product is data, is not an actor cash flow,
|
7898
|
+
// has no ingoing links, has outgoing links ONLY to data objects,
|
7899
|
+
// and has set LB = UB.
|
7899
7900
|
if(!this.is_data || this.name.startsWith('$') ||
|
7900
|
-
!this.allOutputsAreData) return false;
|
7901
|
-
for(let i = 0; i < this.inputs.length; i++) {
|
7902
|
-
if(this.inputs[i].from_node instanceof Process) return false;
|
7903
|
-
}
|
7901
|
+
this.inputs.length || !this.allOutputsAreData) return false;
|
7904
7902
|
return (this.equal_bounds && this.lower_bound.defined);
|
7905
7903
|
}
|
7906
7904
|
|
@@ -8847,11 +8845,30 @@ class Dataset {
|
|
8847
8845
|
|
8848
8846
|
attributeValue(a) {
|
8849
8847
|
// Return the computed result for attribute `a`.
|
8850
|
-
// NOTE: Datasets have ONE attribute (their vector) denoted by the
|
8851
|
-
//
|
8852
|
-
// their value should be obtained using attributeExpression
|
8853
|
-
|
8854
|
-
|
8848
|
+
// NOTE: Datasets have ONE attribute (their vector) denoted by the
|
8849
|
+
// dot ".". All other "attributes" should be modifier selectors,
|
8850
|
+
// and their value should be obtained using `attributeExpression(a)`.
|
8851
|
+
// The empty string denotes "use default", which may have been set
|
8852
|
+
// by the modeler, or may follow from the active combination of a
|
8853
|
+
// running experiment.
|
8854
|
+
if(a === '') {
|
8855
|
+
const x = this.activeModifierExpression;
|
8856
|
+
if(x instanceof Expression) {
|
8857
|
+
x.compute(0);
|
8858
|
+
// Ensure that for dynamic modifier expressions the vector is
|
8859
|
+
// fully computed.
|
8860
|
+
if(!x.isStatic) {
|
8861
|
+
const nt = MODEL.end_period - MODEL.start_period + 1;
|
8862
|
+
for(let t = 1; t <= nt; t++) x.result(t);
|
8863
|
+
}
|
8864
|
+
return x.vector;
|
8865
|
+
}
|
8866
|
+
// No modifier expression? Then return the dataset vector.
|
8867
|
+
return this.vector;
|
8868
|
+
}
|
8869
|
+
if(a === '.') return this.vector;
|
8870
|
+
// Fall-through: return the default value of this dataset.
|
8871
|
+
return this.defaultValue;
|
8855
8872
|
}
|
8856
8873
|
|
8857
8874
|
attributeExpression(a) {
|
@@ -8881,7 +8898,7 @@ class Dataset {
|
|
8881
8898
|
console.log('WARNING: Dataset "' + this.name +
|
8882
8899
|
`" has no default selector "${this.default_selector}"`, this.modifiers);
|
8883
8900
|
}
|
8884
|
-
// Fall-through: return
|
8901
|
+
// Fall-through: return the dataset vector.
|
8885
8902
|
return this.vector;
|
8886
8903
|
}
|
8887
8904
|
|
@@ -9290,23 +9307,11 @@ class ChartVariable {
|
|
9290
9307
|
t_end = tsteps;
|
9291
9308
|
} else {
|
9292
9309
|
// Get the variable's own value (number, vector or expression)
|
9293
|
-
|
9294
|
-
|
9295
|
-
|
9296
|
-
|
9297
|
-
|
9298
|
-
// Check if dataset modifiers match the combination of selectors
|
9299
|
-
// for the active run.
|
9300
|
-
const mm = this.object.matchingModifiers(
|
9301
|
-
MODEL.running_experiment.activeCombination);
|
9302
|
-
// If so, use the first (the list should contain at most 1 selector)
|
9303
|
-
// to select the modifier expression; otherwise, use the unmodified
|
9304
|
-
// vector of the dataset
|
9305
|
-
if(mm.length > 0) {
|
9306
|
-
av = mm[0].expression;
|
9307
|
-
} else {
|
9308
|
-
av = this.object.vector;
|
9309
|
-
}
|
9310
|
+
if(this.object instanceof Dataset && !this.attribute) {
|
9311
|
+
// Special case: Variables that depict a dataset with no explicit
|
9312
|
+
// modifier selector must recompute the vector using the current
|
9313
|
+
// experiment run combination or the default selector.
|
9314
|
+
av = this.object.activeModifierExpression;
|
9310
9315
|
} else if(this.object instanceof DatasetModifier) {
|
9311
9316
|
av = this.object.expression;
|
9312
9317
|
} else {
|
@@ -11075,7 +11080,8 @@ class ExperimentRun {
|
|
11075
11080
|
bm.messages = VM.messages[i];
|
11076
11081
|
this.block_messages.push(bm);
|
11077
11082
|
this.warning_count += bm.warningCount;
|
11078
|
-
|
11083
|
+
// NOTE: When set by the VM, `solver_secs` is a string.
|
11084
|
+
this.solver_seconds += parseFloat(bm.solver_secs);
|
11079
11085
|
}
|
11080
11086
|
}
|
11081
11087
|
|
@@ -491,7 +491,9 @@ function matchingNumber(m, s) {
|
|
491
491
|
// Returns an integer value if string `m` matches selector pattern `s`
|
492
492
|
// (where asterisks match 0 or more characters, and question marks 1
|
493
493
|
// character) and the matching parts jointly denote an integer.
|
494
|
-
|
494
|
+
// NOTE: A "+" must be escaped, "*" and "?" must become groups.
|
495
|
+
let raw = s.replaceAll('+', '\+')
|
496
|
+
.replace(/\*/g, '(.*)').replace(/\?/g, '(.)'),
|
495
497
|
match = m.match(new RegExp(`^${raw}$`)),
|
496
498
|
n = '';
|
497
499
|
if(match) {
|
@@ -601,6 +603,18 @@ function compareSelectors(s1, s2) {
|
|
601
603
|
return 0;
|
602
604
|
}
|
603
605
|
|
606
|
+
function compareCombinations(c1, c2) {
|
607
|
+
// Compare two selector lists.
|
608
|
+
const n = Math.min(c1.length, c2.length);
|
609
|
+
for(let i = 0; i < n; i++) {
|
610
|
+
const cs = compareSelectors(c1[i], c2[i]);
|
611
|
+
if(cs) return cs;
|
612
|
+
}
|
613
|
+
if(c1.length > l) return 1;
|
614
|
+
if(c2.length > l) return -1;
|
615
|
+
return 0;
|
616
|
+
}
|
617
|
+
|
604
618
|
//
|
605
619
|
// Functions that perform set-like operations on lists of string
|
606
620
|
//
|
@@ -1394,9 +1394,13 @@ class ExpressionParser {
|
|
1394
1394
|
'-- number is:', this.context_number,
|
1395
1395
|
'\nTRACE: Expression:', obj.expression.text);
|
1396
1396
|
// Use the context number as "selector" parameter of the VMI.
|
1397
|
-
|
1398
|
-
|
1399
|
-
|
1397
|
+
const arg0 = (by_reference ?
|
1398
|
+
// If equation is "by reference", use VMI_push_entity
|
1399
|
+
// while passing the context number as extra parameter.
|
1400
|
+
{r: obj.dataset, a: obj.selector, cn: this.context_number} :
|
1401
|
+
// Otherwise, use VMI_push_dataset_modifier.
|
1402
|
+
{d: obj.dataset, s: this.context_number, x: obj.expression});
|
1403
|
+
return [arg0, anchor1, offset1, anchor2, offset2];
|
1400
1404
|
}
|
1401
1405
|
}
|
1402
1406
|
}
|
@@ -2452,7 +2456,7 @@ class VirtualMachine {
|
|
2452
2456
|
if(n <= this.BAD_REF) return [true, '#REF?'];
|
2453
2457
|
if(n <= this.ARRAY_INDEX) return [true, '#INDEX!'];
|
2454
2458
|
if(n <= this.BAD_CALC) return [true, '#VALUE!'];
|
2455
|
-
if(n <= this.DIV_ZERO) return [true, '#
|
2459
|
+
if(n <= this.DIV_ZERO) return [true, '#DIV/0!'];
|
2456
2460
|
if(n <= this.CYCLIC) return [true, '#CYCLE!'];
|
2457
2461
|
// Any other number less than or equal to 10^30 is considered as
|
2458
2462
|
// minus infinity.
|
@@ -4767,7 +4771,7 @@ class VirtualMachine {
|
|
4767
4771
|
// is calibrated for 1000 VMI instructions.
|
4768
4772
|
this.tsl = Math.ceil(CONFIGURATION.progress_needle_interval *
|
4769
4773
|
1000 / this.code.length);
|
4770
|
-
if(
|
4774
|
+
if(abl > this.tsl * 5) {
|
4771
4775
|
UI.setMessage('Constructing the Simplex tableau');
|
4772
4776
|
UI.setProgressNeedle(0);
|
4773
4777
|
this.show_progress = true;
|
@@ -5545,7 +5549,7 @@ Solver status = ${json.status}`);
|
|
5545
5549
|
this.round_times.length = 0;
|
5546
5550
|
this.solver_times[bnr - 1] = time;
|
5547
5551
|
const ssecs = this.round_secs.reduce((a, b) => a + b, 0);
|
5548
|
-
this.solver_secs[bnr - 1] = (ssecs ? VM.sig4Dig(ssecs) : '');
|
5552
|
+
this.solver_secs[bnr - 1] = (ssecs ? VM.sig4Dig(ssecs) : '0');
|
5549
5553
|
this.round_secs.length = 0;
|
5550
5554
|
MONITOR.addProgressBlock(bnr, issue, time);
|
5551
5555
|
}
|
@@ -6265,13 +6269,19 @@ function VMI_push_wildcard_entity(x, args) {
|
|
6265
6269
|
} else {
|
6266
6270
|
// Select the first entity in `ee` that matches the wildcard vector
|
6267
6271
|
// index of the expression `x` being executed.
|
6272
|
+
if(x.wildcard_vector_index === false && x.isWildcardExpression &&
|
6273
|
+
MODEL.running_experiment) {
|
6274
|
+
// If no wildcard vector index, try to infer it.
|
6275
|
+
x.wildcard_vector_index = matchingNumberInList(
|
6276
|
+
MODEL.running_experiment.activeCombination, x.attribute);
|
6277
|
+
}
|
6268
6278
|
nn = nn.replace('#', x.wildcard_vector_index);
|
6269
6279
|
for(let i = 0; !obj && i < el.length; i++) {
|
6270
6280
|
if(el[i].name === nn) obj = el[i];
|
6271
6281
|
}
|
6272
6282
|
// If no match, then this indicates a bad reference.
|
6273
6283
|
if(!obj) {
|
6274
|
-
console.log(`ERROR: no match for "${nn}" in eligible entity list`, el);
|
6284
|
+
console.log(`ERROR: no match for "${nn}" in eligible entity list`, el, x);
|
6275
6285
|
x.push(VM.BAD_REF);
|
6276
6286
|
return;
|
6277
6287
|
}
|
@@ -6281,7 +6291,8 @@ function VMI_push_wildcard_entity(x, args) {
|
|
6281
6291
|
// called with the appropriate parameters.
|
6282
6292
|
const attr = args[0].a || obj.defaultAttribute;
|
6283
6293
|
if(args[0].br) {
|
6284
|
-
VMI_push_entity(x, {r: obj, a: attr}
|
6294
|
+
VMI_push_entity(x, [{r: obj, a: attr},
|
6295
|
+
args[1], args[2], args[3], args[4]]);
|
6285
6296
|
return;
|
6286
6297
|
}
|
6287
6298
|
// Otherwise, if the entity is a dataset modifier, this must be an
|
@@ -6824,7 +6835,7 @@ function VMI_mul(x) {
|
|
6824
6835
|
|
6825
6836
|
function VMI_div(x) {
|
6826
6837
|
// Pops the top number on the stack and divides the new top number
|
6827
|
-
// by it. In case of division by zero, the top is replaced by #
|
6838
|
+
// by it. In case of division by zero, the top is replaced by #DIV/0!
|
6828
6839
|
const d = x.pop();
|
6829
6840
|
if(d !== false) {
|
6830
6841
|
if(DEBUGGING) console.log('DIV (' + d.join(', ') + ')');
|
@@ -6838,7 +6849,7 @@ function VMI_div(x) {
|
|
6838
6849
|
|
6839
6850
|
function VMI_mod(x) {
|
6840
6851
|
// Pops the top number on the stack, divides the new top number by it
|
6841
|
-
// (if non-zero, or it pushes error code #
|
6852
|
+
// (if non-zero, or it pushes error code #DIV/0!), takes the fraction
|
6842
6853
|
// part, and multiplies this with the divider; in other words, it
|
6843
6854
|
// performs a "floating point MOD operation"
|
6844
6855
|
const d = x.pop();
|
@@ -7159,39 +7170,41 @@ function VMI_jump(x, index) {
|
|
7159
7170
|
}
|
7160
7171
|
|
7161
7172
|
function VMI_jump_if_false(x, index) {
|
7162
|
-
//
|
7163
|
-
// VM.UNDEFINED)
|
7164
|
-
// as the counter is ALWAYS increased by 1 after calling a VMI function
|
7173
|
+
// Test the top number A on the stack, and if A is FALSE (zero or
|
7174
|
+
// VM.UNDEFINED) set the program counter of the VM to `index` minus 1,
|
7175
|
+
// as the counter is ALWAYS increased by 1 after calling a VMI function.
|
7165
7176
|
const r = x.top(true);
|
7166
7177
|
if(DEBUGGING) console.log(`JUMP-IF-FALSE (${r}, ${index})`);
|
7167
7178
|
if(r === 0 || r === VM.UNDEFINED || r === false) {
|
7168
7179
|
// Only jump on FALSE, leaving the stack "as is", so that in case
|
7169
|
-
// of no THEN the expression result equals the IF condition value
|
7180
|
+
// of no THEN, the expression result equals the IF condition value.
|
7170
7181
|
// NOTE: Also do this on a stack error (r === false)
|
7171
7182
|
x.program_counter = index - 1;
|
7172
7183
|
} else {
|
7173
|
-
// Remove the value from the stack
|
7184
|
+
// Remove the value from the stack.
|
7174
7185
|
x.stack.pop();
|
7175
7186
|
}
|
7176
7187
|
}
|
7177
7188
|
|
7178
7189
|
function VMI_pop_false(x) {
|
7179
|
-
//
|
7180
|
-
// VM.UNDEFINED (but this is not checked)
|
7190
|
+
// Remove the top value from the stack, which should be 0 or
|
7191
|
+
// VM.UNDEFINED (but this is not checked).
|
7181
7192
|
const r = x.stack.pop();
|
7182
7193
|
if(DEBUGGING) console.log(`POP-FALSE (${r})`);
|
7183
7194
|
}
|
7184
7195
|
|
7185
7196
|
function VMI_if_then(x) {
|
7186
7197
|
// NO operation -- as of version 1.0.14, this function only serves as
|
7187
|
-
// operator symbol
|
7188
|
-
|
7198
|
+
// placeholder in operator symbol arrays. The parser should no longer
|
7199
|
+
// code this, so its execution would indicate an error.
|
7200
|
+
console.log('WARNING: IF-THEN instruction is obsolete', x);
|
7189
7201
|
}
|
7190
7202
|
|
7191
7203
|
function VMI_if_else(x) {
|
7192
7204
|
// NO operation -- as of version 1.0.14, this function only serves as
|
7193
|
-
// operator symbol
|
7194
|
-
|
7205
|
+
// placeholder in operator symbol arrays. The parser should no longer
|
7206
|
+
// code this, so its execution would indicate an error.
|
7207
|
+
console.log('WARNING: IF-ELSE instruction is obsolete', x);
|
7195
7208
|
}
|
7196
7209
|
|
7197
7210
|
//
|
@@ -7424,7 +7437,7 @@ function VMI_set_bounds(args) {
|
|
7424
7437
|
}
|
7425
7438
|
}
|
7426
7439
|
|
7427
|
-
function VMI_clear_coefficients(
|
7440
|
+
function VMI_clear_coefficients() {
|
7428
7441
|
if(DEBUGGING) console.log('clear_coefficients');
|
7429
7442
|
VM.coefficients = {};
|
7430
7443
|
VM.cash_in_coefficients = {};
|
@@ -7555,7 +7568,7 @@ function VMI_add_var_to_weighted_sum_coefficients(args) {
|
|
7555
7568
|
VM.sig4Dig(w) + ' * ' + v.variableName + ' (t = ' + t + ')');
|
7556
7569
|
}
|
7557
7570
|
for(let i = 0; i <= d; i++) {
|
7558
|
-
|
7571
|
+
let r = v.result(t);
|
7559
7572
|
if(args.length > 3) r /= (d + 1);
|
7560
7573
|
if(k <= 0) {
|
7561
7574
|
// See NOTE in VMI_add_const_to_coefficient instruction
|
@@ -7779,7 +7792,7 @@ function VMI_add_throughput_to_coefficient(args) {
|
|
7779
7792
|
}
|
7780
7793
|
}
|
7781
7794
|
|
7782
|
-
function VMI_set_objective(
|
7795
|
+
function VMI_set_objective() {
|
7783
7796
|
// Copies the coefficients to the vector for the objective function
|
7784
7797
|
if(DEBUGGING) console.log('set_objective');
|
7785
7798
|
for(let i in VM.coefficients) if(Number(i)) {
|
@@ -7843,7 +7856,7 @@ function VMI_set_add_constraints_flag(args) {
|
|
7843
7856
|
(VM.add_constraints_flag ? 'TRUE' : 'FALSE') + ')');
|
7844
7857
|
}
|
7845
7858
|
|
7846
|
-
function VMI_toggle_add_constraints_flag(
|
7859
|
+
function VMI_toggle_add_constraints_flag() {
|
7847
7860
|
// Toggles the VM's "add constraints" flag
|
7848
7861
|
VM.add_constraints_flag = !VM.add_constraints_flag;
|
7849
7862
|
if(DEBUGGING) console.log('toggle_add_constraints_flag (now ' +
|
@@ -7956,7 +7969,7 @@ function VMI_add_bound_line_constraint(args) {
|
|
7956
7969
|
VM.coefficients[w[i]] = 1;
|
7957
7970
|
}
|
7958
7971
|
VM.rhs = 1;
|
7959
|
-
VMI_add_constraint(VM.EQ)
|
7972
|
+
VMI_add_constraint(VM.EQ);
|
7960
7973
|
// Add constraint (2):
|
7961
7974
|
VMI_clear_coefficients();
|
7962
7975
|
VM.coefficients[VM.offset + vix] = 1;
|
@@ -7964,7 +7977,7 @@ function VMI_add_bound_line_constraint(args) {
|
|
7964
7977
|
VM.coefficients[w[i]] = -x[i];
|
7965
7978
|
}
|
7966
7979
|
// No need to set RHS as it is already reset to 0
|
7967
|
-
VMI_add_constraint(VM.EQ)
|
7980
|
+
VMI_add_constraint(VM.EQ);
|
7968
7981
|
// Add constraint (3):
|
7969
7982
|
VMI_clear_coefficients();
|
7970
7983
|
VM.coefficients[VM.offset + viy] = 1;
|
@@ -8105,23 +8118,24 @@ const
|
|
8105
8118
|
// *** API section for custom operators ***
|
8106
8119
|
//
|
8107
8120
|
|
8108
|
-
// Custom operators are typically used to implement computations on model
|
8109
|
-
// that cannot be coded (efficiently) using standard expressions.
|
8110
|
-
// The first custom operator in this section demonstrates by example how
|
8111
|
-
// operators can be added.
|
8121
|
+
// Custom operators are typically used to implement computations on model
|
8122
|
+
// results that cannot be coded (efficiently) using standard expressions.
|
8123
|
+
// The first custom operator in this section demonstrates by example how
|
8124
|
+
// custom operators can be added.
|
8112
8125
|
|
8113
8126
|
// Custom operators should preferably have a short alphanumeric string as
|
8114
|
-
// their identifying symbol. Custom operators are monadic and reducing,
|
8115
|
-
// they must have a grouping as operand. The number of required
|
8116
|
-
// be checked at run time by the VM instruction for this
|
8127
|
+
// their identifying symbol. Custom operators are monadic and reducing,
|
8128
|
+
// i.e., they must have a grouping as operand. The number of required
|
8129
|
+
// arguments must be checked at run time by the VM instruction for this
|
8130
|
+
// operator.
|
8117
8131
|
|
8118
8132
|
// Each custom operator must have its own Virtual Machine instruction
|
8119
8133
|
|
8120
8134
|
function VMI_profitable_units(x) {
|
8121
|
-
//
|
8122
|
-
// number of profitable units having a standard capacity (number),
|
8123
|
-
// level (vector) of the process that represents multiple such
|
8124
|
-
// marginal cost (constant) and the market price (vector)
|
8135
|
+
// Replace the argument list that should be at the top of the stack by
|
8136
|
+
// the number of profitable units having a standard capacity (number),
|
8137
|
+
// given the level (vector) of the process that represents multiple such
|
8138
|
+
// units, the marginal cost (constant) and the market price (vector).
|
8125
8139
|
const d = x.top();
|
8126
8140
|
// Check whether the top stack element is a grouping of the correct size
|
8127
8141
|
// that contains arguments of the correct type
|
@@ -8134,7 +8148,7 @@ function VMI_profitable_units(x) {
|
|
8134
8148
|
d[3].entity.attributeExpression(d[3].attribute)) &&
|
8135
8149
|
(d.length === 4 || (typeof d[4] === 'number' &&
|
8136
8150
|
(d.length === 5 || typeof d[5] === 'number')))) {
|
8137
|
-
// Valid parameters => get the data required for computation
|
8151
|
+
// Valid parameters => get the data required for computation.
|
8138
8152
|
const
|
8139
8153
|
mup = d[0].entity, // the multi-unit process
|
8140
8154
|
ub = mup.upper_bound.result(0), // NOTE: UB is assumed to be static
|
@@ -8145,7 +8159,7 @@ function VMI_profitable_units(x) {
|
|
8145
8159
|
pt = (d.length > 4 ? d[4] : 0), // the profit threshold (0 by default)
|
8146
8160
|
// the time horizon (by default the length of the simulation period)
|
8147
8161
|
nt = (d.length > 5 ? d[5] : MODEL.end_period - MODEL.start_period + 1);
|
8148
|
-
// Handle exceptional values of `uc` and `mc
|
8162
|
+
// Handle exceptional values of `uc` and `mc`.
|
8149
8163
|
if(uc <= VM.BEYOND_MINUS_INFINITY || mc <= VM.BEYOND_MINUS_INFINITY) {
|
8150
8164
|
x.retop(Math.min(uc, mc));
|
8151
8165
|
return;
|
@@ -8155,16 +8169,16 @@ function VMI_profitable_units(x) {
|
|
8155
8169
|
return;
|
8156
8170
|
}
|
8157
8171
|
|
8158
|
-
// NOTE: NPU is not time-dependent => result is stored in cache
|
8159
|
-
// As expressions may contain several NPU operators, create a unique
|
8160
|
-
// based on its parameters
|
8172
|
+
// NOTE: NPU is not time-dependent => result is stored in cache.
|
8173
|
+
// As expressions may contain several NPU operators, create a unique
|
8174
|
+
// key based on its parameters.
|
8161
8175
|
const cache_key = ['npu', mup.code, ub, uc, mc, mpe.code, mpa, pt].join('_');
|
8162
8176
|
if(x.cache[cache_key]) {
|
8163
8177
|
x.retop(x.cache[cache_key]);
|
8164
8178
|
return;
|
8165
8179
|
}
|
8166
8180
|
|
8167
|
-
// mp can be a single value, a vector, or an expression
|
8181
|
+
// `mp` can be a single value, a vector, or an expression.
|
8168
8182
|
let mp = mpe.attributeValue(mpa);
|
8169
8183
|
if(mp === null) {
|
8170
8184
|
mp = mpe.attributeExpression(mpa);
|
@@ -8179,8 +8193,8 @@ function VMI_profitable_units(x) {
|
|
8179
8193
|
nu = Math.ceil(ub / uc), // Number of units
|
8180
8194
|
r = [];
|
8181
8195
|
if(mp && mp instanceof Expression) {
|
8182
|
-
// NOTE:
|
8183
|
-
mp.compute();
|
8196
|
+
// NOTE: An expression may not have been (fully) computed yet.
|
8197
|
+
mp.compute(0);
|
8184
8198
|
if(mp.isStatic) {
|
8185
8199
|
mp = mp.result(0);
|
8186
8200
|
} else {
|
@@ -8276,11 +8290,11 @@ function VMI_highest_cumulative_consecutive_deviation(x) {
|
|
8276
8290
|
e = d[0].entity,
|
8277
8291
|
a = d[0].attribute;
|
8278
8292
|
let vector = e.attributeValue(a);
|
8279
|
-
// NOTE:
|
8293
|
+
// NOTE: Equations can also be passed by reference.
|
8280
8294
|
if(e === MODEL.equations_dataset) {
|
8281
8295
|
const x = e.modifiers[a].expression;
|
8282
|
-
// NOTE: an expression may not have been (fully) computed yet
|
8283
|
-
x.compute();
|
8296
|
+
// NOTE: an expression may not have been (fully) computed yet.
|
8297
|
+
x.compute(0);
|
8284
8298
|
if(!x.isStatic) {
|
8285
8299
|
const nt = MODEL.end_period - MODEL.start_period + 1;
|
8286
8300
|
for(let t = 1; t <= nt; t++) x.result(t);
|
@@ -8390,6 +8404,181 @@ DYNAMIC_SYMBOLS.push('hccd');
|
|
8390
8404
|
// Add to this list only if operation makes an expression level-based
|
8391
8405
|
// LEVEL_BASED_CODES.push(VMI_...);
|
8392
8406
|
|
8407
|
+
function correlation_or_slope(x, c_or_s) {
|
8408
|
+
// Replaces the argument list that should be at the top of the stack by
|
8409
|
+
// either Spearman's correlation (r) or the slope (b) of the regression
|
8410
|
+
// line y = a + bx for the two vectors X and Y that are passed as the
|
8411
|
+
// two arguments of this function. Reason to combine these two statistics
|
8412
|
+
// in one function is because the required operations are very similar.
|
8413
|
+
// NOTES:
|
8414
|
+
// (1) This function codes for two different operators and therefore
|
8415
|
+
// is a helper function. The two operators must each have their
|
8416
|
+
// own VM instruction -- see immediately after this function.
|
8417
|
+
// (2) String `c_or_s` must be either 'correl' or 'slope'.
|
8418
|
+
// (3) The operands for this function must be vectors, not numbers,
|
8419
|
+
// so in the Linny-R expression they must be passed "by reference".
|
8420
|
+
const
|
8421
|
+
d = x.top(),
|
8422
|
+
vmi = c_or_s;
|
8423
|
+
// Check whether the top stack element is a grouping of two variables.
|
8424
|
+
if(d instanceof Array && d.length === 2 &&
|
8425
|
+
typeof d[0] === 'object' && d[0].hasOwnProperty('entity') &&
|
8426
|
+
typeof d[1] === 'object' && d[1].hasOwnProperty('entity')) {
|
8427
|
+
// Convert the two variables to vectors.
|
8428
|
+
const vector = {x: {}, y: {}};
|
8429
|
+
for(let k in vector) if(vector.hasOwnProperty(k)) {
|
8430
|
+
const
|
8431
|
+
i = ['x', 'y'].indexOf(k),
|
8432
|
+
e = d[i].entity,
|
8433
|
+
a = d[i].attribute;
|
8434
|
+
vector[k].e = e;
|
8435
|
+
vector[k].a = a;
|
8436
|
+
vector[k].v = e.attributeValue(a);
|
8437
|
+
vector[k].name = e.displayName + (a ? '|' + a : '');
|
8438
|
+
vector[k].id = e.identifier;
|
8439
|
+
// NOTE: Equations can also be passed by reference.
|
8440
|
+
if(e === MODEL.equations_dataset) {
|
8441
|
+
const eq = e.modifiers[UI.nameToID(a)].expression;
|
8442
|
+
// Level-based equations require that the model has run.
|
8443
|
+
if(eq.is_level_based && !MODEL.solved) {
|
8444
|
+
x.retop(VM.NOT_COMPUTED);
|
8445
|
+
return;
|
8446
|
+
}
|
8447
|
+
// NOTE: An equation may not have been (fully) computed yet.
|
8448
|
+
eq.compute(0, x.wildcard_vector_index);
|
8449
|
+
if(!eq.isStatic) {
|
8450
|
+
const nt = MODEL.end_period - MODEL.start_period + 1;
|
8451
|
+
for(let t = 1; t <= nt; t++) eq.result(t, x.wildcard_vector_index);
|
8452
|
+
}
|
8453
|
+
vector[k].v = eq.vector;
|
8454
|
+
}
|
8455
|
+
}
|
8456
|
+
// If either operand is level-based, return "not computed" if the
|
8457
|
+
// model has not been run yet.
|
8458
|
+
if((VM.level_based_attr.indexOf(vector.x.a) >= 0 ||
|
8459
|
+
VM.level_based_attr.indexOf(vector.y.a) >= 0) &&
|
8460
|
+
!MODEL.solved) {
|
8461
|
+
x.retop(VM.NOT_COMPUTED);
|
8462
|
+
return;
|
8463
|
+
}
|
8464
|
+
if(Array.isArray(vector.x.v) && Array.isArray(vector.y.v)) {
|
8465
|
+
// Valid parameters => compute the terms used in the formulas
|
8466
|
+
// for correlation (r) and regression (slope and intercept)
|
8467
|
+
// NOTE: Statistics are not time-dependent, so the result is stored
|
8468
|
+
// in the expression's cache. As expressions may contain several
|
8469
|
+
// correl and slope operators, create a unique key based on the
|
8470
|
+
// operator name and its two operands.
|
8471
|
+
const cache_key = [vmi, vector.x.id, vector.x.a,
|
8472
|
+
vector.y.id, vector.y.a].join('_');
|
8473
|
+
if(x.cache[cache_key]) {
|
8474
|
+
x.retop(x.cache[cache_key]);
|
8475
|
+
return;
|
8476
|
+
}
|
8477
|
+
if(true||DEBUGGING) {
|
8478
|
+
console.log(`-- ${vmi}(${vector.x.name}, ${vector.y.name})`);
|
8479
|
+
}
|
8480
|
+
// NOTE: Vectors should have equal length.
|
8481
|
+
const N = Math.min(vector.x.v.length, vector.y.v.length);
|
8482
|
+
if(!N) {
|
8483
|
+
// No data => result should be "division by zero"
|
8484
|
+
x.retop(VM.DIV_ZERO);
|
8485
|
+
return;
|
8486
|
+
}
|
8487
|
+
// Calculate dsq = N*variance for X and Y.
|
8488
|
+
for(let k in vector) if(vector.hasOwnProperty(k)) {
|
8489
|
+
let sum = 0;
|
8490
|
+
// NOTE: Ignore first element of vector (t=0).
|
8491
|
+
for(let i = 1; i < N; i++) {
|
8492
|
+
const v = vector[k].v[i];
|
8493
|
+
// Handle exceptional values in vector.
|
8494
|
+
if(v <= VM.BEYOND_MINUS_INFINITY || v >= VM.BEYOND_PLUS_INFINITY) {
|
8495
|
+
x.retop(v);
|
8496
|
+
return;
|
8497
|
+
}
|
8498
|
+
sum += v;
|
8499
|
+
}
|
8500
|
+
vector[k].sum = sum;
|
8501
|
+
const mean = sum / N;
|
8502
|
+
vector[k].mean = mean;
|
8503
|
+
let dsq = 0;
|
8504
|
+
// NOTE: Ignore first element of vector (t=0).
|
8505
|
+
for(let i = 1; i < N; i++) {
|
8506
|
+
const d = vector[k].v[i] - mean;
|
8507
|
+
dsq += d * d;
|
8508
|
+
}
|
8509
|
+
vector[k].dsq = dsq;
|
8510
|
+
}
|
8511
|
+
// Divisor is sqrt(dsqX * dsqY). If zero, return #DIV/0
|
8512
|
+
const divisor = Math.sqrt(vector.x.dsq * vector.y.dsq);
|
8513
|
+
if(divisor < VM.NEAR_ZERO) {
|
8514
|
+
x.retop(VM.DIV_ZERO);
|
8515
|
+
return;
|
8516
|
+
}
|
8517
|
+
// Calculate N*covariance of X and Y.
|
8518
|
+
let covar = 0;
|
8519
|
+
// NOTE: Ignore first element of vector (t=0).
|
8520
|
+
for(let i = 1; i < N; i++) {
|
8521
|
+
covar += (vector.x.v[i] - vector.x.mean) * (vector.y.v[i] - vector.y.mean);
|
8522
|
+
}
|
8523
|
+
// Correlation = covarXY / sqrt(dsqX * dsqY), slope = covarXY / dsqX.
|
8524
|
+
// NOTE: dsqX will be non-zero (or divisor would have been zero).
|
8525
|
+
const result = covar / (vmi === 'correl' ? divisor : vector.x.dsq);
|
8526
|
+
// Store the result in the expression's cache.
|
8527
|
+
x.cache[cache_key] = result;
|
8528
|
+
// Push the result onto the stack.
|
8529
|
+
x.retop(result);
|
8530
|
+
return;
|
8531
|
+
}
|
8532
|
+
}
|
8533
|
+
// Fall-trough indicates error
|
8534
|
+
if(DEBUGGING) console.log(vmi + ': invalid parameter(s)\n', d);
|
8535
|
+
x.retop(VM.PARAMS);
|
8536
|
+
}
|
8537
|
+
|
8538
|
+
// NOTE: Separate function for each operator: VMI_correl and VMI_slope.
|
8539
|
+
|
8540
|
+
function VMI_correlation(x) {
|
8541
|
+
correlation_or_slope(x, 'correl');
|
8542
|
+
}
|
8543
|
+
|
8544
|
+
// Add the custom operator instruction to the global lists
|
8545
|
+
// NOTE: All custom operators are monadic (priority 9) and reducing
|
8546
|
+
OPERATORS.push('correl');
|
8547
|
+
MONADIC_OPERATORS.push('correl');
|
8548
|
+
ACTUAL_SYMBOLS.push('correl');
|
8549
|
+
OPERATOR_CODES.push(VMI_correlation);
|
8550
|
+
MONADIC_CODES.push(VMI_correlation);
|
8551
|
+
REDUCING_CODES.push(VMI_correlation);
|
8552
|
+
SYMBOL_CODES.push(VMI_correlation);
|
8553
|
+
PRIORITIES.push(9);
|
8554
|
+
// Add to this list only if operation makes an expression dynamic
|
8555
|
+
// DYNAMIC_SYMBOLS.push('...');
|
8556
|
+
// Add to this list only if operation makes an expression random
|
8557
|
+
// RANDOM_CODES.push(VMI_...);
|
8558
|
+
// Add to this list only if operation makes an expression level-based
|
8559
|
+
// LEVEL_BASED_CODES.push(VMI_...);
|
8560
|
+
|
8561
|
+
function VMI_slope(x) {
|
8562
|
+
correlation_or_slope(x, 'slope');
|
8563
|
+
}
|
8564
|
+
|
8565
|
+
// Add the custom operator instruction to the global lists
|
8566
|
+
// NOTE: All custom operators are monadic (priority 9) and reducing
|
8567
|
+
OPERATORS.push('slope');
|
8568
|
+
MONADIC_OPERATORS.push('slope');
|
8569
|
+
ACTUAL_SYMBOLS.push('slope');
|
8570
|
+
OPERATOR_CODES.push(VMI_slope);
|
8571
|
+
MONADIC_CODES.push(VMI_slope);
|
8572
|
+
REDUCING_CODES.push(VMI_slope);
|
8573
|
+
SYMBOL_CODES.push(VMI_slope);
|
8574
|
+
PRIORITIES.push(9);
|
8575
|
+
// Add to this list only if operation makes an expression dynamic
|
8576
|
+
// DYNAMIC_SYMBOLS.push('...');
|
8577
|
+
// Add to this list only if operation makes an expression random
|
8578
|
+
// RANDOM_CODES.push(VMI_...);
|
8579
|
+
// Add to this list only if operation makes an expression level-based
|
8580
|
+
// LEVEL_BASED_CODES.push(VMI_...);
|
8581
|
+
|
8393
8582
|
/*** END of custom operator API section ***/
|
8394
8583
|
|
8395
8584
|
///////////////////////////////////////////////////////////////////////
|
@@ -8399,4 +8588,4 @@ if(NODE) module.exports = {
|
|
8399
8588
|
Expression: Expression,
|
8400
8589
|
ExpressionParser: ExpressionParser,
|
8401
8590
|
VirtualMachine: VirtualMachine
|
8402
|
-
}
|
8591
|
+
};
|