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