linny-r 1.1.13 → 1.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.1.13",
3
+ "version": "1.1.14",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -2168,8 +2168,8 @@ td.equation-expression {
2168
2168
 
2169
2169
  /* SERIES modal dialog */
2170
2170
  #series-dlg {
2171
- width: 156px;
2172
- height: 282px;
2171
+ width: 165px;
2172
+ height: 320px;
2173
2173
  }
2174
2174
 
2175
2175
  #series-default-lbl {
@@ -2182,7 +2182,7 @@ td.equation-expression {
2182
2182
  position: absolute;
2183
2183
  top: 24px;
2184
2184
  left: 79px;
2185
- width: 73px;
2185
+ width: calc(100% - 83px);
2186
2186
  font-size: 12px;
2187
2187
  margin-bottom: 2px;
2188
2188
  }
@@ -2202,13 +2202,13 @@ td.equation-expression {
2202
2202
  #series-array {
2203
2203
  position: absolute;
2204
2204
  top: 45px;
2205
- left: 90px;
2205
+ left: 77px;
2206
2206
  }
2207
2207
 
2208
2208
  #series-array-lbl {
2209
2209
  position: absolute;
2210
2210
  top: 47px;
2211
- left: 112px;
2211
+ left: 99px;
2212
2212
  }
2213
2213
 
2214
2214
  #series-no-time-msg {
@@ -2216,7 +2216,7 @@ td.equation-expression {
2216
2216
  top: 66px;
2217
2217
  left: 1px;
2218
2218
  width: calc(100% - 2px);
2219
- height: 27px;
2219
+ height: 30px;
2220
2220
  z-index: 1;
2221
2221
  background-color: inherit;
2222
2222
  font-style: italic;
@@ -2235,7 +2235,7 @@ td.equation-expression {
2235
2235
  position: absolute;
2236
2236
  top: 67px;
2237
2237
  left: 60px;
2238
- width: 43px;
2238
+ width: 50px;
2239
2239
  font-size: 12px;
2240
2240
  margin-bottom: 2px;
2241
2241
  }
@@ -2243,7 +2243,7 @@ td.equation-expression {
2243
2243
  #series-time-unit {
2244
2244
  position: absolute;
2245
2245
  top: 67px;
2246
- left: 108px;
2246
+ left: 116px;
2247
2247
  height: 19px;
2248
2248
  width: 45px;
2249
2249
  font-size: 12px;
@@ -2251,40 +2251,43 @@ td.equation-expression {
2251
2251
 
2252
2252
  #series-method-lbl {
2253
2253
  position: absolute;
2254
- top: 90px;
2254
+ top: 91px;
2255
2255
  left: 2px;
2256
2256
  }
2257
2257
 
2258
2258
  #series-method {
2259
2259
  position: absolute;
2260
- top: 88px;
2261
- left: 49px;
2262
- height: 19px;
2263
- width: 105px;
2264
- font-size: 11px;
2260
+ top: 89px;
2261
+ left: 50px;
2262
+ height: 21px;
2263
+ width: 111px;
2264
+ font-size: 12px;
2265
2265
  }
2266
2266
 
2267
2267
  #series-remote {
2268
2268
  position: absolute;
2269
- top: 110px;
2270
- left: 2px;
2269
+ top: 113px;
2270
+ left: 3px;
2271
+ width: calc(100% - 8px);
2271
2272
  }
2272
2273
 
2273
2274
  #series-url {
2274
- width: 150px;
2275
+ width: 100%;
2275
2276
  font-size: 12px;
2276
2277
  }
2277
2278
 
2278
2279
  #series-data-lbl {
2279
2280
  position: absolute;
2280
- top: 129px;
2281
+ top: 133px;
2281
2282
  left: 2px;
2282
2283
  }
2283
2284
 
2284
2285
  #series-data {
