@abi-software/simulationvuer 0.4.6 → 0.5.0

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.
@@ -1,35 +1,12 @@
1
1
  <template>
2
- <div class="simulation-vuer" v-loading="simulationBeingComputed" :element-loading-text="simulationBeingComputedLabel">
3
- <p v-show="mode === -1" class="default error"><span class="error">Error:</span> an unknown model was provided.</p>
4
- <div class="main" v-show="mode !== -1">
2
+ <div class="simulation-vuer" v-loading="simulationBeingComputed" element-loading-text="Loading simulation results...">
3
+ <p v-if="!hasValidJson" class="default error"><span class="error">Error:</span> an unknown or invalid model was provided.</p>
4
+ <div class="main" v-if="hasValidJson">
5
5
  <div class="main-left">
6
6
  <p class="default title">{{title}}</p>
7
7
  <el-divider></el-divider>
8
8
  <p class="default input-parameters">Input parameters</p>
9
- <div v-show="mode === 0">
10
- <p class="default simulation-mode">Simulation mode</p>
11
- <el-select class="simulation-mode" popper-class="simulation-mode-popper" :popper-append-to-body="false" v-model="simulationMode" size="mini" @change="simulationModeChanged()">
12
- <el-option v-for="simulationMode in simulationModes" :key="simulationMode.value" :label="simulationMode.label" :value="simulationMode.value" />
13
- </el-select>
14
- <div class="sliders-and-fields">
15
- <p class="default first-slider-and-field">Stimulation level</p>
16
- <el-slider v-model="stimulationLevel" :max="10" :show-tooltip="false" :show-input="false" :disabled="simulationMode == 0" />
17
- <el-input-number class="slider-and-field" v-model="stimulationLevel" size="mini" :controls="false" :min="0" :max="10" :disabled="simulationMode == 0" />
18
- </div>
19
- </div>
20
- <div v-show="mode === 1">
21
- <div class="sliders-and-fields">
22
- <p class="default first-slider-and-field">Spike frequency</p>
23
- <el-slider v-model="simulationSpikeFrequency" :max="1000" :show-tooltip="false" :show-input="false" />
24
- <el-input-number class="slider-and-field" v-model="simulationSpikeFrequency" size="mini" :controls="false" :min="0" :max="1000" />
25
- <p class="default slider-and-field">Spike number</p>
26
- <el-slider v-model="simulationSpikeNumber" :max="30" :show-tooltip="false" :show-input="false" />
27
- <el-input-number class="slider-and-field" v-model="simulationSpikeNumber" size="mini" :controls="false" :min="0" :max="30" />
28
- <p class="default slider-and-field">Spike amplitude</p>
29
- <el-slider v-model="simulationSpikeAmplitude" :max="30" :show-tooltip="false" :show-input="false" />
30
- <el-input-number class="slider-and-field" v-model="simulationSpikeAmplitude" size="mini" :controls="false" :min="0" :max="30" />
31
- </div>
32
- </div>
9
+ <div ref="input" />
33
10
  <div class="primary-button">
34
11
  <el-button type="primary" size="mini" @click="runSimulation()">Run simulation</el-button>
35
12
  </div>
@@ -39,14 +16,10 @@
39
16
  <div class="secondary-button">
40
17
  <el-button size="mini" @click="viewDataset()">View dataset</el-button>
41
18
  </div>
42
- <p class="default note">{{note}}</p>
43
- </div>
44
- <div class="main-right" v-if="mode === 0" v-show="simulationValid">
45
- <PlotVuer :layout-input="simulationPotentialLayout" :dataInput="simulationPotentialData" :plotType="'plotly-only'" />
19
+ <p class="default note">Additional parameters are available on oSPARC</p>
46
20
  </div>
47
- <div class="main-right composite" v-if="mode === 1" v-show="simulationValid">
48
- <PlotVuer :layout-input="simulationSpikeLayout" :dataInput="simulationSpikeData" :plotType="'plotly-only'" />
49
- <PlotVuer :layout-input="simulationPotentialLayout" :dataInput="simulationPotentialData" :plotType="'plotly-only'" />
21
+ <div class="main-right" ref="output" v-show="simulationValid">
22
+ <PlotVuer v-for="(outputPlot, index) in json.output.plots" :key="`output-${index}`" :layout-input="layout[index]" :dataInput="simulationData[index]" :plotType="'plotly-only'" />
50
23
  </div>
