linny-r 1.6.0 → 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-controller.js +47 -35
- package/static/scripts/linny-r-gui-documentation-manager.js +36 -32
- package/static/scripts/linny-r-gui-equation-manager.js +10 -7
- package/static/scripts/linny-r-gui-experiment-manager.js +14 -3
- package/static/scripts/linny-r-gui-finder.js +1 -0
- package/static/scripts/linny-r-gui-paper.js +25 -23
- package/static/scripts/linny-r-milp.js +18 -15
- package/static/scripts/linny-r-model.js +143 -91
- package/static/scripts/linny-r-utils.js +33 -8
- package/static/scripts/linny-r-vm.js +250 -60
@@ -1561,13 +1561,21 @@ class LinnyRModel {
|
|
1561
1561
|
//
|
1562
1562
|
|
1563
1563
|
alignToGrid() {
|
1564
|
-
// Move all positioned model elements to the nearest grid point
|
1564
|
+
// Move all positioned model elements to the nearest grid point.
|
1565
1565
|
if(!this.align_to_grid) return;
|
1566
1566
|
let move = false;
|
1567
1567
|
const fc = this.focal_cluster;
|
1568
1568
|
// NOTE: Do not align notes to the grid. This will permit more
|
1569
1569
|
// precise positioning, while aligning will not improve the layout
|
1570
1570
|
// of the diagram because notes are not connected to arrows.
|
1571
|
+
// However, when notes relate to nearby nodes, preserve their relative
|
1572
|
+
// position to this node.
|
1573
|
+
for(let i = 0; i < fc.notes.length; i++) {
|
1574
|
+
const
|
1575
|
+
note = fc.notes[i],
|
1576
|
+
nbn = note.nearbyNode;
|
1577
|
+
note.nearby_pos = (nbn ? {node: nbn, oldx: nbn.x, oldy: nbn.y} : null);
|
1578
|
+
}
|
1571
1579
|
for(let i = 0; i < fc.processes.length; i++) {
|
1572
1580
|
move = fc.processes[i].alignToGrid() || move;
|
1573
1581
|
}
|
@@ -1577,11 +1585,25 @@ class LinnyRModel {
|
|
1577
1585
|
for(let i = 0; i < fc.sub_clusters.length; i++) {
|
1578
1586
|
move = fc.sub_clusters[i].alignToGrid() || move;
|
1579
1587
|
}
|
1580
|
-
if(move)
|
1588
|
+
if(move) {
|
1589
|
+
// Reposition "associated" notes.
|
1590
|
+
for(let i = 0; i < fc.notes.length; i++) {
|
1591
|
+
const
|
1592
|
+
note = fc.notes[i],
|
1593
|
+
nbp = note.nearby_pos;
|
1594
|
+
if(nbp) {
|
1595
|
+
// Adjust (x, y) so as to retain the relative position.
|
1596
|
+
note.x += nbp.node.x - npb.oldx;
|
1597
|
+
note.y += nbp.node.y - npb.oldy;
|
1598
|
+
note.nearby_pos = null;
|
1599
|
+
}
|
1600
|
+
}
|
1601
|
+
UI.drawDiagram(this);
|
1602
|
+
}
|
1581
1603
|
}
|
1582
1604
|
|
1583
1605
|
translateGraph(dx, dy) {
|
1584
|
-
// Move all entities in the focal cluster by (dx, dy) pixels
|
1606
|
+
// Move all entities in the focal cluster by (dx, dy) pixels.
|
1585
1607
|
if(!dx && !dy) return;
|
1586
1608
|
const fc = this.focal_cluster;
|
1587
1609
|
for(let i = 0; i < fc.processes.length; i++) {
|
@@ -1600,9 +1622,9 @@ class LinnyRModel {
|
|
1600
1622
|
fc.notes[i].x += dx;
|
1601
1623
|
fc.notes[i].y += dy;
|
1602
1624
|
}
|
1603
|
-
// NOTE: force drawing, because SVG must immediately be downloadable
|
1625
|
+
// NOTE: force drawing, because SVG must immediately be downloadable.
|
1604
1626
|
UI.drawDiagram(this);
|
1605
|
-
// If dragging, add (dx, dy) to the properties of the top "move" UndoEdit
|
1627
|
+
// If dragging, add (dx, dy) to the properties of the top "move" UndoEdit.
|
1606
1628
|
if(UI.dragged_node) UNDO_STACK.addOffset(dx, dy);
|
1607
1629
|
}
|
1608
1630
|
|
@@ -1748,8 +1770,8 @@ class LinnyRModel {
|
|
1748
1770
|
miny = Math.min(miny, obj.y - obj.height / 2);
|
1749
1771
|
}
|
1750
1772
|
}
|
1751
|
-
// Translate entire graph if some elements are above and/or left of
|
1752
|
-
// paper edge
|
1773
|
+
// Translate entire graph if some elements are above and/or left of
|
1774
|
+
// the paper edge.
|
1753
1775
|
if(minx < 0 || miny < 0) {
|
1754
1776
|
// NOTE: limit translation to 5 pixels to prevent "run-away effect"
|
1755
1777
|
this.translateGraph(Math.min(5, -minx), Math.min(5, -miny));
|
@@ -2186,8 +2208,8 @@ class LinnyRModel {
|
|
2186
2208
|
}
|
2187
2209
|
|
2188
2210
|
deleteSelection() {
|
2189
|
-
//
|
2190
|
-
// and selected links
|
2211
|
+
// Remove all selected nodes (with their associated links and constraints)
|
2212
|
+
// and selected links.
|
2191
2213
|
// NOTE: This method implements the DELETE action, and hence should be
|
2192
2214
|
// undoable. The UndoEdit is created by the calling routine; the methods
|
2193
2215
|
// that actually delete model elements append their XML to the XML attribute
|
@@ -2195,7 +2217,7 @@ class LinnyRModel {
|
|
2195
2217
|
let obj,
|
2196
2218
|
fc = this.focal_cluster;
|
2197
2219
|
// Update the documentation manager (GUI only) if selection contains the
|
2198
|
-
// current entity
|
2220
|
+
// current entity.
|
2199
2221
|
if(DOCUMENTATION_MANAGER) DOCUMENTATION_MANAGER.clearEntity(this.selection);
|
2200
2222
|
// First delete links and constraints.
|
2201
2223
|
for(let i = this.selection.length - 1; i >= 0; i--) {
|
@@ -3222,6 +3244,29 @@ class LinnyRModel {
|
|
3222
3244
|
sl.push(this.datasets[obj].displayName, this.datasets[obj].comments);
|
3223
3245
|
}
|
3224
3246
|
}
|
3247
|
+
const keys = Object.keys(this.equations_dataset.modifiers);
|
3248
|
+
sl.push('_____Equations');
|
3249
|
+
for(let i = 0; i < keys.length; i++) {
|
3250
|
+
const m = this.equations_dataset.modifiers[keys[i]];
|
3251
|
+
if(!m.selector.startsWith(':')) {
|
3252
|
+
sl.push(m.displayName, '`' + m.expression.text + '`\n');
|
3253
|
+
}
|
3254
|
+
}
|
3255
|
+
sl.push('_____Methods');
|
3256
|
+
for(let i = 0; i < keys.length; i++) {
|
3257
|
+
const m = this.equations_dataset.modifiers[keys[i]];
|
3258
|
+
if(m.selector.startsWith(':')) {
|
3259
|
+
let markup = '\n\nDoes not apply to any entity.';
|
3260
|
+
if(m.expression.eligible_prefixes) {
|
3261
|
+
const el = Object.keys(m.expression.eligible_prefixes)
|
3262
|
+
.sort(compareSelectors);
|
3263
|
+
if(el.length > 0) markup = '\n\nApplies to ' +
|
3264
|
+
pluralS(el.length, 'prefixed entity group') +
|
3265
|
+
':\n- ' + el.join('\n- ');
|
3266
|
+
}
|
3267
|
+
sl.push(m.displayName, '`' + m.expression.text + '`' + markup);
|
3268
|
+
}
|
3269
|
+
}
|
3225
3270
|
sl.push('_____Charts');
|
3226
3271
|
for(let i = 0; i < this.charts.length; i++) {
|
3227
3272
|
sl.push(this.charts[i].title, this.charts[i].comments);
|
@@ -4817,7 +4862,7 @@ class ObjectWithXYWH {
|
|
4817
4862
|
|
4818
4863
|
alignToGrid() {
|
4819
4864
|
// Align this object to the grid, and return TRUE if this involved
|
4820
|
-
// a move
|
4865
|
+
// a move.
|
4821
4866
|
const
|
4822
4867
|
ox = this.x,
|
4823
4868
|
oy = this.y,
|
@@ -5113,7 +5158,7 @@ class Note extends ObjectWithXYWH {
|
|
5113
5158
|
// If attribute omitted, use default attribute of entity type.
|
5114
5159
|
const attr = (ena.length > 1 ? ena[1].trim() : obj.defaultAttribute);
|
5115
5160
|
let val = null;
|
5116
|
-
// NOTE:
|
5161
|
+
// NOTE: For datasets, use the active modifier if no attribute.
|
5117
5162
|
if(!attr && obj instanceof Dataset) {
|
5118
5163
|
val = obj.activeModifierExpression;
|
5119
5164
|
} else {
|
@@ -6139,8 +6184,8 @@ class Cluster extends NodeBox {
|
|
6139
6184
|
}
|
6140
6185
|
|
6141
6186
|
addProductPosition(p, x=null, y=null) {
|
6142
|
-
// Add a product position for product `p` to this cluster unless such
|
6143
|
-
// already exists
|
6187
|
+
// Add a product position for product `p` to this cluster unless such
|
6188
|
+
// "pp" already exists, and then return this (new) product position.
|
6144
6189
|
let pp = this.indexOfProduct(p);
|
6145
6190
|
if(pp >= 0) {
|
6146
6191
|
pp = this.product_positions[pp];
|
@@ -6158,7 +6203,8 @@ class Cluster extends NodeBox {
|
|
6158
6203
|
}
|
6159
6204
|
|
6160
6205
|
containsProduct(p) {
|
6161
|
-
// Return the subcluster of this cluster that contains product `p`,
|
6206
|
+
// Return the subcluster of this cluster that contains product `p`,
|
6207
|
+
// or NULL if `p` does not occur in this cluster.
|
6162
6208
|
if(this.indexOfProduct(p) >= 0) return this;
|
6163
6209
|
for(let i = 0; i < this.sub_clusters.length; i++) {
|
6164
6210
|
if(this.sub_clusters[i].containsProduct(p)) {
|
@@ -7848,13 +7894,11 @@ class Product extends Node {
|
|
7848
7894
|
}
|
7849
7895
|
|
7850
7896
|
get isConstant() {
|
7851
|
-
// Return TRUE if this product is data,
|
7852
|
-
//
|
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.
|
7853
7900
|
if(!this.is_data || this.name.startsWith('$') ||
|
7854
|
-
!this.allOutputsAreData) return false;
|
7855
|
-
for(let i = 0; i < this.inputs.length; i++) {
|
7856
|
-
if(this.inputs[i].from_node instanceof Process) return false;
|
7857
|
-
}
|
7901
|
+
this.inputs.length || !this.allOutputsAreData) return false;
|
7858
7902
|
return (this.equal_bounds && this.lower_bound.defined);
|
7859
7903
|
}
|
7860
7904
|
|
@@ -8712,8 +8756,8 @@ class Dataset {
|
|
8712
8756
|
return d.join(';');
|
8713
8757
|
}
|
8714
8758
|
|
8715
|
-
// Returns a string denoting the properties of this dataset.
|
8716
8759
|
get propertiesString() {
|
8760
|
+
// Return a string denoting the properties of this dataset.
|
8717
8761
|
if(this.data.length === 0) return '';
|
8718
8762
|
let time_prop;
|
8719
8763
|
if(this.array) {
|
@@ -8724,12 +8768,12 @@ class Dataset {
|
|
8724
8768
|
DATASET_MANAGER.method_symbols[
|
8725
8769
|
DATASET_MANAGER.methods.indexOf(this.method)]].join('');
|
8726
8770
|
}
|
8727
|
-
// Circular arrow symbolizes "repeating"
|
8771
|
+
// Circular arrow symbolizes "repeating".
|
8728
8772
|
return ' (' + time_prop + (this.periodic ? ' \u21BB' : '') + ')';
|
8729
8773
|
}
|
8730
8774
|
|
8731
8775
|
unpackDataString(str) {
|
8732
|
-
//
|
8776
|
+
// Convert semicolon-separated data to a numeric array.
|
8733
8777
|
this.data.length = 0;
|
8734
8778
|
if(str) {
|
8735
8779
|
const numbers = str.split(';');
|
@@ -8742,12 +8786,12 @@ class Dataset {
|
|
8742
8786
|
}
|
8743
8787
|
|
8744
8788
|
computeVector() {
|
8745
|
-
//
|
8746
|
-
// lasting one unit on the model time scale
|
8789
|
+
// Convert data to a vector on the time scale of the model, i.e.,
|
8790
|
+
// 1 time step lasting one unit on the model time scale.
|
8747
8791
|
|
8748
|
-
// NOTE:
|
8749
|
-
//
|
8750
|
-
//
|
8792
|
+
// NOTE: A dataset can also be defined as an "array", which differs
|
8793
|
+
// from a time series in that the vector is filled with the data values
|
8794
|
+
// "as is" to permit accessing a specific value at index #.
|
8751
8795
|
if(this.array) {
|
8752
8796
|
this.vector = this.data.slice();
|
8753
8797
|
return;
|
@@ -8755,16 +8799,16 @@ class Dataset {
|
|
8755
8799
|
// Like all vectors, vector[0] corresponds to initial value, and vector[1]
|
8756
8800
|
// to the model setting "Optimize from step t=..."
|
8757
8801
|
// NOTES:
|
8758
|
-
// (1)
|
8802
|
+
// (1) The first number of a datasets time series is ALWAYS assumed to
|
8759
8803
|
// correspond to t=1, whereas the simulation may be set to start later!
|
8760
|
-
// (2)
|
8804
|
+
// (2) Model run length includes 1 look-ahead period.
|
8761
8805
|
VM.scaleDataToVector(this.data, this.vector, this.timeStepDuration,
|
8762
8806
|
MODEL.timeStepDuration, MODEL.runLength, MODEL.start_period,
|
8763
8807
|
this.defaultValue, this.periodic, this.method);
|
8764
8808
|
}
|
8765
8809
|
|
8766
8810
|
computeStatistics() {
|
8767
|
-
//
|
8811
|
+
// Compute descriptive statistics for data (NOT vector!).
|
8768
8812
|
if(this.data.length === 0) {
|
8769
8813
|
this.min = VM.UNDEFINED;
|
8770
8814
|
this.max = VM.UNDEFINED;
|
@@ -8789,29 +8833,48 @@ class Dataset {
|
|
8789
8833
|
}
|
8790
8834
|
|
8791
8835
|
get statisticsAsString() {
|
8792
|
-
//
|
8836
|
+
// Return descriptive statistics in human-readable form.
|
8793
8837
|
let s = 'N = ' + this.data.length;
|
8794
8838
|
if(N > 0) {
|
8795
|
-
s += ', range = ['
|
8796
|
-
', mean = '
|
8797
|
-
VM.sig4Dig(this.standard_deviation);
|
8839
|
+
s += [', range = [', VM.sig4Dig(this.min), ', ', VM.sig4Dig(this.max),
|
8840
|
+
'], mean = ', VM.sig4Dig(this.mean), ', s.d. = ',
|
8841
|
+
VM.sig4Dig(this.standard_deviation)].join('');
|
8798
8842
|
}
|
8799
8843
|
return s;
|
8800
8844
|
}
|
8801
8845
|
|
8802
8846
|
attributeValue(a) {
|
8803
|
-
//
|
8804
|
-
// NOTE: Datasets have ONE attribute (their vector) denoted by the
|
8805
|
-
//
|
8806
|
-
// their value should be obtained using attributeExpression
|
8807
|
-
|
8808
|
-
|
8847
|
+
// Return the computed result for attribute `a`.
|
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;
|
8809
8872
|
}
|
8810
8873
|
|
8811
8874
|
attributeExpression(a) {
|
8812
|
-
//
|
8875
|
+
// Return the expression for selector `a` (also considering wildcard
|
8813
8876
|
// modifiers), or NULL if no such selector exists.
|
8814
|
-
// NOTE:
|
8877
|
+
// NOTE: Selectors no longer are case-sensitive.
|
8815
8878
|
if(a) {
|
8816
8879
|
const mm = this.matchingModifiers([a]);
|
8817
8880
|
if(mm.length > 0) return mm[0].expression;
|
@@ -8822,29 +8885,29 @@ class Dataset {
|
|
8822
8885
|
get activeModifierExpression() {
|
8823
8886
|
if(MODEL.running_experiment) {
|
8824
8887
|
// If an experiment is running, check if dataset modifiers match the
|
8825
|
-
// combination of selectors for the active run
|
8888
|
+
// combination of selectors for the active run.
|
8826
8889
|
const mm = this.matchingModifiers(MODEL.running_experiment.activeCombination);
|
8827
|
-
// If so, use the first match
|
8890
|
+
// If so, use the first match.
|
8828
8891
|
if(mm.length > 0) return mm[0].expression;
|
8829
8892
|
}
|
8830
8893
|
if(this.default_selector) {
|
8831
|
-
// If no experiment (so "normal" run), use default selector if specified
|
8894
|
+
// If no experiment (so "normal" run), use default selector if specified.
|
8832
8895
|
const dm = this.modifiers[UI.nameToID(this.default_selector)];
|
8833
8896
|
if(dm) return dm.expression;
|
8834
|
-
// Exception should never occur, but check anyway and log it
|
8897
|
+
// Exception should never occur, but check anyway and log it.
|
8835
8898
|
console.log('WARNING: Dataset "' + this.name +
|
8836
8899
|
`" has no default selector "${this.default_selector}"`, this.modifiers);
|
8837
8900
|
}
|
8838
|
-
// Fall-through: return
|
8901
|
+
// Fall-through: return the dataset vector.
|
8839
8902
|
return this.vector;
|
8840
8903
|
}
|
8841
8904
|
|
8842
8905
|
addModifier(selector, node=null, ioc=null) {
|
8843
8906
|
let s = selector;
|
8844
|
-
//
|
8907
|
+
// First sanitize the selector.
|
8845
8908
|
if(this === MODEL.equations_dataset) {
|
8846
8909
|
// Equation identifiers cannot contain characters that have special
|
8847
|
-
// meaning in a variable identifier
|
8910
|
+
// meaning in a variable identifier.
|
8848
8911
|
s = s.replace(/[\*\|\[\]\{\}\@\#]/g, '');
|
8849
8912
|
if(s !== selector) {
|
8850
8913
|
UI.warn('Equation name cannot contain [, ], {, }, |, @, # or *');
|
@@ -8868,26 +8931,26 @@ class Dataset {
|
|
8868
8931
|
return null;
|
8869
8932
|
}
|
8870
8933
|
} else {
|
8871
|
-
// Prefix it when the IO context argument is defined
|
8934
|
+
// Prefix it when the IO context argument is defined.
|
8872
8935
|
if(ioc) s = ioc.actualName(s);
|
8873
8936
|
}
|
8874
|
-
// If equation already exists, return its modifier
|
8937
|
+
// If equation already exists, return its modifier.
|
8875
8938
|
const id = UI.nameToID(s);
|
8876
8939
|
if(this.modifiers.hasOwnProperty(id)) return this.modifiers[id];
|
8877
|
-
// New equation identifier must not equal some entity ID
|
8940
|
+
// New equation identifier must not equal some entity ID.
|
8878
8941
|
const obj = MODEL.objectByName(s);
|
8879
8942
|
if(obj) {
|
8880
|
-
// NOTE:
|
8943
|
+
// NOTE: Also pass selector, or warning will display dataset name.
|
8881
8944
|
UI.warningEntityExists(obj);
|
8882
8945
|
return null;
|
8883
8946
|
}
|
8884
8947
|
} else {
|
8885
8948
|
// Standard dataset modifier selectors are much more restricted, but
|
8886
|
-
// to be user-friendly, special chars are removed automatically
|
8949
|
+
// to be user-friendly, special chars are removed automatically.
|
8887
8950
|
s = s.replace(/[^a-zA-Z0-9\+\-\%\_\*\?]/g, '');
|
8888
8951
|
let msg = '';
|
8889
8952
|
if(s !== selector) msg = UI.WARNING.SELECTOR_SYNTAX;
|
8890
|
-
// A selector can only contain 1 star
|
8953
|
+
// A selector can only contain 1 star.
|
8891
8954
|
if(s.indexOf('*') !== s.lastIndexOf('*')) msg = UI.WARNING.SINGLE_WILDCARD;
|
8892
8955
|
if(msg) {
|
8893
8956
|
UI.warn(msg);
|
@@ -8898,12 +8961,12 @@ class Dataset {
|
|
8898
8961
|
UI.warn(UI.WARNING.INVALID_SELECTOR);
|
8899
8962
|
return null;
|
8900
8963
|
}
|
8901
|
-
// Then add a dataset modifier to this dataset
|
8964
|
+
// Then add a dataset modifier to this dataset.
|
8902
8965
|
const id = UI.nameToID(s);
|
8903
8966
|
if(!this.modifiers.hasOwnProperty(id)) {
|
8904
8967
|
this.modifiers[id] = new DatasetModifier(this, s);
|
8905
8968
|
}
|
8906
|
-
// Finally, initialize it when the XML node argument is defined
|
8969
|
+
// Finally, initialize it when the XML node argument is defined.
|
8907
8970
|
if(node) this.modifiers[id].initFromXML(node);
|
8908
8971
|
return this.modifiers[id];
|
8909
8972
|
}
|
@@ -8927,7 +8990,7 @@ class Dataset {
|
|
8927
8990
|
for(let i = 0; i < sl.length; i++) {
|
8928
8991
|
ml.push(this.modifiers[sl[i]].asXML);
|
8929
8992
|
}
|
8930
|
-
// NOTE: "black-boxed" datasets are stored anonymously without comments
|
8993
|
+
// NOTE: "black-boxed" datasets are stored anonymously without comments.
|
8931
8994
|
const id = UI.nameToID(n);
|
8932
8995
|
if(MODEL.black_box_entities.hasOwnProperty(id)) {
|
8933
8996
|
n = MODEL.black_box_entities[id];
|
@@ -8959,7 +9022,7 @@ class Dataset {
|
|
8959
9022
|
this.periodic = nodeParameterValue(node, 'periodic') === '1';
|
8960
9023
|
this.array = nodeParameterValue(node, 'array') === '1';
|
8961
9024
|
this.black_box = nodeParameterValue(node, 'black-box') === '1';
|
8962
|
-
// NOTE:
|
9025
|
+
// NOTE: Array-type datasets are by definition input => not an outcome.
|
8963
9026
|
if(!this.array) this.outcome = nodeParameterValue(node, 'outcome') === '1';
|
8964
9027
|
this.url = xmlDecoded(nodeContentByTag(node, 'url'));
|
8965
9028
|
if(this.url) {
|
@@ -8985,10 +9048,10 @@ class Dataset {
|
|
8985
9048
|
}
|
8986
9049
|
|
8987
9050
|
rename(name, notify=true) {
|
8988
|
-
// Change the name of this dataset
|
9051
|
+
// Change the name of this dataset.
|
8989
9052
|
// When `notify` is FALSE, notifications are suppressed while the
|
8990
|
-
// number of affected datasets and expressions are counted
|
8991
|
-
// NOTE:
|
9053
|
+
// number of affected datasets and expressions are counted.
|
9054
|
+
// NOTE: Prevent renaming the equations dataset (just in case).
|
8992
9055
|
if(this === MODEL.equations_dataset) return;
|
8993
9056
|
name = UI.cleanName(name);
|
8994
9057
|
if(!UI.validName(name)) {
|
@@ -9012,31 +9075,31 @@ class Dataset {
|
|
9012
9075
|
}
|
9013
9076
|
|
9014
9077
|
resetExpressions() {
|
9015
|
-
// Recalculate vector to adjust to model time scale and run length
|
9078
|
+
// Recalculate vector to adjust to model time scale and run length.
|
9016
9079
|
this.computeVector();
|
9017
|
-
// Reset all modifier expressions
|
9080
|
+
// Reset all modifier expressions.
|
9018
9081
|
for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
|
9019
|
-
// NOTE: "empty" expressions for modifiers default to dataset default
|
9082
|
+
// NOTE: "empty" expressions for modifiers default to dataset default.
|
9020
9083
|
this.modifiers[m].expression.reset(this.defaultValue);
|
9021
9084
|
this.modifiers[m].expression_cache = {};
|
9022
9085
|
}
|
9023
9086
|
}
|
9024
9087
|
|
9025
9088
|
compileExpressions() {
|
9026
|
-
// Recompile all modifier expressions
|
9089
|
+
// Recompile all modifier expressions.
|
9027
9090
|
for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
|
9028
9091
|
this.modifiers[m].expression.compile();
|
9029
9092
|
}
|
9030
9093
|
}
|
9031
9094
|
|
9032
9095
|
differences(ds) {
|
9033
|
-
// Return "dictionary" of differences, or NULL if none
|
9096
|
+
// Return "dictionary" of differences, or NULL if none.
|
9034
9097
|
const d = differences(this, ds, UI.MC.DATASET_PROPS);
|
9035
|
-
// Check for differences in data
|
9098
|
+
// Check for differences in data.
|
9036
9099
|
if(this.dataString !== ds.dataString) {
|
9037
9100
|
d.data = {A: this.statisticsAsString, B: ds.statisticsAsString};
|
9038
9101
|
}
|
9039
|
-
// Check for differences in modifiers
|
9102
|
+
// Check for differences in modifiers.
|
9040
9103
|
const mdiff = {};
|
9041
9104
|
for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
|
9042
9105
|
const
|
@@ -9055,7 +9118,7 @@ class Dataset {
|
|
9055
9118
|
mdiff[m] = [UI.MC.DELETED, dsm.selector, dsm.expression.text];
|
9056
9119
|
}
|
9057
9120
|
}
|
9058
|
-
// Only add modifiers property if differences were detected
|
9121
|
+
// Only add modifiers property if differences were detected.
|
9059
9122
|
if(Object.keys(mdiff).length > 0) d.modifiers = mdiff;
|
9060
9123
|
if(Object.keys(d).length > 0) return d;
|
9061
9124
|
return null;
|
@@ -9064,7 +9127,7 @@ class Dataset {
|
|
9064
9127
|
} // END of class Dataset
|
9065
9128
|
|
9066
9129
|
|
9067
|
-
// CLASS ChartVariable defines properties of chart time series
|
9130
|
+
// CLASS ChartVariable defines properties of chart time series.
|
9068
9131
|
class ChartVariable {
|
9069
9132
|
constructor(c) {
|
9070
9133
|
this.chart = c;
|
@@ -9244,23 +9307,11 @@ class ChartVariable {
|
|
9244
9307
|
t_end = tsteps;
|
9245
9308
|
} else {
|
9246
9309
|
// Get the variable's own value (number, vector or expression)
|
9247
|
-
|
9248
|
-
|
9249
|
-
|
9250
|
-
|
9251
|
-
|
9252
|
-
// Check if dataset modifiers match the combination of selectors
|
9253
|
-
// for the active run.
|
9254
|
-
const mm = this.object.matchingModifiers(
|
9255
|
-
MODEL.running_experiment.activeCombination);
|
9256
|
-
// If so, use the first (the list should contain at most 1 selector)
|
9257
|
-
// to select the modifier expression; otherwise, use the unmodified
|
9258
|
-
// vector of the dataset
|
9259
|
-
if(mm.length > 0) {
|
9260
|
-
av = mm[0].expression;
|
9261
|
-
} else {
|
9262
|
-
av = this.object.vector;
|
9263
|
-
}
|
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;
|
9264
9315
|
} else if(this.object instanceof DatasetModifier) {
|
9265
9316
|
av = this.object.expression;
|
9266
9317
|
} else {
|
@@ -11029,7 +11080,8 @@ class ExperimentRun {
|
|
11029
11080
|
bm.messages = VM.messages[i];
|
11030
11081
|
this.block_messages.push(bm);
|
11031
11082
|
this.warning_count += bm.warningCount;
|
11032
|
-
|
11083
|
+
// NOTE: When set by the VM, `solver_secs` is a string.
|
11084
|
+
this.solver_seconds += parseFloat(bm.solver_secs);
|
11033
11085
|
}
|
11034
11086
|
}
|
11035
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
|
//
|
@@ -743,17 +757,28 @@ function xmlDecoded(str) {
|
|
743
757
|
}
|
744
758
|
|
745
759
|
function customizeXML(str) {
|
746
|
-
// NOTE:
|
760
|
+
// NOTE: This function can be customized to pre-process a model file,
|
747
761
|
// for example to rename entities in one go -- USE WITH CARE!
|
748
|
-
//
|
749
|
-
|
762
|
+
// To prevent unintended customization, check whether the model name
|
763
|
+
// ends with "!!CUSTOMIZE". This check ensures that the modeler must
|
764
|
+
// first save the model with this text as the (end of the) model name
|
765
|
+
// and then load it again for the customization to be performed.
|
766
|
+
if(str.indexOf('!!CUSTOMIZE</name><author>') >= 0) {
|
767
|
+
// Modify `str` -- by default, do nothing, but typical modifications
|
768
|
+
// will replace RexEx patterns by other strings.
|
769
|
+
|
750
770
|
/*
|
751
|
-
|
752
|
-
|
753
|
-
|
771
|
+
const
|
772
|
+
re = /xyz/gi,
|
773
|
+
r = 'abc';
|
754
774
|
*/
|
755
775
|
|
756
|
-
|
776
|
+
// Trace the changes to the console.
|
777
|
+
console.log('Customizing:', re, r);
|
778
|
+
console.log('Matches:', str.match(re));
|
779
|
+
str = str.replace(re, r);
|
780
|
+
}
|
781
|
+
// Finally, return the modified string.
|
757
782
|
return str;
|
758
783
|
}
|
759
784
|
|