2285
- width: 150px;
2286
- height: 120px;
2287
- margin: 125px 3px;
2286
+ position: absolute;
2287
+ bottom: 13px;
2288
+ width: calc(100% - 6px);
2289
+ height: calc(100% - 165px);
2290
+ margin: 3px;
2288
2291
  }
2289
2292
 
2290
2293
  #series-line {
@@ -140,7 +140,7 @@ class Controller {
140
140
  'scale_unit', 'equal_bounds', 'price', 'is_source', 'is_sink', 'is_buffer',
141
141
  'is_data', 'integer_level', 'no_slack'],
142
142
  DATASET_PROPS: ['comments', 'default_value', 'time_scale', 'time_unit',
143
- 'method', 'periodic', 'array', 'url'],
143
+ 'method', 'periodic', 'array', 'url', 'default_selector'],
144
144
  LINK_PROPS: ['comments', 'multiplier', 'relative_rate', 'share_of_cost',
145
145
  'flow_delay'],
146
146
  CONSTRAINT_PROPS: ['comments', 'no_slack', 'share_of_cost'],
@@ -8833,7 +8833,7 @@ class GUIDatasetManager extends DatasetManager {
8833
8833
  if(d === sd) sdid += i;
8834
8834
  dl.push(['<tr id="dstr', i, '" class="dataset',
8835
8835
  (d === sd ? ' sel-set' : ''),
8836
- '" onclick="DATASET_MANAGER.selectDataset(\'',
8836
+ '" onclick="DATASET_MANAGER.selectDataset(event, \'',
8837
8837
  dnl[i], '\');" onmouseover="DATASET_MANAGER.showInfo(\'', dnl[i],
8838
8838
  '\', event.shiftKey);"><td', cls, '>', d.displayName,
8839
8839
  '</td></tr>'].join(''));
@@ -8907,14 +8907,19 @@ class GUIDatasetManager extends DatasetManager {
8907
8907
  for(let i = 0; i < msl.length; i++) {
8908
8908
  const
8909
8909
  m = sd.modifiers[UI.nameToID(msl[i])],
8910
- clk = '" onclick="DATASET_MANAGER.selectModifier(\'' +
8910
+ defsel = (m.selector === sd.default_selector),
8911
+ clk = '" onclick="DATASET_MANAGER.selectModifier(event, \'' +
8911
8912
  m.selector + '\'';
8912
8913
  if(m === sm) smid += i;
8913
8914
  ml.push(['<tr id="dsmtr', i, '" class="dataset-modif',
8914
8915
  (m === sm ? ' sel-set' : ''),
8915
8916
  '"><td class="dataset-selector',
8916
8917
  (m.hasWildcards ? ' wildcard' : ''),
8918
+ '" title="Shift-click to ', (defsel ? 'clear' : 'set as'),
8919
+ ' default modifier',
8917
8920
  clk, ', false);">',
8921
+ (defsel ? '<img src="images/solve.png" style="height: 14px;' +
8922
+ ' width: 14px; margin: 0 1px -3px -1px;">' : ''),
8918
8923
  m.selector, '</td><td class="dataset-expression',
8919
8924
  clk, ');">', m.expression.text, '</td></tr>'].join(''));
8920
8925
  }
@@ -8961,46 +8966,54 @@ class GUIDatasetManager extends DatasetManager {
8961
8966
  this.updateDialog();
8962
8967
  }
8963
8968
 
8964
- selectDataset(id) {
8965
- // Select dataset, or edit it when double-clicked
8969
+ selectDataset(event, id) {
8970
+ // Select dataset, or edit it when Alt- or double-clicked
8966
8971
  const
8967
- d = MODEL.datasets[id],
8972
+ d = MODEL.datasets[id] || null,
8968
8973
  now = Date.now(),
8969
- dt = now - this.last_time_selected;
8974
+ dt = now - this.last_time_selected,
8975
+ // Consider click to be "double" if it occurred less than 300 ms ago
8976
+ edit = event.altKey || (d === this.selected_dataset && dt < 300);
8977
+ this.selected_dataset = d;
8970
8978
  this.last_time_selected = now;
8971
- if(d === this.selected_dataset) {
8972
- // Consider click to be "double" if it occurred less than 300 ms ago
8973
- if(dt < 300) {
8974
- this.last_time_selected = 0;
8975
- this.editData();
8976
- return;
8977
- }
8979
+ if(d && edit) {
8980
+ this.last_time_selected = 0;
8981
+ this.editData();
8982
+ return;
8978
8983
  }
8979
- this.selected_dataset = MODEL.datasets[id];
8980
8984
  this.updateDialog();
8981
8985
  }
8982
8986
 
8983
- selectModifier(id, x=true) {
8987
+ selectModifier(event, id, x=true) {
8984
8988
  // Select modifier, or when double-clicked, edit its expression or the
8985
8989
  // name of the modifier
8986
8990
  if(this.selected_dataset) {
8987
8991
  const m = this.selected_dataset.modifiers[UI.nameToID(id)],
8988
8992
  now = Date.now(),
8989
- dt = now - this.last_time_selected;
8993
+ dt = now - this.last_time_selected,
8994
+ // NOTE: Alt-click and double-click indicate: edit
8995
+ // Consider click to be "double" if the same modifier was clicked
8996
+ // less than 300 ms ago
8997
+ edit = event.altKey || (m === this.selected_modifier && dt < 300);
8990
8998
  this.last_time_selected = now;
8991
- if(m === this.selected_modifier) {
8992
- // Consider click to be "double" if it occurred less than 300 ms ago
8993
- if(dt < 300) {
8994
- this.last_time_selected = 0;
8995
- if(x) {
8996
- this.editExpression();
8997
- } else {
8998
- this.promptForSelector('rename');
8999
- }
9000
- return;
8999
+ if(event.shiftKey) {
9000
+ // Toggle dataset default selector
9001
+ if(m.selector === this.selected_dataset.default_selector) {
9002
+ this.selected_dataset.default_selector = '';
9003
+ } else {
9004
+ this.selected_dataset.default_selector = m.selector;
9001
9005
  }
9002
9006
  }
9003
9007
  this.selected_modifier = m;
9008
+ if(edit) {
9009
+ this.last_time_selected = 0;
9010
+ if(x) {
9011
+ this.editExpression();
9012
+ } else {
9013
+ this.promptForSelector('rename');
9014
+ }
9015
+ return;
9016
+ }
9004
9017
  } else {
9005
9018
  this.selected_modifier = null;
9006
9019
  }
@@ -9167,6 +9180,10 @@ class GUIDatasetManager extends DatasetManager {
9167
9180
  m = this.selected_dataset.addModifier(sel);
9168
9181
  // NULL can result when new name is invalid
9169
9182
  if(!m) return;
9183
+ // If selected modifier was the dataset default selector, update it
9184
+ if(oldm.selector === this.selected_dataset.default_selector) {
9185
+ this.selected_dataset.default_selector = m.selector;
9186
+ }
9170
9187
  // If only case has changed, just update the selector
9171
9188
  // NOTE: normal dataset selector, so remove all invalid characters
9172
9189
  if(m === oldm) {
@@ -9248,8 +9265,14 @@ class GUIDatasetManager extends DatasetManager {
9248
9265
  }
9249
9266
 
9250
9267
  deleteModifier() {
9268
+ // Delete modifier from selected dataset
9251
9269
  const m = this.selected_modifier;
9252
9270
  if(m) {
9271
+ // If it was the dataset default modifier, clear the default
9272
+ if(m.selector === this.selected_dataset.default_selector) {
9273
+ this.selected_dataset.default_selector = '';
9274
+ }
9275
+ // Then simply remove the object
9253
9276
  delete this.selected_dataset.modifiers[UI.nameToID(m.selector)];
9254
9277
  this.selected_modifier = null;
9255
9278
  this.updateModifiers();
@@ -9418,7 +9441,7 @@ class EquationManager {
9418
9441
  const
9419
9442
  m = ed.modifiers[UI.nameToID(msl[i])],
9420
9443
  mp = (m.parameters ? '\\' + m.parameters.join('\\') : ''),
9421
- clk = '" onclick="EQUATION_MANAGER.selectModifier(\'' +
9444
+ clk = '" onclick="EQUATION_MANAGER.selectModifier(event, \'' +
9422
9445
  m.selector + '\'';
9423
9446
  if(m === sm) smid += i;
9424
9447
  ml.push(['<tr id="eqmtr', i, '" class="dataset-modif',
@@ -9444,28 +9467,27 @@ class EquationManager {
9444
9467
  // @@TO DO: Display documentation for the equation => extra comments field?
9445
9468
  }
9446
9469
 
9447
- selectModifier(id, x=true) {
9448
- // Select modifier, or when double-clicked, edit its expression or the
9449
- // name of the modifier
9470
+ selectModifier(event, id, x=true) {
9471
+ // Select modifier, or when Alt- or double-clicked, edit its expression
9472
+ // or the equation name (= name of the modifier)
9450
9473
  if(MODEL.equations_dataset) {
9451
9474
  const
9452
- m = MODEL.equations_dataset.modifiers[UI.nameToID(id)],
9475
+ m = MODEL.equations_dataset.modifiers[UI.nameToID(id)] || null,
9453
9476
  now = Date.now(),
9454
- dt = now - this.last_time_selected;
9477
+ dt = now - this.last_time_selected,
9478
+ // Consider click to be "double" if it occurred less than 300 ms ago
9479
+ edit = event.altKey || (m === this.selected_modifier && dt < 300);
9455
9480
  this.last_time_selected = now;
9456
- if(m === this.selected_modifier) {
9457
- // Consider click to be "double" if it occurred less than 300 ms ago
9458
- if(dt < 300) {
9459
- this.last_time_selected = 0;
9460
- if(x) {
9461
- this.editEquation();
9462
- } else {
9463
- this.promptForName();
9464
- }
9465
- return;
9481
+ this.selected_modifier = m;
9482
+ if(m && edit) {
9483
+ this.last_time_selected = 0;
9484
+ if(x) {
9485
+ this.editEquation();
9486
+ } else {
9487
+ this.promptForName();
9466
9488
  }
9489
+ return;
9467
9490
  }
9468
- this.selected_modifier = m;
9469
9491
  } else {
9470
9492
  this.selected_modifier = null;
9471
9493
  }
@@ -6301,6 +6301,37 @@ class Node extends NodeBox {
6301
6301
  }
6302
6302
  return cac;
6303
6303
  }
6304
+
6305
+ convertLegacyBoundData(lb_data, ub_data) {
6306
+ // Convert time series data for LB and UB in legacy models to datasets,
6307
+ // and replace attribute expressions by references to these datasets
6308
+ if(!lb_data && !ub_data) return;
6309
+ const same = lb_data === ub_data;
6310
+ if(lb_data) {
6311
+ const
6312
+ dsn = this.displayName + (same ? '' : ' LOWER') + ' BOUND DATA',
6313
+ ds = MODEL.addDataset(dsn);
6314
+ // Use the LB attribute as default value for the dataset
6315
+ ds.default_value = parseFloat(this.lower_bound.text);
6316
+ ds.data = stringToFloatArray(lb_data);
6317
+ ds.computeVector();
6318
+ ds.computeStatistics();
6319
+ this.lower_bound.text = `[${dsn}]`;
6320
+ if(same) this.equal_bounds = true;
6321
+ MODEL.legacy_datasets = true;
6322
+ }
6323
+ if(ub_data && !same) {
6324
+ const
6325
+ dsn = this.displayName + ' UPPER BOUND DATA',
6326
+ ds = MODEL.addDataset(dsn);
6327
+ ds.default_value = parseFloat(this.upper_bound.text);
6328
+ ds.data = stringToFloatArray(ub_data);
6329
+ ds.computeVector();
6330
+ ds.computeStatistics();
6331
+ this.upper_bound.text = `[${dsn}]`;
6332
+ MODEL.legacy_datasets = true;
6333
+ }
6334
+ }
6304
6335
 
6305
6336
  actualLevel(t) {
6306
6337
  // Returns the production level c.q. stock level for this node in time step t
@@ -6459,6 +6490,13 @@ class Process extends Node {
6459
6490
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
6460
6491
  this.lower_bound.text = xmlDecoded(nodeContentByTag(node, 'lower-bound'));
6461
6492
  this.upper_bound.text = xmlDecoded(nodeContentByTag(node, 'upper-bound'));
6493
+ // legacy models can have LB and UB hexadecimal data strings
6494
+ this.convertLegacyBoundData(nodeContentByTag(node, 'lower-bound-data'),
6495
+ nodeContentByTag(node, 'upper-bound-data'));
6496
+ if(nodeParameterValue(node, 'reversible') === '1') {
6497
+ // For legacy "reversible" processes, the LB is set to -UB
6498
+ this.lower_bound.text = '-' + this.upper_bound.text;
6499
+ }
6462
6500
  // NOTE: legacy models have no initial level field => default to 0
6463
6501
  const ilt = xmlDecoded(nodeContentByTag(node, 'initial-level'));
6464
6502
  this.initial_level.text = ilt || '0';
@@ -6871,47 +6909,25 @@ class Product extends Node {
6871
6909
  this.equal_bounds = nodeParameterValue(node, 'equal-bounds') === '1';
6872
6910
  this.integer_level = nodeParameterValue(node, 'integer-level') === '1';
6873
6911
  this.no_slack = nodeParameterValue(node, 'no-slack') === '1';
6874
- // legacy models have tag "hidden" instead of "no-links"
6912
+ // Legacy models have tag "hidden" instead of "no-links"
6875
6913
  this.no_links = (nodeParameterValue(node, 'no-links') ||
6876
6914
  nodeParameterValue(node, 'hidden')) === '1';
6877
6915
  this.scale_unit = MODEL.addScaleUnit(
6878
6916
  xmlDecoded(nodeContentByTag(node, 'unit')));
6879
- // legacy models have tag "profit" instead of "price"
6917
+ // Legacy models have tag "profit" instead of "price"
6880
6918
  let pp = nodeContentByTag(node, 'price');
6881
6919
  if(!pp) pp = nodeContentByTag(node, 'profit');
6882
6920
  this.price.text = xmlDecoded(pp);
6921
+ // Legacy models can have price time series data as hexadecimal string
6922
+ this.convertLegacyPriceData(nodeContentByTag(node, 'profit-data'));
6883
6923
  this.lower_bound.text = xmlDecoded(nodeContentByTag(node, 'lower-bound'));
6884
6924
  this.upper_bound.text = xmlDecoded(nodeContentByTag(node, 'upper-bound'));
6885
6925
  // legacy models can have LB and UB hexadecimal data strings
6886
- const
6887
- lb_data = nodeContentByTag(node, 'lower-bound-data'),
6888
- ub_data = nodeContentByTag(node, 'upper-bound-data'),
6889
- same = lb_data === ub_data;
6890
- if(lb_data) {
6891
- const
6892
- dsn = this.displayName + (same ? '' : ' LOWER') + ' BOUND DATA',
6893
- ds = MODEL.addDataset(dsn);
6894
- ds.default_value = parseFloat(this.lower_bound.text);
6895
- ds.data = stringToFloatArray(lb_data);
6896
- ds.computeVector();
6897
- ds.computeStatistics();
6898
- this.lower_bound.text = `[${dsn}]`;
6899
- if(same) this.equal_bounds = true;
6900
- MODEL.legacy_datasets = true;
6901
- }
6902
- if(ub_data && !same) {
6903
- const
6904
- dsn = this.displayName + ' UPPER BOUND DATA',
6905
- ds = MODEL.addDataset(dsn);
6906
- ds.default_value = parseFloat(this.upper_bound.text);
6907
- ds.data = stringToFloatArray(ub_data);
6908
- ds.computeVector();
6909
- ds.computeStatistics();
6910
- this.upper_bound.text = `[${dsn}]`;
6911
- MODEL.legacy_datasets = true;
6912
- }
6913
- this.initial_level.text = xmlDecoded(
6914
- nodeContentByTag(node, 'initial-level'));
6926
+ this.convertLegacyBoundData(nodeContentByTag(node, 'lower-bound-data'),
6927
+ nodeContentByTag(node, 'upper-bound-data'));
6928
+ // NOTE: legacy models have no initial level field => default to 0
6929
+ const ilt = xmlDecoded(nodeContentByTag(node, 'initial-level'));
6930
+ this.initial_level.text = ilt || '0';
6915
6931
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
6916
6932
  this.x = safeStrToInt(nodeContentByTag(node, 'x-coord'));
6917
6933
  this.y = safeStrToInt(nodeContentByTag(node, 'y-coord'));
@@ -6927,6 +6943,23 @@ class Product extends Node {
6927
6943
  this.resize();
6928
6944
  }
6929
6945
 
6946
+ convertLegacyPriceData(data) {
6947
+ // Convert time series data for prices in legacy models to a dataset,
6948
+ // and replace the price expression by a reference to this dataset
6949
+ if(data) {
6950
+ const
6951
+ dsn = this.displayName + ' PRICE DATA',
6952
+ ds = MODEL.addDataset(dsn);
6953
+ // Use the price attribute as default value for the dataset
6954
+ ds.default_value = parseFloat(this.price.text);
6955
+ ds.data = stringToFloatArray(data);
6956
+ ds.computeVector();
6957
+ ds.computeStatistics();
6958
+ this.price.text = `[${dsn}]`;
6959
+ MODEL.legacy_datasets = true;
6960
+ }
6961
+ }
6962
+
6930
6963
  get defaultAttribute() {
6931
6964
  return 'L';
6932
6965
  }
@@ -7396,6 +7429,8 @@ class Dataset {
7396
7429
  // *model* time step t = 0
7397
7430
  this.vector = [];
7398
7431
  this.modifiers = {};
7432
+ // Selector to be used when model is run normally, i.e., no experiment
7433
+ this.default_selector = '';
7399
7434
  }
7400
7435
 
7401
7436
  get type() {
@@ -7781,7 +7816,8 @@ class Dataset {
7781
7816
  '</method><url>', xmlEncoded(this.url),
7782
7817
  '</url><data>', xmlEncoded(this.dataString),
7783
7818
  '</data><modifiers>', ml.join(''),
7784
- '</modifiers></dataset>'].join('');
7819
+ '</modifiers><default-selector>', xmlEncoded(this.default_selector),
7820
+ '</default-selector></dataset>'].join('');
7785
7821
  return xml;
7786
7822
  }
7787
7823
 
@@ -7812,6 +7848,12 @@ class Dataset {
7812
7848
  }
7813
7849
  }
7814
7850
  }
7851
+ const ds = xmlDecoded(nodeContentByTag(node, 'default-selector'));
7852
+ if(ds && !this.modifiers[ds]) {
7853
+ UI.warn(`Dataset <tt>${this.name}</tt> has no selector <tt>${ds}</tt>`);
7854
+ } else {
7855
+ this.default_selector = ds;
7856
+ }
7815
7857
  }
7816
7858
 
7817
7859
  rename(name) {
@@ -564,7 +564,9 @@ function hexToFloat(s) {
564
564
  const
565
565
  sign = (n >> 31 ? -1 : 1),
566
566
  exp = Math.pow(2, ((n >> 23) & 0xFF) - 127);
567
- return sign * (n & 0x7fffff | 0x800000) * 1.0 / Math.pow(2, 23) * exp;
567
+ f = sign * (n & 0x7fffff | 0x800000) * 1.0 / Math.pow(2, 23) * exp;
568
+ // NOTE: must consider precision of 32-bit floating point numbers!
569
+ return parseFloat(f.toPrecision(7));
568
570
  }
569
571
 
570
572
  function stringToFloatArray(s) {
@@ -714,8 +714,11 @@ class ExpressionParser {
714
714
  return [x, anchor1, offset1, anchor2, offset2];
715
715
  }
716
716
  }
717
+
718
+ //
717
719
  // NOTE: for experiment results, the method will ALWAYS have returned
718
720
  // a result, so what follows does not apply to experiment results
721
+ //
719
722
 
720
723
  // Attribute name (optional) follows object-attribute separator
721
724
  s = name.split(UI.OA_SEPARATOR);
@@ -725,7 +728,7 @@ class ExpressionParser {
725
728
  // ... so restore name if itself contains other vertical bars
726
729
  name = s.join(UI.OA_SEPARATOR).trim();
727
730
  if(!attr) {
728
- // Explicit empty attribute, e.g., [name|]
731
+ // Explicit *empty* attribute, e.g., [name|]
729
732
  // NOTE: this matters for datasets having specifiers: the vertical
730
733
  // bar indicates "do not infer a modifier from a running experiment,
731
734
  // but use the data"
@@ -817,8 +820,12 @@ class ExpressionParser {
817
820
  (attr ? ' and have attribute ' + attr : '');
818
821
  return false;
819
822
  }
823
+
824
+ //
820
825
  // NOTE: for statistics, the method will ALWAYS have returned a result,
821
826
  // so what follows does not apply to statistics results
827
+ //
828
+
822
829
  let by_reference = false;
823
830
  if(name === '.') {
824
831
  // NOTE: when name is a single dot, it refers to the data of a dataset
@@ -5227,15 +5234,26 @@ function VMI_push_dataset_modifier(x, args) {
5227
5234
  MODEL.end_period - MODEL.start_period + MODEL.look_ahead + 1, t));
5228
5235
  }
5229
5236
  if(ms) {
5230
- // If modifier selector is specified, use the associated expression ...
5237
+ // If modifier selector is specified, use the associated expression
5231
5238
  obj = mx;
5232
- // ... else check whether an experiment is running UNLESS "use data" is TRUE
5233
- } else if(MODEL.running_experiment && !ud) {
5234
- // If so, check if dataset modifiers match the combination of selectors
5235
- // for the active run
5236
- const mm = ds.matchingModifiers(MODEL.running_experiment.activeCombination);
5237
- // If so, use the first match
5238
- if(mm.length > 0) obj = mm[0].expression;
5239
+ } else if(!ud) {
5240
+ if(MODEL.running_experiment) {
5241
+ // If an experiment is running, check if dataset modifiers match the
5242
+ // combination of selectors for the active run
5243
+ const mm = ds.matchingModifiers(MODEL.running_experiment.activeCombination);
5244
+ // If so, use the first match
5245
+ if(mm.length > 0) obj = mm[0].expression;
5246
+ } else if(ds.default_selector) {
5247
+ // If no expriment (so "normal" run), use default selector if specified
5248
+ const dm = ds.modifiers[ds.default_selector];
5249
+ if(dm) {
5250
+ obj = dm.expression;
5251
+ } else {
5252
+ // Exception should never occur, but check anyway and log it
5253
+ console.log('WARNING: Dataset "' + ds.name +
5254
+ `" has no default selector "${ds.default_selector}"`);
5255
+ }
5256
+ }
5239
5257
  }
5240
5258
  // By default, use the dataset default value
5241
5259
  let v = ds.defaultValue,