|
1 | 1 | import logging |
| 2 | +from pathlib import Path |
2 | 3 |
|
3 | 4 | import pandas as pd |
4 | 5 | import pytest |
5 | 6 |
|
6 | 7 | from petab import v1, v2 |
| 8 | +from petab.v1.models.sbml_model import SbmlModel |
7 | 9 | from petab.v2 import Problem |
8 | 10 | from petab.v2.petab1to2 import petab1to2, v1v2_observable_df |
9 | 11 |
|
@@ -148,6 +150,201 @@ def test_petab1to2_remote(): |
148 | 150 | assert len(problem.measurements) |
149 | 151 |
|
150 | 152 |
|
| 153 | +def _petab1to2_with_conditions( |
| 154 | + tmp_path, condition_df: pd.DataFrame, conditions: list[tuple[str, str]] |
| 155 | +) -> Problem: |
| 156 | + """Convert a minimal PEtab v1 problem with the given condition table. |
| 157 | +
|
| 158 | + ``conditions`` are (preequilibrationConditionId, simulationConditionId) |
| 159 | + pairs, one measurement per pair. |
| 160 | + """ |
| 161 | + problem = v1.Problem( |
| 162 | + model=SbmlModel.from_antimony( |
| 163 | + "A = 1; B = 0; k1 = 0.5; A -> B; k1 * A" |
| 164 | + ), |
| 165 | + condition_df=v1.get_condition_df(condition_df), |
| 166 | + observable_df=v1.get_observable_df( |
| 167 | + pd.DataFrame( |
| 168 | + { |
| 169 | + v1.C.OBSERVABLE_ID: ["obs_B"], |
| 170 | + v1.C.OBSERVABLE_FORMULA: ["B"], |
| 171 | + v1.C.NOISE_FORMULA: [0.1], |
| 172 | + } |
| 173 | + ) |
| 174 | + ), |
| 175 | + measurement_df=v1.get_measurement_df( |
| 176 | + pd.DataFrame( |
| 177 | + { |
| 178 | + v1.C.OBSERVABLE_ID: "obs_B", |
| 179 | + v1.C.PREEQUILIBRATION_CONDITION_ID: [ |
| 180 | + preeq for preeq, _ in conditions |
| 181 | + ], |
| 182 | + v1.C.SIMULATION_CONDITION_ID: [ |
| 183 | + sim for _, sim in conditions |
| 184 | + ], |
| 185 | + v1.C.TIME: 1.0, |
| 186 | + v1.C.MEASUREMENT: 0.5, |
| 187 | + } |
| 188 | + ) |
| 189 | + ), |
| 190 | + parameter_df=v1.get_parameter_df( |
| 191 | + pd.DataFrame( |
| 192 | + { |
| 193 | + v1.C.PARAMETER_ID: ["k1"], |
| 194 | + v1.C.PARAMETER_SCALE: [v1.C.LIN], |
| 195 | + v1.C.LOWER_BOUND: [1e-3], |
| 196 | + v1.C.UPPER_BOUND: [1e3], |
| 197 | + v1.C.NOMINAL_VALUE: [0.5], |
| 198 | + v1.C.ESTIMATE: [1], |
| 199 | + } |
| 200 | + ) |
| 201 | + ), |
| 202 | + ) |
| 203 | + return petab1to2(problem.to_files_generic(tmp_path)) |
| 204 | + |
| 205 | + |
| 206 | +def _measured_periods( |
| 207 | + problem: Problem, |
| 208 | +) -> list[list[tuple[float, list[str]]] | None]: |
| 209 | + """The (time, conditionIds) of the experiment periods per measurement.""" |
| 210 | + return [ |
| 211 | + None |
| 212 | + if m.experiment_id is None |
| 213 | + else [ |
| 214 | + (p.time, p.condition_ids) for p in problem[m.experiment_id].periods |
| 215 | + ] |
| 216 | + for m in problem.measurements |
| 217 | + ] |
| 218 | + |
| 219 | + |
| 220 | +def test_petab1to2_empty_conditions_preequilibration(tmp_path): |
| 221 | + """Pre-equilibration is kept if the condition table sets nothing.""" |
| 222 | + problem = _petab1to2_with_conditions( |
| 223 | + tmp_path, |
| 224 | + pd.DataFrame({v1.C.CONDITION_ID: ["c0", "steady"]}), |
| 225 | + [("steady", "c0")], |
| 226 | + ) |
| 227 | + |
| 228 | + assert problem.conditions == [] |
| 229 | + assert _measured_periods(problem) == [ |
| 230 | + [(v2.C.TIME_PREEQUILIBRATION, []), (0, [])] |
| 231 | + ] |
| 232 | + assert problem.experiments[0].has_preequilibration |
| 233 | + |
| 234 | + |
| 235 | +def test_petab1to2_mixed_empty_conditions(tmp_path): |
| 236 | + """Conditions that set nothing are handled condition-wise.""" |
| 237 | + problem = _petab1to2_with_conditions( |
| 238 | + tmp_path, |
| 239 | + pd.DataFrame( |
| 240 | + { |
| 241 | + v1.C.CONDITION_ID: ["c0", "c1", "preeq_set", "preeq_empty"], |
| 242 | + "A": [1.0, None, 2.0, None], |
| 243 | + } |
| 244 | + ), |
| 245 | + [ |
| 246 | + ("preeq_set", "c0"), |
| 247 | + ("preeq_set", "c1"), |
| 248 | + ("preeq_empty", "c0"), |
| 249 | + ("preeq_empty", "c1"), |
| 250 | + ("", "c0"), |
| 251 | + ("", "c1"), |
| 252 | + ], |
| 253 | + ) |
| 254 | + |
| 255 | + assert {c.id for c in problem.conditions} == {"c0", "preeq_set"} |
| 256 | + preeq = v2.C.TIME_PREEQUILIBRATION |
| 257 | + assert _measured_periods(problem) == [ |
| 258 | + [(preeq, ["preeq_set"]), (0, ["c0"])], |
| 259 | + [(preeq, ["preeq_set"]), (0, [])], |
| 260 | + [(preeq, []), (0, ["c0"])], |
| 261 | + [(preeq, []), (0, [])], |
| 262 | + [(0, ["c0"])], |
| 263 | + # no pre-equilibration, no changes -> no experiment |
| 264 | + None, |
| 265 | + ] |
| 266 | + |
| 267 | + |
| 268 | +def test_petab1to2_empty_conditions_no_preequilibration(tmp_path): |
| 269 | + """Without pre-equilibration, conditions that set nothing don't need an |
| 270 | + experiment.""" |
| 271 | + problem = _petab1to2_with_conditions( |
| 272 | + tmp_path, |
| 273 | + pd.DataFrame({v1.C.CONDITION_ID: ["c0"]}), |
| 274 | + [("", "c0")], |
| 275 | + ) |
| 276 | + |
| 277 | + assert problem.experiments == [] |
| 278 | + assert _measured_periods(problem) == [None] |
| 279 | + |
| 280 | + |
| 281 | +def test_petab1to2_validate(tmp_path): |
| 282 | + """Test that `validate=False` allows converting problems that fail |
| 283 | + linting for reasons unrelated to the conversion.""" |
| 284 | + v1_problem = v1.Problem( |
| 285 | + model=SbmlModel.from_antimony( |
| 286 | + "A -> B; k1 * A; A = 1; B = 0; k1 = 0.5" |
| 287 | + ), |
| 288 | + condition_df=v1.get_condition_df( |
| 289 | + pd.DataFrame({v1.C.CONDITION_ID: ["c0"], "A": [1.0]}) |
| 290 | + ), |
| 291 | + observable_df=v1.get_observable_df( |
| 292 | + pd.DataFrame( |
| 293 | + { |
| 294 | + v1.C.OBSERVABLE_ID: ["obs_B"], |
| 295 | + v1.C.OBSERVABLE_FORMULA: ["B"], |
| 296 | + v1.C.NOISE_FORMULA: [0.1], |
| 297 | + } |
| 298 | + ) |
| 299 | + ), |
| 300 | + measurement_df=v1.get_measurement_df( |
| 301 | + pd.DataFrame( |
| 302 | + { |
| 303 | + v1.C.OBSERVABLE_ID: ["obs_B", "obs_B"], |
| 304 | + v1.C.SIMULATION_CONDITION_ID: ["c0", "c0"], |
| 305 | + v1.C.TIME: [1.0, 2.0], |
| 306 | + v1.C.MEASUREMENT: [0.3, 0.5], |
| 307 | + v1.C.DATASET_ID: ["d1", "d1"], |
| 308 | + } |
| 309 | + ) |
| 310 | + ), |
| 311 | + parameter_df=v1.get_parameter_df( |
| 312 | + pd.DataFrame( |
| 313 | + { |
| 314 | + v1.C.PARAMETER_ID: ["k1"], |
| 315 | + v1.C.PARAMETER_SCALE: [v1.C.LIN], |
| 316 | + v1.C.LOWER_BOUND: [1e-3], |
| 317 | + v1.C.UPPER_BOUND: [1e3], |
| 318 | + v1.C.NOMINAL_VALUE: [0.5], |
| 319 | + v1.C.ESTIMATE: [1], |
| 320 | + } |
| 321 | + ) |
| 322 | + ), |
| 323 | + # refers to a dataset that is not in the measurement table |
| 324 | + visualization_df=pd.DataFrame( |
| 325 | + { |
| 326 | + v1.C.PLOT_ID: ["p1"], |
| 327 | + v1.C.DATASET_ID: ["no_such_dataset"], |
| 328 | + v1.C.X_VALUES: [v1.C.TIME], |
| 329 | + } |
| 330 | + ), |
| 331 | + ) |
| 332 | + yaml_file = v1_problem.to_files_generic(tmp_path / "v1") |
| 333 | + |
| 334 | + with pytest.raises(ValueError, match="does not pass linting"): |
| 335 | + petab1to2(yaml_file) |
| 336 | + |
| 337 | + problem = petab1to2(yaml_file, validate=False) |
| 338 | + assert isinstance(problem, Problem) |
| 339 | + assert len(problem.measurements) == 2 |
| 340 | + |
| 341 | + output_dir = tmp_path / "v2" |
| 342 | + output_dir.mkdir() |
| 343 | + petab1to2(yaml_file, output_dir, validate=False) |
| 344 | + problem = Problem.from_yaml(output_dir / Path(yaml_file).name) |
| 345 | + assert len(problem.measurements) == 2 |
| 346 | + |
| 347 | + |
151 | 348 | try: |
152 | 349 | import benchmark_models_petab |
153 | 350 |
|
|
0 commit comments