Skip to content

Commit 433031e

Browse files
committed
more tests
1 parent 7874190 commit 433031e

File tree

6 files changed

+165
-19
lines changed

6 files changed

+165
-19
lines changed

doc/modules.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ API Reference
1616
petab.v1.core
1717
petab.v1.distributions
1818
petab.v1.lint
19+
petab.v1.math
1920
petab.v1.measurements
2021
petab.v1.models
2122
petab.v1.observables

petab/v1/math/sympify.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
def sympify_petab(expr: str | int | float) -> sp.Expr | sp.Basic:
1616
"""Convert PEtab math expression to sympy expression.
1717
18+
.. note::
19+
20+
All symbols in the returned expression will have the `real=True`
21+
assumption.
22+
1823
Args:
1924
expr: PEtab math expression.
2025
@@ -26,6 +31,10 @@ def sympify_petab(expr: str | int | float) -> sp.Expr | sp.Basic:
2631
The sympy expression corresponding to `expr`.
2732
Boolean values are converted to numeric values.
2833
"""
34+
if isinstance(expr, sp.Expr):
35+
# TODO: check if only PEtab-compatible symbols and functions are used
36+
return expr
37+
2938
if isinstance(expr, int) or isinstance(expr, np.integer):
3039
return sp.Integer(expr)
3140
if isinstance(expr, float) or isinstance(expr, np.floating):

petab/v2/core.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ def _is_finite_or_neg_inf(v: float, info: ValidationInfo) -> float:
5656
return v
5757

5858

59+
def _is_finite_or_pos_inf(v: float, info: ValidationInfo) -> float:
60+
if not np.isfinite(v) and v != np.inf:
61+
raise ValueError(
62+
f"{info.field_name} value must be finite or inf but got {v}"
63+
)
64+
return v
65+
66+
5967
def _not_nan(v: float, info: ValidationInfo) -> float:
6068
if np.isnan(v):
6169
raise ValueError(f"{info.field_name} value must not be nan.")
@@ -201,18 +209,24 @@ def _sympify(cls, v):
201209
def _placeholders(
202210
self, type_: Literal["observable", "noise"]
203211
) -> set[sp.Symbol]:
204-
# TODO: add field validator to check for 1-based consecutive numbering
205-
t = f"{re.escape(type_)}Parameter"
206-
o = re.escape(self.id)
207-
pattern = re.compile(rf"(?:^|\W)({t}\d+_{o})(?=\W|$)")
208212
formula = (
209213
self.formula
210214
if type_ == "observable"
211215
else self.noise_formula
212216
if type_ == "noise"
213217
else None
214218
)
215-
return {s for s in formula.free_symbols if pattern.match(str(s))}
219+
if formula is None or formula.is_number:
220+
return set()
221+
222+
if not (free_syms := formula.free_symbols):
223+
return set()
224+
225+
# TODO: add field validator to check for 1-based consecutive numbering
226+
t = f"{re.escape(type_)}Parameter"
227+
o = re.escape(self.id)
228+
pattern = re.compile(rf"(?:^|\W)({t}\d+_{o})(?=\W|$)")
229+
return {s for s in free_syms if pattern.match(str(s))}
216230

