linny-r 2.1.4 → 2.1.5

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": "2.1.4",
3
+ "version": "2.1.5",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/static/index.html CHANGED
@@ -2381,6 +2381,14 @@ NOTE: * and ? will be interpreted as wildcards"
2381
2381
  <div id="variable-color" title="Click to copy, Shift-click to paste"></div>
2382
2382
  <div id="variable-paste-color"></div>
2383
2383
  </div>
2384
+ <div id="variable-absolute-div"
2385
+ title="Plot the absolute value of this variable"
2386
+ style="margin: 2px -2px">
2387
+ <div id="variable-absolute" class="box clear"></div>
2388
+ <div style="display:inline-block; vertical-align:top; margin-top: 3px">
2389
+ Absolute
2390
+ </div>
2391
+ </div>
2384
2392
  <div id="variable-scale-div" style="margin-top: 3px">
2385
2393
  <div style="display:inline-block; vertical-align:top; margin-top: 2px">
2386
2394
  Scale by:
@@ -3201,10 +3209,16 @@ where X can be one or several of these letters: ABCDELPQ">
3201
3209
  <select id="confirm-add-chart-variables-attribute"></select>
3202
3210
  of <span id="confirm-add-chart-variables-count"></span>
3203
3211
  </div>
3212
+ <div>
3213
+ <div id="confirm-add-chart-variables-absolute" class="box clear"></div>
3214
+ <div style="display:inline-block; vertical-align:top; margin-top: 3px">
3215
+ Plot absolute values
3216
+ </div>
3217
+ </div>
3204
3218
  <div>
3205
3219
  <div id="confirm-add-chart-variables-stacked" class="box clear"></div>
3206
3220
  <div style="display:inline-block; vertical-align:top; margin-top: 3px">
3207
- Display as stacked areas
3221
+ Plot as stacked areas
3208
3222
  </div>
3209
3223
  </div>
3210
3224
  </div>
@@ -2918,8 +2918,8 @@ td.dataset-selector {
2918
2918
  }
2919
2919
 
2920
2920
  td.equation-selector {
2921
- min-width: 120px;
2922
- max-width: 240px;
2921
+ padding-right: 6px;
2922
+ max-width: 200px;
2923
2923
  white-space: nowrap;
2924
2924
  overflow: hidden;
2925
2925
  text-overflow: ellipsis;
@@ -2964,11 +2964,12 @@ td.equation-expression {
2964
2964
  }
2965
2965
 
2966
2966
  td.equation-expression {
2967
- min-width: 60%;
2967
+ width: 85%;
2968
2968
  }
2969
2969
 
2970
2970
  td.equation-expression-multi {
2971
2971
  border-left: solid 1px Silver;
2972
+ width: 85%;
2972
2973
  white-space: normal;
2973
2974
  overflow-x: hidden;
2974
2975
  overflow-y: auto;
@@ -3428,6 +3429,9 @@ td.vbl-desc-lead::after {
3428
3429
  color: #b00080;
3429
3430
  }
3430
3431
 
3432
+ td.vbl-abs {
3433
+ color: #0050d0 !important;
3434
+ }
3431
3435
 
3432
3436
  #chart-variables {
3433
3437
  position: absolute;
@@ -3604,8 +3608,8 @@ img.v-disab {
3604
3608
  }
3605
3609
 
3606
3610
  #variable-dlg {
3607
- width: 215px;
3608
- height: 148px;
3611
+ width: 245px;
3612
+ height: 170px;
3609
3613
  }
3610
3614
 
3611
3615
  #variable-dlg-name {
@@ -986,11 +986,14 @@ class SensitivityAnalysis {
986
986
  UI.alert(`Parameter ${p} is not a dataset or expression`);
987
987
  }
988
988
  }
989
+ // Create the SA chart having a variable for each SA outcome.
989
990
  this.chart = new Chart(this.chart_title);
990
991
  for(const o of MODEL.sensitivity_outcomes) {
991
992
  this.chart.addVariable(...o.split(UI.OA_SEPARATOR));
992
993
  }
994
+ // Create the SA experiment.
993
995
  this.experiment = new Experiment(this.experiment_title);
996
+ // Add the SA chart so the outcomes become result variables.
994
997
  this.experiment.charts = [this.chart];
995
998
  this.experiment.inferVariables();
996
999
  // This experiment always uses the same combination: the base selectors.
@@ -125,7 +125,7 @@ class GUIChartManager extends ChartManager {
125
125
  document.getElementById('chart-copy-data-btn').addEventListener(
126
126
  'click', () => CHART_MANAGER.copyData());
127
127
  document.getElementById('chart-copy-table-btn').addEventListener(
128
- 'click', () => CHART_MANAGER.copyTable());
128
+ 'click', (event) => CHART_MANAGER.copyTable(event.shiftKey));
129
129
  document.getElementById('chart-save-btn').addEventListener(
130
130
  'click', () => CHART_MANAGER.downloadChart(event.shiftKey));
