1+ """PEtab simulator base class and related functions."""
2+
13import abc
24import numpy as np
35import pathlib
1012
1113
1214class Simulator (abc .ABC ):
13- """
14- Base class that specific simulators should inherit.
15+ """Base class that specific simulators should inherit.
16+
1517 Specific simulators should minimally implement the
1618 `simulate_without_noise` method.
1719 Example (AMICI): https://bit.ly/33SUSG4
@@ -32,10 +34,14 @@ class Simulator(abc.ABC):
3234 directory and its contents may be modified and deleted, and
3335 should be considered ephemeral.
3436 """
35- def __init__ (self ,
36- petab_problem : petab .Problem ,
37- working_dir : Optional [Union [pathlib .Path , str ]] = None ):
38- """
37+
38+ def __init__ (
39+ self ,
40+ petab_problem : petab .Problem ,
41+ working_dir : Optional [Union [pathlib .Path , str ]] = None ,
42+ ):
43+ """Initialize the simulator.
44+
3945 Initialize the simulator with sufficient information to perform a
4046 simulation. If no working directory is specified, a temporary one is
4147 created.
@@ -64,14 +70,16 @@ def __init__(self,
6470 self .rng = np .random .default_rng ()
6571
6672 def remove_working_dir (self , force : bool = False , ** kwargs ) -> None :
67- """
68- Remove the simulator working directory and all files within (see the
69- `__init__` method arguments) .
73+ """Remove the simulator working directory, and all files within.
74+
75+ See the `__init__` method arguments.
7076
7177 Arguments:
7278 force:
7379 If True, the working directory is removed regardless of
7480 whether it is a temporary directory.
81+ **kwargs:
82+ Additional keyword arguments are passed to `shutil.rmtree`.
7583 """
7684 if force or self .temporary_working_dir :
7785 shutil .rmtree (self .working_dir , ** kwargs )
@@ -85,17 +93,18 @@ def remove_working_dir(self, force: bool = False, **kwargs) -> None:
8593
8694 @abc .abstractmethod
8795 def simulate_without_noise (self ) -> pd .DataFrame :
88- """
89- Simulate a PEtab problem. This is an abstract method that should be
90- implemented in a simulation package. Links to examples of this are in
91- the class docstring.
96+ """Simulate the PEtab problem.
97+
98+ This is an abstract method that should be implemented with a simulation
99+ package. Examples of this are referenced in the class docstring.
92100
93101 Returns:
94102 Simulated data, as a PEtab measurements table, which should be
95103 equivalent to replacing all values in the `petab.C.MEASUREMENT`
96104 column of the measurements table (of the PEtab problem supplied to
97105 the `__init__` method), with simulated values.
98106 """
107+ raise NotImplementedError
99108
100109 def simulate (
101110 self ,
@@ -109,6 +118,9 @@ def simulate(
109118 noise: If True, noise is added to simulated data.
110119 noise_scaling_factor:
111120 A multiplier of the scale of the noise distribution.
121+ **kwargs:
122+ Additional keyword arguments are passed to
123+ `simulate_without_noise`.
112124
113125 Returns:
114126 Simulated data, as a PEtab measurements table.
@@ -122,6 +134,7 @@ def add_noise(
122134 self ,
123135 simulation_df : pd .DataFrame ,
124136 noise_scaling_factor : float = 1 ,
137+ ** kwargs
125138 ) -> pd .DataFrame :
126139 """Add noise to simulated data.
127140
@@ -130,6 +143,8 @@ def add_noise(
130143 A PEtab measurements table that contains simulated data.
131144 noise_scaling_factor:
132145 A multiplier of the scale of the noise distribution.
146+ **kwargs:
147+ Additional keyword arguments are passed to `sample_noise`.
133148
134149 Returns:
135150 Simulated data with noise, as a PEtab measurements table.
@@ -143,6 +158,7 @@ def add_noise(
143158 self .noise_formulas ,
144159 self .rng ,
145160 noise_scaling_factor ,
161+ ** kwargs ,
146162 )
147163 for _ , row in simulation_df_with_noise .iterrows ()
148164 ]
@@ -156,6 +172,7 @@ def sample_noise(
156172 noise_formulas : Optional [Dict [str , sp .Expr ]] = None ,
157173 rng : Optional [np .random .Generator ] = None ,
158174 noise_scaling_factor : float = 1 ,
175+ zero_bounded : bool = False ,
159176) -> float :
160177 """Generate a sample from a PEtab noise distribution.
161178
@@ -176,6 +193,10 @@ def sample_noise(
176193 A NumPy random generator.
177194 noise_scaling_factor:
178195 A multiplier of the scale of the noise distribution.
196+ zero_bounded:
197+ Return zero if the sign of the return value and `simulated_value`
198+ differ. Can be used to ensure non-negative and non-positive values,
199+ if the sign of `simulated_value` should not change.
179200
180201 Returns:
181202 The sample from the PEtab noise distribution.
@@ -200,9 +221,20 @@ def sample_noise(
200221 .loc [measurement_row [petab .C .OBSERVABLE_ID ]]
201222 .get (petab .C .NOISE_DISTRIBUTION , petab .C .NORMAL )
202223 )
224+ # an empty noise distribution column in an observables table can result in
225+ # `noise_distribution == float('nan')`
226+ if pd .isna (noise_distribution ):
227+ noise_distribution = petab .C .NORMAL
203228
204229 # below is e.g.: `np.random.normal(loc=simulation, scale=noise_value)`
205- return getattr (rng , noise_distribution )(
230+ simulated_value_with_noise = getattr (rng , noise_distribution )(
206231 loc = simulated_value ,
207232 scale = noise_value * noise_scaling_factor
208233 )
234+
235+ if (
236+ zero_bounded and
237+ np .sign (simulated_value ) != np .sign (simulated_value_with_noise )
238+ ):
239+ return 0.0
240+ return simulated_value_with_noise
0 commit comments