51
24
  <div class="main-right" v-show="!simulationValid">
52
25
  <p class="default error"><span class="error">Error:</span> <span v-html="errorMessage"></span>.</p>
@@ -60,8 +33,11 @@ import Vue from "vue";
60
33
  import { PlotVuer } from "@abi-software/plotvuer";
61
34
  import "@abi-software/plotvuer/dist/plotvuer.css";
62
35
  import { Aside, Button, Container, Divider, InputNumber, Loading, Main, Option, Select, Slider } from "element-ui";
63
-
64
- var NoSimulationData = [{}];
36
+ import { jsonForNormalModel, jsonForCompositeModel } from "./configuration.js";
37
+ import { evaluateValue, evaluateSimulationValue } from "./common.js";
38
+ import { validJson } from "./json.js";
39
+ import Ui from "./ui.js";
40
+ import { prepareForUi } from "./ui.js";
65
41
 
66
42
  Vue.use(Aside);
67
43
  Vue.use(Button);
@@ -81,84 +57,27 @@ export default {
81
57
  },
82
58
  props: {
83
59
  apiLocation: {
60
+ required: true,
84
61
  type: String,
85
- default: "",
86
62
  },
87
63
  entry: {
88
- /**
89
- * Object containing information for the current simulation.
90
- */
91
- entry: Object,
64
+ required: true,
65
+ type: Object,
92
66
  },
93
67
  },
