Skip to content

Commit ab2e812

Browse files
authored
Merge branch 'main' into empty_columns_fine
2 parents ee39883 + 6a55afc commit ab2e812

21 files changed

+1342
-146
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ It will require Python>=3.10 to run. (We are following the
2727

2828
Development versions of the PEtab library can be installed using
2929

30-
pip3 install https://github.com/PEtab-dev/libpetab-python/archive/develop.zip
30+
pip3 install https://github.com/PEtab-dev/libpetab-python/archive/main.zip
3131

32-
(replace `develop` by the branch or commit you would like to install).
32+
(replace `main` by the branch or commit you would like to install).
3333

3434
When setting up a new parameter estimation problem, the most useful tools will
3535
be:

doc/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
"display_github": True,
115115
"github_user": "petab-dev",
116116
"github_repo": "libpetab-python",
117-
"github_version": "develop",
117+
"github_version": "main",
118118
"conf_py_path": "/doc",
119119
}
120120

petab/petablint.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77
import sys
88

9+
import pydantic
910
from colorama import Fore
1011
from colorama import init as init_colorama
1112
from jsonschema.exceptions import ValidationError as SchemaValidationError
@@ -160,10 +161,23 @@ def main():
160161
try:
161162
validate(args.yaml_file_name)
162163
except SchemaValidationError as e:
164+
path = ""
165+
if e.absolute_path:
166+
# construct a path to the error location inside the YAML file
167+
path = list(e.absolute_path)
168+
path = (
169+
f" at {path[0]}"
170+
+ "".join(f"[{str(p)}]" for p in path[1:])
171+
+ ": "
172+
)
163173
logger.error(
164-
f"Provided YAML file does not adhere to PEtab schema: {e}"
174+
"Provided YAML file does not adhere to the PEtab schema"
175+
f"{path}: {e.args[0]}"
165176
)
166177
sys.exit(1)
178+
except ValueError as e:
179+
logger.error(e)
180+
sys.exit(1)
167181

168182
if petab.is_composite_problem(args.yaml_file_name):
169183
# TODO: further checking:
@@ -179,12 +193,24 @@ def main():
179193
case 2:
180194
from petab.v2.lint import lint_problem
181195

