modelbase2 0.6.0__py3-none-any.whl → 0.7.0__py3-none-any.whl

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.
modelbase2/simulator.py CHANGED
@@ -10,7 +10,7 @@ Classes:
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- from dataclasses import dataclass
13
+ from dataclasses import dataclass, field
14
14
  from typing import TYPE_CHECKING, Literal, Self, cast, overload
15
15
 
16
16
  import numpy as np
@@ -18,13 +18,13 @@ import pandas as pd
18
18
 
19
19
  from modelbase2.integrators import DefaultIntegrator
20
20
 
21
- __all__ = ["Simulator"]
21
+ __all__ = ["Result", "Simulator"]
22
22
 
23
23
  if TYPE_CHECKING:
24
- from collections.abc import Callable
24
+ from collections.abc import Callable, Iterator
25
25
 
26
26
  from modelbase2.model import Model
27
- from modelbase2.types import ArrayLike, IntegratorProtocol
27
+ from modelbase2.types import Array, ArrayLike, IntegratorProtocol
28
28
 
29
29
 
30
30
  def _normalise_split_results(
@@ -56,6 +56,240 @@ def _normalise_split_results(
56
56
  return results
57
57
 
58
58
 
59
+ @dataclass(slots=True)
60
+ class Result:
61
+ """Simulation results."""
62
+
63
+ model: Model
64
+ _raw_variables: list[pd.DataFrame]
65
+ _parameters: list[dict[str, float]]
66
+ _dependent: list[pd.DataFrame] = field(default_factory=list)
67
+
68
+ @property
69
+ def variables(self) -> pd.DataFrame:
70
+ """Simulation variables."""
71
+ return self.get_variables(
72
+ include_derived=True,
73
+ include_readouts=True,
74
+ concatenated=True,
75
+ normalise=None,
76
+ )
77
+
78
+ @property
79
+ def fluxes(self) -> pd.DataFrame:
80
+ """Simulation fluxes."""
81
+ return self.get_fluxes()
82
+
83
+ def __iter__(self) -> Iterator[pd.DataFrame]:
84
+ """Iterate over the concentration and flux response coefficients."""
85
+ return iter((self.variables, self.fluxes))
86
+
87
+ def _get_dependent(
88
+ self,
89
+ *,
90
+ include_readouts: bool = True,
91
+ ) -> list[pd.DataFrame]:
92
+ # Already computed
93
+ if len(self._dependent) > 0:
94
+ return self._dependent
95
+
96
+ # Compute new otherwise
97
+ for res, p in zip(self._raw_variables, self._parameters, strict=True):
98
+ self.model.update_parameters(p)
99
+ self._dependent.append(
100
+ self.model.get_dependent_time_course(
101
+ variables=res,
102
+ include_readouts=include_readouts,
103
+ )
104
+ )
105
+ return self._dependent
106
+
107
+ def _select_variables(
108
+ self,
109
+ dependent: list[pd.DataFrame],
110
+ *,
111
+ include_derived: bool,
112
+ include_readouts: bool,
113
+ ) -> list[pd.DataFrame]:
114
+ names = self.model.get_variable_names()
115
+ if include_derived:
116
+ names.extend(self.model.get_derived_variable_names())
117
+ if include_readouts:
118
+ names.extend(self.model.get_readout_names())
119
+ return [i.loc[:, names] for i in dependent]
120
+
121
+ def _select_fluxes(
122
+ self,
123
+ dependent: list[pd.DataFrame],
124
+ *,
125
+ include_surrogates: bool,
126
+ ) -> list[pd.DataFrame]:
127
+ names = self.model.get_reaction_names()
128
+ if include_surrogates:
129
+ names.extend(self.model.get_surrogate_reaction_names())
130
+ return [i.loc[:, names] for i in dependent]
131
+
132
+ def _adjust_data(
133
+ self,
134
+ data: list[pd.DataFrame],
135
+ normalise: float | ArrayLike | None = None,
136
+ *,
137
+ concatenated: bool = True,
138
+ ) -> pd.DataFrame | list[pd.DataFrame]:
139
+ if normalise is not None:
140
+ data = _normalise_split_results(data, normalise=normalise)
141
+ if concatenated:
142
+ return pd.concat(data, axis=0)
143
+ return data
144
+
145
+ @overload
146
+ def get_variables( # type: ignore
147
+ self,
148
+ *,
149
+ include_derived: bool = True,
150
+ include_readouts: bool = True,
151
+ concatenated: Literal[False],
152
+ normalise: float | ArrayLike | None = None,
153
+ ) -> list[pd.DataFrame]: ...
154
+
155
+ @overload
156
+ def get_variables(
157
+ self,
158
+ *,
159
+ include_derived: bool = True,
160
+ include_readouts: bool = True,
161
+ concatenated: Literal[True],
162
+ normalise: float | ArrayLike | None = None,
163
+ ) -> pd.DataFrame: ...
164
+
165
+ @overload
166
+ def get_variables(
167
+ self,
168
+ *,
169
+ include_derived: bool = True,
170
+ include_readouts: bool = True,
171
+ concatenated: bool = True,
172
+ normalise: float | ArrayLike | None = None,
173
+ ) -> pd.DataFrame: ...
174
+
175
+ def get_variables(
176
+ self,
177
+ *,
178
+ include_derived: bool = True,
179
+ include_readouts: bool = True,
180
+ concatenated: bool = True,
181
+ normalise: float | ArrayLike | None = None,
182
+ ) -> pd.DataFrame | list[pd.DataFrame]:
183
+ """Get the variables over time.
184
+
185
+ Examples:
186
+ >>> Result().get_variables()
187
+ Time ATP NADPH
188
+ 0.000000 1.000000 1.000000
189
+ 0.000100 0.999900 0.999900
190
+ 0.000200 0.999800 0.999800
191
+
192
+ """
193
+ if not include_derived and not include_readouts:
194
+ return self._adjust_data(
195
+ self._raw_variables,
196
+ normalise=normalise,
197
+ concatenated=concatenated,
198
+ )
199
+
200
+ variables = self._select_variables(
201
+ self._get_dependent(),
202
+ include_derived=include_derived,
203
+ include_readouts=include_readouts,
204
+ )
205
+ return self._adjust_data(
206
+ variables, normalise=normalise, concatenated=concatenated
207
+ )
208
+
209
+ @overload
210
+ def get_fluxes( # type: ignore
211
+ self,
212
+ *,
213
+ include_surrogates: bool = True,
214
+ normalise: float | ArrayLike | None = None,
215
+ concatenated: Literal[False],
216
+ ) -> list[pd.DataFrame]: ...
217
+
218
+ @overload
219
+ def get_fluxes(
220
+ self,
221
+ *,
222
+ include_surrogates: bool = True,
223
+ normalise: float | ArrayLike | None = None,
224
+ concatenated: Literal[True],
225
+ ) -> pd.DataFrame: ...
226
+
227
+ @overload
228
+ def get_fluxes(
229
+ self,
230
+ *,
231
+ include_surrogates: bool = True,
232
+ normalise: float | ArrayLike | None = None,
233
+ concatenated: bool = True,
234
+ ) -> pd.DataFrame: ...
235
+
236
+ def get_fluxes(
237
+ self,
238
+ *,
239
+ include_surrogates: bool = True,
240
+ normalise: float | ArrayLike | None = None,
241
+ concatenated: bool = True,
242
+ ) -> pd.DataFrame | list[pd.DataFrame]:
243
+ """Get the flux results.
244
+
245
+ Examples:
246
+ >>> Result.get_fluxes()
247
+ Time v1 v2
248
+ 0.000000 1.000000 10.00000
249
+ 0.000100 0.999900 9.999000
250
+ 0.000200 0.999800 9.998000
251
+
252
+ Returns:
253
+ pd.DataFrame: DataFrame of fluxes.
254
+
255
+ """
256
+ fluxes = self._select_fluxes(
257
+ self._get_dependent(),
258
+ include_surrogates=include_surrogates,
259
+ )
260
+ return self._adjust_data(
261
+ fluxes,
262
+ normalise=normalise,
263
+ concatenated=concatenated,
264
+ )
265
+
266
+ def get_combined(self) -> pd.DataFrame:
267
+ """Get the variables and fluxes as a single pandas.DataFrame.
268
+
269
+ Examples:
270
+ >>> Result.get_combined()
271
+ Time ATP NADPH v1 v2
272
+ 0.000000 1.000000 1.000000 1.000000 10.00000
273
+ 0.000100 0.999900 0.999900 0.999900 9.999000
274
+ 0.000200 0.999800 0.999800 0.999800 9.998000
275
+
276
+ Returns:
277
+ pd.DataFrame: DataFrame of fluxes.
278
+
279
+ """
280
+ return pd.concat((self.variables, self.fluxes), axis=1)
281
+
282
+ def get_new_y0(self) -> dict[str, float]:
283
+ """Get the new initial conditions after the simulation.
284
+
285
+ Examples:
286
+ >>> Simulator(model).simulate_to_steady_state().get_new_y0()
287
+ {"ATP": 1.0, "NADPH": 1.0}
288
+
289
+ """
290
+ return dict(self.variables.iloc[-1])
291
+
292
+
59
293
  @dataclass(
60
294
  init=False,
61
295
  slots=True,
@@ -68,19 +302,23 @@ class Simulator:
68
302
  model: Model instance to simulate.
69
303
  y0: Initial conditions for the simulation.
70
304
  integrator: Integrator protocol to use for the simulation.
71
- concs: List of DataFrames containing concentration results.
305
+ variables: List of DataFrames containing concentration results.
72
306
  dependent: List of DataFrames containing argument values.
73
307
  simulation_parameters: List of dictionaries containing simulation parameters.
74
308
 
75
309
  """
76
310
 
77
311
  model: Model
78
- y0: ArrayLike
312
+ y0: dict[str, float]
79
313
  integrator: IntegratorProtocol
80
- concs: list[pd.DataFrame] | None
314
+ variables: list[pd.DataFrame] | None
81
315
  dependent: list[pd.DataFrame] | None
82
316
  simulation_parameters: list[dict[str, float]] | None
83
317
 
318
+ # For resets (e.g. update variable)
319
+ _integrator_type: Callable[[Callable, ArrayLike], IntegratorProtocol]
320
+ _time_shift: float | None
321
+
84
322
  def __init__(
85
323
  self,
86
324
  model: Model,
@@ -94,63 +332,44 @@ class Simulator:
94
332
  """Initialize the Simulator.
95
333
 
96
334
  Args:
97
- model (Model): The model to be simulated.
98
- y0 (dict[str, float] | None, optional): Initial conditions for the model variables.
99
- If None, the initial conditions are obtained from the model. Defaults to None.
100
- integrator (Callable[[Callable, ArrayLike], IntegratorProtocol], optional): The integrator to use for the simulation.
101
- Defaults to DefaultIntegrator.
102
- test_run (bool, optional): If True, performs a test run to ensure the model's methods
103
- (get_full_concs, get_fluxes, get_right_hand_side) work correctly with the initial conditions.
104
- Defaults to True.
335
+ model: The model to be simulated.
336
+ y0: Initial conditions for the model variables.
337
+ If None, the initial conditions are obtained from the model.
338
+ integrator: The integrator to use for the simulation.
339
+ test_run (bool, optional): If True, performs a test run for better error messages
105
340
 
106
341
  """
107
342
  self.model = model
108
- y0 = model.get_initial_conditions() if y0 is None else y0
109
- self.y0 = [y0[k] for k in model.get_variable_names()]
343
+ self.y0 = model.get_initial_conditions() if y0 is None else y0
110
344
 
111
- self.integrator = integrator(self.model, self.y0)
112
- self.concs = None
345
+ self._integrator_type = integrator
346
+ self._time_shift = None
347
+ self.variables = None
113
348
  self.simulation_parameters = None
114
349
 
115
350
  if test_run:
116
- y0 = dict(zip(model.get_variable_names(), self.y0, strict=True))
117
- self.model.get_right_hand_side(y0, 0)
118
-
119
- def _save_simulation_results(
120
- self,
121
- *,
122
- results: pd.DataFrame,
123
- skipfirst: bool,
124
- ) -> None:
125
- """Save simulation results.
351
+ self.model.get_right_hand_side(self.y0, time=0)
126
352
 
127
- Args:
128
- results: DataFrame containing the simulation results.
129
- skipfirst: Whether to skip the first row of results.
130
-
131
- """
132
- if self.concs is None:
133
- self.concs = [results]
134
- elif skipfirst:
135
- self.concs.append(results.iloc[1:, :])
136
- else:
137
- self.concs.append(results)
353
+ self._initialise_integrator()
138
354
 
139
- if self.simulation_parameters is None:
140
- self.simulation_parameters = []
141
- self.simulation_parameters.append(self.model.parameters)
355
+ def _initialise_integrator(self) -> None:
356
+ y0 = self.y0
357
+ self.integrator = self._integrator_type(
358
+ self.model,
359
+ [y0[k] for k in self.model.get_variable_names()],
360
+ )
142
361
 
143
362
  def clear_results(self) -> None:
144
363
  """Clear simulation results."""
145
- self.concs = None
364
+ self.variables = None
146
365
  self.dependent = None
147
366
  self.simulation_parameters = None
148
- if self.integrator is not None:
149
- self.integrator.reset()
367
+ self._time_shift = None
368
+ self._initialise_integrator()
150
369
 
151
370
  def _handle_simulation_results(
152
371
  self,
153
- time: ArrayLike | None,
372
+ time: Array | None,
154
373
  results: ArrayLike | None,
155
374
  *,
156
375
  skipfirst: bool,
@@ -169,6 +388,9 @@ class Simulator:
169
388
  self.clear_results()
170
389
  return
171
390
 
391
+ if self._time_shift is not None:
392
+ time += self._time_shift
393
+
172
394
  # NOTE: IMPORTANT!
173
395
  # model._get_rhs sorts the return array by model.get_variable_names()
174
396
  # Do NOT change this ordering
@@ -177,7 +399,17 @@ class Simulator:
177
399
  index=time,
178
400
  columns=self.model.get_variable_names(),
179
401
  )
180
- self._save_simulation_results(results=results_df, skipfirst=skipfirst)
402
+
403
+ if self.variables is None:
404
+ self.variables = [results_df]
405
+ elif skipfirst:
406
+ self.variables.append(results_df.iloc[1:, :])
407
+ else:
408
+ self.variables.append(results_df)
409
+
410
+ if self.simulation_parameters is None:
411
+ self.simulation_parameters = []
412
+ self.simulation_parameters.append(self.model.parameters)
181
413
 
182
414
  def simulate(
183
415
  self,
@@ -201,7 +433,11 @@ class Simulator:
201
433
  Self: The Simulator instance with updated results.
202
434
 
203
435
  """
436
+ if self._time_shift is not None:
437
+ t_end -= self._time_shift
438
+
204
439
  time, results = self.integrator.integrate(t_end=t_end, steps=steps)
440
+
205
441
  self._handle_simulation_results(time, results, skipfirst=True)
206
442
  return self
207
443
 
@@ -223,6 +459,10 @@ class Simulator:
223
459
  Self: The Simulator instance with updated results.
224
460
 
225
461
  """
462
+ if self._time_shift is not None:
463
+ time_points = np.array(time_points, dtype=float)
464
+ time_points -= self._time_shift
465
+
226
466
  time, results = self.integrator.integrate_time_course(time_points=time_points)
227
467
  self._handle_simulation_results(time, results, skipfirst=True)
228
468
  return self
@@ -256,7 +496,7 @@ class Simulator:
256
496
  rel_norm=rel_norm,
257
497
  )
258
498
  self._handle_simulation_results(
259
- [time] if time is not None else None,
499
+ np.array([time], dtype=float) if time is not None else None,
260
500
  [results] if results is not None else None, # type: ignore
261
501
  skipfirst=False,
262
502
  )
@@ -287,329 +527,37 @@ class Simulator:
287
527
  t_end = cast(pd.Timedelta, t_end)
288
528
  self.model.update_parameters(pars.to_dict())
289
529
  self.simulate(t_end.total_seconds(), steps=time_points_per_step)
290
- if self.concs is None:
530
+ if self.variables is None:
291
531
  break
292
532
  return self
293
533
 
294
- def _get_dependent_vectorised(
295
- self,
296
- concs: list[pd.DataFrame],
297
- params: list[dict[str, float]],
298
- *,
299
- include_readouts: bool = True,
300
- ) -> list[pd.DataFrame]:
301
- dependent: list[pd.DataFrame] = []
302
-
303
- for res, p in zip(concs, params, strict=True):
304
- self.model.update_parameters(p)
305
- dependent.append(
306
- self.model.get_dependent_time_course(
307
- concs=res,
308
- include_readouts=include_readouts,
309
- )
310
- )
311
- return dependent
312
-
313
- @overload
314
- def get_concs( # type: ignore
315
- self,
316
- *,
317
- normalise: float | ArrayLike | None = None,
318
- concatenated: Literal[False],
319
- ) -> None | list[pd.DataFrame]: ...
320
-
321
- @overload
322
- def get_concs(
323
- self,
324
- *,
325
- normalise: float | ArrayLike | None = None,
326
- concatenated: Literal[True],
327
- ) -> None | pd.DataFrame: ...
328
-
329
- @overload
330
- def get_concs(
331
- self,
332
- *,
333
- normalise: float | ArrayLike | None = None,
334
- concatenated: Literal[True] = True,
335
- ) -> None | pd.DataFrame: ...
336
-
337
- def get_concs(
338
- self,
339
- *,
340
- normalise: float | ArrayLike | None = None,
341
- concatenated: bool = True,
342
- ) -> None | pd.DataFrame | list[pd.DataFrame]:
343
- """Get the concentration results.
534
+ def get_result(self) -> Result | None:
535
+ """Get result of the simulation.
344
536
 
345
537
  Examples:
346
- >>> Simulator(model).get_concs()
538
+ >>> variables, fluxes = Simulator(model).simulate().get_result()
539
+ >>> variables
347
540
  Time ATP NADPH
348
541
  0.000000 1.000000 1.000000
349
542
  0.000100 0.999900 0.999900
350
543
  0.000200 0.999800 0.999800
351
-
352
- Returns:
353
- pd.DataFrame: DataFrame of concentrations.
354
-
355
- """
356
- if self.concs is None:
357
- return None
358
-
359
- results = self.concs.copy()
360
- if normalise is not None:
361
- results = _normalise_split_results(results=results, normalise=normalise)
362
- if concatenated:
363
- return pd.concat(results, axis=0)
364
-
365
- return results
366
-
367
- # Get results depending on the raw results
368
-
369
- def _get_dependent(self) -> list[pd.DataFrame] | None:
370
- if (concs := self.concs) is None:
371
- return None
372
- if (params := self.simulation_parameters) is None:
373
- return None
374
-
375
- return self._get_dependent_vectorised(concs, params)
376
-
377
- @overload
378
- def get_full_concs( # type: ignore
379
- self,
380
- *,
381
- normalise: float | ArrayLike | None = None,
382
- concatenated: Literal[False],
383
- include_readouts: bool = True,
384
- dependent: list[pd.DataFrame] | None = None,
385
- ) -> list[pd.DataFrame] | None: ...
386
-
387
- @overload
388
- def get_full_concs(
389
- self,
390
- *,
391
- normalise: float | ArrayLike | None = None,
392
- concatenated: Literal[True],
393
- include_readouts: bool = True,
394
- dependent: list[pd.DataFrame] | None = None,
395
- ) -> pd.DataFrame | None: ...
396
-
397
- @overload
398
- def get_full_concs(
399
- self,
400
- *,
401
- normalise: float | ArrayLike | None = None,
402
- concatenated: bool = True,
403
- include_readouts: bool = True,
404
- dependent: list[pd.DataFrame] | None = None,
405
- ) -> pd.DataFrame | None: ...
406
-
407
- def get_full_concs(
408
- self,
409
- *,
410
- normalise: float | ArrayLike | None = None,
411
- concatenated: bool = True,
412
- include_readouts: bool = True,
413
- dependent: list[pd.DataFrame] | None = None,
414
- ) -> pd.DataFrame | list[pd.DataFrame] | None:
415
- """Get the full concentration results, including derived quantities.
416
-
417
- Examples:
418
- >>> Simulator(model).get_full_concs()
419
- Time ATP NADPH
420
- 0.000000 1.000000 1.000000
421
- 0.000100 0.999900 0.999900
422
- 0.000200 0.999800 0.999800
423
-
424
- Returns: DataFrame of full concentrations.
425
-
426
- """
427
- if (concs := self.concs) is None:
428
- return None
429
- if (params := self.simulation_parameters) is None:
430
- return None
431
- if dependent is None:
432
- dependent = self._get_dependent_vectorised(concs, params)
433
-
434
- # Filter to full concs
435
- names = (
436
- self.model.get_variable_names() + self.model.get_derived_variable_names()
437
- )
438
- if include_readouts:
439
- names.extend(self.model.get_readout_names())
440
- dependent = [i.loc[:, names] for i in dependent]
441
-
442
- if normalise is not None:
443
- dependent = _normalise_split_results(
444
- results=dependent,
445
- normalise=normalise,
446
- )
447
- if concatenated:
448
- return pd.concat(dependent, axis=0)
449
- return dependent
450
-
451
- @overload
452
- def get_fluxes( # type: ignore
453
- self,
454
- *,
455
- normalise: float | ArrayLike | None = None,
456
- concatenated: Literal[False],
457
- dependent: list[pd.DataFrame] | None = None,
458
- ) -> list[pd.DataFrame] | None: ...
459
-
460
- @overload
461
- def get_fluxes(
462
- self,
463
- *,
464
- normalise: float | ArrayLike | None = None,
465
- concatenated: Literal[True],
466
- dependent: list[pd.DataFrame] | None = None,
467
- ) -> pd.DataFrame | None: ...
468
-
469
- @overload
470
- def get_fluxes(
471
- self,
472
- *,
473
- normalise: float | ArrayLike | None = None,
474
- concatenated: bool = True,
475
- dependent: list[pd.DataFrame] | None = None,
476
- ) -> pd.DataFrame | None: ...
477
-
478
- def get_fluxes(
479
- self,
480
- *,
481
- normalise: float | ArrayLike | None = None,
482
- concatenated: bool = True,
483
- dependent: list[pd.DataFrame] | None = None,
484
- ) -> pd.DataFrame | list[pd.DataFrame] | None:
485
- """Get the flux results.
486
-
487
- Examples:
488
- >>> Simulator(model).get_fluxes()
544
+ >>> fluxes
489
545
  Time v1 v2
490
546
  0.000000 1.000000 10.00000
491
547
  0.000100 0.999900 9.999000
492
548
  0.000200 0.999800 9.998000
493
549
 
494
- Returns:
495
- pd.DataFrame: DataFrame of fluxes.
496
-
497
550
  """
498
- if (concs := self.concs) is None:
551
+ if (variables := self.variables) is None:
499
552
  return None
500
- if (params := self.simulation_parameters) is None:
553
+ if (parameters := self.simulation_parameters) is None:
501
554
  return None
502
- if dependent is None:
503
- dependent = self._get_dependent_vectorised(concs, params)
504
-
505
- # Filter to fluxes
506
- names = self.model.get_reaction_names()
507
- dependent = [i.loc[:, names] for i in dependent]
508
- if normalise is not None:
509
- dependent = _normalise_split_results(
510
- results=dependent,
511
- normalise=normalise,
512
- )
513
- if concatenated:
514
- return pd.concat(dependent, axis=0)
515
- return dependent
516
-
517
- def get_concs_and_fluxes(self) -> tuple[pd.DataFrame | None, pd.DataFrame | None]:
518
- """Get the concentrations and fluxes.
519
-
520
- Examples:
521
- >>> Simulator(model).get_concs_and_fluxes()
522
- (concs, fluxes)
523
-
524
-
525
- Returns:
526
- tuple[pd.DataFrame, pd.DataFrame]: Tuple of concentrations and fluxes.
527
-
528
- """
529
- if (concs := self.concs) is None:
530
- return None, None
531
- if (params := self.simulation_parameters) is None:
532
- return None, None
533
-
534
- dependent = self._get_dependent_vectorised(concs, params)
535
-
536
- return self.get_concs(), self.get_fluxes(dependent=dependent)
537
-
538
- def get_full_concs_and_fluxes(
539
- self,
540
- *,
541
- include_readouts: bool = True,
542
- ) -> tuple[pd.DataFrame | None, pd.DataFrame | None]:
543
- """Get the full concentrations and fluxes.
544
-
545
- >>> Simulator(model).get_full_concs_and_fluxes()
546
- (full_concs, full_fluxes)
547
-
548
- Args:
549
- include_readouts: Whether to include readouts in the results.
550
-
551
- Returns:
552
- Full concentrations and fluxes
553
-
554
- """
555
- if (concs := self.concs) is None:
556
- return None, None
557
- if (params := self.simulation_parameters) is None:
558
- return None, None
559
- dependent = self._get_dependent_vectorised(concs, params)
560
-
561
- return (
562
- self.get_full_concs(include_readouts=include_readouts, dependent=dependent),
563
- self.get_fluxes(dependent=dependent),
555
+ return Result(
556
+ model=self.model,
557
+ _raw_variables=variables,
558
+ _parameters=parameters,
564
559
  )
565
560
 
566
- def get_results(self) -> pd.DataFrame | None:
567
- """Get the combined results of concentrations and fluxes.
568
-
569
- Examples:
570
- >>> Simulator(model).get_results()
571
- Time ATP NADPH v1 v2
572
- 0.000000 1.000000 1.000000 1.000000 1.000000
573
- 0.000100 0.999900 0.999900 0.999900 0.999900
574
- 0.000200 0.999800 0.999800 0.999800 0.999800
575
-
576
- Returns:
577
- pd.DataFrame: Combined DataFrame of concentrations and fluxes.
578
-
579
- """
580
- c, v = self.get_concs_and_fluxes()
581
- if c is None or v is None:
582
- return None
583
- return pd.concat((c, v), axis=1)
584
-
585
- def get_full_results(self) -> pd.DataFrame | None:
586
- """Get the combined full results of concentrations and fluxes.
587
-
588
- Examples:
589
- >>> Simulator(model).get_full_results()
590
- Time ATP NADPH v1 v2
591
- 0.000000 1.000000 1.000000 1.000000 1.000000
592
- 0.000100 0.999900 0.999900 0.999900 0.999900
593
- 0.000200 0.999800 0.999800 0.999800 0.999800
594
-
595
- """
596
- c, v = self.get_full_concs_and_fluxes()
597
- if c is None or v is None:
598
- return None
599
- return pd.concat((c, v), axis=1)
600
-
601
- def get_new_y0(self) -> dict[str, float] | None:
602
- """Get the new initial conditions after the simulation.
603
-
604
- Examples:
605
- >>> Simulator(model).get_new_y0()
606
- {"ATP": 1.0, "NADPH": 1.0}
607
-
608
- """
609
- if (res := self.get_concs()) is None:
610
- return None
611
- return dict(res.iloc[-1])
612
-
613
561
  def update_parameter(self, parameter: str, value: float) -> Self:
614
562
  """Updates the value of a specified parameter in the model.
615
563
 
@@ -665,3 +613,38 @@ class Simulator:
665
613
  """
666
614
  self.model.scale_parameters(parameters)
667
615
  return self
616
+
617
+ def update_variable(self, variable: str, value: float) -> Self:
618
+ """Updates the value of a specified value in the simulation.
619
+
620
+ Examples:
621
+ >>> Simulator(model).update_variable("k1", 0.1)
622
+
623
+ Args:
624
+ variable: name of the model variable
625
+ value: new value
626
+
627
+ """
628
+ return self.update_variables({variable: value})
629
+
630
+ def update_variables(self, variables: dict[str, float]) -> Self:
631
+ """Updates the value of a specified value in the simulation.
632
+
633
+ Examples:
634
+ >>> Simulator(model).update_variables({"k1": 0.1})
635
+
636
+ Args:
637
+ variables: {variable: value} pairs
638
+
639
+ """
640
+ sim_variables = self.variables
641
+
642
+ # In case someone calls this before the first simulation
643
+ if sim_variables is None:
644
+ self.y0 = self.y0 | variables
645
+ return self
646
+
647
+ self.y0 = sim_variables[-1].iloc[-1, :].to_dict() | variables
648
+ self._time_shift = float(sim_variables[-1].index[-1])
649
+ self._initialise_integrator()
650
+ return self