94
- data: function () {
95
- let title = this.entry?this.entry.name:"";
96
-
68
+ data: function() {
97
69
  return {
98
- mode: 0,
70
+ mode: 0, //---GRY--- TO BE DELETED!
71
+ json: {},
72
+ hasValidJson: true,
99
73
  errorMessage: "",
100
- note: "Additional parameters are available on oSPARC",
101
- stimulationLevel: 0,
102
- simulationMode: 0,
103
- simulationModes: [
104
- {
105
- label: "Normal sinus rhythm",
106
- value: 0,
107
- },
108
- {
109
- label: "Stellate stimulation",
110
- value: 1,
111
- },
112
- {
113
- label: "Vagal stimulation",
114
- value: 2,
115
- },
116
- ],
117
- simulationSpikeLayout: {
118
- xaxis: {
119
- title: {
120
- text: "Time (s)",
121
- font: {
122
- size: 10,
123
- },
124
- },
125
- },
126
- yaxis: {
127
- title: {
128
- text: "Spike amplitude",
129
- font: {
130
- size: 10,
131
- },
132
- }
133
- },
134
- },
135
- simulationSpikeFrequency: 300,
136
- simulationSpikeNumber: 10,
137
- simulationSpikeAmplitude: 10, // The real value is 100 times smaller.
138
- simulationSpikeData: NoSimulationData,
139
- simulationPotentialLayout: {
140
- xaxis: {
141
- title: {
142
- text: "Time (s)",
143
- font: {
144
- size: 10,
145
- },
146
- },
147
- },
148
- yaxis: {
149
- title: {
150
- text: "Membrane potential (mV)",
151
- font: {
152
- size: 10,
153
- },
154
- }
155
- },
156
- },
157
- simulationPotentialData: NoSimulationData,
74
+ layout: [],
75
+ simulationData: [],
76
+ simulationDataId: {},
158
77
  simulationBeingComputed: false,
159
- simulationBeingComputedLabel: "Loading simulation results...",
160
78
  simulationValid: true,
161
- title: title,
79
+ title: (this.entry !== undefined)?this.entry.name:"",
80
+ ui: null,
162
81
  };
163
82
  },
164
83
  methods: {
@@ -168,65 +87,49 @@ export default {
168
87
  viewDataset() {
169
88
  window.open(this.entry.dataset, "_blank");
170
89
  },
171
- simulationModeChanged() {
172
- this.simulationPotentialData = NoSimulationData;
173
- this.simulationValid = true;
174
- },
175
90
  runSimulation() {
176
91
  this.simulationBeingComputed = true;
177
92
 
178
- var request = {
93
+ let request = {
179
94
  model_url: this.entry.resource,
180
- json_config: {
181
- output: ["Membrane/V"],
182
- },
95
+ json_config: {},
183
96
  };
184
97
 
185
98
  // Specify the ending point and point interval for the normal mode (since
186
99
  // our resource is a CellML file).
187
100
 
188
- if (this.mode === 0) {
189
- request.json_config["simulation"] = {
190
- "Ending point": 3,
191
- "Point interval": 0.001,
101
+ if (this.json.simulation !== undefined) {
102
+ request.json_config.simulation = {
103
+ "Ending point": this.json.simulation.endingPoint,
104
+ "Point interval": this.json.simulation.pointInterval,
192
105
  };
193
106
  }
194
107
 
195
- // Apply a stellate/vagal stimulation, if needed.
196
- // Notes:
197
- // - this.simulationMode:
198
- // - 0: normal sinus rhythm;
199
- // - 1: stellate stimulation; and
200
- // - 2: vagal stimulation.
201
- // - this.stimulationLevel has a value in [0; 10], but to compute ACh, we
202
- // need a value in [0; 1].
203
-
204
- if (this.simulationMode !== 0) {
205
- request.json_config["parameters"] = {
206
- "Rate_modulation_experiments/Iso_1_uM": 1.0,
207
- "Rate_modulation_experiments/ACh": this.simulationMode === 1 ? (1.0 - 0.1 * this.stimulationLevel) * 22.0e-6 : 22.0e-6 + 0.1 * this.stimulationLevel * (38.0e-6 - 22.0e-6),
208
- };
209
- }
108
+ // Specify the simulation parameters.
210
109
 
211
- // Apply the spike settings, if needed.
110
+ if (this.json.parameters != undefined) {
111
+ request.json_config.parameters = {};
212
112
 
213
- if (this.mode === 1) {
214
- request.json_config["parameters"] = {
215
- "Brain_stem/t_period": this.simulationSpikeFrequency,
216
- "Brain_stem/w_n": this.simulationSpikeNumber,
217
- "Brain_stem/w_value": 0.01 * this.simulationSpikeAmplitude,
218
- };
113
+ this.json.parameters.forEach((parameter) => {
114
+ request.json_config.parameters[parameter.name] = evaluateValue(this.ui, parameter.value);
115
+ });
219
116
  }
220
117
 
221
- // Request the spikes if we are in composite mode.
118
+ // Specify what we want to retrieve.
119
+
120
+ if (this.json.output.data !== undefined) {
121
+ let index = -1;
122
+
123
+ request.json_config.output = [];
222
124
 
223
- if (this.mode === 1) {
224
- request.json_config["output"].push("Brain_stem/w");
125
+ this.json.output.data.forEach((outputData) => {
126
+ request.json_config.output[++index] = outputData.name;
127
+ });
225
128
  }
226
129
 
227
130
  // Run the simulation.
228
131
 
229
- var xmlhttp = new XMLHttpRequest();
132
+ let xmlhttp = new XMLHttpRequest();
230
133
 
231
134
  xmlhttp.open("POST", this.apiLocation + "/simulation", true);
232
135
  xmlhttp.setRequestHeader("Content-type", "application/json");
@@ -235,33 +138,32 @@ export default {
235
138
  this.simulationBeingComputed = false;
236
139
 
237
140
  if (xmlhttp.status === 200) {
238
- var response = JSON.parse(xmlhttp.responseText);
141
+ let response = JSON.parse(xmlhttp.responseText);
239
142
 
240
143
  this.simulationValid = response.status === "ok";
241
144
 
242
145
  if (this.simulationValid) {
243
- // Retrieve the spike data, if needed. (Our default spike
244
- // amplitude is 0.1, but we normalise it to 10.)
146
+ // Retrieve and post-process the simulation data.
147
+
148
+ let index = -1;
149
+ let iMax = response.results[this.simulationDataId[Object.keys(this.simulationDataId)[0]]].length;
150
+
151
+ this.json.output.plots.forEach((outputPlot) => {
152
+ let xValue = [];
153
+ let yValue = [];
245
154
 
246
- if (this.mode === 1) {
247
- this.simulationSpikeData = [
155
+ for (let i = 0; i < iMax; ++i) {
156
+ xValue[i] = evaluateSimulationValue(this, response.results, outputPlot.xValue, i);
157
+ yValue[i] = evaluateSimulationValue(this, response.results, outputPlot.yValue, i);
158
+ }
159
+
160
+ this.simulationData[++index] = [
248
161
  {
249
- x: response.results["environment/time"],
250
- y: response.results["Brain_stem/w"].map(function (element) {
251
- return 100 * element;
252
- }),
162
+ x: xValue,
163
+ y: yValue,
253
164
  },
254
165
  ];
255
- }
256
-
257
- // Retrieve the potential data.
258
-
259
- this.simulationPotentialData = [
260
- {
261
- x: response.results["environment/time"],
262
- y: response.results["Membrane/V"],
263
- },
264
- ];
166
+ });
265
167
  } else {
266
168
  this.errorMessage = response.description;
267
169
  }
@@ -275,21 +177,40 @@ export default {
275
177
  xmlhttp.send(JSON.stringify(request));
276
178
  },
277
179
  },
278
- mounted: function () {
279
- // Determine the mode in which we should run:
280
- // - -1: unknown mode;
281
- // - 0: normal mode; and
282
- // - 1: composite mode.
180
+ created: function() {
181
+ // Manually (for now) specify the JSON configuration to be used by either
182
+ // the normal model or the composite model.
283
183
 
284
184
  this.mode = -1;
285
185
 
286
- if (this.entry) {
186
+ if (this.entry !== undefined) {
287
187
  if (this.entry.resource === "https://models.physiomeproject.org/workspace/486/rawfile/55879cbc485e2d4c41f3dc6d60424b849f94c4ee/HumanSAN_Fabbri_Fantini_Wilders_Severi_2017.cellml") {
288
188
  this.mode = 0;
189
+ this.json = jsonForNormalModel();
289
190
  } else if (this.entry.resource === "https://models.physiomeproject.org/workspace/698/rawfile/f3fc911063ac72ed44e84c0c5af28df41c25d452/fabbri_et_al_based_composite_SAN_model.sedml") {
290
191
  this.mode = 1;
192
+ this.json = jsonForCompositeModel();
291
193
  }
292
194
  }
195
+
196
+ // Make sure that the JSON configuration is valid.
197
+
198
+ this.hasValidJson = validJson(this.json);
199
+
200
+ if (!this.hasValidJson) {
201
+ return;
202
+ }
203
+
204
+ // Prepare ourselves for the UI by initialising some data.
205
+
206
+ prepareForUi(this);
207
+ },
208
+ mounted: function() {
209
+ // Finish mounting by creating the UI for the given simulation dataset.
210
+
211
+ if (this.hasValidJson) {
212
+ this.ui = new Ui(this);
213
+ }
293
214
  },
294
215
  };
295
216
  </script>
@@ -304,18 +225,18 @@ export default {
304
225
  margin: -8px 0 8px 0;
305
226
  width: 191px;
306
227
  }
307
- >>> .el-input-number.slider-and-field {
228
+ >>> .el-input-number.scalar {
308
229
  margin-top: -8px;
309
230
  width: 0;
310
231
  height: 0;
311
232
  }
312
- >>> .el-input-number.slider-and-field .el-input {
233
+ >>> .el-input-number.scalar .el-input {
313
234
  width: 60px;
314
235
  }
315
- >>> .el-input-number.slider-and-field .el-input__inner:focus {
236
+ >>> .el-input-number.scalar .el-input__inner:focus {
316
237
  border-color: #8300bf;
317
238
  }
318
- >>> .el-select.simulation-mode {
239
+ >>> .el-select.discrete {
319
240
  margin-bottom: 16px;
320
241
  }
321
242
  >>> .el-slider {
@@ -329,20 +250,20 @@ export default {
329
250
  >>> .el-slider__button {
330
251
  border-color: #8300bf;
331
252
  }
332
- .simulation-mode {
253
+ .discrete {
333
254
  margin-left: 8px;
334
255
  }
335
- .simulation-mode >>> .el-input__inner {
256
+ .discrete >>> .el-input__inner {
336
257
  font-family: Asap, sans-serif;
337
258
  }
338
- .simulation-mode >>> .el-input__inner:focus,
339
- .simulation-mode >>> .el-input.is-focus .el-input__inner {
259
+ .discrete >>> .el-input__inner:focus,
260
+ .discrete >>> .el-input.is-focus .el-input__inner {
340
261
  border-color: #8300bf;
341
262
  }
342
- .simulation-mode-popper .el-select-dropdown__item {
263
+ .discrete-popper .el-select-dropdown__item {
343
264
  font-family: Asap, sans-serif;
344
265
  }
345
- .simulation-mode-popper .el-select-dropdown__item.selected {
266
+ .discrete-popper .el-select-dropdown__item.selected {
346
267
  font-weight: normal;
347
268
  color: #8300bf;
348
269
  }
@@ -358,9 +279,33 @@ div.main-left {
358
279
  height: 100%;
359
280
  overflow: auto;
360
281
  }
361
- div.main-right.composite {
282
+ div.main-right.x1 {
283
+ height: 100%;
284
+ }
285
+ div.main-right.x2 {
362
286
  height: 50%;
363
287
  }
288
+ div.main-right.x3 {
289
+ height: 33.333%;
290
+ }
291
+ div.main-right.x4 {
292
+ height: 25%;
293
+ }
294
+ div.main-right.x5 {
295
+ height: 20%;
296
+ }
297
+ div.main-right.x6 {
298
+ height: 16.667%;
299
+ }
300
+ div.main-right.x7 {
301
+ height: 14.286%;
302
+ }
303
+ div.main-right.x8 {
304
+ height: 12.5%;
305
+ }
306
+ div.main-right.x9 {
307
+ height: 11.111%;
308
+ }
364
309
  >>> div.main-right div.controls {
365
310
  height: 0;
366
311
  }
@@ -413,8 +358,8 @@ p.default {
413
358
  p.error {
414
359
  margin-left: 16px;
415
360
  }
416
- p.first-slider-and-field,
417
- p.slider-and-field {
361
+ p.first-scalar,
362
+ p.scalar {
418
363
  grid-column-start: 1;
419
364
  grid-column-end: 3;
420
365
  margin-bottom: 8px;
@@ -431,13 +376,14 @@ p.input-parameters {
431
376
  p.title {
432
377
  line-height: 20px;
433
378
  }
434
- p.simulation-mode {
379
+ p.discrete {
380
+ margin-top: 0;
435
381
  margin-bottom: 4px;
436
382
  }
437
- p.first-slider-and-field {
383
+ p.first-scalar {
438
384
  margin-top: 0;
439
385
  }
440
- p.slider-and-field {
386
+ p.scalar {
441
387
  margin-top: 6px;
442
388
  }
443
389
  span.error {
@@ -0,0 +1,29 @@
1
+ function doEvaluateValue(value, from, to) {
2
+ if (from !== undefined) {
3
+ let re = new RegExp(`\\b${ from }\\b`, 'g');
4
+
5
+ value = value.replace(re, to);
6
+ }
7
+
8
+ return value;
9
+ }
10
+
11
+ export function evaluateValue(ui, value) {
12
+ ui.discreteElements.forEach((discreteElement) => {
13
+ value = doEvaluateValue(value, discreteElement.id, discreteElement.select.vModel);
14
+ });
15
+
16
+ ui.scalarElements.forEach((scalarElement) => {
17
+ value = doEvaluateValue(value, scalarElement.id, scalarElement.input_number.vModel);
18
+ });
19
+
20
+ return Function("return " + value + ";")();
21
+ }
22
+
23
+ export function evaluateSimulationValue(parent, results, value, i) {
24
+ parent.json.output.data.forEach((data) => {
25
+ value = doEvaluateValue(value, data.id, results[parent.simulationDataId[data.id]][i]);
26
+ });
27
+
28
+ return Function("return " + value + ";")();
29
+ }
@@ -0,0 +1,139 @@
1
+ export function jsonForNormalModel() {
2
+ return {
3
+ input: [
4
+ {
5
+ defaultValue: 0,
6
+ id: "sm",
7
+ name: "Simulation mode",
8
+ possibleValues: [
9
+ {
10
+ name: "Normal sinus rhythm",
11
+ value: 0,
12
+ },
13
+ {
14
+ name: "Stellate stimulation",
15
+ value: 1,
16
+ },
17
+ {
18
+ name: "Vagal stimulation",
19
+ value: 2,
20
+ },
21
+ ],
22
+ },
23
+ {
24
+ defaultValue: 0,
25
+ enabled: "(sm == 1) || (sm == 2)",
26
+ id: "sl",
27
+ maximumValue: 10,
28
+ minimumValue: 0,
29
+ name: "Stimulation level",
30
+ },
31
+ ],
32
+ output: {
33
+ data: [
34
+ {
35
+ id: "time",
36
+ name: "environment/time",
37
+ },
38
+ {
39
+ id: "vm",
40
+ name: "Membrane/V",
41
+ },
42
+ ],
43
+ plots: [
44
+ {
45
+ xAxisTitle: "Time (s)",
46
+ xValue: "time",
47
+ yAxisTitle: "Membrane potential (mV)",
48
+ yValue: "vm",
49
+ },
50
+ ],
51
+ },
52
+ parameters: [
53
+ {
54
+ name: "Rate_modulation_experiments/Iso_1_uM",
55
+ value: "(sm == 0) ? 0.0 : 1.0",
56
+ },
57
+ {
58
+ name: "Rate_modulation_experiments/ACh",
59
+ value: "(sm == 0) ? 0.0 : (sm === 1) ? (1.0 - 0.1 * sl) * 22.0e-6 : 22.0e-6 + 0.1 * sl * (38.0e-6 - 22.0e-6)",
60
+ },
61
+ ],
62
+ simulation: {
63
+ endingPoint: 3.0,
64
+ pointInterval: 0.001,
65
+ },
66
+ };
67
+ }
68
+
69
+ export function jsonForCompositeModel() {
70
+ return {
71
+ input: [
72
+ {
73
+ defaultValue: 300,
74
+ id: "sf",
75
+ maximumValue: 1000,
76
+ minimumValue: 0,
77
+ name: "Spike frequency",
78
+ },
79
+ {
80
+ defaultValue: 10,
81
+ id: "sn",
82
+ maximumValue: 30,
83
+ minimumValue: 0,
84
+ name: "Spike number",
85
+ },
86
+ {
87
+ defaultValue: 10,
88
+ id: "sa",
89
+ maximumValue: 30,
90
+ minimumValue: 0,
91
+ name: "Spike amplitude",
92
+ },
93
+ ],
94
+ output: {
95
+ data: [
96
+ {
97
+ id: "sa",
98
+ name: "Brain_stem/w",
99
+ },
100
+ {
101
+ id: "time",
102
+ name: "environment/time",
103
+ },
104
+ {
105
+ id: "vm",
106
+ name: "Membrane/V",
107
+ },
108
+ ],
109
+ plots: [
110
+ {
111
+ xAxisTitle: "Time (s)",
112
+ xValue: "time",
113
+ yAxisTitle: "Spike amplitude",
114
+ yValue: "100.0 * sa",
115
+ },
116
+ {
117
+ xAxisTitle: "Time (s)",
118
+ xValue: "time",
119
+ yAxisTitle: "Membrane potential (mV)",
120
+ yValue: "vm",
121
+ },
122
+ ],
123
+ },
124
+ parameters: [
125
+ {
126
+ name: "Brain_stem/t_period",
127
+ value: "sf",
128
+ },
129
+ {
130
+ name: "Brain_stem/w_n",
131
+ value: "sn",
132
+ },
133
+ {
134
+ name: "Brain_stem/w_value",
135
+ value: "0.01 * sa",
136
+ },
137
+ ],
138
+ };
139
+ }