217231
@property
218232
def observable_placeholders(self) -> set[sp.Symbol]:
@@ -600,7 +614,9 @@ class Measurement(BaseModel):
600614
#: The experiment ID.
601615
experiment_id: str | None = Field(alias=C.EXPERIMENT_ID, default=None)
602616
#: The time point of the measurement in time units as defined in the model.
603-
time: float = Field(alias=C.TIME)
617+
time: Annotated[float, AfterValidator(_is_finite_or_pos_inf)] = Field(
618+
alias=C.TIME
619+
)
604620
#: The measurement value.
605621
measurement: Annotated[float, AfterValidator(_not_nan)] = Field(
606622
alias=C.MEASUREMENT

petab/v2/problem.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,10 @@ def get_path(filename):
203203

204204
if yaml.is_composite_problem(yaml_config):
205205
raise ValueError(
206-
"petab.Problem.from_yaml() can only be used for "
206+
"petab.v2.Problem.from_yaml() can only be used for "
207207
"yaml files comprising a single model. "
208208
"Consider using "
209-
"petab.CompositeProblem.from_yaml() instead."
209+
"petab.v2.CompositeProblem.from_yaml() instead."
210210
)
211211
config = ProblemConfig(
212212
**yaml_config, base_path=base_path, filepath=yaml_file
@@ -350,13 +350,13 @@ def from_dfs(
350350
def from_combine(filename: Path | str) -> Problem:
351351
"""Read PEtab COMBINE archive (http://co.mbine.org/documents/archive).
352352
353-
See also :py:func:`petab.create_combine_archive`.
353+
See also :py:func:`petab.v2.create_combine_archive`.
354354
355355
Arguments:
356356
filename: Path to the PEtab-COMBINE archive
357357
358358
Returns:
359-
A :py:class:`petab.Problem` instance.
359+
A :py:class:`petab.v2.Problem` instance.
360360
"""
361361
# function-level import, because module-level import interfered with
362362
# other SWIG interfaces
@@ -882,7 +882,7 @@ def add_condition(
882882
id_: The condition id
883883
name: The condition name
884884
kwargs: Entities to be added to the condition table in the form
885-
`target_id=(value_type, target_value)`.
885+
`target_id=target_value`.
886886
"""
887887
if not kwargs:
888888
raise ValueError("Cannot add condition without any changes")
@@ -1132,19 +1132,25 @@ class ExtensionConfig(BaseModel):
11321132
class ProblemConfig(BaseModel):
11331133
"""The PEtab problem configuration."""
11341134

1135+
#: The path to the PEtab problem configuration.
11351136
filepath: str | AnyUrl | None = Field(
11361137
None,
11371138
description="The path to the PEtab problem configuration.",
11381139
exclude=True,
11391140
)
1141+
#: The base path to resolve relative paths.
11401142
base_path: str | AnyUrl | None = Field(
11411143
None,
11421144
description="The base path to resolve relative paths.",
11431145
exclude=True,
11441146
)
1147+
#: The PEtab format version.
11451148
format_version: str = "2.0.0"
1149+
#: The path to the parameter file, relative to ``base_path``.
11461150
parameter_file: str | AnyUrl | None = None
1151+
#: The list of problems in the configuration.
11471152
problems: list[SubProblem] = []
1153+
#: Extensiions used by the problem.
11481154
extensions: list[ExtensionConfig] = []
11491155

11501156
def to_yaml(self, filename: str | Path):

tests/v1/math/test_math.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ def test_ids():
7474
"""Test symbols in expressions."""
7575
assert sympify_petab("bla * 2") == 2.0 * sp.Symbol("bla", real=True)
7676

77+
# test that sympy expressions that are invalid in PEtab raise an error
78+
# TODO: handle these cases after
79+
# https://github.com/PEtab-dev/libpetab-python/pull/364
80+
# sympify_petab(sp.Symbol("föö"))
81+
7782

7883
def test_syntax_error():
7984
"""Test exceptions upon syntax errors."""

tests/v2/test_core.py

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import tempfile
22
from pathlib import Path
33

4+
import pytest
45
import sympy as sp
6+
from pydantic import ValidationError
7+
from sympy.abc import x, y
58

6-
from petab.v2.core import (
7-
Change,
8-
Condition,
9-
ConditionsTable,
10-
Experiment,
11-
ExperimentPeriod,
12-
ObservablesTable,
13-
)
9+
from petab.v2.core import *
1410
from petab.v2.petab1to2 import petab1to2
1511

1612
example_dir_fujita = Path(__file__).parents[2] / "doc/example/example_Fujita"
@@ -73,3 +69,116 @@ def test_conditions_table_add_changes():
7369
conditions_table += c2
7470

7571
assert conditions_table.conditions == [c1, c2]
72+
73+
74+
def test_measurments():
75+
Measurement(
76+
observable_id="obs1", time=1, experiment_id="exp1", measurement=1
77+
)
78+
Measurement(
79+
observable_id="obs1", time="1", experiment_id="exp1", measurement="1"
80+
)
81+
Measurement(
82+
observable_id="obs1", time="inf", experiment_id="exp1", measurement="1"
83+
)
84+
85+
Measurement(
86+
observable_id="obs1",
87+
time=1,
88+
experiment_id="exp1",
89+
measurement=1,
90+
observable_parameters=["p1"],
91+
noise_parameters=["n1"],
92+
)
93+
94+
Measurement(
95+
observable_id="obs1",
96+
time=1,
97+
experiment_id="exp1",
98+
measurement=1,
99+
observable_parameters=[1],
100+
noise_parameters=[2],
101+
)
102+
103+
Measurement(
104+
observable_id="obs1",
105+
time=1,
106+
experiment_id="exp1",
107+
measurement=1,
108+
observable_parameters=[sp.sympify("x ** y")],
109+
noise_parameters=[sp.sympify("x ** y")],
110+
)
111+
112+
assert (
113+
Measurement(
114+
observable_id="obs1",
115+
time=1,
116+
experiment_id="exp1",
117+
measurement=1,
118+
non_petab=1,
119+
).non_petab
120+
== 1
121+
)
122+
123+
with pytest.raises(ValidationError, match="got -inf"):
124+
Measurement(
125+
observable_id="obs1",
126+
time="-inf",
127+
experiment_id="exp1",
128+
measurement=1,
129+
)
130+
131+
with pytest.raises(ValidationError, match="Invalid ID"):
132+
Measurement(
133+
observable_id="1_obs", time=1, experiment_id="exp1", measurement=1
134+
)
135+
136+
with pytest.raises(ValidationError, match="Invalid ID"):
137+
Measurement(
138+
observable_id="obs", time=1, experiment_id=" exp1", measurement=1
139+
)
140+
141+
142+
def test_observable():
143+
Observable(id="obs1", formula=x + y)
144+
Observable(id="obs1", formula="x + y", noise_formula="x + y")
145+
Observable(id="obs1", formula=1, noise_formula=2)
146+
Observable(
147+
id="obs1",
148+
formula="x + y",
149+
noise_formula="x + y",
150+
observable_parameters=["p1"],
151+
noise_parameters=["n1"],
152+
)
153+
Observable(
154+
id="obs1",
155+
formula=sp.sympify("x + y"),
156+
noise_formula=sp.sympify("x + y"),
157+
observable_parameters=[sp.Symbol("p1")],
158+
noise_parameters=[sp.Symbol("n1")],
159+
)
160+
assert Observable(id="obs1", formula="x + y", non_petab=1).non_petab == 1
161+
162+
o = Observable(id="obs1", formula=x + y)
163+
assert o.observable_placeholders == set()
164+
assert o.noise_placeholders == set()
165+
166+
o = Observable(
167+
id="obs1",
168+
formula="observableParameter1_obs1",
169+
noise_formula="noiseParameter1_obs1",
170+
)
171+
assert o.observable_placeholders == {
172+
sp.Symbol("observableParameter1_obs1", real=True),
173+
}
174+
assert o.noise_placeholders == {
175+
sp.Symbol("noiseParameter1_obs1", real=True)
176+
}
177+
178+
# TODO: this should raise an error
179+
# (numbering is not consecutive / not starting from 1)
180+
# TODO: clarify if observableParameter0_obs1 would be allowed
181+
# as regular parameter
182+
#
183+
# with pytest.raises(ValidationError):
184+
# Observable(id="obs1", formula="observableParameter2_obs1")

0 commit comments

Comments
 (0)