182-
validation_issues = lint_problem(args.yaml_file_name)
183-
if validation_issues:
184-
validation_issues.log(logger=logger)
196+
try:
197+
validation_issues = lint_problem(args.yaml_file_name)
198+
if validation_issues:
199+
# Handle petab.v2.lint.ValidationTask issues
200+
validation_issues.log(logger=logger)
201+
sys.exit(1)
202+
logger.info("PEtab format check completed successfully.")
203+
sys.exit(0)
204+
except pydantic.ValidationError as e:
205+
# Handle Pydantic validation errors
206+
for err in e.errors():
207+
loc = ", ".join(str(loc) for loc in err["loc"])
208+
msg = err["msg"]
209+
# TODO: include model info here once available
210+
# https://github.com/pydantic/pydantic/issues/7224
211+
logger.error(f"Error in field(s) `{loc}`: {msg}")
185212
sys.exit(1)
186-
logger.info("PEtab format check completed successfully.")
187-
sys.exit(0)
213+
188214
case _:
189215
logger.error(
190216
"The provided PEtab files are of unsupported version "

petab/v1/calculate.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Functions performing various calculations."""
22

33
import numbers
4+
import operator
45
from functools import reduce
56

67
import numpy as np
@@ -139,19 +140,17 @@ def calculate_residuals_for_table(
139140
# apply scaling
140141
observable = observable_df.loc[row[OBSERVABLE_ID]]
141142
trafo = observable.get(OBSERVABLE_TRANSFORMATION, LIN)
142-
simulation = petab.scale(simulation, trafo)
143-
measurement = petab.scale(measurement, trafo)
143+
scaled_simulation = petab.scale(simulation, trafo)
144+
scaled_measurement = petab.scale(measurement, trafo)
144145

145146
# non-normalized residual is just the difference
146-
residual = simulation - measurement
147+
residual = scaled_measurement - scaled_simulation
147148

148-
noise_value = 1
149149
if normalize:
150-
# look up noise standard deviation
151-
noise_value = evaluate_noise_formula(
150+
# divide by standard deviation
151+
residual /= evaluate_noise_formula(
152152
row, noise_formulas, parameter_df, simulation
153153
)
154-
residual /= noise_value
155154

156155
# fill in value
157156
residual_df.loc[irow, RESIDUAL] = residual
@@ -169,13 +168,10 @@ def get_symbolic_noise_formulas(observable_df) -> dict[str, sp.Expr]:
169168
"""
170169
noise_formulas = {}
171170
# iterate over observables
172-
for row in observable_df.itertuples():
173-
observable_id = row.Index
174-
if NOISE_FORMULA not in observable_df.columns:
175-
noise_formula = None
176-
else:
177-
noise_formula = sympify_petab(row.noiseFormula)
178-
noise_formulas[observable_id] = noise_formula
171+
for observable_id, row in observable_df.iterrows():
172+
noise_formulas[observable_id] = (
173+
sympify_petab(row.noiseFormula) if NOISE_FORMULA in row else None
174+
)
179175
return noise_formulas
180176

181177

@@ -364,7 +360,7 @@ def calculate_llh_for_table(
364360
(simulation_df[col] == row[col]) | petab.is_empty(row[col])
365361
for col in compared_cols
366362
]
367-
mask = reduce(lambda x, y: x & y, masks)
363+
mask = reduce(operator.and_, masks)
368364

369365
simulation = simulation_df.loc[mask][SIMULATION].iloc[0]
370366

@@ -375,7 +371,7 @@ def calculate_llh_for_table(
375371

376372
# get noise standard deviation
377373
noise_value = evaluate_noise_formula(
378-
row, noise_formulas, parameter_df, petab.scale(simulation, scale)
374+
row, noise_formulas, parameter_df, simulation
379375
)
380376

381377
# get noise distribution

petab/v1/measurements.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ def assert_overrides_match_parameter_count(
289289
)
290290
}
291291
if NOISE_FORMULA in observable_df.columns
292-
else {obs_id: 0 for obs_id in observable_df.index.values}
292+
else dict.fromkeys(observable_df.index.values, 0)
293293
)
294294

295295
for _, row in measurement_df.iterrows():

petab/v1/sampling.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def sample_from_prior(
3333
bounds=tuple(bounds),
3434
transformation=scaling,
3535
)
36-
return prior.sample(shape=(n_starts,))
36+
return prior.sample(shape=(n_starts,), x_scaled=True)
3737

3838

