modelbase2 0.5.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.
72
- args: List of DataFrames containing argument values.
305
+ variables: List of DataFrames containing concentration results.
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
81
- args: list[pd.DataFrame] | None
314
+ variables: list[pd.DataFrame] | None
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,66 +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
113
- self.args = None
345
+ self._integrator_type = integrator
346
+ self._time_shift = None
347
+ self.variables = None
114
348
  self.simulation_parameters = None
115
349
 
116
350
  if test_run:
117
- y0 = dict(zip(model.get_variable_names(), self.y0, strict=True))
118
- self.model.get_full_concs(y0, 0)
119
- self.model.get_fluxes(y0, 0)
120
- self.model.get_right_hand_side(y0, 0)
121
-
122
- def _save_simulation_results(
123
- self,
124
- *,
125
- results: pd.DataFrame,
126
- skipfirst: bool,
127
- ) -> None:
128
- """Save simulation results.
129
-
130
- Args:
131
- results: DataFrame containing the simulation results.
132
- skipfirst: Whether to skip the first row of results.
351
+ self.model.get_right_hand_side(self.y0, time=0)
133
352
 
134
- """
135
- if self.concs is None:
136
- self.concs = [results]
137
- elif skipfirst:
138
- self.concs.append(results.iloc[1:, :])
139
- else:
140
- self.concs.append(results)
353
+ self._initialise_integrator()
141
354
 
142
- if self.simulation_parameters is None:
143
- self.simulation_parameters = []
144
- 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
+ )
145
361
 
146
362
  def clear_results(self) -> None:
147
363
  """Clear simulation results."""
148
- self.concs = None
149
- self.args = None
364
+ self.variables = None
365
+ self.dependent = None
150
366
  self.simulation_parameters = None
151
- if self.integrator is not None:
152
- self.integrator.reset()
367
+ self._time_shift = None
368
+ self._initialise_integrator()
153
369
 
154
370
  def _handle_simulation_results(
155
371
  self,
156
- time: ArrayLike | None,
372
+ time: Array | None,
157
373
  results: ArrayLike | None,
158
374
  *,
159
375
  skipfirst: bool,
@@ -172,15 +388,28 @@ class Simulator:
172
388
  self.clear_results()
173
389
  return
174
390
 
391
+ if self._time_shift is not None:
392
+ time += self._time_shift
393
+
175
394
  # NOTE: IMPORTANT!
176
- # model._get_rhs sorts the return array by model.get_compounds()
395
+ # model._get_rhs sorts the return array by model.get_variable_names()
177
396
  # Do NOT change this ordering
178
397
  results_df = pd.DataFrame(
179
398
  results,
180
399
  index=time,
181
400
  columns=self.model.get_variable_names(),
182
401
  )
183
- 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)
184
413
 