131
131
  document.getElementById('chart-widen-btn').addEventListener(
@@ -164,8 +164,8 @@ class GUIChartManager extends ChartManager {
164
164
  'click', (event) => CHART_MANAGER.copyPasteColor(event));
165
165
  // NOTE: Uses the color picker developed by James Daniel.
166
166
  this.color_picker = new iro.ColorPicker("#color-picker", {
167
- width: 92,
168
- height: 92,
167
+ width: 115,
168
+ height: 115,
169
169
  color: '#a00',
170
170
  markerRadius: 10,
171
171
  padding: 1,
@@ -391,7 +391,7 @@ class GUIChartManager extends ChartManager {
391
391
  (cv.visible ? ' checked' : ' clear'),
392
392
  '" onclick="CHART_MANAGER.toggleVariable(', i,
393
393
  ');"></div></td><td class="v-name vbl-', cv.sorted,
394
- '">', cv.displayName,
394
+ (cv.absolute ? ' vbl-abs' : ''), '">', cv.displayName,
395
395
  '</td></tr>'].join(''));
396
396
  }
397
397
  this.variables_table.innerHTML = ol.join('');
@@ -641,7 +641,7 @@ class GUIChartManager extends ChartManager {
641
641
  for(const cv of c.variables) {
642
642
  const nv = new ChartVariable(nc);
643
643
  nv.setProperties(cv.object, cv.attribute, cv.stacked,
644
- cv.color, cv.scale_factor, cv.line_width, cv.sorted);
644
+ cv.color, cv.scale_factor, cv.absolute, cv.line_width, cv.sorted);
645
645
  nc.variables.push(nv);
646
646
  }
647
647
  this.chart_index = MODEL.indexOfChart(nc.title);
@@ -844,6 +844,7 @@ class GUIChartManager extends ChartManager {
844
844
  const cv = MODEL.charts[this.chart_index].variables[this.variable_index];
845
845
  document.getElementById('variable-dlg-name').innerHTML = cv.displayName;
846
846
  UI.setBox('variable-stacked', cv.stacked);
847
+ UI.setBox('variable-absolute', cv.absolute);
847
848
  // Pass TRUE tiny flag to permit very small scaling factors.
848
849
  this.variable_modal.element('scale').value = VM.sig4Dig(cv.scale_factor, true);
849
850
  this.variable_modal.element('width').value = VM.sig4Dig(cv.line_width);
@@ -928,6 +929,7 @@ class GUIChartManager extends ChartManager {
928
929
  c = MODEL.charts[this.chart_index],
929
930
  cv = c.variables[this.variable_index];
930
931
  cv.stacked = UI.boxChecked('variable-stacked');
932
+ cv.absolute = UI.boxChecked('variable-absolute');
931
933
  cv.scale_factor = s;
932
934
  // Prevent negative or near-zero line width.
933
935
  cv.line_width = Math.max(0.001, w);
@@ -1092,9 +1094,9 @@ class GUIChartManager extends ChartManager {
1092
1094
  }
1093
1095
  }
1094
1096
 
1095
- copyTable() {
1096
- UI.copyHtmlToClipboard(this.table_panel.innerHTML);
1097
- UI.notify('Table copied to clipboard (as HTML)');
1097
+ copyTable(plain) {
1098
+ UI.copyHtmlToClipboard(this.table_panel.innerHTML, plain);
1099
+ UI.notify('Table copied to clipboard (as ', (plain ? 'text' : 'HTML'), ')');
1098
1100
  }
1099
1101
 
1100
1102
  copyStatistics() {
@@ -424,13 +424,17 @@ class Finder {
424
424
  const
425
425
  c = MODEL.charts[ci],
426
426
  a = md.element('attribute').value,
427
- s = UI.boxChecked('confirm-add-chart-variables-stacked'),
427
+ abs = UI.boxChecked('confirm-add-chart-variables-absolute'),
428
+ stack = UI.boxChecked('confirm-add-chart-variables-stacked'),
428
429
  enl = [];
429
430
  for(const e of this.entities) enl.push(e.name);
430
431
  enl.sort((a, b) => UI.compareFullNames(a, b, true));
431
432
  for(const en of enl) {
432
433
  const vi = c.addVariable(en, a);
433
- if(vi !== null) c.variables[vi].stacked = s;
434
+ if(vi !== null) {
435
+ c.variables[vi].absolute = abs;
436
+ c.variables[vi].stacked = stack;
437
+ }
434
438
  }
435
439
  CHART_MANAGER.updateDialog();
436
440
  md.hide();
@@ -395,6 +395,9 @@ class Paper {
395
395
  id = 'i_n_a_c_t_i_v_e__t_r_i_a_n_g_l_e__t_i_p__ID';
396
396
  this.inactive_triangle = `url(#${id})`;
397
397
  this.addMarker(defs, id, tri, 8, 'silver');
398
+ id = 'i_g_n_o_r_e__t_r_i_a_n_g_l_e__t_i_p__ID';
399
+ this.ignore_triangle = `url(#${id})`;
400
+ this.addMarker(defs, id, tri, 8, this.palette.ignore);
398
401
  id = 'o_p_e_n__t_r_i_a_n_g_l_e__t_i_p__ID*';
399
402
  this.open_triangle = `url(#${id})`;
400
403
  this.addMarker(defs, id, tri, 7.5, 'white');
@@ -1034,9 +1037,9 @@ class Paper {
1034
1037
 
1035
1038
  // Arrows having both "from" and "to" are displayed as "real" arrows
1036
1039
  // The hidden nodes list must contain the nodes that have no position
1037
- // in the cluster being drawn
1038
- // NOTE: products are "hidden" typically when this arrow represents multiple
1039
- // links, but also if it is a single link from a cluster to a process
1040
+ // in the cluster being drawn.
1041
+ // NOTE: Products are "hidden" typically when this arrow represents multiple
1042
+ // links, but also if it is a single link from a cluster to a process.
1040
1043
  const
1041
1044
  from_c = from_nb instanceof Cluster,
1042
1045
  to_c = to_nb instanceof Cluster,
@@ -1049,12 +1052,12 @@ class Paper {
1049
1052
  fn = lnk.from_node,
1050
1053
  tn = lnk.to_node;
1051
1054
  if(fn instanceof Product && fn != from_nb && fn != to_nb) {
1052
- // Add node only if they not already shown at EITHER end of the arrow
1055
+ // Add node only if they not already shown at EITHER end of the arrow.
1053
1056
  addDistinct(fn, arrw.hidden_nodes);
1054
- // Count number of data flows represented by arrow
1057
+ // Count number of data flows represented by arrow.
1055
1058
  if(tn.is_data) data_flows++;
1056
1059
  }
1057
- // NOTE: no ELSE IF, because BOTH link nodes can be products
1060
+ // NOTE: No ELSE IF, because BOTH link nodes can be products.
1058
1061
  if(tn instanceof Product && tn != from_nb && tn != to_nb) {
1059
1062
  addDistinct(tn, arrw.hidden_nodes);
1060
1063
  // Count number of data flows represented by arrow
@@ -1063,7 +1066,7 @@ class Paper {
1063
1066
  }
1064
1067
  }
1065
1068
 
1066
- // NEXT: some more local variables
1069
+ // NEXT: Some more local variables.
1067
1070
  fnx = from_nb.x + dx;
1068
1071
  fny = from_nb.y + dy;
1069
1072
  fnw = from_nb.width;
@@ -1088,19 +1091,19 @@ class Paper {
1088
1091
  tnh = 24;
1089
1092
  }
1090
1093
 
1091
- // Do not draw arrow if so short that it is hidden by its FROM and TO nodes
1094
+ // Do not draw arrow if so short that it is hidden by its FROM and TO nodes.
1092
1095
  if((Math.abs(fnx - tnx) < (fnw + tnw)/2) &&
1093
1096
  (Math.abs(fny - tny) <= (fnh + tnh)/2)) {
1094
1097
  return false;
1095
1098
  }
1096
1099
 
1097
- // Adjust node heights if nodes are thick-rimmed
1100
+ // Adjust node heights if nodes are thick-rimmed.
1098
1101
  if((from_nb instanceof Product) && from_nb.is_buffer) fnh += 2;
1099
1102
  if((to_nb instanceof Product) && to_nb.is_buffer) tnh += 2;
1100
- // Get horizontal distance dx and vertical distance dy of the node centers
1103
+ // Get horizontal distance dx and vertical distance dy of the node centers.
1101
1104
  dx = tnx - fnx;
1102
1105
  dy = tny - fny;
1103
- // If dx is less than half a pixel, draw a vertical line
1106
+ // If dx is less than half a pixel, draw a vertical line.
1104
1107
  if(Math.abs(dx) < 0.5) {
1105
1108
  arrw.from_x = fnx;
1106
1109
  arrw.to_x = fnx;
@@ -1112,11 +1115,11 @@ class Paper {
1112
1115
  arrw.to_y = tny + tnh/2;
1113
1116
  }
1114
1117
  } else {
1115
- // Now dx > 0, so no division by zero can occur when calculating dy/dx
1116
- // First compute X and Y of tail (FROM node)
1118
+ // Now dx > 0, so no division by zero can occur when calculating dy/dx.
1119
+ // First compute X and Y of tail (FROM node).
1117
1120
  w = (from_nb instanceof Product ? from_nb.frame_width : fnw);
1118
1121
  if(Math.abs(dy / dx) >= Math.abs(fnh / w)) {
1119
- // Arrow connects to horizontal edge
1122
+ // Arrow connects to horizontal edge.
1120
1123
  arrw.from_y = (dy > 0 ? fny + fnh/2 : fny - fnh/2);
1121
1124
  arrw.from_x = fnx + fnh/2 * dx / Math.abs(dy);
1122
1125
  } else if(from_nb instanceof Product) {
@@ -1127,7 +1130,7 @@ class Paper {
1127
1130
  dd = fnw/2;
1128
1131
  nn = (-dd - Math.sqrt(rr - aa * dd * dd + aa * rr)) / (1 + aa);
1129
1132
  if(dx > 0) {
1130
- // link points towards the right
1133
+ // link points towards the right.
1131
1134
  arrw.from_x = fnx - nn;
1132
1135
  arrw.from_y = fny - nn * dy / dx;
1133
1136
  } else {
@@ -1135,28 +1138,28 @@ class Paper {
1135
1138
  arrw.from_y = fny + nn * dy / dx;
1136
1139
  }
1137
1140
  } else {
1138
- // Rectangular box
1141
+ // Rectangular box.
1139
1142
  arrw.from_x = (dx > 0 ? fnx + w/2 : fnx - w/2);
1140
1143
  arrw.from_y = fny + w/2 * dy / Math.abs(dx);
1141
1144
  }
1142
- // Then compute X and Y of head (TO node)
1145
+ // Then compute X and Y of head (TO node).
1143
1146
  w = (to_nb instanceof Product ? to_nb.frame_width : tnw);
1144
1147
  dx = arrw.from_x - tnx;
1145
1148
  dy = arrw.from_y - tny;
1146
1149
  if(Math.abs(dx) > 0) {
1147
1150
  if(Math.abs(dy / dx) >= Math.abs(tnh / w)) {
1148
- // Connects to horizontal edge
1151
+ // Connects to horizontal edge.
1149
1152
  arrw.to_y = (dy > 0 ? tny + tnh/2 : tny - tnh/2);
1150
1153
  arrw.to_x = tnx + tnh/2 * dx / Math.abs(dy);
1151
1154
  } else if(to_nb instanceof Product) {
1152
- // Node with semicircular sides}
1155
+ // Node with semicircular sides.
1153
1156
  tnw = to_nb.frame_width;
1154
1157
  rr = (tnh/2) * (tnh/2); // R square
1155
1158
  aa = (dy / dx) * (dy / dx); // A square
1156
1159
  dd = tnw/2;
1157
1160
  nn = (-dd - Math.sqrt(rr - aa*(dd*dd - rr))) / (1 + aa);
1158
1161
  if(dx > 0) {
1159
- // Link points towards the right
1162
+ // Link points towards the right.
1160
1163
  arrw.to_x = tnx - nn;
1161
1164
  arrw.to_y = tny - nn * dy / dx;
1162
1165
  } else {
@@ -1164,30 +1167,30 @@ class Paper {
1164
1167
  arrw.to_y = tny + nn * dy / dx;
1165
1168
  }
1166
1169
  } else {
1167
- // Rectangular node
1170
+ // Rectangular node.
1168
1171
  arrw.to_x = (dx > 0 ? tnx + w/2 : tnx - w/2);
1169
1172
  arrw.to_y = tny + w/2 * dy / Math.abs(dx);
1170
1173
  }
1171
1174
  }
1172
1175
  }
1173
1176
 
1174
- // Assume default arrow properties
1177
+ // Assume default arrow properties.
1175
1178
  sda = 'none';
1176
1179
  stroke_color = (ignored ? this.palette.ignore : this.palette.node_rim);
1177
1180
  stroke_width = 1.5;
1178
1181
  arrow_start = 'none';
1179
- arrow_end = this.triangle;
1180
- // Default multi-flow values are: NO multiflow, NOT congested or reversed
1182
+ arrow_end = (ignored ? this.ignore_triangle : this.triangle);
1183
+ // Default multi-flow values are: NO multiflow, NOT congested or reversed.
1181
1184
  let mf = [0, 0, 0, false, false],
1182
1185
  reversed = false;
1183
1186
  // These may need to be modified due to actual flow, etc.
1184
1187
  if(arrw.links.length === 1) {
1185
- // Display link properties of a specific link if arrow is plain
1188
+ // Display link properties of a specific link if arrow is plain.
1186
1189
  luc = arrw.links[0];
1187
- ignored = MODEL.ignored_entities[luc.identifier];
1190
+ ignored = ignored || MODEL.ignored_entities[luc.identifier];
1188
1191
  if(MODEL.solved && !ignored) {
1189
1192
  // Draw arrow in dark blue if a flow occurs, or in a lighter gray
1190
- // if NO flow occurs
1193
+ // if NO flow occurs.
1191
1194
  af = luc.actualFlow(MODEL.t);
1192
1195
  if(Math.abs(af) > VM.SIG_DIF_FROM_ZERO) {
1193
1196
  // NOTE: negative flow should affect arrow heads only when link has
@@ -1203,12 +1206,13 @@ class Paper {
1203
1206
  arrow_end = this.active_triangle;
1204
1207
  }
1205
1208
  } else {
1206
- stroke_color = (MODEL.ignored_entities[luc.identifier] ?
1207
- this.palette.ignore : 'silver');
1209
+ stroke_color = 'silver';
1208
1210
  arrow_end = this.inactive_triangle;
1209
1211
  }
1210
- } else {
1212
+ } else if(ignored) {
1211
1213
  af = VM.UNDEFINED;
1214
+ stroke_color = this.palette.ignore;
1215
+ arrow_end = this.ignore_triangle;
1212
1216
  }
1213
1217
  if(luc.from_node instanceof Process) {
1214
1218
  proc = luc.from_node;
@@ -1225,12 +1229,12 @@ class Paper {
1225
1229
  arrow_end = this.feedback_triangle;
1226
1230
  }
1227
1231
  }
1228
- // Data link => dotted line
1232
+ // Data link => dotted line.
1229
1233
  if(luc.dataOnly) {
1230
1234
  sda = UI.sda.dot;
1231
1235
  }
1232
1236
  if(luc.selected) {
1233
- // Draw arrow line thick and in red
1237
+ // Draw arrow line thick and in red.
1234
1238
  stroke_color = this.palette.select;
1235
1239
  stroke_width = 2;
1236
1240
  if(arrow_end == this.open_wedge) {
@@ -1239,7 +1243,6 @@ class Paper {
1239
1243
  arrow_end = this.selected_triangle;
1240
1244
  }
1241
1245
  }
1242
- if(ignored) stroke_color = this.palette.ignore;
1243
1246
  } else {
1244
1247
  // A composite arrow is visualized differently, depending on the number
1245
1248
  // of related products and the direction of the underlying links:
@@ -1264,23 +1267,23 @@ class Paper {
1264
1267
  if(arrw.bidirectional) arrow_start = arrow_end;
1265
1268
  }
1266
1269
  // Correct the start and end points of the shaft for the stroke width
1267
- // and size and number of the arrow heads
1268
- // NOTE: re-use of dx and dy for different purpose!
1270
+ // and size and number of the arrow heads.
1271
+ // NOTE: Re-use of dx and dy for different purpose!
1269
1272
  dx = arrw.to_x - arrw.from_x;
1270
1273
  dy = arrw.to_y - arrw.from_y;
1271
1274
  l = Math.sqrt(dx * dx + dy * dy);
1272
1275
  let cdx = 0, cdy = 0;
1273
1276
  if(l > 0) {
1274
- // Amount to shorten the line to accommodate arrow head
1275
- // NOTE: for thicker arrows, subtract a bit more
1277
+ // Amount to shorten the line to accommodate arrow head.
1278
+ // NOTE: For thicker arrows, subtract a bit more.
1276
1279
  cdx = (4 + 1.7 * (stroke_width - 1.5)) * dx / l;
1277
1280
  cdy = (4 + 1.7 * (stroke_width - 1.5)) * dy / l;
1278
1281
  }
1279
1282
  if(reversed) {
1280
- // Adjust end points by 1/2 px for rounded stroke end
1283
+ // Adjust end points by 1/2 px for rounded stroke end.
1281
1284
  bpx = arrw.to_x - 0.5*dx / l;
1282
1285
  bpy = arrw.to_y - 0.5*dy / l;
1283
- // Adjust start points for arrow head(s)
1286
+ // Adjust start points for arrow head(s).
1284
1287
  epx = arrw.from_x + cdx;
1285
1288
  epy = arrw.from_y + cdy;
1286
1289
  if(arrw.bidirectional) {
@@ -1288,10 +1291,10 @@ class Paper {
1288
1291
  bpy -= cdy;
1289
1292
  }
1290
1293
  } else {
1291
- // Adjust start points by 1/2 px for rounded stroke end
1294
+ // Adjust start points by 1/2 px for rounded stroke end.
1292
1295
  bpx = arrw.from_x + 0.5*dx / l;
1293
1296
  bpy = arrw.from_y + 0.5*dy / l;
1294
- // Adjust end points for arrow head(s)
1297
+ // Adjust end points for arrow head(s).
1295
1298
  epx = arrw.to_x - cdx;
1296
1299
  epy = arrw.to_y - cdy;
1297
1300
  if(arrw.bidirectional) {
@@ -1299,8 +1302,8 @@ class Paper {
1299
1302
  bpy += cdy;
1300
1303
  }
1301
1304
  }
1302
- // Calculate actual (multi)flow, as this co-determines the color of the arrow
1303
- if(MODEL.solved) {
1305
+ // Calculate actual (multi)flow, as this co-determines the color of the arrow.
1306
+ if(MODEL.solved && !ignored) {
1304
1307
  if(!luc) {
1305
1308
  mf = arrw.multiFlows;
1306
1309
  af = mf[1] + mf[2];
@@ -1315,22 +1318,20 @@ class Paper {
1315
1318
  } else {
1316
1319
  arrow_end = this.active_triangle;
1317
1320
  }
1318
- if(arrw.bidirectional) {
1319
- arrow_start = arrow_end;
1320
- }
1321
1321
  } else {
1322
1322
  if(stroke_color != this.palette.select) stroke_color = 'silver';
1323
1323
  if(arrow_end === this.double_triangle) {
1324
1324
  arrow_end = this.inactive_double_triangle;
1325
- if(arrw.bidirectional) {
1326
- arrow_start = this.inactive_double_triangle;
1327
- }
1328
1325
  }
1329
1326
  }
1330
1327
  } else {
1331
1328
  af = VM.UNDEFINED;
1329
+ if(ignored && stroke_color != this.palette.select) {
1330
+ stroke_color = this.palette.ignore;
1331
+ arrow_end = this.ignore_triangle;
1332
+ }
1332
1333
  }
1333
-
1334
+ if(arrw.bidirectional) arrow_start = arrow_end;
1334
1335
  // Draw arrow shaft
1335
1336
  if(stroke_width === 3 && data_flows) {
1336
1337
  // Hollow shaft arrow: dotted when *all* represented links are
@@ -1509,7 +1510,7 @@ class Paper {
1509
1510
 
1510
1511
  // Draw the actual flow
1511
1512
  const absf = Math.abs(af);
1512
- if(l > 0 && af < VM.UNDEFINED && absf > VM.SIG_DIF_FROM_ZERO) {
1513
+ if(!ignored && l > 0 && af < VM.UNDEFINED && absf > VM.SIG_DIF_FROM_ZERO) {
1513
1514
  const ffill = {fill:'white', opacity:0.8};
1514
1515
  if(luc || mf[0] == 1) {
1515
1516
  // Draw flow data halfway the arrow only if calculated and non-zero.
@@ -124,7 +124,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
124
124
  this.color_scales.rb.addEventListener('click', csf);
125
125
  this.color_scales.no.addEventListener('click', csf);
126
126
  document.getElementById('sa-copy-btn').addEventListener(
127
- 'click', () => SENSITIVITY_ANALYSIS.copyTableToClipboard());
127
+ 'click', (event) => SENSITIVITY_ANALYSIS.copyTableToClipboard(event.shiftKey));
128
128
  document.getElementById('sa-copy-data-btn').addEventListener(
129
129
  'click', () => SENSITIVITY_ANALYSIS.copyDataToClipboard());
130
130
  this.outcome_name = document.getElementById('sa-outcome-name');
@@ -255,7 +255,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
255
255
  // Otherwise, display list of all dataset selectors in docu-viewer.
256
256
  if(DOCUMENTATION_MANAGER.visible) {
257
257
  const
258
- ds_dict = MODEL.listOfAllSelectors,
258
+ ds_dict = MODEL.dictOfAllSelectors,
259
259
  html = [],
260
260
  sl = Object.keys(ds_dict).sort((a, b) => UI.compareFullNames(a, b, true));
261
261
  for(const s of sl) {
@@ -346,7 +346,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
346
346
  sl = this.base_selectors.value.replace(/[\;\,]/g, ' ').trim().replace(
347
347
  /[^a-zA-Z0-9\+\-\%\_\s]/g, '').split(/\s+/),
348
348
  bs = sl.join(' '),
349
- sd = MODEL.listOfAllSelectors,
349
+ sd = MODEL.dictOfAllSelectors,
350
350
  us = [];
351
351
  for(const s of sl) if(s.length > 0 && !(s in sd)) us.push(s);
352
352
  if(us.length > 0) {
@@ -760,9 +760,8 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
760
760
  this.updateData();
761
761
  }
762
762
 
763
- copyTableToClipboard() {
764
- UI.copyHtmlToClipboard(this.scroll_area.innerHTML);
765
- UI.notify('Table copied to clipboard (as HTML)');
763
+ copyTableToClipboard(plain) {
764
+ UI.copyHtmlToClipboard(this.scroll_area.innerHTML, plain);
766
765
  }
767
766
 
768
767
  copyDataToClipboard() {
@@ -488,6 +488,8 @@ module.exports = class MILPSolver {
488
488
  } else {
489
489
  inttol = Math.max(1e-9, Math.min(0.1, inttol));
490
490
  }
491
+ // Use integer tolerance setting as "near zero" threshold.
492
+ this.near_zero = inttol;
491
493
  // Default relative MIP gap is 1e-4.
492
494
  if(isNaN(mipgap)) {
493
495
  mipgap = 1e-4;
@@ -578,7 +580,8 @@ module.exports = class MILPSolver {
578
580
  x_values.push(0);
579
581
  col++;
580
582
  }
581
- x_values.push(x_dict[v]);
583
+ // Return near-zero values as 0.
584
+ x_values.push(Math.abs(x_dict[v]) < this.near_zero ? 0 : x_dict[v]);
582
585
  col++;
583
586
  }
584
587
  // Add zeros to vector for remaining columns.
@@ -1037,11 +1037,11 @@ class LinnyRModel {
1037
1037
  // NOTE: A dimension is a list of one or more relevant selectors.
1038
1038
  this.dimensions.length = 0;
1039
1039
  // NOTE: Ignore the equations dataset.
1040
- for(let d in this.datasets) if(this.datasets.hasOwnProperty(d) &&
1041
- this.datasets[d] !== this.equations_dataset) {
1040
+ for(let k in this.datasets) if(this.datasets.hasOwnProperty(k) &&
1041
+ this.datasets[k] !== this.equations_dataset) {
1042
1042
  // Get the selector list for this dataset.
1043
1043
  // NOTE: Ignore wildcard selectors!
1044
- this.processSelectorList(this.datasets[d].plainSelectors);
1044
+ this.processSelectorList(this.datasets[k].plainSelectors);
1045
1045
  }
1046
1046
  // Analyze constraint bound lines in the same way.
1047
1047
  for(let k in this.constraints) if(this.constraints.hasOwnProperty(k)) {
@@ -3075,10 +3075,12 @@ class LinnyRModel {
3075
3075
  if(n) {
3076
3076
  // NOTE: Use a "dummy experiment object" as parent for SA runs.
3077
3077
  const dummy = {title: SENSITIVITY_ANALYSIS.experiment_title};
3078
+ let r = 0;
3078
3079
  for(const c of n.childNodes) if(c.nodeName === 'experiment-run') {
3079
- const xr = new ExperimentRun(dummy, i);
3080
+ const xr = new ExperimentRun(dummy, r);
3080
3081
  xr.initFromXML(c);
3081
3082
  this.sensitivity_runs.push(xr);
3083
+ r++;
3082
3084
  }
3083
3085
  }
3084
3086
  n = childNodeByTag(node, 'experiments');
@@ -3337,9 +3339,9 @@ class LinnyRModel {
3337
3339
  return [c.dataAsString, c.statisticsAsString];
3338
3340
  }
3339
3341
 
3340
- get listOfAllSelectors() {
3341
- // Returns list of all dataset modifier selectors as a "dictionary"
3342
- // like so: {selector_1: [list of datasets], ...}
3342
+ get dictOfAllSelectors() {
3343
+ // Returns "dictionary" of all dataset modifier selectors like so:
3344
+ // {selector_1: [list of datasets], ...}
3343
3345
  const ds_dict = {};
3344
3346
  for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
3345
3347
  const ds = this.datasets[k];
@@ -9755,13 +9757,14 @@ class ChartVariable {
9755
9757
  this.wildcard_index = false;
9756
9758
  }
9757
9759
 
9758
- setProperties(obj, attr, stck, clr, sf=1, lw=1, vis=true, sort='not') {
9760
+ setProperties(obj, attr, stck, clr, sf=1, abs=false, lw=1, vis=true, sort='not') {
9759
9761
  // Sets the defining properties for this chart variable.
9760
9762
  this.object = obj;
9761
9763
  this.attribute = attr;
9762
9764
  this.stacked = stck;
9763
9765
  this.color = clr;
9764
9766
  this.scale_factor = sf;
9767
+ this.absolute = abs;
9765
9768
  this.line_width = lw;
9766
9769
  this.visible = vis;
9767
9770
  this.sorted = sort;
@@ -9777,9 +9780,11 @@ class ChartVariable {
9777
9780
  // Returns the display name for this variable. This is the name of
9778
9781
  // the Linny-R entity and its attribute, followed by its scale factor
9779
9782
  // unless it equals 1 (no scaling).
9780
- const sf = (this.scale_factor === 1 ? '' :
9781
- // NOTE: Pass tiny = TRUE to permit very small scaling factors.
9782
- ` (x${VM.sig4Dig(this.scale_factor, true)})`);
9783
+ const
9784
+ bar = (this.absolute ? '\u2503' : ''),
9785
+ sf = (this.scale_factor === 1 ? '' :
9786
+ // NOTE: Pass tiny = TRUE to permit very small scaling factors.
9787
+ ` (x${VM.sig4Dig(this.scale_factor, true)})`);
9783
9788
  // Display name of equation is just the equations dataset selector.
9784
9789
  if(this.object instanceof DatasetModifier) {
9785
9790
  let eqn = this.object.selector;
@@ -9795,7 +9800,7 @@ class ChartVariable {
9795
9800
  // method name (leading colon replaced by the prefixer ": ").
9796
9801
  eqn = this.chart.prefix + UI.PREFIXER + eqn.substring(1);
9797
9802
  }
9798
- return eqn + sf;
9803
+ return bar + eqn + bar + sf;
9799
9804
  }
9800
9805
  // NOTE: Same holds for "dummy variables" added for wildcard
9801
9806
  // dataset selectors.
@@ -9804,11 +9809,13 @@ class ChartVariable {
9804
9809
  if(this.wildcard_index !== false) {
9805
9810
  eqn = eqn.replace('??', this.wildcard_index);
9806
9811
  }
9807
- return eqn + sf;
9812
+ return bar + eqn + bar + sf;
9808
9813
  }
9809
9814
  // NOTE: Do not display the vertical bar if no attribute is specified.
9810
- if(!this.attribute) return this.object.displayName + sf;
9811
- return this.object.displayName + UI.OA_SEPARATOR + this.attribute + sf;
9815
+ if(!this.attribute) {
9816
+ return bar + this.object.displayName + bar + sf;
9817
+ }
9818
+ return bar + this.object.displayName + '|' + this.attribute + bar + sf;
9812
9819
  }
9813
9820
 
9814
9821
  get asXML() {
@@ -9818,7 +9825,9 @@ class ChartVariable {
9818
9825
  if(MODEL.black_box_entities.hasOwnProperty(id)) {
9819
9826
  id = UI.nameToID(MODEL.black_box_entities[id]);
9820
9827
  }
9821
- const xml = ['<chart-variable', (this.stacked ? ' stacked="1"' : ''),
9828
+ const xml = ['<chart-variable',
9829
+ (this.stacked ? ' stacked="1"' : ''),
9830
+ (this.absolute ? ' absolute="1"' : ''),
9822
9831
  (this.visible ? ' visible="1"' : ''),
9823
9832
  (this.wildcard_index !== false ?
9824
9833
  ` wildcard-index="${this.wildcard_index}"` : ''),
@@ -9876,6 +9885,7 @@ class ChartVariable {
9876
9885
  nodeParameterValue(node, 'stacked') === '1',
9877
9886
  nodeContentByTag(node, 'color'),
9878
9887
  safeStrToFloat(nodeContentByTag(node, 'scale-factor')),
9888
+ nodeParameterValue(node, 'absolute') === '1',
9879
9889
  safeStrToFloat(nodeContentByTag(node, 'line-width')),
9880
9890
  nodeParameterValue(node, 'visible') === '1',
9881
9891
  nodeParameterValue(node, 'sorted') || 'not');
@@ -9970,7 +9980,10 @@ class ChartVariable {
9970
9980
  v = 0;
9971
9981
  }
9972
9982
  // Scale the value unless run result (these are already scaled!).
9973
- if(!rr) v *= this.scale_factor;
9983
+ if(!rr) {
9984
+ v *= this.scale_factor;
9985
+ if(this.absolute) v = Math.abs(v);
9986
+ }
9974
9987
  this.vector.push(v);
9975
9988
  // Do not include values for t = 0 in statistics.
9976
9989
  if(t > 0) {
@@ -10184,7 +10197,7 @@ class Chart {
10184
10197
  return '#c00000';
10185
10198
  }
10186
10199
 
10187
- addVariable(n, a) {
10200
+ addVariable(n, a='') {
10188
10201
  // Add variable [entity name `n`|attribute `a`] to the chart unless
10189
10202
  // it is already in the variable list.
10190
10203
  let dn = n + UI.OA_SEPARATOR + a;
@@ -10220,7 +10233,7 @@ class Chart {
10220
10233
  }
10221
10234
  } else {
10222
10235
  const v = new ChartVariable(this);
10223
- v.setProperties(obj, a, false, this.nextAvailableDefaultColor, 1, 1);
10236
+ v.setProperties(obj, a, false, this.nextAvailableDefaultColor);
10224
10237
  this.variables.push(v);
10225
10238
  }
10226
10239
  return this.variables.length - 1;
@@ -11197,9 +11210,9 @@ class ExperimentRunResult {
11197
11210
  this.x_variable = true;
11198
11211
  this.object_id = v.object.identifier;
11199
11212
  this.attribute = v.attribute;
11200
- this.was_ignored = MODEL.ignored_entities[this.object_id];
11213
+ this.was_ignored = MODEL.ignored_entities[this.object_id] || false;
11201
11214
  if(this.was_ignored) {
11202
- // Chart variable entity was ignored => all results are undefined
11215
+ // Chart variable entity was ignored => all results are undefined.
11203
11216
  this.vector = [];
11204
11217
  this.N = VM.UNDEFINED;
11205
11218
  this.sum = VM.UNDEFINED;
@@ -11236,6 +11249,11 @@ class ExperimentRunResult {
11236
11249
  // statistic.
11237
11250
  this.last = (this.vector.length > 0 ?
11238
11251
  this.vector[this.vector.length - 1] : VM.UNDEFINED);
11252
+ // NOTE: For sensitivity analyses, the vector is NOT stored because
11253
+ // the SA reports only the descriptive statistics.
11254
+ if(this.run.experiment === SENSITIVITY_ANALYSIS.experiment) {
11255
+ this.vector.length = 0;
11256
+ }
11239
11257
  }
11240
11258
  } else if(v instanceof Dataset) {
11241
11259
  // This dataset will be an "outcome" dataset => store statistics only
@@ -11329,12 +11347,12 @@ class ExperimentRunResult {
11329
11347
  }
11330
11348
  // The vector MAY need to be scaled to model time by different methods,
11331
11349
  // but since this is likely to be rare, such scaling is performed
11332
- // "lazily", so the method-specific vectors are initially set to NULL.
11350
+ // "lazily", so the method-specific vectors are initially empty.
11333
11351
  this.resetScaledVectors();
11334
11352
  }
11335
11353
 
11336
11354
  resetScaledVectors() {
11337
- // Set the special vectors to null, so they will be recalculated.
11355
+ // Clear the special vectors, so they will be recalculated.
11338
11356
  this.scaled_vectors = {'NEAREST': [], 'MEAN': [], 'SUM': [], 'MAX': []};
11339
11357
  }
11340
11358
 
@@ -11612,8 +11630,12 @@ class ExperimentRun {
11612
11630
  // NOTE: All equations are also considered to be outcomes EXCEPT
11613
11631
  // methods (selectors starting with a colon).
11614
11632
  this.eq_list = [];
11615
- const eml = Object.keys(MODEL.equations_dataset.modifiers);
11616
- for(const em of eml) if(!em.startsWith(':')) this.eq_list.push(em);
11633
+ // NOTE: For sensitivity analyses, equations are NOT outcomes, as all
11634
+ // SA outcomes must be specified explicitly.
11635
+ if(this.experiment !== SENSITIVITY_ANALYSIS.experiment) {
11636
+ const eml = Object.keys(MODEL.equations_dataset.modifiers);
11637
+ for(const em of eml) if(!em.startsWith(':')) this.eq_list.push(em);
11638
+ }
11617
11639
  const
11618
11640
  cv = this.experiment.variables.length,
11619
11641
  oc = this.oc_list.length,
@@ -11918,7 +11940,7 @@ class Experiment {
11918
11940
  }
11919
11941
  return index;
11920
11942
  }
11921
-
11943
+
11922
11944
  isDimensionSelector(s) {
11923
11945
  // Return TRUE if `s` is a dimension selector in this experiment.
11924
11946
  for(const dim of this.dimensions) if(dim.indexOf(s) >= 0) return true;
@@ -11928,6 +11950,15 @@ class Experiment {
11928
11950
  return false;
11929
11951
  }
11930
11952
 
11953
+ get allDimensionSelectors() {
11954
+ // Return list of all dimension selectors in this experiment.
11955
+ const
11956
+ dict = MODEL.dictOfAllSelectors,
11957
+ dims = [];
11958
+ for(let s in dict) if(this.isDimensionSelector(s)) dims.push(s);
11959
+ return dims;
11960
+ }
11961
+
11931
11962
  get asXML() {
11932
11963
  let d = '';
11933
11964
  for(const dim of this.dimensions) {
@@ -12152,12 +12183,6 @@ class Experiment {
12152
12183
  }
12153
12184
  }
12154
12185
 
12155
- get allDimensionSelectors() {
12156
- const sl = Object.keys(MODEL.listOfAllSelectors);
12157
- // Add selectors of actor, iterator and settings dimensions.
12158
- return sl;
12159
- }
12160
-
12161
12186
  orthogonalSelectors(c) {
12162
12187
  // Return TRUE iff the selectors in set `c` all are elements of
12163
12188
  // different experiment dimensions.
@@ -885,7 +885,7 @@ class ExpressionParser {
885
885
  // of commas, semicolons and spaces.
886
886
  x.r = run_spec.split(/[\,\;\/\s]+/g);
887
887
  // NOTE: The VMI instruction accepts `x.r` to be a list of selectors
888
- // or an integer number.
888
+ // or an integer number.
889
889
  } else {
890
890
  // If the specifier does start with a #, trim it...
891
891
  run_spec = run_spec.substring(1);
@@ -933,6 +933,20 @@ class ExpressionParser {
933
933
  x.x = MODEL.experiments[n];
934
934
  }
935
935
  }
936
+ // If run specifier `x.r` is a list, check whether all elements in the
937
+ // list are selectors in a dimension of experiment `x.x` (if specified).
938
+ // If experiment is unknown, check against the list of all selectors
939
+ // defined in the model.
940
+ if(Array.isArray(x.r)) {
941
+ const
942
+ sl = (x.x instanceof Experiment ? x.x.allDimensionSelectors :
943
+ Object.keys(MODEL.dictOfAllSelectors)),
944
+ unknown = complement(x.r, sl);
945
+ if(unknown.length) {
946
+ msg = pluralS(unknown.length, 'unknown selector') + ': <tt>' +
947
+ unknown.join(' ') + '</tt>';
948
+ }
949
+ }
936
950
  // END of code for parsing an experiment result specifier.
937
951
  // Now proceed with parsing the variable name.
938
952
 
@@ -7269,12 +7283,12 @@ function VMI_push_run_result(x, args) {
7269
7283
  rr = r.results[rri],
7270
7284
  tsd = r.time_step_duration,
7271
7285
  // Get the delta-t multiplier: divide model time step duration
7272
- // by time step duration of the experiment run if they differ
7286
+ // by time step duration of the experiment run if they differ.
7273
7287
  dtm = (Math.abs(tsd - model_dt) < VM.NEAR_ZERO ? 1 : model_dt / tsd);
7274
7288
  let stat = rrspec.s;
7275
- // For outcome datasets without specific statistic, default to LAST
7289
+ // For outcome datasets without specific statistic, default to LAST.
7276
7290
  if(!(stat || rr.x_variable)) stat = 'LAST';
7277
- // For a valid experiment variable, the default value is 0
7291
+ // For a valid experiment variable, the default value is 0.
7278
7292
  v = 0;
7279
7293
  if(stat) {
7280
7294
  if(stat === 'LAST') {
@@ -7302,14 +7316,14 @@ function VMI_push_run_result(x, args) {
7302
7316
  console.log(trc.join(''));
7303
7317
  }
7304
7318
  } else {
7305
- // No statistic => return the vector for local time step
7319
+ // No statistic => return the vector for local time step,
7306
7320
  // using here, too, the delta-time-modifier to adjust the offsets
7307
7321
  // for different time steps per experiment.
7308
7322
  const tot = twoOffsetTimeStep(x.step[x.step.length - 1],
7309
7323
  args[1], args[2], args[3], args[4], dtm, x);
7310
7324
  // Scale the (midpoint) time step (at current model run time scale)
7311
7325
  // to the experiment run time scale and get the run result value.
7312
- // NOTE: the .m property specifies the time scaling method, and
7326
+ // NOTE: The .m property specifies the time scaling method, and
7313
7327
  // the .p property whether the run result vector should be used as
7314
7328
  // a periodic time series.
7315
7329
  v = rr.valueAtModelTime(tot[0], model_dt, rrspec.m, rrspec.p);
@@ -7325,6 +7339,8 @@ function VMI_push_run_result(x, args) {
7325
7339
  }
7326
7340
  }
7327
7341
  }
7342
+ // Truncate near-zero values.
7343
+ if(Math.abs(v) < VM.SIG_DIF_FROM_ZERO) v = 0;
7328
7344
  x.push(v);
7329
7345
  }
7330
7346
 
@@ -8413,11 +8429,16 @@ function VMI_set_bounds(args) {
8413
8429
  VM.variables[vi - 1][0],'] t = ', VM.t, ' LB = ', VM.sig4Dig(l),
8414
8430
  ', UB = ', VM.sig4Dig(u), fixed].join(''), l, u, inf_val);
8415
8431
  } else if(u < l) {
8416
- // Warn that "impossible" bounds would have been set...
8417
- const vk = vbl.displayName;
8418
- if(!VM.bound_issues[vk]) VM.bound_issues[vk] = [];
8419
- VM.bound_issues[vk].push(VM.t);
8420
- // ... and set LB to UB, so that lowest value is bounding.
8432
+ // Check the difference, as this may be negligible.
8433
+ if(u - l < VM.SIG_DIF_FROM_ZERO) {
8434
+ u = Math.round(u * 1e5) / 1e5;
8435
+ } else {
8436
+ // If substantial, warn that "impossible" bounds would have been set.
8437
+ const vk = vbl.displayName;
8438
+ if(!VM.bound_issues[vk]) VM.bound_issues[vk] = [];
8439
+ VM.bound_issues[vk].push(VM.t);
8440
+ }
8441
+ // Set LB to UB, so that lowest value is bounding.
8421
8442
  l = u;
8422
8443
  }
8423
8444
  // NOTE: Since the VM vectors for lower bounds and upper bounds are