3939
def sample_parameter_startpoints(

petab/v1/simulate.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,20 +241,39 @@ def sample_noise(
241241
simulated_value,
242242
)
243243

244-
# default noise distribution is petab.C.NORMAL
245-
noise_distribution = petab_problem.observable_df.loc[
244+
observable_row = petab_problem.observable_df.loc[
246245
measurement_row[petab.C.OBSERVABLE_ID]
247-
].get(petab.C.NOISE_DISTRIBUTION, petab.C.NORMAL)
246+
]
247+
# default noise distribution is petab.C.NORMAL
248+
noise_distribution = observable_row.get(
249+
petab.C.NOISE_DISTRIBUTION, petab.C.NORMAL
250+
)
248251
# an empty noise distribution column in an observables table can result in
249252
# `noise_distribution == float('nan')`
250253
if pd.isna(noise_distribution):
251254
noise_distribution = petab.C.NORMAL
252255

256+
observable_transformation = observable_row.get(
257+
petab.C.OBSERVABLE_TRANSFORMATION, petab.C.LIN
258+
)
259+
transform = lambda x: x # noqa: E731
260+
# observableTransformation=log -> the log of the simulated value is
261+
# distributed according to `noise_distribution`
262+
if observable_transformation == petab.C.LOG:
263+
simulated_value = np.log(simulated_value)
264+
transform = np.exp
265+
elif observable_transformation == petab.C.LOG10:
266+
simulated_value = np.log10(simulated_value)
267+
transform = lambda x: np.power(10, x) # noqa: E731
268+
253269
# below is e.g.: `np.random.normal(loc=simulation, scale=noise_value)`
254270
simulated_value_with_noise = getattr(rng, noise_distribution)(
255271
loc=simulated_value, scale=noise_value * noise_scaling_factor
256272
)
257273

274+
# apply observable transformation, ensure `float` type
275+
simulated_value_with_noise = float(transform(simulated_value_with_noise))
276+
258277
if zero_bounded and np.sign(simulated_value) != np.sign(
259278
simulated_value_with_noise
260279
):

petab/v1/visualize/plot_residuals.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ def plot_goodness_of_fit(
134134
petab_problem: Problem,
135135
simulations_df: str | Path | pd.DataFrame,
136136
size: tuple = (10, 7),
137+
color=None,
137138
ax: plt.Axes | None = None,
138139
) -> matplotlib.axes.Axes:
139140
"""
@@ -148,6 +149,9 @@ def plot_goodness_of_fit(
148149
output data file.
149150
size:
150151
Figure size.
152+
color:
153+
The marker colors, matches the `c` parameter of
154+
`matplotlib.pyplot.scatter`.
151155
ax:
152156
Axis object.
153157
@@ -171,8 +175,8 @@ def plot_goodness_of_fit(
171175
parameter_dfs=petab_problem.parameter_df,
172176
)[0]
173177
slope, intercept, r_value, p_value, std_err = stats.linregress(
174-
petab_problem.measurement_df["measurement"],
175178
simulations_df["simulation"],
179+
petab_problem.measurement_df["measurement"],
176180
) # x, y
177181

178182
if ax is None:
@@ -182,6 +186,7 @@ def plot_goodness_of_fit(
182186
ax.scatter(
183187
petab_problem.measurement_df["measurement"],
184188
simulations_df["simulation"],
189+
c=color,
185190
)
186191

187192
ax.axis("square")

petab/v1/visualize/plotting.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -861,9 +861,15 @@ def parse_from_id_list(
861861
"""
862862
if ids_per_plot is None:
863863
# this is the default case. If no grouping is specified,
864-
# all observables are plotted. One observable per plot.
865-
unique_obs_list = self._data_df[OBSERVABLE_ID].unique()
866-
ids_per_plot = [[obs_id] for obs_id in unique_obs_list]
864+
# each group_by category will be plotted on a separate plot
865+
unique_ids_list = self._data_df[
866+
{
867+
"dataset": DATASET_ID,
868+
"observable": OBSERVABLE_ID,
869+
"simulation": SIMULATION_CONDITION_ID,
870+
}[group_by]
871+
].unique()
872+
ids_per_plot = [[id_] for id_ in unique_ids_list]
867873

868874
if group_by == "dataset" and DATASET_ID not in self._data_df:
869875
raise ValueError(

petab/v1/yaml.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,14 @@ def validate_yaml_syntax(
7777
# but let's still use the latest PEtab schema for full validation
7878
version = yaml_config.get(FORMAT_VERSION, None)
7979
version = (
80-
parse_version(version)[:2]
81-
if version
82-
else list(SCHEMAS.values())[-1]
80+
parse_version(version)[:2] if version else list(SCHEMAS.keys())[-1]
8381
)
8482

8583
try:
8684
schema = SCHEMAS[version]
8785
except KeyError as e:
8886
raise ValueError(
89-
"Unknown PEtab version given in problem "
87+
"No or unknown PEtab version given in problem "
9088
f"specification: {version}"
9189
) from e
9290
schema = load_yaml(schema)

0 commit comments

Comments
 (0)