185
414
  def simulate(
186
415
  self,
@@ -204,7 +433,11 @@ class Simulator:
204
433
  Self: The Simulator instance with updated results.
205
434
 
206
435
  """
436
+ if self._time_shift is not None:
437
+ t_end -= self._time_shift
438
+
207
439
  time, results = self.integrator.integrate(t_end=t_end, steps=steps)
440
+
208
441
  self._handle_simulation_results(time, results, skipfirst=True)
209
442
  return self
210
443
 
@@ -226,6 +459,10 @@ class Simulator:
226
459
  Self: The Simulator instance with updated results.
227
460
 
228
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
+
229
466
  time, results = self.integrator.integrate_time_course(time_points=time_points)
230
467
  self._handle_simulation_results(time, results, skipfirst=True)
231
468
  return self
@@ -259,7 +496,7 @@ class Simulator:
259
496
  rel_norm=rel_norm,
260
497
  )
261
498
  self._handle_simulation_results(
262
- [time] if time is not None else None,
499
+ np.array([time], dtype=float) if time is not None else None,
263
500
  [results] if results is not None else None, # type: ignore
264
501
  skipfirst=False,
265
502
  )
@@ -290,298 +527,37 @@ class Simulator:
290
527
  t_end = cast(pd.Timedelta, t_end)
291
528
  self.model.update_parameters(pars.to_dict())
292
529
  self.simulate(t_end.total_seconds(), steps=time_points_per_step)
293
- if self.concs is None:
530
+ if self.variables is None:
294
531
  break
295
532
  return self
296
533
 
297
- def _get_args_vectorised(
298
- self,
299
- concs: list[pd.DataFrame],
300
- params: list[dict[str, float]],
301
- *,
302
- include_readouts: bool = True,
303
- ) -> list[pd.DataFrame]:
304
- args: list[pd.DataFrame] = []
305
-
306
- for res, p in zip(concs, params, strict=True):
307
- self.model.update_parameters(p)
308
- args.append(
309
- self.model.get_args_time_course(
310
- concs=res,
311
- include_readouts=include_readouts,
312
- )
313
- )
314
- return args
315
-
316
- @overload
317
- def get_concs( # type: ignore
318
- self,
319
- *,
320
- normalise: float | ArrayLike | None = None,
321
- concatenated: Literal[False],
322
- ) -> None | list[pd.DataFrame]: ...
323
-
324
- @overload
325
- def get_concs(
326
- self,
327
- *,
328
- normalise: float | ArrayLike | None = None,
329
- concatenated: Literal[True],
330
- ) -> None | pd.DataFrame: ...
331
-
332
- @overload
333
- def get_concs(
334
- self,
335
- *,
336
- normalise: float | ArrayLike | None = None,
337
- concatenated: Literal[True] = True,
338
- ) -> None | pd.DataFrame: ...
339
-
340
- def get_concs(
341
- self,
342
- *,
343
- normalise: float | ArrayLike | None = None,
344
- concatenated: bool = True,
345
- ) -> None | pd.DataFrame | list[pd.DataFrame]:
346
- """Get the concentration results.
347
-
348
- Examples:
349
- >>> Simulator(model).get_concs()
350
- Time ATP NADPH
351
- 0.000000 1.000000 1.000000
352
- 0.000100 0.999900 0.999900
353
- 0.000200 0.999800 0.999800
354
-
355
- Returns:
356
- pd.DataFrame: DataFrame of concentrations.
357
-
358
- """
359
- if self.concs is None:
360
- return None
361
-
362
- results = self.concs.copy()
363
- if normalise is not None:
364
- results = _normalise_split_results(results=results, normalise=normalise)
365
- if concatenated:
366
- return pd.concat(results, axis=0)
367
-
368
- return results
369
-
370
- @overload
371
- def get_full_concs( # type: ignore
372
- self,
373
- *,
374
- normalise: float | ArrayLike | None = None,
375
- concatenated: Literal[False],
376
- include_readouts: bool = True,
377
- ) -> list[pd.DataFrame] | None: ...
378
-
379
- @overload
380
- def get_full_concs(
381
- self,
382
- *,
383
- normalise: float | ArrayLike | None = None,
384
- concatenated: Literal[True],
385
- include_readouts: bool = True,
386
- ) -> pd.DataFrame | None: ...
387
-
388
- @overload
389
- def get_full_concs(
390
- self,
391
- *,
392
- normalise: float | ArrayLike | None = None,
393
- concatenated: bool = True,
394
- include_readouts: bool = True,
395
- ) -> pd.DataFrame | None: ...
396
-
397
- def get_full_concs(
398
- self,
399
- *,
400
- normalise: float | ArrayLike | None = None,
401
- concatenated: bool = True,
402
- include_readouts: bool = True,
403
- ) -> pd.DataFrame | list[pd.DataFrame] | None:
404
- """Get the full concentration results, including derived quantities.
534
+ def get_result(self) -> Result | None:
535
+ """Get result of the simulation.
405
536
 
406
537
  Examples:
407
- >>> Simulator(model).get_full_concs()
538
+ >>> variables, fluxes = Simulator(model).simulate().get_result()
539
+ >>> variables
408
540
  Time ATP NADPH
409
541
  0.000000 1.000000 1.000000
410
542
  0.000100 0.999900 0.999900
411
543
  0.000200 0.999800 0.999800
412
-
413
- Returns: DataFrame of full concentrations.
414
-
415
- """
416
- if (concs := self.concs) is None:
417
- return None
418
- if (params := self.simulation_parameters) is None:
419
- return None
420
- if (args := self.args) is None:
421
- args = self._get_args_vectorised(concs, params)
422
-
423
- names = (
424
- self.model.get_variable_names() + self.model.get_derived_variable_names()
425
- )
426
- if include_readouts:
427
- names.extend(self.model.get_readout_names())
428
- full_concs = [i.loc[:, names] for i in args]
429
- if normalise is not None:
430
- full_concs = _normalise_split_results(
431
- results=full_concs,
432
- normalise=normalise,
433
- )
434
- if concatenated:
435
- return pd.concat(full_concs, axis=0)
436
- return full_concs
437
-
438
- @overload
439
- def get_fluxes( # type: ignore
440
- self,
441
- *,
442
- normalise: float | ArrayLike | None = None,
443
- concatenated: Literal[False],
444
- ) -> list[pd.DataFrame] | None: ...
445
-
446
- @overload
447
- def get_fluxes(
448
- self,
449
- *,
450
- normalise: float | ArrayLike | None = None,
451
- concatenated: Literal[True],
452
- ) -> pd.DataFrame | None: ...
453
-
454
- @overload
455
- def get_fluxes(
456
- self,
457
- *,
458
- normalise: float | ArrayLike | None = None,
459
- concatenated: bool = True,
460
- ) -> pd.DataFrame | None: ...
461
-
462
- def get_fluxes(
463
- self,
464
- *,
465
- normalise: float | ArrayLike | None = None,
466
- concatenated: bool = True,
467
- ) -> pd.DataFrame | list[pd.DataFrame] | None:
468
- """Get the flux results.
469
-
470
- Examples:
471
- >>> Simulator(model).get_fluxes()
544
+ >>> fluxes
472
545
  Time v1 v2
473
546
  0.000000 1.000000 10.00000
474
547
  0.000100 0.999900 9.999000
475
548
  0.000200 0.999800 9.998000
476
549
 
477
- Returns:
478
- pd.DataFrame: DataFrame of fluxes.
479
-
480
550
  """
481
- if (concs := self.concs) is None:
551
+ if (variables := self.variables) is None:
482
552
  return None
483
- if (params := self.simulation_parameters) is None:
553
+ if (parameters := self.simulation_parameters) is None:
484
554
  return None
485
- if (args := self.args) is None:
486
- args = self._get_args_vectorised(concs, params)
487
-
488
- fluxes: list[pd.DataFrame] = []
489
- for y, p in zip(args, params, strict=True):
490
- self.model.update_parameters(p)
491
- fluxes.append(self.model.get_fluxes_time_course(args=y))
492
-
493
- if normalise is not None:
494
- fluxes = _normalise_split_results(
495
- results=fluxes,
496
- normalise=normalise,
497
- )
498
- if concatenated:
499
- return pd.concat(fluxes, axis=0)
500
- return fluxes
501
-
502
- def get_concs_and_fluxes(self) -> tuple[pd.DataFrame | None, pd.DataFrame | None]:
503
- """Get the concentrations and fluxes.
504
-
505
- Examples:
506
- >>> Simulator(model).get_concs_and_fluxes()
507
- (concs, fluxes)
508
-
509
-
510
- Returns:
511
- tuple[pd.DataFrame, pd.DataFrame]: Tuple of concentrations and fluxes.
512
-
513
- """
514
- return self.get_concs(), self.get_fluxes()
515
-
516
- def get_full_concs_and_fluxes(
517
- self,
518
- *,
519
- include_readouts: bool = True,
520
- ) -> tuple[pd.DataFrame | None, pd.DataFrame | None]:
521
- """Get the full concentrations and fluxes.
522
-
523
- >>> Simulator(model).get_full_concs_and_fluxes()
524
- (full_concs, full_fluxes)
525
-
526
- Args:
527
- include_readouts: Whether to include readouts in the results.
528
-
529
- Returns:
530
- Full concentrations and fluxes
531
-
532
- """
533
- return (
534
- self.get_full_concs(include_readouts=include_readouts),
535
- self.get_fluxes(),
555
+ return Result(
556
+ model=self.model,
557
+ _raw_variables=variables,
558
+ _parameters=parameters,
536
559
  )
537
560
 
538
- def get_results(self) -> pd.DataFrame | None:
539
- """Get the combined results of concentrations and fluxes.
540
-
541
- Examples:
542
- >>> Simulator(model).get_results()
543
- Time ATP NADPH v1 v2
544
- 0.000000 1.000000 1.000000 1.000000 1.000000
545
- 0.000100 0.999900 0.999900 0.999900 0.999900
546
- 0.000200 0.999800 0.999800 0.999800 0.999800
547
-
548
- Returns:
549
- pd.DataFrame: Combined DataFrame of concentrations and fluxes.
550
-
551
- """
552
- c, v = self.get_concs_and_fluxes()
553
- if c is None or v is None:
554
- return None
555
- return pd.concat((c, v), axis=1)
556
-
557
- def get_full_results(self) -> pd.DataFrame | None:
558
- """Get the combined full results of concentrations and fluxes.
559
-
560
- Examples:
561
- >>> Simulator(model).get_full_results()
562
- Time ATP NADPH v1 v2
563
- 0.000000 1.000000 1.000000 1.000000 1.000000
564
- 0.000100 0.999900 0.999900 0.999900 0.999900
565
- 0.000200 0.999800 0.999800 0.999800 0.999800
566
-
567
- """
568
- c, v = self.get_full_concs_and_fluxes()
569
- if c is None or v is None:
570
- return None
571
- return pd.concat((c, v), axis=1)
572
-
573
- def get_new_y0(self) -> dict[str, float] | None:
574
- """Get the new initial conditions after the simulation.
575
-
576
- Examples:
577
- >>> Simulator(model).get_new_y0()
578
- {"ATP": 1.0, "NADPH": 1.0}
579
-
580
- """
581
- if (res := self.get_concs()) is None:
582
- return None
583
- return dict(res.iloc[-1])
584
-
585
561
  def update_parameter(self, parameter: str, value: float) -> Self:
586
562
  """Updates the value of a specified parameter in the model.
587
563
 
@@ -637,3 +613,38 @@ class Simulator:
637
613
  """
638
614
  self.model.scale_parameters(parameters)
639
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