Skip to content

Commit d842f66

Browse files
testing of sensitivity analysis
1 parent 113ec1c commit d842f66

File tree

6 files changed

+418
-15
lines changed

6 files changed

+418
-15
lines changed

release-notes/0.4.0.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ following changes:
77
## Features & fixes
88
* Fixed logging issue (#184)
99
* Support for Morris global sensitivity analysis (#179)
10+
* Testing of sensitivity analysis (#185)
1011

1112
Your pymetadata team

src/sbmlsim/sensitivity/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
AnalysisGroup,
2222
)
2323
from .parameters import (
24+
ParameterType,
2425
SensitivityParameter,
2526
)
2627
from .sensitivity_fast import FASTSensitivityAnalysis
@@ -30,6 +31,7 @@
3031
from .sensitivity_morris import MorrisSensitivityAnalysis
3132

3233
__all__ = [
34+
"ParameterType",
3335
"SensitivityParameter",
3436
"SensitivityAnalysis",
3537
"SensitivitySimulation",

src/sbmlsim/sensitivity/parameters.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Tools and helpers to handle parameters for sensitivity analysis."""
2+
23
from __future__ import annotations
34

45
from enum import Enum
@@ -16,6 +17,7 @@
1617

1718
class ParameterType(str, Enum):
1819
"""Types of model parameters."""
20+
1921
DATA = "data"
2022
SCALING = "scaling"
2123
NA = "na"
@@ -24,6 +26,7 @@ class ParameterType(str, Enum):
2426

2527
class SensitivityParameter(BaseModel):
2628
"""Parameter for SensitivityAnalysis."""
29+
2730
model_config = ConfigDict(use_enum_values=True)
2831

2932
uid: str
@@ -39,13 +42,14 @@ def __hash__(self):
3942
return hash(self.uid)
4043

4144
@staticmethod
42-
def parameters_set_bounds(parameters: Iterable[SensitivityParameter],
43-
bounds: Iterable[tuple]) -> None:
45+
def parameters_set_bounds(
46+
parameters: Iterable[SensitivityParameter], bounds: Iterable[tuple]
47+
) -> None:
4448
"""Set bounds for sensitivity analysis."""
4549

4650
parameters_d = {p.uid: p for p in parameters}
4751

48-
for (key, lb, ub, ptype) in bounds:
52+
for key, lb, ub, ptype in bounds:
4953
if key not in parameters_d:
5054
console.print(f"unused bounds definition: {key} = [{lb}, {ub}]")
5155
else:
@@ -55,20 +59,22 @@ def parameters_set_bounds(parameters: Iterable[SensitivityParameter],
5559
p.type = ptype
5660

5761
@staticmethod
58-
def parameters_to_df(parameters: Iterable[SensitivityParameter],
59-
sort: bool = True) -> pd.DataFrame:
62+
def parameters_to_df(
63+
parameters: Iterable[SensitivityParameter], sort: bool = True
64+
) -> pd.DataFrame:
6065
"""Create parameter table from parameters."""
6166
items = []
6267
for item in parameters:
6368
d_item = item.model_dump()
6469
# better printing of type
65-
d_item["type"] = d_item["type"].value
70+
# d_item["type"] = d_item["type"].value
6671
items.append(d_item)
6772

6873
df = pd.DataFrame(items)
6974
if sort:
70-
df.sort_values(by=["type", "uid"], ascending=True, inplace=True,
71-
ignore_index=True)
75+
df.sort_values(
76+
by=["type", "uid"], ascending=True, inplace=True, ignore_index=True
77+
)
7278
return df
7379

7480
@classmethod
@@ -79,12 +85,10 @@ def parameter_to_latex(
7985
) -> None:
8086
"""Latex parameter table."""
8187
df = cls.parameters_to_df(parameters)
82-
tex_str = df.to_latex(
83-
None, index=False, float_format="{:.3g}".format
84-
)
88+
tex_str = df.to_latex(None, index=False, float_format="{:.3g}".format)
8589
tex_str = tex_str.replace("_", r"\_")
8690

87-
with open(tex_path, 'w') as f:
91+
with open(tex_path, "w") as f:
8892
f.write(tex_str)
8993

9094
@staticmethod
@@ -122,7 +126,8 @@ def parameter_from_sbase(sbase: libsbml.SBase) -> SensitivityParameter:
122126
# handle the species concentration
123127
ruid = uid
124128
if (sbase.getTypeCode() == libsbml.SpeciesType) and (
125-
sbase.getHasOnlySubstanceUnits()):
129+
sbase.getHasOnlySubstanceUnits()
130+
):
126131
ruid = f"[{uid}]"
127132

128133
value = r.getValue(ruid)
@@ -171,13 +176,15 @@ def parameter_from_sbase(sbase: libsbml.SBase) -> SensitivityParameter:
171176
elif s.isSetInitialAmount() and np.isnan(s.getInitialAmount()):
172177
exclude_ids.add(sid)
173178
elif s.isSetInitialConcentration() and np.isnan(
174-
s.getInitialConcentration()):
179+
s.getInitialConcentration()
180+
):
175181
exclude_ids.add(sid)
176182
if exclude_zero:
177183
if s.isSetInitialAmount() and np.isclose(s.getInitialAmount(), 0.0):
178184
exclude_ids.add(sid)
179185
elif s.isSetInitialConcentration() and np.isclose(
180-
s.getInitialConcentration(), 0.0):
186+
s.getInitialConcentration(), 0.0
187+
):
181188
exclude_ids.add(sid)
182189

183190
if s.getConstant() is True or s.getBoundaryCondition() is True:

tests/sensitivity/test_analysis.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from dataclasses import asdict
2+
3+
from sbmlsim.sensitivity import SensitivityOutput, AnalysisGroup
4+
5+
6+
# -----------------------------------------------------------------------------
7+
# SensitivityOutput
8+
# -----------------------------------------------------------------------------
9+
10+
11+
def test_sensitivity_output_creation() -> None:
12+
out = SensitivityOutput(
13+
uid="auc_plasma",
14+
name="AUC plasma",
15+
unit="mg*h/L",
16+
)
17+
18+
assert out.uid == "auc_plasma"
19+
assert out.name == "AUC plasma"
20+
assert out.unit == "mg*h/L"
21+
22+
23+
def test_sensitivity_output_unit_optional() -> None:
24+
out = SensitivityOutput(
25+
uid="cmax",
26+
name="Cmax",
27+
unit=None,
28+
)
29+
30+
assert out.unit is None
31+
32+
33+
def test_sensitivity_output_equality() -> None:
34+
o1 = SensitivityOutput("auc", "AUC", "mg*h/L")
35+
o2 = SensitivityOutput("auc", "AUC", "mg*h/L")
36+
37+
assert o1 == o2
38+
39+
40+
def test_sensitivity_output_asdict() -> None:
41+
out = SensitivityOutput("auc", "AUC", "mg*h/L")
42+
43+
d = asdict(out)
44+
45+
assert d == {
46+
"uid": "auc",
47+
"name": "AUC",
48+
"unit": "mg*h/L",
49+
}
50+
51+
52+
# -----------------------------------------------------------------------------
53+
# AnalysisGroup
54+
# -----------------------------------------------------------------------------
55+
56+
57+
def test_analysis_group_creation() -> None:
58+
group = AnalysisGroup(
59+
uid="renal_impairment",
60+
name="Renal impairment",
61+
changes={"GFR": 0.5, "CLr": 0.6},
62+
color="blue",
63+
)
64+
65+
assert group.uid == "renal_impairment"
66+
assert group.name == "Renal impairment"
67+
assert group.changes == {"GFR": 0.5, "CLr": 0.6}
68+
assert group.color == "blue"
69+
70+
71+
def test_analysis_group_color_optional() -> None:
72+
group = AnalysisGroup(
73+
uid="baseline",
74+
name="Baseline",
75+
changes={},
76+
color=None,
77+
)
78+
79+
assert group.color is None
80+
81+
82+
def test_analysis_group_changes_mutable() -> None:
83+
group = AnalysisGroup(
84+
uid="test",
85+
name="Test",
86+
changes={"k1": 1.0},
87+
color=None,
88+
)
89+
90+
group.changes["k2"] = 2.0
91+
92+
assert group.changes == {"k1": 1.0, "k2": 2.0}
93+
94+
95+
def test_analysis_group_equality() -> None:
96+
g1 = AnalysisGroup(
97+
uid="hepatic",
98+
name="Hepatic impairment",
99+
changes={"CL": 0.7},
100+
color="red",
101+
)
102+
g2 = AnalysisGroup(
103+
uid="hepatic",
104+
name="Hepatic impairment",
105+
changes={"CL": 0.7},
106+
color="red",
107+
)
108+
109+
assert g1 == g2
110+
111+
112+
def test_analysis_group_asdict() -> None:
113+
group = AnalysisGroup(
114+
uid="dose_up",
115+
name="Dose increase",
116+
changes={"Dose": 2.0},
117+
color="green",
118+
)
119+
120+
d = asdict(group)
121+
122+
assert d == {
123+
"uid": "dose_up",
124+
"name": "Dose increase",
125+
"changes": {"Dose": 2.0},
126+
"color": "green",
127+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import math
2+
3+
import pandas as pd
4+
import pytest
5+
6+
from sbmlsim.sensitivity import ParameterType, SensitivityParameter
7+
8+
9+
# -----------------------------------------------------------------------------
10+
# Fixtures
11+
# -----------------------------------------------------------------------------
12+
13+
14+
@pytest.fixture
15+
def simple_parameters():
16+
return [
17+
SensitivityParameter(
18+
uid="k1",
19+
name="rate constant 1",
20+
value=1.0,
21+
type=ParameterType.NA,
22+
),
23+
SensitivityParameter(
24+
uid="k2",
25+
name="rate constant 2",
26+
value=2.0,
27+
type=ParameterType.NA,
28+
),
29+
]
30+
31+
32+
# -----------------------------------------------------------------------------
33+
# Basic model behavior
34+
# -----------------------------------------------------------------------------
35+
36+
37+
def test_parameter_type_enum_values():
38+
assert ParameterType.DATA.value == "data"
39+
assert ParameterType.SCALING.value == "scaling"
40+
assert ParameterType.NA.value == "na"
41+
assert ParameterType.FIT.value == "fitted"
42+
43+
44+
def test_sensitivity_parameter_defaults():
45+
p = SensitivityParameter(uid="p1", name="param")
46+
47+
assert p.uid == "p1"
48+
assert p.name == "param"
49+
assert math.isnan(p.value)
50+
assert math.isnan(p.lower_bound)
51+
assert math.isnan(p.upper_bound)
52+
assert p.unit is None
53+
assert p.type == ParameterType.NA
54+
assert p.reference == ""
55+
56+
57+
def test_hash_is_based_on_uid():
58+
p1 = SensitivityParameter(uid="x", name="a")
59+
p2 = SensitivityParameter(uid="x", name="b")
60+
p3 = SensitivityParameter(uid="y", name="a")
61+
62+
assert hash(p1) == hash(p2)
63+
assert hash(p1) != hash(p3)
64+
65+
66+
# -----------------------------------------------------------------------------
67+
# Bounds handling
68+
# -----------------------------------------------------------------------------
69+
70+
71+
def test_parameters_set_bounds(simple_parameters):
72+
bounds = [
73+
("k1", 0.1, 10.0, ParameterType.FIT),
74+
("k2", 1.0, 5.0, ParameterType.SCALING),
75+
]
76+
77+
SensitivityParameter.parameters_set_bounds(simple_parameters, bounds)
78+
79+
p1, p2 = simple_parameters
80+
81+
assert p1.lower_bound == 0.1
82+
assert p1.upper_bound == 10.0
83+
assert p1.type == ParameterType.FIT
84+
85+
assert p2.lower_bound == 1.0
86+
assert p2.upper_bound == 5.0
87+
assert p2.type == ParameterType.SCALING
88+
89+
90+
def test_parameters_to_df(simple_parameters):
91+
df = SensitivityParameter.parameters_to_df(simple_parameters, sort=False)
92+
93+
assert isinstance(df, pd.DataFrame)
94+
assert set(df.columns) == {
95+
"uid",
96+
"name",
97+
"value",
98+
"lower_bound",
99+
"upper_bound",
100+
"unit",
101+
"type",
102+
"reference",
103+
}
104+
105+
assert len(df) == 2
106+
assert df.loc[0, "type"] == "na"
107+
108+
109+
def test_parameter_to_latex_creates_file(tmp_path):
110+
params = [
111+
SensitivityParameter(uid="k_cat", name="k_cat", value=1.23456),
112+
]
113+
114+
tex_path = tmp_path / "params.tex"
115+
SensitivityParameter.parameter_to_latex(tex_path, params)
116+
117+
assert tex_path.exists()
118+
119+
content = tex_path.read_text()
120+
assert "\\begin{tabular}" in content
121+
assert "k\\_cat" in content # underscore escaped
122+
assert "1.23" in content # formatted float

0 commit comments

Comments
 (0)