linny-r 1.9.2 → 2.0.1
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/LICENSE +1 -1
- package/README.md +4 -4
- package/package.json +1 -1
- package/server.js +1 -1
- package/static/images/eq-negated.png +0 -0
- package/static/images/power.png +0 -0
- package/static/images/tex.png +0 -0
- package/static/index.html +225 -10
- package/static/linny-r.css +458 -8
- package/static/scripts/linny-r-ctrl.js +6 -4
- package/static/scripts/linny-r-gui-actor-manager.js +1 -1
- package/static/scripts/linny-r-gui-chart-manager.js +20 -13
- package/static/scripts/linny-r-gui-constraint-editor.js +410 -50
- package/static/scripts/linny-r-gui-controller.js +127 -12
- package/static/scripts/linny-r-gui-dataset-manager.js +28 -20
- package/static/scripts/linny-r-gui-documentation-manager.js +11 -3
- package/static/scripts/linny-r-gui-equation-manager.js +1 -1
- package/static/scripts/linny-r-gui-experiment-manager.js +1 -1
- package/static/scripts/linny-r-gui-expression-editor.js +7 -1
- package/static/scripts/linny-r-gui-file-manager.js +31 -13
- package/static/scripts/linny-r-gui-finder.js +1 -1
- package/static/scripts/linny-r-gui-model-autosaver.js +1 -1
- package/static/scripts/linny-r-gui-monitor.js +1 -1
- package/static/scripts/linny-r-gui-paper.js +108 -25
- package/static/scripts/linny-r-gui-power-grid-manager.js +529 -0
- package/static/scripts/linny-r-gui-receiver.js +1 -1
- package/static/scripts/linny-r-gui-repository-browser.js +1 -1
- package/static/scripts/linny-r-gui-scale-unit-manager.js +1 -1
- package/static/scripts/linny-r-gui-sensitivity-analysis.js +1 -1
- package/static/scripts/linny-r-gui-tex-manager.js +110 -0
- package/static/scripts/linny-r-gui-undo-redo.js +1 -1
- package/static/scripts/linny-r-milp.js +1 -1
- package/static/scripts/linny-r-model.js +1016 -155
- package/static/scripts/linny-r-utils.js +3 -3
- package/static/scripts/linny-r-vm.js +714 -248
- package/static/show-diff.html +1 -1
- package/static/show-png.html +1 -1
@@ -10,7 +10,7 @@ the Linny-R project.
|
|
10
10
|
*/
|
11
11
|
|
12
12
|
/*
|
13
|
-
Copyright (c) 2017-
|
13
|
+
Copyright (c) 2017-2024 Delft University of Technology
|
14
14
|
|
15
15
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
16
16
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -56,6 +56,7 @@ class LinnyRModel {
|
|
56
56
|
this.decimal_comma = CONFIGURATION.decimal_comma;
|
57
57
|
// NOTE: Default scale unit list comprises only the primitive base unit
|
58
58
|
this.scale_units = {'1': new ScaleUnit('1', '1', '1')};
|
59
|
+
this.power_grids = {};
|
59
60
|
this.actors = {};
|
60
61
|
this.products = {};
|
61
62
|
this.processes = {};
|
@@ -81,7 +82,7 @@ class LinnyRModel {
|
|
81
82
|
this.ignored_entities = {};
|
82
83
|
this.black_box = false;
|
83
84
|
this.black_box_entities = {};
|
84
|
-
|
85
|
+
|
85
86
|
// Actor related properties
|
86
87
|
this.actor_list = [];
|
87
88
|
this.rounds = 1;
|
@@ -96,6 +97,7 @@ class LinnyRModel {
|
|
96
97
|
this.look_ahead = 0;
|
97
98
|
this.grid_pixels = 20;
|
98
99
|
this.align_to_grid = true;
|
100
|
+
this.with_power_flow = false;
|
99
101
|
this.infer_cost_prices = false;
|
100
102
|
this.report_results = false;
|
101
103
|
this.show_block_arrows = true;
|
@@ -279,6 +281,32 @@ class LinnyRModel {
|
|
279
281
|
return ok;
|
280
282
|
}
|
281
283
|
|
284
|
+
powerGridByID(id) {
|
285
|
+
// Return power grid identified by hex string `id`.
|
286
|
+
if(this.power_grids.hasOwnProperty(id)) return this.power_grids[id];
|
287
|
+
return null;
|
288
|
+
}
|
289
|
+
|
290
|
+
powerGridByName(n) {
|
291
|
+
// Return power grid identified by name `n`.
|
292
|
+
for(let k in this.power_grids) if(this.power_grids.hasOwnProperty(k)) {
|
293
|
+
const pg = this.power_grids[k];
|
294
|
+
if(ciCompare(pg.name, n) === 0) return pg;
|
295
|
+
}
|
296
|
+
return null;
|
297
|
+
}
|
298
|
+
|
299
|
+
get powerGridsWithKVL() {
|
300
|
+
// Return list of power grids for which Kirchhoff's voltage law must
|
301
|
+
// be enforced.
|
302
|
+
const pgl = [];
|
303
|
+
for(let k in this.power_grids) if(this.power_grids.hasOwnProperty(k)) {
|
304
|
+
const pg = this.power_grids[k];
|
305
|
+
if(pg.kirchhoff) pgl.push(pg);
|
306
|
+
}
|
307
|
+
return pgl;
|
308
|
+
}
|
309
|
+
|
282
310
|
noteByID(id) {
|
283
311
|
// NOTE: Note object identifiers have syntax #cluster name#time stamp#
|
284
312
|
const parts = id.split('#');
|
@@ -812,8 +840,25 @@ class LinnyRModel {
|
|
812
840
|
return -1;
|
813
841
|
}
|
814
842
|
|
843
|
+
validSelector(name) {
|
844
|
+
// Return sanitized selector name, or empty string if invalid.
|
845
|
+
const s = name.replace(/[^a-zA-Z0-9\+\-\%\_\*\?]/g, '');
|
846
|
+
let msg = '';
|
847
|
+
if(s !== name) {
|
848
|
+
msg = UI.WARNING.SELECTOR_SYNTAX;
|
849
|
+
} else if(name.indexOf('*') !== name.lastIndexOf('*')) {
|
850
|
+
// A selector can only contain 1 star.
|
851
|
+
msg = UI.WARNING.SINGLE_WILDCARD;
|
852
|
+
}
|
853
|
+
if(msg) {
|
854
|
+
UI.warn(msg);
|
855
|
+
return '';
|
856
|
+
}
|
857
|
+
return s;
|
858
|
+
}
|
859
|
+
|
815
860
|
isDimensionSelector(s) {
|
816
|
-
//
|
861
|
+
// Return TRUE if `s` is a dimension selector in some experiment.
|
817
862
|
for(let i = 0; i < this.experiments.length; i++) {
|
818
863
|
if(this.experiments[i].isDimensionSelector(s)) return true;
|
819
864
|
}
|
@@ -860,37 +905,51 @@ class LinnyRModel {
|
|
860
905
|
return this.end_period - this.start_period + 1 + this.look_ahead;
|
861
906
|
}
|
862
907
|
|
908
|
+
processSelectorList(sl) {
|
909
|
+
// Checke whether selector list `sl` constitutes a new dimension.
|
910
|
+
// Ignore lists of fewer than 2 "plain" selectors.
|
911
|
+
if(sl.length > 1) {
|
912
|
+
let newdim = true;
|
913
|
+
// Merge into dimension if there are shared selectors.
|
914
|
+
for(let i = 0; i < this.dimensions.length; i++) {
|
915
|
+
const c = complement(sl, this.dimensions[i]);
|
916
|
+
if(c.length < sl.length) {
|
917
|
+
if(c.length > 0) this.dimensions[i].push(...c);
|
918
|
+
newdim = false;
|
919
|
+
break;
|
920
|
+
}
|
921
|
+
}
|
922
|
+
// If only new selectors, add the list as a dimension.
|
923
|
+
if(newdim) {
|
924
|
+
this.dimensions.push(sl);
|
925
|
+
}
|
926
|
+
}
|
927
|
+
}
|
928
|
+
|
863
929
|
inferDimensions() {
|
864
930
|
// Generate the list of dimensions for experimental design.
|
865
931
|
// NOTE: A dimension is a list of one or more relevant selectors.
|
866
|
-
let newdim;
|
867
932
|
this.dimensions.length = 0;
|
868
933
|
// NOTE: Ignore the equations dataset.
|
869
934
|
for(let d in this.datasets) if(this.datasets.hasOwnProperty(d) &&
|
870
935
|
this.datasets[d] !== this.equations_dataset) {
|
936
|
+
// Get the selector list for this dataset.
|
937
|
+
const ds = this.datasets[d];
|
938
|
+
// NOTE: Ignore wildcard selectors!
|
939
|
+
this.processSelectorList(ds.plainSelectors);
|
940
|
+
}
|
941
|
+
// Analyze constraint bound lines in the same way.
|
942
|
+
for(let k in this.constraints) if(this.constraints.hasOwnProperty(k)) {
|
871
943
|
// Get selector list
|
872
|
-
const
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
newdim = true;
|
879
|
-
// Merge into dimension if there are shared selectors
|
880
|
-
for(let i = 0; i < this.dimensions.length; i++) {
|
881
|
-
const c = complement(sl, this.dimensions[i]);
|
882
|
-
if(c.length < sl.length) {
|
883
|
-
if(c.length > 0) this.dimensions[i].push(...c);
|
884
|
-
newdim = false;
|
885
|
-
break;
|
886
|
-
}
|
887
|
-
}
|
888
|
-
// If only new selectors, add the list as a dimension
|
889
|
-
if(newdim) {
|
890
|
-
this.dimensions.push(sl);
|
891
|
-
}
|
944
|
+
const c = this.constraints[k];
|
945
|
+
for(let i = 0; i < c.bound_lines.length; i++) {
|
946
|
+
const sl = c.bound_lines[i].selectorList;
|
947
|
+
// NOTE: Ignore the first selector "(default)".
|
948
|
+
sl.shift();
|
949
|
+
this.processSelectorList(sl);
|
892
950
|
}
|
893
951
|
}
|
952
|
+
|
894
953
|
}
|
895
954
|
|
896
955
|
expandDimension(sl) {
|
@@ -1246,8 +1305,16 @@ class LinnyRModel {
|
|
1246
1305
|
return VM.UNDEFINED;
|
1247
1306
|
}
|
1248
1307
|
|
1308
|
+
addPowerGrid(id, node=null) {
|
1309
|
+
// Add a power grid to the model.
|
1310
|
+
let pg = new PowerGrid(id);
|
1311
|
+
if(node) pg.initFromXML(node);
|
1312
|
+
this.power_grids[id] = pg;
|
1313
|
+
return pg;
|
1314
|
+
}
|
1315
|
+
|
1249
1316
|
addNote(node=null) {
|
1250
|
-
// Add a note to the focal cluster
|
1317
|
+
// Add a note to the focal cluster.
|
1251
1318
|
let n = new Note(this.focal_cluster);
|
1252
1319
|
if(node) n.initFromXML(node);
|
1253
1320
|
this.focal_cluster.notes.push(n);
|
@@ -2667,10 +2734,12 @@ class LinnyRModel {
|
|
2667
2734
|
this.encrypt = nodeParameterValue(node, 'encrypt') === '1';
|
2668
2735
|
this.decimal_comma = nodeParameterValue(node, 'decimal-comma') === '1';
|
2669
2736
|
this.align_to_grid = nodeParameterValue(node, 'align-to-grid') === '1';
|
2737
|
+
this.with_power_flow = nodeParameterValue(node, 'power-flow') === '1';
|
2670
2738
|
this.infer_cost_prices = nodeParameterValue(node, 'cost-prices') === '1';
|
2671
2739
|
this.report_results = nodeParameterValue(node, 'report-results') === '1';
|
2672
2740
|
this.show_block_arrows = nodeParameterValue(node, 'block-arrows') === '1';
|
2673
|
-
|
2741
|
+
// NOTE: Diagnosis option should default to TRUE unless *set* to FALSE.
|
2742
|
+
this.always_diagnose = nodeParameterValue(node, 'diagnose') !== '0';
|
2674
2743
|
this.show_notices = nodeParameterValue(node, 'show-notices') === '1';
|
2675
2744
|
this.name = xmlDecoded(nodeContentByTag(node, 'name'));
|
2676
2745
|
this.author = xmlDecoded(nodeContentByTag(node, 'author'));
|
@@ -2729,6 +2798,16 @@ class LinnyRModel {
|
|
2729
2798
|
}
|
2730
2799
|
}
|
2731
2800
|
}
|
2801
|
+
// Power grids are not "entities", and can be included "as is"
|
2802
|
+
n = childNodeByTag(node, 'powergrids');
|
2803
|
+
if(n && n.childNodes) {
|
2804
|
+
for(i = 0; i < n.childNodes.length; i++) {
|
2805
|
+
c = n.childNodes[i];
|
2806
|
+
if(c.nodeName === 'grid') {
|
2807
|
+
this.addPowerGrid(nodeContentByTag(c, 'id'), c);
|
2808
|
+
}
|
2809
|
+
}
|
2810
|
+
}
|
2732
2811
|
// When including a model, actors may be bound to an existing actor
|
2733
2812
|
n = childNodeByTag(node, 'actors');
|
2734
2813
|
if(n && n.childNodes) {
|
@@ -3017,14 +3096,18 @@ class LinnyRModel {
|
|
3017
3096
|
'" current="', this.current_time_step,
|
3018
3097
|
'" rounds="', this.rounds,
|
3019
3098
|
'" no-actor-round-flags="', this.actors[UI.nameToID(UI.NO_ACTOR)].round_flags,
|
3099
|
+
// NOTE: Add the diagnosis option *explicitly* to differentiate
|
3100
|
+
// between older models saved *without* this option, and newer
|
3101
|
+
// models having this option switched off.
|
3102
|
+
'" diagnose="', (this.always_diagnose ? '1' : '0'),
|
3020
3103
|
'"'].join('');
|
3021
3104
|
if(this.encrypt) p += ' encrypt="1"';
|
3022
3105
|
if(this.decimal_comma) p += ' decimal-comma="1"';
|
3023
3106
|
if(this.align_to_grid) p += ' align-to-grid="1"';
|
3107
|
+
if(this.with_power_flow) p += ' power-flow="1"';
|
3024
3108
|
if(this.infer_cost_prices) p += ' cost-prices="1"';
|
3025
3109
|
if(this.report_results) p += ' report-results="1"';
|
3026
3110
|
if(this.show_block_arrows) p += ' block-arrows="1"';
|
3027
|
-
if(this.always_diagnose) p += ' diagnose="1"';
|
3028
3111
|
if(this.show_notices) p += ' show-notices="1"';
|
3029
3112
|
let xml = this.xml_header + ['<model', p, '><name>', xmlEncoded(this.name),
|
3030
3113
|
'</name><author>', xmlEncoded(this.author),
|
@@ -3050,7 +3133,11 @@ class LinnyRModel {
|
|
3050
3133
|
for(obj in this.scale_units) if(this.scale_units.hasOwnProperty(obj)) {
|
3051
3134
|
xml += this.scale_units[obj].asXML;
|
3052
3135
|
}
|
3053
|
-
xml += '</scaleunits><
|
3136
|
+
xml += '</scaleunits><powergrids>';
|
3137
|
+
for(obj in this.power_grids) if(this.power_grids.hasOwnProperty(obj)) {
|
3138
|
+
xml += this.power_grids[obj].asXML;
|
3139
|
+
}
|
3140
|
+
xml += '</powergrids><actors>';
|
3054
3141
|
for(obj in this.actors) {
|
3055
3142
|
// NOTE: do not to save "(no actor)"
|
3056
3143
|
if(this.actors.hasOwnProperty(obj) && obj != UI.nameToID(UI.NO_ACTOR)) {
|
@@ -3471,8 +3558,9 @@ class LinnyRModel {
|
|
3471
3558
|
let n = 0;
|
3472
3559
|
for(let i = 0; i < constraints.length; i++) {
|
3473
3560
|
const c = constraints[i];
|
3474
|
-
if(
|
3475
|
-
(c.
|
3561
|
+
if(!MODEL.ignored_entities[c.identifier] &&
|
3562
|
+
((c.to_node === p && c.soc_direction === VM.SOC_X_Y) ||
|
3563
|
+
(c.from_node === p && c.soc_direction === VM.SOC_Y_X))) n++;
|
3476
3564
|
}
|
3477
3565
|
return n;
|
3478
3566
|
},
|
@@ -3484,7 +3572,8 @@ class LinnyRModel {
|
|
3484
3572
|
for(let i = 0; i < p.inputs.length; i++) {
|
3485
3573
|
const l = p.inputs[i];
|
3486
3574
|
// NOTE: Only process --> product links can carry cost.
|
3487
|
-
if(l.
|
3575
|
+
if(!MODEL.ignored_entities[l.identifier] &&
|
3576
|
+
l.from_node instanceof Process) {
|
3488
3577
|
tuple.n++;
|
3489
3578
|
if(l.share_of_cost === 0) tuple.nosoc++;
|
3490
3579
|
const d = l.actualDelay(t);
|
@@ -3656,12 +3745,15 @@ class LinnyRModel {
|
|
3656
3745
|
const p = processes[i];
|
3657
3746
|
let cp = 0;
|
3658
3747
|
for(let j = 0; j < p.inputs.length; j++) {
|
3659
|
-
const
|
3660
|
-
if(
|
3661
|
-
|
3662
|
-
|
3663
|
-
|
3664
|
-
|
3748
|
+
const l = p.inputs[j];
|
3749
|
+
if(!MODEL.ignored_entities[l.identifier]) {
|
3750
|
+
const ucp = l.unit_cost_price;
|
3751
|
+
if(ucp === VM.UNDEFINED) {
|
3752
|
+
cp = VM.UNDEFINED;
|
3753
|
+
break;
|
3754
|
+
} else {
|
3755
|
+
cp += ucp;
|
3756
|
+
}
|
3665
3757
|
}
|
3666
3758
|
}
|
3667
3759
|
// NOTE: Also check constraints that transfer cost to `p`.
|
@@ -3682,31 +3774,33 @@ class LinnyRModel {
|
|
3682
3774
|
// NOTE: ignore SoC, as this affects the CP of the product, but
|
3683
3775
|
// NOT the CP of the process producing it.
|
3684
3776
|
for(let j = 0; j < p.outputs.length; j++) {
|
3685
|
-
const
|
3686
|
-
|
3687
|
-
|
3688
|
-
|
3689
|
-
|
3690
|
-
|
3691
|
-
|
3692
|
-
|
3693
|
-
|
3694
|
-
|
3695
|
-
|
3696
|
-
|
3697
|
-
|
3698
|
-
|
3699
|
-
|
3700
|
-
|
3701
|
-
|
3702
|
-
|
3703
|
-
|
3704
|
-
|
3705
|
-
|
3706
|
-
|
3707
|
-
|
3708
|
-
|
3709
|
-
|
3777
|
+
const l = p.outputs[j];
|
3778
|
+
if(!MODEL.ignored_entities[l.identifier]) {
|
3779
|
+
const
|
3780
|
+
// NOTE: For output links always use current price.
|
3781
|
+
px = l.to_node.price,
|
3782
|
+
pr = (px.defined ? px.result(t) : 0),
|
3783
|
+
// For levels, consider delay: earlier if delay > 0.
|
3784
|
+
dt = t - l.actualDelay(t);
|
3785
|
+
if(pr < 0) {
|
3786
|
+
// Only consider negative prices.
|
3787
|
+
if(l.multiplier === VM.LM_LEVEL) {
|
3788
|
+
// Treat links with level multiplier similar to input links,
|
3789
|
+
// as this computes CP even when actual level = 0.
|
3790
|
+
// NOTE: Subtract (!) so as to ADD the cost.
|
3791
|
+
cp -= pr * l.relative_rate.result(dt);
|
3792
|
+
} else {
|
3793
|
+
// For other types, multiply price by actual flow / level
|
3794
|
+
// NOTE: actualFlow already considers delay => use t, not dt.
|
3795
|
+
const af = l.actualFlow(t);
|
3796
|
+
if(af > VM.NEAR_ZERO) {
|
3797
|
+
// Prevent division by zero.
|
3798
|
+
// NOTE: Level can be zero even if actual flow > 0!
|
3799
|
+
let al = p.nonZeroLevel(dt, l.multiplier);
|
3800
|
+
// NOTE: Scale to level only when level > 1, or fixed
|
3801
|
+
// costs for start-up or first commit will be amplified.
|
3802
|
+
if(al > VM.NEAR_ZERO) cp -= pr * af / Math.max(al, 1);
|
3803
|
+
}
|
3710
3804
|
}
|
3711
3805
|
}
|
3712
3806
|
}
|
@@ -3767,7 +3861,8 @@ class LinnyRModel {
|
|
3767
3861
|
cp_sccp = VM.COMPUTING;
|
3768
3862
|
for(let j = 0; j < p.inputs.length; j++) {
|
3769
3863
|
const l = p.inputs[j];
|
3770
|
-
if(l.
|
3864
|
+
if(!MODEL.ignored_entities[l.identifier] &&
|
3865
|
+
l.from_node instanceof Process) {
|
3771
3866
|
cp = l.from_node.costPrice(t - l.actualDelay(t));
|
3772
3867
|
if(cp === VM.UNDEFINED && l.share_of_cost > 0) {
|
3773
3868
|
// Contributing CP still unknown => break from FOR loop.
|
@@ -4757,11 +4852,97 @@ class ScaleUnit {
|
|
4757
4852
|
}
|
4758
4853
|
}
|
4759
4854
|
|
4855
|
+
// CLASS PowerGrid
|
4856
|
+
class PowerGrid {
|
4857
|
+
constructor(id) {
|
4858
|
+
// NOTE: PowerGrids are uniquely identified by a hexadecimal string.
|
4859
|
+
this.id = id;
|
4860
|
+
this.name = '';
|
4861
|
+
this.comments = '';
|
4862
|
+
// Default color is blue (arbitrary choice).
|
4863
|
+
this.color = '#0000ff';
|
4864
|
+
// Default voltage is 150 kV (arbitrary choice).
|
4865
|
+
this.kilovolts = 150;
|
4866
|
+
// Default power unit is MW (will be used as unit of grid nodes)
|
4867
|
+
this.power_unit = 'MW';
|
4868
|
+
this.kirchhoff = true;
|
4869
|
+
// Loss approximation can be 0 (no losses), 1 (linear with load),
|
4870
|
+
// 2 (quadratic, approximated with two linear tangents) or
|
4871
|
+
// 3 (quadratic, approximated with three linear tangents).
|
4872
|
+
this.loss_approximation = 0;
|
4873
|
+
}
|
4874
|
+
|
4875
|
+
get type() {
|
4876
|
+
return 'Grid';
|
4877
|
+
}
|
4878
|
+
|
4879
|
+
get displayName() {
|
4880
|
+
return this.name;
|
4881
|
+
}
|
4882
|
+
|
4883
|
+
get voltage() {
|
4884
|
+
// Return the voltage in the most compact human-readable notation.
|
4885
|
+
const kv = this.kilovolts;
|
4886
|
+
if(kv < 0.5) return Math.round(kv * 1000) + ' V';
|
4887
|
+
if(kv < 10) return kv.toPrecision(2) + ' kV';
|
4888
|
+
if(kv >= 999.5) return (kv * 0.001).toPrecision(2) + ' MV';
|
4889
|
+
return Math.round(kv) + ' kV';
|
4890
|
+
}
|
4891
|
+
|
4892
|
+
// NOTE: The functions below are based on Birchfield et al. (2017)
|
4893
|
+
// A Metric-Based Validation Process to Assess the Realism of Synthetic
|
4894
|
+
// Power Grids. Energies, 10(1233). DOI: 10.3390/en10081233.
|
4895
|
+
// Table 3 in this paper lists for 6 voltage levels (115 - 500 kV) the
|
4896
|
+
// median of per unit per km reactance, and Table 4 lists for the same
|
4897
|
+
// voltage levels the median of X/R ratios.
|
4898
|
+
|
4899
|
+
get reactancePerKm() {
|
4900
|
+
// Approximate resistance based on reactance per km and voltage.
|
4901
|
+
// The curve 28 * (kV ^ -1.9) fits quite well on the Table 3 data.
|
4902
|
+
return 28 * Math.pow(this.kilovolts, -1.9);
|
4903
|
+
}
|
4904
|
+
|
4905
|
+
get resistancePerKm() {
|
4906
|
+
// Approximate resistance based on reactance per km and voltage.
|
4907
|
+
// The line 0.032 * kV + 1 fits quite well on the Table 4 data.
|
4908
|
+
// NOTE: Scaled by 0.003 because approximation gave unrealistic
|
4909
|
+
// high losses. This is a "quick fix" for lack of better knowledge.
|
4910
|
+
return 0.003 * this.reactancePerKm / (0.032 * this.kilovolts + 1);
|
4911
|
+
}
|
4912
|
+
|
4913
|
+
get asXML() {
|
4914
|
+
let p = ` loss-approximation="${this.loss_approximation}"`;
|
4915
|
+
if(this.kirchhoff) p += ' kirchhoff="1"';
|
4916
|
+
return ['<grid', p, '><id>', this.id,
|
4917
|
+
'</id><name>', xmlEncoded(this.name),
|
4918
|
+
'</name><notes>', xmlEncoded(this.comments),
|
4919
|
+
'</notes><color>', this.color,
|
4920
|
+
'</color><kilovolts>', this.kilovolts,
|
4921
|
+
'</kilovolts><power-unit>', this.power_unit,
|
4922
|
+
'</power-unit></grid>'].join('');
|
4923
|
+
}
|
4924
|
+
|
4925
|
+
initFromXML(node) {
|
4926
|
+
this.loss_approximation = safeStrToInt(
|
4927
|
+
nodeParameterValue(node, 'loss-approximation'), 0);
|
4928
|
+
this.name = xmlDecoded(nodeContentByTag(node, 'name'));
|
4929
|
+
this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
|
4930
|
+
this.kirchhoff = nodeParameterValue(node, 'kirchhoff') === '1';
|
4931
|
+
this.color = nodeContentByTag(node, 'color');
|
4932
|
+
this.kilovolts = safeStrToFloat(nodeContentByTag(node, 'kilovolts'), 150);
|
4933
|
+
this.power_unit = nodeContentByTag(node, 'power-unit') || 'MW';
|
4934
|
+
}
|
4935
|
+
|
4936
|
+
} // END of class PowerGrid
|
4937
|
+
|
4938
|
+
|
4760
4939
|
// CLASS Actor
|
4761
4940
|
class Actor {
|
4762
4941
|
constructor(name) {
|
4763
4942
|
this.name = name;
|
4764
4943
|
this.comments = '';
|
4944
|
+
// By default, actors are labeled in formulas with the letter a.
|
4945
|
+
this.TEX_id = 'a';
|
4765
4946
|
// Actors have 1 input attribute: W
|
4766
4947
|
this.weight = new Expression(this, 'W', '1');
|
4767
4948
|
// Actors have 3 result attributes: CF, CI and CO
|
@@ -4815,11 +4996,13 @@ class Actor {
|
|
4815
4996
|
return ['<actor round-flags="', this.round_flags,
|
4816
4997
|
'"><name>', xmlEncoded(this.name),
|
4817
4998
|
'</name><notes>', xmlEncoded(this.comments),
|
4818
|
-
'</notes><
|
4999
|
+
'</notes><tex-id>', this.TEX_id,
|
5000
|
+
'</tex-id><weight>', this.weight.asXML,
|
4819
5001
|
'</weight></actor>'].join('');
|
4820
5002
|
}
|
4821
5003
|
|
4822
5004
|
initFromXML(node) {
|
5005
|
+
this.TEX_id = xmlDecoded(nodeContentByTag(node, 'tex-id') || 'a');
|
4823
5006
|
this.weight.text = xmlDecoded(nodeContentByTag(node, 'weight'));
|
4824
5007
|
if(IO_CONTEXT) IO_CONTEXT.rewrite(this.weight);
|
4825
5008
|
this.comments = nodeContentByTag(node, 'notes');
|
@@ -5443,12 +5626,12 @@ class NodeBox extends ObjectWithXYWH {
|
|
5443
5626
|
}
|
5444
5627
|
|
5445
5628
|
get infoLineName() {
|
5446
|
-
//
|
5629
|
+
// Return display name plus VM variable indices when debugging.
|
5447
5630
|
let n = this.displayName;
|
5448
|
-
// NOTE: Display nothing if entity is "black-boxed"
|
5631
|
+
// NOTE: Display nothing if entity is "black-boxed".
|
5449
5632
|
if(n.startsWith(UI.BLACK_BOX)) return '';
|
5450
5633
|
n = `<em>${this.type}:</em> ${n}`;
|
5451
|
-
// For clusters, add how many processes and products they contain
|
5634
|
+
// For clusters, add how many processes and products they contain.
|
5452
5635
|
if(this instanceof Cluster) {
|
5453
5636
|
let d = '';
|
5454
5637
|
if(this.all_processes) {
|
@@ -5459,7 +5642,19 @@ class NodeBox extends ObjectWithXYWH {
|
|
5459
5642
|
}
|
5460
5643
|
if(d) n += `<span class="node-details">${d}</span>`;
|
5461
5644
|
}
|
5462
|
-
if(
|
5645
|
+
if(!MODEL.solved) return n;
|
5646
|
+
const g = this.grid;
|
5647
|
+
if(g) {
|
5648
|
+
const
|
5649
|
+
ub = VM.sig4Dig(this.upper_bound.result(MODEL.t)),
|
5650
|
+
l = this.length_in_km,
|
5651
|
+
x = VM.sig4Dig(g.reactancePerKm * l, true),
|
5652
|
+
r = VM.sig4Dig(g.resistancePerKm * l, true),
|
5653
|
+
alr = VM.sig4Dig(this.actualLossRate(MODEL.t) * 100, true);
|
5654
|
+
n += ` [${g.name}] ${ub} ${g.power_unit}, ${l} km,` +
|
5655
|
+
` x = ${x}, R = ${r}, loss rate = ${alr}%`;
|
5656
|
+
}
|
5657
|
+
if(DEBUGGING) {
|
5463
5658
|
n += ' [';
|
5464
5659
|
if(this instanceof Process || this instanceof Product) {
|
5465
5660
|
n += this.level_var_index;
|
@@ -5551,6 +5746,15 @@ class NodeBox extends ObjectWithXYWH {
|
|
5551
5746
|
// Change this object's name and actor.
|
5552
5747
|
this.actor = MODEL.addActor(actor_name);
|
5553
5748
|
this.name = name;
|
5749
|
+
// Ensure that actor cash flow data products have valid properties
|
5750
|
+
if(this.name.startsWith('$')) {
|
5751
|
+
this.scale_unit = MODEL.currency_unit;
|
5752
|
+
this.initial_level.text = '';
|
5753
|
+
this.price.text = '';
|
5754
|
+
this.is_data = true;
|
5755
|
+
this.is_buffer = false;
|
5756
|
+
this.integer_level = false;
|
5757
|
+
}
|
5554
5758
|
// Update actor list in case some actor name is no longer used.
|
5555
5759
|
MODEL.cleanUpActors();
|
5556
5760
|
MODEL.replaceEntityInExpressions(old_name, this.displayName);
|
@@ -7330,9 +7534,12 @@ class Node extends NodeBox {
|
|
7330
7534
|
}
|
7331
7535
|
|
7332
7536
|
get needsOnOffData() {
|
7333
|
-
//
|
7537
|
+
// Return TRUE if this node requires a binary ON/OFF variable.
|
7334
7538
|
// This means that at least one output link must have the "start-up",
|
7335
|
-
// "positive", "zero"
|
7539
|
+
// "positive", "zero", "shut-down", "spinning reserve" or
|
7540
|
+
// "first commit" multiplier.
|
7541
|
+
// NOTE: As of version 2.0.0, power grid processes also need ON/OFF.
|
7542
|
+
if(this.grid) return true;
|
7336
7543
|
for(let i = 0; i < this.outputs.length; i++) {
|
7337
7544
|
if(VM.LM_NEEDING_ON_OFF.indexOf(this.outputs[i].multiplier) >= 0) {
|
7338
7545
|
return true;
|
@@ -7341,8 +7548,19 @@ class Node extends NodeBox {
|
|
7341
7548
|
return false;
|
7342
7549
|
}
|
7343
7550
|
|
7551
|
+
get needsIsZeroData() {
|
7552
|
+
// Return TRUE if this node requires a binary IS ZERO variable.
|
7553
|
+
// This means that at least one output link must have the "zero"
|
7554
|
+
// multiplier.
|
7555
|
+
for(let i = 0; i < this.outputs.length; i++) {
|
7556
|
+
if(this.outputs[i].multiplier === VM.LM_ZERO) return true;
|
7557
|
+
}
|
7558
|
+
return false;
|
7559
|
+
}
|
7560
|
+
|
7344
7561
|
get needsStartUpData() {
|
7345
|
-
//
|
7562
|
+
// Return TRUE iff this node has an output data link for start-up
|
7563
|
+
// or first commit.
|
7346
7564
|
for(let i = 0; i < this.outputs.length; i++) {
|
7347
7565
|
const m = this.outputs[i].multiplier;
|
7348
7566
|
if(m === VM.LM_STARTUP || m === VM.LM_FIRST_COMMIT) return true;
|
@@ -7351,16 +7569,15 @@ class Node extends NodeBox {
|
|
7351
7569
|
}
|
7352
7570
|
|
7353
7571
|
get needsShutDownData() {
|
7354
|
-
//
|
7572
|
+
// Return TRUE iff this node has an output data link for shut-down.
|
7355
7573
|
for(let i = 0; i < this.outputs.length; i++) {
|
7356
|
-
|
7357
|
-
if(m === VM.LM_SHUTDOWN) return true;
|
7574
|
+
if(this.outputs[i].multiplier === VM.LM_SHUTDOWN) return true;
|
7358
7575
|
}
|
7359
7576
|
return false;
|
7360
7577
|
}
|
7361
7578
|
|
7362
7579
|
get needsFirstCommitData() {
|
7363
|
-
//
|
7580
|
+
// Return TRUE iff this node has an output data link for first commit.
|
7364
7581
|
for(let i = 0; i < this.outputs.length; i++) {
|
7365
7582
|
if(this.outputs[i].multiplier === VM.LM_FIRST_COMMIT) return true;
|
7366
7583
|
}
|
@@ -7368,7 +7585,7 @@ class Node extends NodeBox {
|
|
7368
7585
|
}
|
7369
7586
|
|
7370
7587
|
get linksToFirstCommitDataProduct() {
|
7371
|
-
//
|
7588
|
+
// Return data product P iff this node has an output link to P, and P has
|
7372
7589
|
// an output link for first commit
|
7373
7590
|
for(let i = 0; i < this.outputs.length; i++) {
|
7374
7591
|
const p = this.outputs[i].to_node;
|
@@ -7387,7 +7604,12 @@ class Node extends NodeBox {
|
|
7387
7604
|
}
|
7388
7605
|
|
7389
7606
|
setPredecessors() {
|
7390
|
-
// Recursive function to create list of all nodes that precede this one
|
7607
|
+
// Recursive function to create list of all nodes that precede this one.
|
7608
|
+
// NOTE: As of version 2.0.0, feedback links are no longer displayed
|
7609
|
+
// as such. To permit re-enabling this function, the functional part
|
7610
|
+
// of this method has been commented out.
|
7611
|
+
|
7612
|
+
/*
|
7391
7613
|
for(let i = 0; i < this.inputs.length; i++) {
|
7392
7614
|
const l = this.inputs[i];
|
7393
7615
|
if(!l.visited) {
|
@@ -7405,6 +7627,7 @@ class Node extends NodeBox {
|
|
7405
7627
|
}
|
7406
7628
|
}
|
7407
7629
|
}
|
7630
|
+
*/
|
7408
7631
|
return this.predecessors;
|
7409
7632
|
}
|
7410
7633
|
|
@@ -7549,6 +7772,99 @@ class Node extends NodeBox {
|
|
7549
7772
|
}
|
7550
7773
|
return nn;
|
7551
7774
|
}
|
7775
|
+
|
7776
|
+
get TEXforBinaries() {
|
7777
|
+
// Return LaTeX code for formulas that compute binary variables.
|
7778
|
+
// In the equations, binary variables are underlined to distinguish
|
7779
|
+
// them from (semi-)continuous variables.
|
7780
|
+
const
|
7781
|
+
NL = '\\\\\n',
|
7782
|
+
tex = [NL],
|
7783
|
+
x = (this.level_to_zero ? '\\hat{x}' : 'x'),
|
7784
|
+
sub = (MODEL.start_period !== MODEL.end_period ?
|
7785
|
+
'_{' + this.code + ',t}' : '_' + this.code),
|
7786
|
+
sub_1 = '_{' + this.code +
|
7787
|
+
(MODEL.start_period !== MODEL.end_period ? ',t-1}' : ',0}');
|
7788
|
+
if(this.needsOnOffData) {
|
7789
|
+
// Equations to compute OO variable (denoted as u for "up").
|
7790
|
+
// (a) L[t] - LB[t]*OO[t] >= 0
|
7791
|
+
tex.push(x + sub, '- LB' + sub, '\\mathbf{u}' + sub, '\\ge 0', NL);
|
7792
|
+
// (b) L[t] - UB[t]*OO[t] <= 0
|
7793
|
+
tex.push(x + sub, '- UB' + sub, '\\mathbf{u}' + sub, '\\le 0', NL);
|
7794
|
+
}
|
7795
|
+
if(this.needsIsZeroData) {
|
7796
|
+
// Equation to compute IZ variable (denoted as d for "down").
|
7797
|
+
// (c) OO[t] + IZ[t] = 1
|
7798
|
+
tex.push('\\mathbf{u}' + sub, '+ \\mathbf{d}' + sub, '= 0', NL);
|
7799
|
+
// (d) L[t] + IZ[t] >= LB[t]
|
7800
|
+
// NOTE: for semicontinuous variables, use 0 instead of LB[t]
|
7801
|
+
tex.push(x + sub, '+ \\mathbf{d}' + sub, '\\ge',
|
7802
|
+
(this.level_to_zero ? '0' : 'LB' + sub), NL);
|
7803
|
+
}
|
7804
|
+
if(this.needsStartUpData) {
|
7805
|
+
// Equation to compute start-up variable (denoted as su).
|
7806
|
+
// (e) OO[t-1] - OO[t] + SU[t] >= 0
|
7807
|
+
tex.push('\\mathbf{u}' + sub_1, '- \\mathbf{u}' + sub,
|
7808
|
+
'+ \\mathbf{su}' + sub, '\\ge 0', NL);
|
7809
|
+
// (f) OO[t] - SU[t] >= 0
|
7810
|
+
tex.push('\\mathbf{u}' + sub, '- \\mathbf{su}' + sub, '\\ge 0', NL);
|
7811
|
+
// (g) OO[t-1] + OO[t] + SU[t] <= 2
|
7812
|
+
tex.push('\\mathbf{u}' + sub_1, '+ \\mathbf{u}' + sub,
|
7813
|
+
'+ \\mathbf{su}' + sub, '\\le 2', NL);
|
7814
|
+
}
|
7815
|
+
if(this.needsShutDownData) {
|
7816
|
+
// Equation to compute shutdown variable (denoted as sd-circumflex).
|
7817
|
+
// (e2) OO[t] - OO[t-1] + SD[t] >= 0
|
7818
|
+
tex.push('\\mathbf{u}' + sub, '- \\mathbf{u}' + sub_1,
|
7819
|
+
'+ \\mathbf{sd}' + sub, '\\ge 0', NL);
|
7820
|
+
// (f2) OO[t] + SD[t] <= 1
|
7821
|
+
tex.push('\\mathbf{u}' + sub, '+ \\mathbf{sd}' + sub, '\\le 1', NL);
|
7822
|
+
// (g2) SD[t] - OO[t-1] - OO[t] <= 0
|
7823
|
+
tex.push('\\mathbf{sd}' + sub, '- \\mathbf{u}' + sub_1,
|
7824
|
+
'- \\mathbf{su}' + sub, '\\le 0', NL);
|
7825
|
+
}
|
7826
|
+
if(this.needsFirstCommitData) {
|
7827
|
+
// Equation to compute first commit variables (denoted as fc).
|
7828
|
+
// To detect a first commit, start-ups are counted using an extra
|
7829
|
+
// variable SC (denoted as suc) and then similar equations are
|
7830
|
+
// added to detect "start-up" for this counter. This means one more
|
7831
|
+
// binary SO (denoted as suo for "start-up occurred").
|
7832
|
+
// (h) SC[t] - SC[t-1] - SU[t] = 0
|
7833
|
+
tex.push('\\mathbf{suc}' + sub, '- \\mathbf{suc}' + sub_1,
|
7834
|
+
'- \\mathbf{su}' + sub, '= 0', NL);
|
7835
|
+
// (i) SC[t] - SO[t] >= 0
|
7836
|
+
tex.push('\\mathbf{suc}' + sub, '- \\mathbf{suo}' + sub, '\\ge 0', NL);
|
7837
|
+
// (j) SC[t] - run length * SO[t] <= 0
|
7838
|
+
tex.push('\\mathbf{suc}' + sub, '- N\\! \\mathbf{suo}' + sub, '\\le 0', NL);
|
7839
|
+
// (k) SO[t-1] - SO[t] + FC[t] >= 0
|
7840
|
+
tex.push('\\mathbf{suo}' + sub_1, '- \\mathbf{suc}' + sub,
|
7841
|
+
'+ \\mathbf{fc}' + sub, '\\ge 0', NL);
|
7842
|
+
// (l) SO[t] - FC[t] >= 0
|
7843
|
+
tex.push('\\mathbf{suo}' + sub, '- \\mathbf{fc}' + sub, '\\ge 0', NL);
|
7844
|
+
// (m) SO[t-1] + SO[t] + FC[t] <= 2
|
7845
|
+
tex.push('\\mathbf{suo}' + sub_1, '+ \\mathbf{suo}' + sub,
|
7846
|
+
'+ \\mathbf{fc}' + sub, '\\le 2', NL);
|
7847
|
+
}
|
7848
|
+
|
7849
|
+
/*
|
7850
|
+
To calculate the peak increase values, we need two continuous
|
7851
|
+
"chunk variables", i.e., only 1 tableau column per chunk, not 1 for
|
7852
|
+
each time step. These variables BPI and CPI will compute the highest
|
7853
|
+
value (for all t in the block (B) and for the chunk (C)) of the
|
7854
|
+
difference L[t] - block peak (BP) of previous block. This requires
|
7855
|
+
one equation for every t = 1, ..., block length:
|
7856
|
+
(n) L[t] - BPI[b] <= BP[b-1] (where b denotes the block number)
|
7857
|
+
plus one equation for every t = block length + 1 to chunk length:
|
7858
|
+
(o) L[t] - BPI[b] - CPI[b] <= BP[b-1]
|
7859
|
+
This ensures that CPI is the *additional* increase in the look-ahead
|
7860
|
+
Then use BPI[b] in first time step if block, and CPI[b] at first
|
7861
|
+
time step of the look-ahead period to compute the actual flow for
|
7862
|
+
the "peak increase" links. For all other time steps this AF equals 0.
|
7863
|
+
|
7864
|
+
*/
|
7865
|
+
|
7866
|
+
return tex.join(' ');
|
7867
|
+
}
|
7552
7868
|
|
7553
7869
|
} // END of class Node
|
7554
7870
|
|
@@ -7557,6 +7873,8 @@ class Node extends NodeBox {
|
|
7557
7873
|
class Process extends Node {
|
7558
7874
|
constructor(cluster, name, actor) {
|
7559
7875
|
super(cluster, name, actor);
|
7876
|
+
// By default, processes have the letter p, products the letter q.
|
7877
|
+
this.TEX_id = 'p';
|
7560
7878
|
// NOTE: A process can change level once in PACE steps (default 1/1).
|
7561
7879
|
// This means that for a simulation perio of N time steps, this process will
|
7562
7880
|
// have a vector of only N / PACE decision variables (plus associated
|
@@ -7572,6 +7890,11 @@ class Process extends Node {
|
|
7572
7890
|
this.level_to_zero = false;
|
7573
7891
|
// Process node can be collapsed to take up less space in the diagram
|
7574
7892
|
this.collapsed = false;
|
7893
|
+
// Process can represent a power grid element, in which case it needs
|
7894
|
+
// properties for calculating power flow.
|
7895
|
+
this.power_grid = null;
|
7896
|
+
this.length_in_km = 0;
|
7897
|
+
this.reactance = 0;
|
7575
7898
|
// Processes have 3 more result attributes: CP, CF, CI and CO
|
7576
7899
|
this.cash_flow = [];
|
7577
7900
|
this.cash_in = [];
|
@@ -7591,6 +7914,36 @@ class Process extends Node {
|
|
7591
7914
|
get typeLetter() {
|
7592
7915
|
return 'P';
|
7593
7916
|
}
|
7917
|
+
|
7918
|
+
get grid() {
|
7919
|
+
if(MODEL.with_power_flow) return this.power_grid;
|
7920
|
+
return null;
|
7921
|
+
}
|
7922
|
+
|
7923
|
+
get gridEdge() {
|
7924
|
+
// Return "FROM node -> TO node" as string if this is a grid process.
|
7925
|
+
const g = this.grid;
|
7926
|
+
if(!g) return '';
|
7927
|
+
let fn = null,
|
7928
|
+
tn = null;
|
7929
|
+
for(let i = 0; i < this.inputs.length; i++) {
|
7930
|
+
const l = this.inputs[i];
|
7931
|
+
if(l.multiplier == VM.LM_LEVEL) {
|
7932
|
+
fn = l.from_node;
|
7933
|
+
break;
|
7934
|
+
}
|
7935
|
+
}
|
7936
|
+
for(let i = 0; i < this.outputs.length; i++) {
|
7937
|
+
const l = this.outputs[i];
|
7938
|
+
if(l.multiplier == VM.LM_LEVEL && !l.to_node.is_data) {
|
7939
|
+
tn = l.from_node;
|
7940
|
+
break;
|
7941
|
+
}
|
7942
|
+
}
|
7943
|
+
fn = (fn ? fn.displayName : '???');
|
7944
|
+
tn = (tn ? tn.displayName : '???');
|
7945
|
+
return `${fn} ${UI.LINK_ARROW} ${tn}`;
|
7946
|
+
}
|
7594
7947
|
|
7595
7948
|
get attributes() {
|
7596
7949
|
const a = {name: this.displayName};
|
@@ -7641,20 +7994,25 @@ class Process extends Node {
|
|
7641
7994
|
if(this.integer_level) p += ' integer-level="1"';
|
7642
7995
|
if(this.level_to_zero) p += ' level-to-zero="1"';
|
7643
7996
|
if(this.equal_bounds) p += ' equal-bounds="1"';
|
7997
|
+
// NOTE: Save power grid related properties even when grid element
|
7998
|
+
// is not checked (so properties re-appear when re-checked).
|
7644
7999
|
return ['<process', p, '><name>', xmlEncoded(n),
|
7645
8000
|
'</name><owner>', xmlEncoded(this.actor.name),
|
7646
8001
|
'</owner><notes>', cmnts,
|
7647
8002
|
'</notes><upper-bound>', this.upper_bound.asXML,
|
7648
8003
|
'</upper-bound><lower-bound>', this.lower_bound.asXML,
|
7649
8004
|
'</lower-bound><initial-level>', this.initial_level.asXML,
|
7650
|
-
'</initial-level><
|
7651
|
-
'</pace
|
8005
|
+
'</initial-level><tex-id>', this.TEX_id,
|
8006
|
+
'</tex-id><pace>', this.pace_expression.asXML,
|
8007
|
+
'</pace><grid-id>', (this.power_grid ? this.power_grid.id : ''),
|
8008
|
+
'</grid-id><length>', this.length_in_km,
|
8009
|
+
'</length><x-coord>', x,
|
7652
8010
|
'</x-coord><y-coord>', y,
|
7653
8011
|
'</y-coord></process>'].join('');
|
7654
8012
|
}
|
7655
8013
|
|
7656
8014
|
initFromXML(node) {
|
7657
|
-
// NOTE:
|
8015
|
+
// NOTE: Do not set code while importing, as new code must be assigned!
|
7658
8016
|
if(!IO_CONTEXT) this.code = nodeParameterValue(node, 'code');
|
7659
8017
|
this.collapsed = nodeParameterValue(node, 'collapsed') === '1';
|
7660
8018
|
this.integer_level = nodeParameterValue(node, 'integer-level') === '1';
|
@@ -7664,23 +8022,28 @@ class Process extends Node {
|
|
7664
8022
|
this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
|
7665
8023
|
this.lower_bound.text = xmlDecoded(nodeContentByTag(node, 'lower-bound'));
|
7666
8024
|
this.upper_bound.text = xmlDecoded(nodeContentByTag(node, 'upper-bound'));
|
7667
|
-
// legacy models can have LB and UB hexadecimal data strings
|
8025
|
+
// legacy models can have LB and UB hexadecimal data strings.
|
7668
8026
|
this.convertLegacyBoundData(nodeContentByTag(node, 'lower-bound-data'),
|
7669
8027
|
nodeContentByTag(node, 'upper-bound-data'));
|
7670
8028
|
if(nodeParameterValue(node, 'reversible') === '1') {
|
7671
|
-
// For legacy "reversible" processes, the LB is set to -UB
|
8029
|
+
// For legacy "reversible" processes, the LB is set to -UB.
|
7672
8030
|
this.lower_bound.text = '-' + this.upper_bound.text;
|
7673
8031
|
}
|
7674
|
-
// NOTE:
|
8032
|
+
// NOTE: Legacy models have no initial level field => default to 0.
|
7675
8033
|
const ilt = xmlDecoded(nodeContentByTag(node, 'initial-level'));
|
7676
8034
|
this.initial_level.text = ilt || '0';
|
7677
|
-
// NOTE:
|
8035
|
+
// NOTE: Until version 1.0.16, pace was stored as a node parameter.
|
7678
8036
|
const pace_text = nodeParameterValue(node, 'pace') +
|
7679
8037
|
xmlDecoded(nodeContentByTag(node, 'pace'));
|
7680
|
-
// NOTE:
|
8038
|
+
// NOTE: Legacy models have no pace field => default to 1.
|
7681
8039
|
this.pace_expression.text = pace_text || '1';
|
7682
|
-
// NOTE:
|
8040
|
+
// NOTE: Immediately evaluate pace expression as integer.
|
7683
8041
|
this.pace = Math.max(1, Math.floor(this.pace_expression.result(1)));
|
8042
|
+
this.TEX_id = xmlDecoded(nodeContentByTag(node, 'tex-id') || 'p');
|
8043
|
+
this.power_grid = MODEL.powerGridByID(nodeContentByTag(node, 'grid-id'));
|
8044
|
+
this.length_in_km = safeStrToFloat(nodeContentByTag(node, 'length'), 0);
|
8045
|
+
// NOTE: Reactance may be an empty string to indicate "infer from length".
|
8046
|
+
this.reactance = nodeContentByTag(node, 'reactance');
|
7684
8047
|
this.x = safeStrToInt(nodeContentByTag(node, 'x-coord'));
|
7685
8048
|
this.y = safeStrToInt(nodeContentByTag(node, 'y-coord'));
|
7686
8049
|
if(IO_CONTEXT) {
|
@@ -7776,6 +8139,50 @@ class Process extends Node {
|
|
7776
8139
|
return (ub.isStatic ? ub.result(0) : VM.PLUS_INFINITY);
|
7777
8140
|
}
|
7778
8141
|
|
8142
|
+
lossRates(t) {
|
8143
|
+
// Returns a list of loss rates for the slope variables associated
|
8144
|
+
// with this process at time t.
|
8145
|
+
// NOTE: Rates depend on upper bound, which may be dynamic.
|
8146
|
+
// Source: section 4.4 of Neumann et al. (2022) Assessments of linear
|
8147
|
+
// power flow and transmission loss approximations in coordinated
|
8148
|
+
// capacity expansion problem. Applied Energy, 314: 118859.
|
8149
|
+
// https://doi.org/10.1016/j.apenergy.2022.118859
|
8150
|
+
if(!(this.grid && this.grid.loss_approximation)) return [0];
|
8151
|
+
let ub = this.upper_bound.result(t);
|
8152
|
+
if(ub >= VM.PLUS_INFINITY) {
|
8153
|
+
// When UB = +INF, this is interpreted as "unlimited", which is
|
8154
|
+
// implemented as 99999 grid power units.
|
8155
|
+
ub = VM.UNLIMITED_POWER_FLOW;
|
8156
|
+
}
|
8157
|
+
const
|
8158
|
+
la = this.grid.loss_approximation,
|
8159
|
+
// Let m be the highest per unit loss.
|
8160
|
+
m = ub * this.grid.resistancePerKm * this.length_in_km;
|
8161
|
+
// Linear loss approximation: 1 slope from 0 to m.
|
8162
|
+
if(la === 1) return [m];
|
8163
|
+
// 2-slope approximation of quadratic curve.
|
8164
|
+
if(la === 2) return [m * 0.25, m * 0.75];
|
8165
|
+
// 3-slope approximation of quadratic curve.
|
8166
|
+
return [m / 9, m * 3/9, m * 5/9];
|
8167
|
+
}
|
8168
|
+
|
8169
|
+
actualLossRate(t) {
|
8170
|
+
// Return the actual loss rate, which depends on the power flow
|
8171
|
+
// and the max. power flow (process UB).
|
8172
|
+
const g = this.grid;
|
8173
|
+
if(!g) return 0;
|
8174
|
+
const
|
8175
|
+
lr = this.lossRates(t),
|
8176
|
+
apl = Math.abs(this.actualLevel(t)),
|
8177
|
+
ub = this.upper_bound.result(t),
|
8178
|
+
la = g.loss_approximation,
|
8179
|
+
// Prevent division by 0.
|
8180
|
+
slope = (ub < VM.NEAR_ZERO ? 0 :
|
8181
|
+
// NOTE: Index may exceed # slopes - 1 when level = UB.
|
8182
|
+
Math.min(la - 1, Math.floor(apl * la / ub)));
|
8183
|
+
return lr[slope];
|
8184
|
+
}
|
8185
|
+
|
7779
8186
|
copyPropertiesFrom(p) {
|
7780
8187
|
// Set properties to be identical to those of process `p`
|
7781
8188
|
this.x = p.x;
|
@@ -7789,6 +8196,7 @@ class Process extends Node {
|
|
7789
8196
|
this.equal_bounds = p.equal_bounds;
|
7790
8197
|
this.level_to_zero = p.level_to_zero;
|
7791
8198
|
this.collapsed = p.collapsed;
|
8199
|
+
this.TEX_id = p.TEX_id;
|
7792
8200
|
}
|
7793
8201
|
|
7794
8202
|
differences(p) {
|
@@ -7801,6 +8209,32 @@ class Process extends Node {
|
|
7801
8209
|
if(Object.keys(d).length > 0) return d;
|
7802
8210
|
return null;
|
7803
8211
|
}
|
8212
|
+
|
8213
|
+
get TEXcode() {
|
8214
|
+
// Return LaTeX code for mathematical formula of constraints defined
|
8215
|
+
// by this process.
|
8216
|
+
const
|
8217
|
+
NL = '\\\\\n',
|
8218
|
+
tex = [NL],
|
8219
|
+
sub = (MODEL.start_period !== MODEL.end_period ?
|
8220
|
+
'_{' + this.TEX_id + ',t}' : '_' + this.TEX_id),
|
8221
|
+
lb = (this.lower_bound.defined ? 'LB' + sub + ' \\le' : ''),
|
8222
|
+
ub = (this.upper_bound.defined ? '\\le UB' + sub : '');
|
8223
|
+
// Integer constraint if applicable.
|
8224
|
+
if(this.integer_level) tex.push('x' + sub, '\\in \\mathbb{Z}', NL);
|
8225
|
+
// Bound constraints...
|
8226
|
+
if(lb && this.equal_bounds) {
|
8227
|
+
tex.push('x' + sub, '= LB' + sub);
|
8228
|
+
} else if(lb || ub) {
|
8229
|
+
tex.push(lb, 'x' + sub, ub);
|
8230
|
+
}
|
8231
|
+
// ... with semi-continuity if applicable.
|
8232
|
+
if(lb && this.level_to_zero) tex.push('\\vee x' + sub, '= 0');
|
8233
|
+
tex.push(NL);
|
8234
|
+
// Add equations for associated binary variables.
|
8235
|
+
tex.push(this.TEXforBinaries);
|
8236
|
+
return tex.join(' ');
|
8237
|
+
}
|
7804
8238
|
|
7805
8239
|
} // END of class Process
|
7806
8240
|
|
@@ -7810,6 +8244,8 @@ class Product extends Node {
|
|
7810
8244
|
constructor(cluster, name, actor) {
|
7811
8245
|
super(cluster, name, actor);
|
7812
8246
|
this.scale_unit = MODEL.default_unit;
|
8247
|
+
// By default, processes have the letter p, products the letter q.
|
8248
|
+
this.TEX_id = 'p';
|
7813
8249
|
// For products, the default bounds are [0, 0], and modeler-defined bounds
|
7814
8250
|
// typically are equal
|
7815
8251
|
this.equal_bounds = true;
|
@@ -7848,6 +8284,10 @@ class Product extends Node {
|
|
7848
8284
|
return 'Q';
|
7849
8285
|
}
|
7850
8286
|
|
8287
|
+
get grid() {
|
8288
|
+
return null;
|
8289
|
+
}
|
8290
|
+
|
7851
8291
|
get attributes() {
|
7852
8292
|
const a = {name: this.displayName};
|
7853
8293
|
a.LB = this.lower_bound.asAttribute;
|
@@ -8105,7 +8545,8 @@ class Product extends Node {
|
|
8105
8545
|
'</lower-bound><price>', this.price.asXML,
|
8106
8546
|
'</price><x-coord>', x,
|
8107
8547
|
'</x-coord><y-coord>', y,
|
8108
|
-
'</y-coord
|
8548
|
+
'</y-coord><tex-id>', this.TEX_id,
|
8549
|
+
'</tex-id></product>'].join('');
|
8109
8550
|
return xml;
|
8110
8551
|
}
|
8111
8552
|
|
@@ -8124,6 +8565,7 @@ class Product extends Node {
|
|
8124
8565
|
nodeParameterValue(node, 'hidden')) === '1';
|
8125
8566
|
this.scale_unit = MODEL.addScaleUnit(
|
8126
8567
|
xmlDecoded(nodeContentByTag(node, 'unit')));
|
8568
|
+
this.TEX_id = xmlDecoded(nodeContentByTag(node, 'tex-id') || 'q');
|
8127
8569
|
// Legacy models have tag "profit" instead of "price"
|
8128
8570
|
let pp = nodeContentByTag(node, 'price');
|
8129
8571
|
if(!pp) pp = nodeContentByTag(node, 'profit');
|
@@ -8287,6 +8729,7 @@ class Product extends Node {
|
|
8287
8729
|
this.no_slack = p.no_slack;
|
8288
8730
|
this.initial_level.text = p.initial_level.text;
|
8289
8731
|
this.integer_level = p.integer_level;
|
8732
|
+
this.TEX_id = p.TEX_id;
|
8290
8733
|
// NOTE: do not copy the `no_links` property, nor the import/export status
|
8291
8734
|
}
|
8292
8735
|
|
@@ -8297,6 +8740,70 @@ class Product extends Node {
|
|
8297
8740
|
return null;
|
8298
8741
|
}
|
8299
8742
|
|
8743
|
+
get TEXcode() {
|
8744
|
+
// Return LaTeX code for mathematical formula of constraints defined
|
8745
|
+
// by this product.
|
8746
|
+
const
|
8747
|
+
NL = '\\\\\n',
|
8748
|
+
tex = [NL],
|
8749
|
+
x = (this.level_to_zero ? '\\hat{x}' : 'x'),
|
8750
|
+
sub = (MODEL.start_period !== MODEL.end_period ?
|
8751
|
+
'_{' + this.TEX_id + ',t}' : '_' + this.TEX_id),
|
8752
|
+
param = (x, p) => {
|
8753
|
+
if(!x.defined) return '';
|
8754
|
+
const v = safeStrToFloat(x.text, p + sub);
|
8755
|
+
if(typeof v === 'number') return VM.sig4Dig(v);
|
8756
|
+
return v;
|
8757
|
+
};
|
8758
|
+
// Integer constraint if applicable.
|
8759
|
+
if(this.integer_level) tex.push(x + sub, '\\in \\mathbb{Z}', NL);
|
8760
|
+
// Bounds can be explicit...
|
8761
|
+
let lb = param(this.lower_bound, 'LB'),
|
8762
|
+
ub = param(this.upper_bound, 'UB');
|
8763
|
+
if(lb && this.equal_bounds) ub = lb;
|
8764
|
+
// ... or implicit.
|
8765
|
+
if(!lb && !this.isSourceNode) lb = '0';
|
8766
|
+
if(!ub && !this.isSinkNode) ub = '0';
|
8767
|
+
// Add the bound constraints.
|
8768
|
+
if(lb && ub) {
|
8769
|
+
if(lb === ub) {
|
8770
|
+
tex.push(x + sub, '=', lb, NL);
|
8771
|
+
} else {
|
8772
|
+
tex.push(lb, '\\le ' + x + sub, '\\le', ub, NL);
|
8773
|
+
}
|
8774
|
+
} else if(lb) {
|
8775
|
+
tex.push(x + sub, '\\ge', lb, NL);
|
8776
|
+
} else if(ub) {
|
8777
|
+
tex.push(x + sub, '\\le', ub, NL);
|
8778
|
+
}
|
8779
|
+
// Add the "balance" constraint for links.
|
8780
|
+
tex.push(x + sub, '=');
|
8781
|
+
if(this.is_buffer) {
|
8782
|
+
// Insert X[t-1]
|
8783
|
+
tex.push(x + '_{' + this.code +
|
8784
|
+
(MODEL.start_period !== MODEL.end_period ? ',t-1}' : ',0}'));
|
8785
|
+
}
|
8786
|
+
let first = true;
|
8787
|
+
for(let i = 0; i < this.inputs.length; i++) {
|
8788
|
+
let ltex = this.inputs[i].TEXcode.trim();
|
8789
|
+
if(!first && !ltex.startsWith('-')) tex.push('+');
|
8790
|
+
tex.push(ltex);
|
8791
|
+
first = false;
|
8792
|
+
}
|
8793
|
+
for(let i = 0; i < this.outputs.length; i++) {
|
8794
|
+
let ltex = this.outputs[i].TEXcode.trim();
|
8795
|
+
if(ltex.trim().startsWith('-')) {
|
8796
|
+
ltex = ltex.substring(1);
|
8797
|
+
if(!first) {
|
8798
|
+
ltex = '+ ' + ltex;
|
8799
|
+
first = false;
|
8800
|
+
}
|
8801
|
+
}
|
8802
|
+
tex.push(ltex);
|
8803
|
+
}
|
8804
|
+
return tex.join(' ');
|
8805
|
+
}
|
8806
|
+
|
8300
8807
|
} // END of class Product
|
8301
8808
|
|
8302
8809
|
|
@@ -8507,6 +9014,8 @@ class Link {
|
|
8507
9014
|
actualDelay(t) {
|
8508
9015
|
// Scale the delay expression value of this link to a discrete number
|
8509
9016
|
// of time steps on the model time scale.
|
9017
|
+
// NOTE: For links from grid processes, delays are ignored.
|
9018
|
+
if(this.from_node.grid) return 0;
|
8510
9019
|
let d = Math.floor(VM.SIG_DIF_FROM_ZERO + this.flow_delay.result(t));
|
8511
9020
|
// NOTE: Negative values are permitted. This might invalidate cost
|
8512
9021
|
// price calculation -- to be checked!!
|
@@ -8543,6 +9052,63 @@ class Link {
|
|
8543
9052
|
fc.containsProduct(this.to_node))) return true;
|
8544
9053
|
return false;
|
8545
9054
|
}
|
9055
|
+
|
9056
|
+
get TEXcode() {
|
9057
|
+
// Return LaTeX code for the term for this link in the formula
|
9058
|
+
// for its TO node if this is a product. The TEX routines for
|
9059
|
+
// products will take care of the sign of this term.
|
9060
|
+
const
|
9061
|
+
dyn = MODEL.start_period !== MODEL.end_period,
|
9062
|
+
x = (this.from_node.level_to_zero ? '\\hat(x)' : 'x'),
|
9063
|
+
fsub = (dyn ?
|
9064
|
+
'_{' + this.from_node.TEX_id + ',t}' :
|
9065
|
+
'_' + this.from_node.TEX_id),
|
9066
|
+
fsub_i = fsub.replace(',t}', ',i}'),
|
9067
|
+
rsub = (dyn ?
|
9068
|
+
'_{' + this.from_node.TEX_id +
|
9069
|
+
' \\rightarrow ' + this.to_node.TEX_id + ',t}' :
|
9070
|
+
'_' + this.from_node.TEX_id),
|
9071
|
+
param = (x, p, sub) => {
|
9072
|
+
if(!x.defined) return '';
|
9073
|
+
const v = safeStrToFloat(x.text, p + sub);
|
9074
|
+
if(typeof v === 'number') return (v === 1 ? '' :
|
9075
|
+
(v === -1 ? '-' : VM.sig4Dig(v)));
|
9076
|
+
return v;
|
9077
|
+
},
|
9078
|
+
r = param(this.relative_rate, 'R', rsub),
|
9079
|
+
d = param(this.flow_delay, '\\delta', rsub),
|
9080
|
+
dn = safeStrToInt(d, '?'),
|
9081
|
+
d_1 = (!d || typeof dn === 'number' ? dn + 1 : d + '+1');
|
9082
|
+
if(this.multiplier === VM.LM_LEVEL) {
|
9083
|
+
return r + ' ' + x + fsub;
|
9084
|
+
} else if(this.multiplier === VM.LM_THROUGHPUT) {
|
9085
|
+
return 'THR';
|
9086
|
+
} else if(this.multiplier === VM.LM_INCREASE) {
|
9087
|
+
return 'INC';
|
9088
|
+
} else if(this.multiplier === VM.LM_SUM) {
|
9089
|
+
if(d) return r + '\\sum_{i=t-' + d + '}^{t}{' + x + fsub_i + '}';
|
9090
|
+
return x + fsub;
|
9091
|
+
} else if(this.multiplier === VM.LM_MEAN) {
|
9092
|
+
if(d) return r + '{1 \\over {' + d_1 + '}} \sum_{i=t-' +
|
9093
|
+
d + '}^{t}{' + x + fsub_i + '}';
|
9094
|
+
return x + fsub;
|
9095
|
+
} else if(this.multiplier === VM.LM_STARTUP) {
|
9096
|
+
return r + ' \\mathbf{su}' + fsub;
|
9097
|
+
} else if(this.multiplier === VM.LM_POSITIVE) {
|
9098
|
+
return r + '\\mathbf{u}' + fsub;
|
9099
|
+
} else if(this.multiplier === VM.LM_ZERO) {
|
9100
|
+
return r + '\\mathbf{d}' + fsub;
|
9101
|
+
} else if(this.multiplier === VM.LM_SPINNING_RESERVE) {
|
9102
|
+
return 'SPIN';
|
9103
|
+
} else if(this.multiplier === VM.LM_FIRST_COMMIT) {
|
9104
|
+
return r + '\\mathbf{fc}' + fsub;
|
9105
|
+
} else if(this.multiplier === VM.LM_SHUTDOWN) {
|
9106
|
+
return r + '\\mathbf{sd}' + fsub;
|
9107
|
+
} else if(this.multiplier === VM.LM_PEAK_INC) {
|
9108
|
+
return 'PEAK';
|
9109
|
+
}
|
9110
|
+
return 'Unknown link multiplier: ' + this.multiplier;
|
9111
|
+
}
|
8546
9112
|
|
8547
9113
|
// NOTE: links do not draw themselves; they are visualized by Arrow objects
|
8548
9114
|
|
@@ -8576,6 +9142,7 @@ class DatasetModifier {
|
|
8576
9142
|
// NOTE: Identifier will be unique only for equations.
|
8577
9143
|
return UI.nameToID(this.selector);
|
8578
9144
|
}
|
9145
|
+
|
8579
9146
|
get displayName() {
|
8580
9147
|
// NOTE: When "displayed", dataset modifiers have their selector as name.
|
8581
9148
|
return this.selector;
|
@@ -9022,6 +9589,10 @@ class Dataset {
|
|
9022
9589
|
}
|
9023
9590
|
// Reduce inner spaces to one, and trim outer spaces.
|
9024
9591
|
s = s.replace(/\s+/g, ' ').trim();
|
9592
|
+
if(!s) {
|
9593
|
+
UI.warn(`Invalid equation name "${selector}"`);
|
9594
|
+
return null;
|
9595
|
+
}
|
9025
9596
|
if(s.startsWith(':')) {
|
9026
9597
|
// Methods must have no spaces directly after their leading colon,
|
9027
9598
|
// and must not contain other colons.
|
@@ -9045,21 +9616,9 @@ class Dataset {
|
|
9045
9616
|
return null;
|
9046
9617
|
}
|
9047
9618
|
} else {
|
9048
|
-
// Standard dataset modifier selectors are much more restricted
|
9049
|
-
|
9050
|
-
s
|
9051
|
-
let msg = '';
|
9052
|
-
if(s !== selector) msg = UI.WARNING.SELECTOR_SYNTAX;
|
9053
|
-
// A selector can only contain 1 star.
|
9054
|
-
if(s.indexOf('*') !== s.lastIndexOf('*')) msg = UI.WARNING.SINGLE_WILDCARD;
|
9055
|
-
if(msg) {
|
9056
|
-
UI.warn(msg);
|
9057
|
-
return null;
|
9058
|
-
}
|
9059
|
-
}
|
9060
|
-
if(s.trim().length === 0) {
|
9061
|
-
UI.warn(UI.WARNING.INVALID_SELECTOR);
|
9062
|
-
return null;
|
9619
|
+
// Standard dataset modifier selectors are much more restricted.
|
9620
|
+
s = MODEL.validSelector(s);
|
9621
|
+
if(!s) return;
|
9063
9622
|
}
|
9064
9623
|
// Then add a dataset modifier to this dataset.
|
9065
9624
|
const id = UI.nameToID(s);
|
@@ -9262,7 +9821,8 @@ class ChartVariable {
|
|
9262
9821
|
// the Linny-R entity and its attribute, followed by its scale factor
|
9263
9822
|
// unless it equals 1 (no scaling).
|
9264
9823
|
const sf = (this.scale_factor === 1 ? '' :
|
9265
|
-
|
9824
|
+
// NOTE: Pass tiny = TRUE to permit very small scaling factors.
|
9825
|
+
` (x${VM.sig4Dig(this.scale_factor, true)})`);
|
9266
9826
|
// Display name of equation is just the equations dataset selector.
|
9267
9827
|
if(this.object instanceof DatasetModifier) {
|
9268
9828
|
let eqn = this.object.selector;
|
@@ -9315,9 +9875,9 @@ class ChartVariable {
|
|
9315
9875
|
` wildcard-index="${this.wildcard_index}"` : ''),
|
9316
9876
|
` sorted="${this.sorted}"`,
|
9317
9877
|
'><object-id>', xmlEncoded(id),
|
9318
|
-
'</object-id><attribute>', this.attribute,
|
9878
|
+
'</object-id><attribute>', xmlEncoded(this.attribute),
|
9319
9879
|
'</attribute><color>', this.color,
|
9320
|
-
'</color><scale-factor>', VM.sig4Dig(this.scale_factor),
|
9880
|
+
'</color><scale-factor>', VM.sig4Dig(this.scale_factor, true),
|
9321
9881
|
'</scale-factor><line-width>', VM.sig4Dig(this.line_width),
|
9322
9882
|
'</line-width></chart-variable>'].join('');
|
9323
9883
|
return xml;
|
@@ -9363,7 +9923,7 @@ class ChartVariable {
|
|
9363
9923
|
this.wildcard_index = (wci ? parseInt(wci) : false);
|
9364
9924
|
this.setProperties(
|
9365
9925
|
obj,
|
9366
|
-
nodeContentByTag(node, 'attribute'),
|
9926
|
+
xmlDecoded(nodeContentByTag(node, 'attribute')),
|
9367
9927
|
nodeParameterValue(node, 'stacked') === '1',
|
9368
9928
|
nodeContentByTag(node, 'color'),
|
9369
9929
|
safeStrToFloat(nodeContentByTag(node, 'scale-factor')),
|
@@ -9818,7 +10378,7 @@ class Chart {
|
|
9818
10378
|
}
|
9819
10379
|
|
9820
10380
|
timeScaleAsString(s) {
|
9821
|
-
//
|
10381
|
+
// Return number `s` (in hours) as string with most appropriate time unit.
|
9822
10382
|
if(s < 1/60) return VM.sig2Dig(s * 3600) + 's';
|
9823
10383
|
if(s < 1) return VM.sig2Dig(s * 60) + 'm';
|
9824
10384
|
if(s < 24) return VM.sig2Dig(s) + 'h';
|
@@ -10284,7 +10844,7 @@ class Chart {
|
|
10284
10844
|
this.plot_max_y = maxy;
|
10285
10845
|
y = miny;
|
10286
10846
|
const labels = [];
|
10287
|
-
while(y <=
|
10847
|
+
while(y - maxy <= VM.NEAR_ZERO) {
|
10288
10848
|
// NOTE: Large values having exponents will be "neat" numbers,
|
10289
10849
|
// so then display fewer decimals, as these will be zeroes.
|
10290
10850
|
const v = (Math.abs(y) > 1e5 ? VM.sig2Dig(y) : VM.sig4Dig(y));
|
@@ -12053,61 +12613,345 @@ class Experiment {
|
|
12053
12613
|
} // END of CLASS Experiment
|
12054
12614
|
|
12055
12615
|
|
12616
|
+
// CLASS BoundlineSelector
|
12617
|
+
class BoundlineSelector {
|
12618
|
+
constructor(boundline, selector, x='', g=false) {
|
12619
|
+
this.boundline = boundline;
|
12620
|
+
this.selector = selector;
|
12621
|
+
this.expression = new Expression(boundline, selector, x);
|
12622
|
+
this.grouping = g;
|
12623
|
+
}
|
12624
|
+
|
12625
|
+
get asXML() {
|
12626
|
+
// Prevent saving unidentified selectors.
|
12627
|
+
if(this.selector.trim().length === 0) return '';
|
12628
|
+
return ['<boundline-selector', (this.grouping ? ' points-x="1"' : ''),
|
12629
|
+
'><selector>', xmlEncoded(this.selector),
|
12630
|
+
'</selector><expression>', xmlEncoded(this.expression.text),
|
12631
|
+
'</expression></boundline-selector>'].join('');
|
12632
|
+
}
|
12633
|
+
|
12634
|
+
initFromXML(node) {
|
12635
|
+
this.grouping = nodeParameterValue(node, 'points-x') === '1';
|
12636
|
+
this.expression.text = xmlDecoded(nodeContentByTag(node, 'expression'));
|
12637
|
+
if(IO_CONTEXT) {
|
12638
|
+
// Contextualize the included expression.
|
12639
|
+
IO_CONTEXT.rewrite(this.expression);
|
12640
|
+
}
|
12641
|
+
}
|
12642
|
+
|
12643
|
+
} // END of class BoundlineSelector
|
12644
|
+
|
12645
|
+
|
12056
12646
|
// CLASS BoundLine
|
12057
12647
|
class BoundLine {
|
12058
12648
|
constructor(c) {
|
12059
12649
|
this.constraint = c;
|
12060
12650
|
// Default bound line imposes no constraint: Y >= 0 for all X.
|
12061
12651
|
this.points = [[0, 0], [100, 0]];
|
12652
|
+
this.storePoints();
|
12062
12653
|
this.type = VM.GE;
|
12063
|
-
this
|
12654
|
+
// SVG string for contour of this bound line (to reduce computation).
|
12064
12655
|
this.contour_path = '';
|
12656
|
+
this.point_data = [];
|
12657
|
+
this.url = '';
|
12658
|
+
this.selectors = [];
|
12659
|
+
this.selectors.push(new BoundlineSelector(this, '(default)', '0'));
|
12065
12660
|
}
|
12066
12661
|
|
12067
12662
|
get displayName() {
|
12068
|
-
return this.constraint.displayName + '
|
12069
|
-
VM.constraint_codes[this.type] + ' bound line #' +
|
12070
|
-
this.constraint.bound_lines.indexOf(this)
|
12071
|
-
(this.selectors ? ` (${this.selectors}) ` : '');
|
12663
|
+
return this.constraint.displayName + ' [' +
|
12664
|
+
VM.constraint_codes[this.type] + '] bound line #' +
|
12665
|
+
this.constraint.bound_lines.indexOf(this);
|
12072
12666
|
}
|
12073
12667
|
|
12074
12668
|
get copy() {
|
12075
12669
|
// Return a "clone" of this bound line.
|
12076
12670
|
let bl = new BoundLine(this.constraint);
|
12077
|
-
bl.
|
12078
|
-
|
12079
|
-
|
12080
|
-
bl.points.push([p[0], p[1]]);
|
12081
|
-
}
|
12671
|
+
bl.points_string = this.points_string;
|
12672
|
+
// NOTE: Reset boundline to its initial "as edited" state.
|
12673
|
+
bl.restorePoints();
|
12082
12674
|
bl.type = this.type;
|
12083
|
-
bl.selectors = this.selectors;
|
12084
12675
|
bl.contour_path = this.contour_path;
|
12676
|
+
bl.url = this.url;
|
12677
|
+
bl.point_data.length = 0;
|
12678
|
+
for(let i = 0; i < this.point_data.length; i++) {
|
12679
|
+
bl.point_data.push(this.point_data[i].slice());
|
12680
|
+
}
|
12681
|
+
bl.selectors.length = 0;
|
12682
|
+
for(let i = 0; i < this.selectors.length; i++) {
|
12683
|
+
const s = this.selectors[i];
|
12684
|
+
bl.selectors.push(new BoundlineSelector(s.boundline, s.selector,
|
12685
|
+
s.expression.text, s.grouping));
|
12686
|
+
}
|
12085
12687
|
return bl;
|
12086
12688
|
}
|
12087
12689
|
|
12690
|
+
get staticLine() {
|
12691
|
+
// Return TRUE if bound line has only the default selector, and this
|
12692
|
+
// evaluates as default index = 0.
|
12693
|
+
return this.selectors.length < 2 &&
|
12694
|
+
this.selectors[0].expression.text === '0';
|
12695
|
+
}
|
12696
|
+
|
12697
|
+
get selectorList() {
|
12698
|
+
// Return list of selector names only.
|
12699
|
+
const sl = [];
|
12700
|
+
for(let i = 0; i < this.selectors.length; i++) {
|
12701
|
+
sl.push(this.selectors[i].selector);
|
12702
|
+
}
|
12703
|
+
return sl;
|
12704
|
+
}
|
12705
|
+
|
12706
|
+
selectorByName(name) {
|
12707
|
+
// Return index of selector `n` in the list, or null if not found.
|
12708
|
+
for(let i = 0; i < this.selectors.length; i++) {
|
12709
|
+
if(this.selectors[i].selector === name) return this.selectors[i];
|
12710
|
+
}
|
12711
|
+
return null;
|
12712
|
+
}
|
12713
|
+
|
12714
|
+
addSelector(name, x='') {
|
12715
|
+
// Add selector if new, and return the named selector.
|
12716
|
+
const s = this.selectorByName(name);
|
12717
|
+
if(s) return s;
|
12718
|
+
name = MODEL.validSelector(name);
|
12719
|
+
if(name.indexOf('*') >= 0 || name.indexOf('?') >= 0) {
|
12720
|
+
UI.warn('Bound line selector cannot contain wildcards');
|
12721
|
+
return null;
|
12722
|
+
}
|
12723
|
+
if(name) {
|
12724
|
+
const s = new BoundlineSelector(this, name, x);
|
12725
|
+
this.selectors.push(s);
|
12726
|
+
this.selectors.sort((a, b) => compareSelectors(a.selector, b.selector));
|
12727
|
+
return s;
|
12728
|
+
}
|
12729
|
+
return null;
|
12730
|
+
}
|
12731
|
+
|
12732
|
+
setPointsFromData(index) {
|
12733
|
+
// Get point coordinates from bound line series data at index.
|
12734
|
+
if(index > 0 && index <= this.point_data.length) {
|
12735
|
+
const pd = this.point_data[index - 1];
|
12736
|
+
this.points.length = 0;
|
12737
|
+
for(let i = 0; i < pd.length; i += 2) {
|
12738
|
+
this.points.push([pd[i] * 100, pd[i + 1] * 100]);
|
12739
|
+
}
|
12740
|
+
} else {
|
12741
|
+
// Data is seen as 1-based array => default to original coordinates.
|
12742
|
+
this.restorePoints();
|
12743
|
+
}
|
12744
|
+
}
|
12745
|
+
|
12746
|
+
get activeSelectorIndex() {
|
12747
|
+
// Return the number of the first boundline selector that matches the
|
12748
|
+
// selector combination of the current experiment. Defaults to 0.
|
12749
|
+
if(this.selectors.length < 2) return 0;
|
12750
|
+
const x = MODEL.running_experiment;
|
12751
|
+
if(!x) return 0;
|
12752
|
+
for(let i = 1; i < this.selectors.length; i++) {
|
12753
|
+
if(x.activeCombination.indexOf(this.selectors[i].selector) >= 0) {
|
12754
|
+
return i;
|
12755
|
+
}
|
12756
|
+
}
|
12757
|
+
return 0;
|
12758
|
+
}
|
12759
|
+
|
12760
|
+
setDynamicPoints(t, draw=false) {
|
12761
|
+
// Adapt bound line point coordinates for the current time step
|
12762
|
+
// for the running experiment (if any).
|
12763
|
+
// NOTE: For speed, first perform the quick check whether this line
|
12764
|
+
// is static, as then the points do not change.
|
12765
|
+
if(this.staticLine) return;
|
12766
|
+
const
|
12767
|
+
bls = this.selectors[this.activeSelectorIndex],
|
12768
|
+
r = bls.expression.result(t);
|
12769
|
+
// Log errors on the console, but ignore them.
|
12770
|
+
if(r <= VM.ERROR || r >= VM.EXCEPTION) {
|
12771
|
+
console.log('ERROR: Exception in boundline selector expression', bls, r);
|
12772
|
+
// NOTE: Double-check whether result is a grouping.
|
12773
|
+
} else if(bls.grouping || Array.isArray(r)) {
|
12774
|
+
this.validatePoints(r);
|
12775
|
+
this.points.length = 0;
|
12776
|
+
for(let i = 0; i < r.length; i += 2) {
|
12777
|
+
this.points.push([r[i] * 100, r[i + 1] * 100]);
|
12778
|
+
}
|
12779
|
+
} else {
|
12780
|
+
// NOTE: Data is not a time series but "array type" data. The time
|
12781
|
+
// step co-determines the result. If modelers provide time series
|
12782
|
+
// data, then they shoud use `t` (absolute time) as index expression,
|
12783
|
+
// otherwise `rt` (relative time).
|
12784
|
+
// NOTE: Result is a floating point number, so truncate it to integer.
|
12785
|
+
this.setPointsFromData(Math.floor(r));
|
12786
|
+
}
|
12787
|
+
// Compute and store SVG for thumbnail only when needed.
|
12788
|
+
if(draw) CONSTRAINT_EDITOR.setContourPath(this);
|
12789
|
+
}
|
12790
|
+
|
12791
|
+
restorePoints() {
|
12792
|
+
// Restore point coordinates from original string.
|
12793
|
+
this.points = JSON.parse(this.points_string);
|
12794
|
+
}
|
12795
|
+
|
12796
|
+
storePoints() {
|
12797
|
+
// Set point coordinates to be the original string.
|
12798
|
+
this.points_string = JSON.stringify(this.points);
|
12799
|
+
}
|
12800
|
+
|
12801
|
+
get pointsDataString() {
|
12802
|
+
// Return point coordinates in data format (semicolon-separated numbers).
|
12803
|
+
const pd = [];
|
12804
|
+
for(let i = 0; i < this.points.length; i++) {
|
12805
|
+
const p = this.points[i];
|
12806
|
+
pd.push(p[0], p[1]);
|
12807
|
+
}
|
12808
|
+
return pd.join(';');
|
12809
|
+
}
|
12810
|
+
|
12811
|
+
get maxPoints() {
|
12812
|
+
// Return the highest number of points that this boundline can have.
|
12813
|
+
this.restorePoints();
|
12814
|
+
let n = this.points.length;
|
12815
|
+
for(let i = 0; i < this.point_data.length; i++) {
|
12816
|
+
n = Math.max(n, this.point_data[i].length);
|
12817
|
+
}
|
12818
|
+
const bls = this.selectors[this.activeSelectorIndex];
|
12819
|
+
if(bls.grouping) {
|
12820
|
+
const x = bls.expression;
|
12821
|
+
if(x.isStatic) {
|
12822
|
+
n = Math.max(n, x.result(1).length);
|
12823
|
+
} else {
|
12824
|
+
// For dynamic expressions, the grouping length may vary, so
|
12825
|
+
// we must check for the complete run length.
|
12826
|
+
for(let t = MODEL.start_period; t <= MODEL.end_period; t++) {
|
12827
|
+
n = Math.max(n, x.result(t).length);
|
12828
|
+
}
|
12829
|
+
}
|
12830
|
+
}
|
12831
|
+
return n;
|
12832
|
+
}
|
12833
|
+
|
12834
|
+
validatePoints(pd) {
|
12835
|
+
if(!Array.isArray(pd)) {
|
12836
|
+
console.log(pd); throw "Not an array";
|
12837
|
+
}
|
12838
|
+
// Ensure that array `pd` is a valid series of point coordinates.
|
12839
|
+
if(pd.length) {
|
12840
|
+
// Ensure that number of point coordinates is even (Y defaults to 0).
|
12841
|
+
if(pd.length % 2) pd.push(0);
|
12842
|
+
// Ensure that bound line has at least 2 points.
|
12843
|
+
if(pd.length === 2) pd.push(1, 0);
|
12844
|
+
} else {
|
12845
|
+
// Default to horizontal line Y = 0.
|
12846
|
+
pd.push(0, 0, 1, 0);
|
12847
|
+
}
|
12848
|
+
// NOTE: Point coordinates must lie between 0 and 1, and for the
|
12849
|
+
// X-coordinates, it should hold that X[0] = 0, X[i+1] >= X[i],
|
12850
|
+
// and X[N] = 1.
|
12851
|
+
if(pd[0] !== 0) pd[0] = 0;
|
12852
|
+
if(pd[pd.length - 2] !== 1) pd[pd.length - 2] = 1;
|
12853
|
+
let min = 0,
|
12854
|
+
last = 2;
|
12855
|
+
for(let i = 1; i < pd.length; i++) {
|
12856
|
+
const x = 1 - i % 2;
|
12857
|
+
pd[i] = Math.min(1, Math.max(x ? min : 0, pd[i]));
|
12858
|
+
if(x) {
|
12859
|
+
// Keep track of first point having X = 1, as this should be the
|
12860
|
+
// last point of the boundline.
|
12861
|
+
if(pd[i] > min) last = i + 1;
|
12862
|
+
min = pd[i];
|
12863
|
+
}
|
12864
|
+
}
|
12865
|
+
// Truncate any points after the first point having X = 1.
|
12866
|
+
pd.lengh = last;
|
12867
|
+
}
|
12868
|
+
|
12869
|
+
get pointDataString() {
|
12870
|
+
// Point data is stored as separate lines of semicolon-separated
|
12871
|
+
// floating point numbers, with N-digit precision to keep model files
|
12872
|
+
// compact (default: N = 8)
|
12873
|
+
let d = [];
|
12874
|
+
for(let i = 0; i < this.point_data.length; i++) {
|
12875
|
+
const
|
12876
|
+
opd = this.point_data[i],
|
12877
|
+
pd = [];
|
12878
|
+
for(let j = 0; j < opd.length; j++) {
|
12879
|
+
// Convert number to string with the desired precision.
|
12880
|
+
const f = opd[j].toPrecision(CONFIGURATION.dataset_precision);
|
12881
|
+
// Then parse it again, so that the number will be represented
|
12882
|
+
// (by JavaScript) in the most compact representation.
|
12883
|
+
pd.push(parseFloat(f));
|
12884
|
+
}
|
12885
|
+
this.validatePoints(pd);
|
12886
|
+
// Push point coordinates as space-separated string.
|
12887
|
+
d.push(pd.join(';'));
|
12888
|
+
}
|
12889
|
+
return d.join('\n');
|
12890
|
+
}
|
12891
|
+
|
12892
|
+
unpackPointDataString(str) {
|
12893
|
+
// Convert separate lines of semicolon-separated point data to a
|
12894
|
+
// list of lists of numbers.
|
12895
|
+
this.point_data.length = 0;
|
12896
|
+
str= str.trim();
|
12897
|
+
if(str) {
|
12898
|
+
const lines = str.split('\n');
|
12899
|
+
for(let i = 0; i < lines.length; i++) {
|
12900
|
+
const
|
12901
|
+
numbers = lines[i].split(';'),
|
12902
|
+
pd = [];
|
12903
|
+
for(let i = 0; i < numbers.length; i++) {
|
12904
|
+
pd.push(parseFloat(numbers[i].trim()));
|
12905
|
+
}
|
12906
|
+
this.validatePoints(pd);
|
12907
|
+
this.point_data.push(pd);
|
12908
|
+
}
|
12909
|
+
}
|
12910
|
+
}
|
12911
|
+
|
12088
12912
|
get asXML() {
|
12089
|
-
|
12090
|
-
|
12091
|
-
|
12092
|
-
|
12093
|
-
|
12913
|
+
// NOTE: Save boundline always with points-as-last-edited.
|
12914
|
+
this.restorePoints();
|
12915
|
+
const xml = ['<bound-line type="', this.type,
|
12916
|
+
'"><points>', JSON.stringify(this.points),
|
12917
|
+
'</points><contour>', this.contour_path,
|
12918
|
+
'</contour><url>', xmlEncoded(this.url),
|
12919
|
+
'</url><point-data>', xmlEncoded(this.pointDataString),
|
12920
|
+
'</point-data><selectors>'];
|
12921
|
+
for(let i = 0; i < this.selectors.length; i++) {
|
12922
|
+
xml.push(this.selectors[i].asXML);
|
12923
|
+
}
|
12924
|
+
xml.push('</selectors></bound-line>');
|
12925
|
+
return xml.join('');
|
12094
12926
|
}
|
12095
12927
|
|
12096
12928
|
initFromXML(node) {
|
12097
12929
|
this.type = safeStrToInt(nodeParameterValue(node, 'type'), VM.EQ);
|
12098
|
-
this.
|
12099
|
-
this.
|
12930
|
+
this.points_string = nodeContentByTag(node, 'points');
|
12931
|
+
this.restorePoints();
|
12100
12932
|
this.contour_path = nodeContentByTag(node, 'contour');
|
12101
|
-
|
12102
|
-
|
12103
|
-
|
12104
|
-
|
12105
|
-
|
12106
|
-
|
12107
|
-
|
12108
|
-
|
12109
|
-
|
12110
|
-
|
12933
|
+
this.url = xmlDecoded(nodeContentByTag(node, 'url'));
|
12934
|
+
if(this.url) {
|
12935
|
+
FILE_MANAGER.getRemoteData(this, this.url);
|
12936
|
+
} else {
|
12937
|
+
this.unpackPointDataString(
|
12938
|
+
xmlDecoded(nodeContentByTag(node, 'point-data')));
|
12939
|
+
}
|
12940
|
+
const n = childNodeByTag(node, 'selectors');
|
12941
|
+
if(n && n.childNodes && n.childNodes.length) {
|
12942
|
+
// NOTE: Only overwrite default selector if XML specifies selectors.
|
12943
|
+
this.selectors.length = 0;
|
12944
|
+
for(let i = 0; i < n.childNodes.length; i++) {
|
12945
|
+
const c = n.childNodes[i];
|
12946
|
+
if(c.nodeName === 'boundline-selector') {
|
12947
|
+
const
|
12948
|
+
s = xmlDecoded(nodeContentByTag(c, 'selector')),
|
12949
|
+
bls = new BoundlineSelector(this, s);
|
12950
|
+
bls.initFromXML(c);
|
12951
|
+
this.selectors.push(bls);
|
12952
|
+
}
|
12953
|
+
}
|
12954
|
+
}
|
12111
12955
|
}
|
12112
12956
|
|
12113
12957
|
get needsNoSOS() {
|
@@ -12264,7 +13108,15 @@ class Constraint {
|
|
12264
13108
|
this.bottom_y = 0;
|
12265
13109
|
this.from_offset = 0;
|
12266
13110
|
this.to_offset = 0;
|
12267
|
-
|
13111
|
+
this.reset();
|
13112
|
+
}
|
13113
|
+
|
13114
|
+
reset() {
|
13115
|
+
// Reset run-dependent properties.
|
13116
|
+
for(let i = 0; i < this.bound_lines.length; i++) {
|
13117
|
+
this.bound_lines[i].current = -1;
|
13118
|
+
}
|
13119
|
+
// Slack information is a "sparse vector" that is filled after solving.
|
12268
13120
|
this.slack_info = {};
|
12269
13121
|
}
|
12270
13122
|
|
@@ -12277,9 +13129,10 @@ class Constraint {
|
|
12277
13129
|
}
|
12278
13130
|
|
12279
13131
|
get identifier() {
|
12280
|
-
// NOTE:
|
12281
|
-
// this prevents problems when nodes are renamed
|
12282
|
-
// constraints have FOUR underscores between node IDs
|
13132
|
+
// NOTE: Constraint IDs are based on the node codes rather than IDs,
|
13133
|
+
// as this prevents problems when nodes are renamed. To ensure ID
|
13134
|
+
// uniqueness, constraints have FOUR underscores between node IDs
|
13135
|
+
// (links have three).
|
12283
13136
|
return this.from_node.code + '____' + this.to_node.code;
|
12284
13137
|
}
|
12285
13138
|
|
@@ -12289,7 +13142,7 @@ class Constraint {
|
|
12289
13142
|
}
|
12290
13143
|
|
12291
13144
|
get attributes() {
|
12292
|
-
// NOTE:
|
13145
|
+
// NOTE: This requires some thought, still!
|
12293
13146
|
const a = {name: this.displayName};
|
12294
13147
|
if(MODEL.infer_cost_prices) {
|
12295
13148
|
a.SOC = this.share_of_cost * this.soc_direction;
|
@@ -12303,16 +13156,16 @@ class Constraint {
|
|
12303
13156
|
}
|
12304
13157
|
|
12305
13158
|
attributeValue(a) {
|
12306
|
-
//
|
12307
|
-
// only A (active) and SOC (share of cost)
|
12308
|
-
if(a === 'A') return this.activeVector; //
|
12309
|
-
// NOTE:
|
12310
|
-
if(a === 'SOC') return this.share_of_cost * this.soc_direction;
|
13159
|
+
// Return the computed result for attribute `a`: for constraints,
|
13160
|
+
// only A (active) and SOC (share of cost).
|
13161
|
+
if(a === 'A') return this.activeVector; // Binary vector - see below.
|
13162
|
+
// NOTE: Negative share indicates Y->X direction of cost sharing.
|
13163
|
+
if(a === 'SOC') return this.share_of_cost * this.soc_direction;
|
12311
13164
|
return null;
|
12312
13165
|
}
|
12313
13166
|
|
12314
13167
|
get setsEquality() {
|
12315
|
-
//
|
13168
|
+
// Return TRUE iff this constraint has an EQ bound line.
|
12316
13169
|
for(let i = 0; i < this.bound_lines.length; i++) {
|
12317
13170
|
if(this.bound_lines[i].type === VM.EQ) return true;
|
12318
13171
|
}
|
@@ -12320,35 +13173,42 @@ class Constraint {
|
|
12320
13173
|
}
|
12321
13174
|
|
12322
13175
|
active(t) {
|
12323
|
-
//
|
13176
|
+
// Return 1 if (X, Y) is on the bound line AND Y is not on its own
|
13177
|
+
// bounds, otherwise 0.
|
12324
13178
|
if(!MODEL.solved) return 0;
|
12325
13179
|
const
|
12326
13180
|
fn = this.from_node,
|
12327
13181
|
tn = this.to_node;
|
12328
13182
|
let lbx = fn.lower_bound.result(t),
|
12329
13183
|
lby = tn.lower_bound.result(t);
|
12330
|
-
// NOTE: LB of semi-continuous processes is 0 if LB > 0
|
13184
|
+
// NOTE: LB of semi-continuous processes is 0 if LB > 0.
|
12331
13185
|
if(lbx > 0 && fn instanceof Process & fn.level_to_zero) lbx = 0;
|
12332
13186
|
if(lby > 0 && tn instanceof Process & tn.level_to_zero) lby = 0;
|
12333
13187
|
const
|
12334
13188
|
rx = fn.upper_bound.result(t) - lbx,
|
12335
13189
|
ry = tn.upper_bound.result(t) - lby;
|
12336
13190
|
// Prevent division by zero: when either range is 0, the constraint
|
12337
|
-
// must be active
|
13191
|
+
// must be active.
|
12338
13192
|
if(rx < VM.NEAR_ZERO || ry < VM.NEAR_ZERO) return 1;
|
12339
13193
|
// Otherwise, convert levels to % of range...
|
12340
13194
|
const
|
12341
13195
|
x = (fn.level[t] - lbx) / rx * 100,
|
12342
13196
|
y = (tn.level[t] - lby) / ry * 100;
|
12343
|
-
// ... and then check whether (%X, %Y) lies on the
|
13197
|
+
// ... and then check whether (%X, %Y) lies on the bound line.
|
12344
13198
|
for(let i = 0; i < this.bound_lines.length; i++) {
|
12345
13199
|
const bl = this.bound_lines[i];
|
12346
|
-
|
13200
|
+
// Bound line does NOT constrain when Y is on its own bound.
|
13201
|
+
if((bl.type === VM.LE && Math.abs(y - 100) < VM.NEAR_ZERO) ||
|
13202
|
+
(bl.type === VM.GE && Math.abs(y) < VM.NEAR_ZERO)) continue;
|
13203
|
+
// Actualize bound line points for current time step.
|
13204
|
+
bl.setDynamicPoints(t);
|
13205
|
+
if(bl.pointOnLine(x, y)) return 1;
|
12347
13206
|
}
|
12348
13207
|
return 0;
|
12349
13208
|
}
|
12350
13209
|
|
12351
13210
|
get activeVector() {
|
13211
|
+
// Return active state for all time steps in the optimization period.
|
12352
13212
|
const v = [];
|
12353
13213
|
for(let t = 0; t < MODEL.runLength + 1; t++) v.push(this.active(t));
|
12354
13214
|
return v;
|
@@ -12482,10 +13342,10 @@ class Constraint {
|
|
12482
13342
|
|
12483
13343
|
addBoundLine() {
|
12484
13344
|
// Adds a new bound line to this constraint, and returns this new line
|
12485
|
-
// NOTE:
|
12486
|
-
// exists and has no
|
13345
|
+
// NOTE: Returns the "base" bound line Y >= 0 (for any X) if it already
|
13346
|
+
// exists and has no associated dataset.
|
12487
13347
|
let bl = this.baseLine;
|
12488
|
-
if(bl &&
|
13348
|
+
if(bl && bl.point_data.length === 0) return bl;
|
12489
13349
|
bl = new BoundLine(this);
|
12490
13350
|
this.bound_lines.push(bl);
|
12491
13351
|
return bl;
|
@@ -12548,6 +13408,7 @@ if(NODE) module.exports = {
|
|
12548
13408
|
BlockMessages: BlockMessages,
|
12549
13409
|
ExperimentRun: ExperimentRun,
|
12550
13410
|
Experiment: Experiment,
|
13411
|
+
BoundlineSelector: BoundlineSelector,
|
12551
13412
|
BoundLine: BoundLine,
|
12552
13413
|
Constraint: Constraint
|
12553
13414
|
};
|