Skip to content

Commit b9a3f1a

Browse files
authored
PEtab: fill in fixed parameter values for initial values (#2613)
During PEtab parameter mapping, fill in fixed parameter values for initial values if requested. So far, this was only done for other parameters. Update documentation for amici/petab/parameter_mapping.py Fixes #2610.
1 parent 23e8045 commit b9a3f1a

File tree

2 files changed

+106
-40
lines changed

2 files changed

+106
-40
lines changed

python/sdist/amici/petab/parameter_mapping.py

Lines changed: 96 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import re
2222
from collections.abc import Sequence
2323
from itertools import chain
24-
from typing import Any, Union
24+
from typing import Any
2525
from collections.abc import Collection, Iterator
2626

2727
import amici
@@ -36,6 +36,8 @@
3636
PARAMETER_SCALE,
3737
PREEQUILIBRATION_CONDITION_ID,
3838
SIMULATION_CONDITION_ID,
39+
NOMINAL_VALUE,
40+
ESTIMATE,
3941
)
4042
from petab.v1.models import MODEL_TYPE_PYSB, MODEL_TYPE_SBML
4143
from sympy.abc import _clash
@@ -52,17 +54,17 @@
5254

5355
logger = logging.getLogger(__name__)
5456

55-
SingleParameterMapping = dict[str, Union[numbers.Number, str]]
57+
SingleParameterMapping = dict[str, numbers.Number | str]
5658
SingleScaleMapping = dict[str, str]
5759

5860

5961
class ParameterMappingForCondition:
6062
"""Parameter mapping for condition.
6163
6264
Contains mappings for free parameters, fixed parameters, and fixed
63-
preequilibration parameters, both for parameters and scales.
65+
pre-equilibration parameters, both for parameters and scales.
6466
65-
In the scale mappings, for each simulation parameter the scale
67+
In the scale mappings, for each simulation parameter, the scale
6668
on which the value is passed (and potentially gradients are to be
6769
returned) is given. In the parameter mappings, for each simulation
6870
parameter a corresponding optimization parameter (or a numeric value)
@@ -76,9 +78,9 @@ class ParameterMappingForCondition:
7678
:param scale_map_sim_var:
7779
Scales for free simulation parameters.
7880
:param map_preeq_fix:
79-
Mapping for fixed preequilibration parameters.
81+
Mapping for fixed pre-equilibration parameters.
8082
:param scale_map_preeq_fix:
81-
Scales for fixed preequilibration parameters.
83+
Scales for fixed pre-equilibration parameters.
8284
:param map_sim_fix:
8385
Mapping for fixed simulation parameters.
8486
:param scale_map_sim_fix:
@@ -177,7 +179,7 @@ def __len__(self):
177179
def append(
178180
self, parameter_mapping_for_condition: ParameterMappingForCondition
179181
):
180-
"""Append a condition specific parameter mapping."""
182+
"""Append a condition-specific parameter mapping."""
181183
self.parameter_mappings.append(parameter_mapping_for_condition)
182184

183185
def __repr__(self):
@@ -307,9 +309,10 @@ def unscale_parameters_dict(
307309

308310
def create_parameter_mapping(
309311
petab_problem: petab.Problem,
310-
simulation_conditions: pd.DataFrame | list[dict],
312+
simulation_conditions: pd.DataFrame | list[dict] | None,
311313
scaled_parameters: bool,
312314
amici_model: AmiciModel | None = None,
315+
fill_fixed_parameters: bool = True,
313316
**parameter_mapping_kwargs,
314317
) -> ParameterMapping:
315318
"""Generate AMICI specific parameter mapping.
@@ -325,11 +328,14 @@ def create_parameter_mapping(
325328
are assumed to be in linear scale.
326329
:param amici_model:
327330
AMICI model.
331+
:param fill_fixed_parameters:
332+
Whether to fill in nominal values for fixed parameters
333+
(estimate=0 in the parameters table).
334+
To allow changing fixed PEtab problem parameters,
335+
use ``fill_fixed_parameters=False``.
328336
:param parameter_mapping_kwargs:
329337
Optional keyword arguments passed to
330338
:func:`petab.get_optimization_to_simulation_parameter_mapping`.
331-
To allow changing fixed PEtab problem parameters (``estimate=0``),
332-
use ``fill_fixed_parameters=False``.
333339
:return:
334340
List of the parameter mappings.
335341
"""
@@ -377,6 +383,7 @@ def create_parameter_mapping(
377383
mapping_df=petab_problem.mapping_df,
378384
model=petab_problem.model,
379385
simulation_conditions=simulation_conditions,
386+
fill_fixed_parameters=fill_fixed_parameters,
380387
**dict(
381388
default_parameter_mapping_kwargs, **parameter_mapping_kwargs
382389
),
@@ -388,7 +395,11 @@ def create_parameter_mapping(
388395
simulation_conditions.iterrows(), prelim_parameter_mapping, strict=True
389396
):
390397
mapping_for_condition = create_parameter_mapping_for_condition(
391-
prelim_mapping_for_condition, condition, petab_problem, amici_model
398+
prelim_mapping_for_condition,
399+
condition,
400+
petab_problem,
401+
amici_model,
402+
fill_fixed_parameters=fill_fixed_parameters,
392403
)
393404
parameter_mapping.append(mapping_for_condition)
394405

@@ -400,8 +411,9 @@ def create_parameter_mapping_for_condition(
400411
condition: pd.Series | dict,
401412
petab_problem: petab.Problem,
402413
amici_model: AmiciModel | None = None,
414+
fill_fixed_parameters: bool = True,
403415
) -> ParameterMappingForCondition:
404-
"""Generate AMICI specific parameter mapping for condition.
416+
"""Generate AMICI-specific parameter mapping for a PEtab simulation.
405417
406418
:param parameter_mapping_for_condition:
407419
Preliminary parameter mapping for condition.
@@ -412,10 +424,12 @@ def create_parameter_mapping_for_condition(
412424
Underlying PEtab problem.
413425
:param amici_model:
414426
AMICI model.
415-
427+
:param fill_fixed_parameters:
428+
Whether to fill in nominal values for fixed parameters
429+
(estimate=0 in the parameters table).
416430
:return:
417431
The parameter and parameter scale mappings, for fixed
418-
preequilibration, fixed simulation, and variable simulation
432+
pre-equilibration, fixed simulation, and variable simulation
419433
parameters, and then the respective scalings.
420434
"""
421435
(
@@ -436,10 +450,10 @@ def create_parameter_mapping_for_condition(
436450
if len(condition_map_preeq) and len(condition_map_preeq) != len(
437451
condition_map_sim
438452
):
439-
logger.debug(f"Preequilibration parameter map: {condition_map_preeq}")
453+
logger.debug(f"Pre-equilibration parameter map: {condition_map_preeq}")
440454
logger.debug(f"Simulation parameter map: {condition_map_sim}")
441455
raise AssertionError(
442-
"Number of parameters for preequilbration "
456+
"Number of parameters for pre-equilbration "
443457
"and simulation do not match."
444458
)
445459

@@ -451,8 +465,8 @@ def create_parameter_mapping_for_condition(
451465
# During model generation, parameters for initial concentrations and
452466
# respective initial assignments have been created for the
453467
# relevant species, here we add these parameters to the parameter mapping.
454-
# In absence of preequilibration this could also be handled via
455-
# ExpData.x0, but in the case of preequilibration this would not allow for
468+
# In the absence of pre-equilibration this could also be handled via
469+
# ExpData.x0, but in the case of pre-equilibration this would not allow for
456470
# resetting initial states.
457471

458472
if states_in_condition_table := get_states_in_condition_table(
@@ -485,10 +499,11 @@ def create_parameter_mapping_for_condition(
485499
condition_map_preeq,
486500
condition_scale_map_preeq,
487501
preeq_value,
502+
fill_fixed_parameters=fill_fixed_parameters,
488503
)
489-
# need to set dummy value for preeq parameter anyways, as it
504+
# need to set a dummy value for preeq parameter anyways, as it
490505
# is expected below (set to 0, not nan, because will be
491-
# multiplied with indicator variable in initial assignment)
506+
# multiplied with the indicator variable in initial assignment)
492507
condition_map_sim[init_par_id] = 0.0
493508
condition_scale_map_sim[init_par_id] = LIN
494509

@@ -503,6 +518,7 @@ def create_parameter_mapping_for_condition(
503518
condition_map_sim,
504519
condition_scale_map_sim,
505520
value,
521+
fill_fixed_parameters=fill_fixed_parameters,
506522
)
507523
# set dummy value as above
508524
if condition_map_preeq:
@@ -549,11 +565,11 @@ def create_parameter_mapping_for_condition(
549565
condition_scale_map_sim_fix = {}
550566

551567
logger.debug(
552-
"Fixed parameters preequilibration: " f"{condition_map_preeq_fix}"
568+
"Fixed parameters pre-equilibration: " f"{condition_map_preeq_fix}"
553569
)
554570
logger.debug("Fixed parameters simulation: " f"{condition_map_sim_fix}")
555571
logger.debug(
556-
"Variable parameters preequilibration: " f"{condition_map_preeq_var}"
572+
"Variable parameters pre-equilibration: " f"{condition_map_preeq_var}"
557573
)
558574
logger.debug("Variable parameters simulation: " f"{condition_map_sim_var}")
559575

@@ -579,21 +595,46 @@ def create_parameter_mapping_for_condition(
579595

580596

581597
def _set_initial_state(
582-
petab_problem,
583-
condition_id,
584-
element_id,
585-
init_par_id,
586-
par_map,
587-
scale_map,
588-
value,
589-
):
598+
petab_problem: petab.Problem,
599+
condition_id: str,
600+
element_id: str,
601+
init_par_id: str,
602+
par_map: petab.ParMappingDict,
603+
scale_map: petab.ScaleMappingDict,
604+
value: str | float,
605+
fill_fixed_parameters: bool = True,
606+
) -> None:
607+
"""
608+
Update the initial value for a model entity in the parameter mapping
609+
according to the PEtab conditions table.
610+
611+
:param petab_problem: The PEtab problem
612+
:param condition_id: The current condition ID
613+
:param element_id: Element for which to set the initial value
614+
:param init_par_id: The parameter ID that refers to the initial value
615+
:param par_map: Parameter value mapping
616+
:param scale_map: Parameter scale mapping
617+
:param value: The initial value for `element_id` in `condition_id`
618+
:param fill_fixed_parameters:
619+
Whether to fill in nominal values for fixed parameters
620+
(estimate=0 in the parameters table).
621+
"""
590622
value = petab.to_float_if_float(value)
623+
# NaN indicates that the initial value is to be taken from the model
624+
# (if this is the pre-equilibration condition, or the simulation condition
625+
# when no pre-equilibration condition is set) or is not to be reset
626+
# (if this is the simulation condition following pre-equilibration)-
627+
# The latter is not handled here.
591628
if pd.isna(value):
592629
if petab_problem.model.type_id == MODEL_TYPE_SBML:
593630
value = _get_initial_state_sbml(petab_problem, element_id)
594631
elif petab_problem.model.type_id == MODEL_TYPE_PYSB:
595632
value = _get_initial_state_pysb(petab_problem, element_id)
596-
633+
else:
634+
raise NotImplementedError(
635+
f"Model type {petab_problem.model.type_id} not supported."
636+
)
637+
# the initial value can be a numeric value or a sympy expression
597638
try:
598639
value = float(value)
599640
except (ValueError, TypeError):
@@ -614,14 +655,24 @@ def _set_initial_state(
614655
f"defined for the condition {condition_id} in "
615656
"the PEtab conditions table. The initial value is "
616657
f"now set to {value}, which is the initial value "
617-
"defined in the SBML model."
658+
"defined in the original model."
618659
)
660+
619661
par_map[init_par_id] = value
620662
if isinstance(value, float):
621663
# numeric initial state
622664
scale_map[init_par_id] = petab.LIN
623665
else:
624666
# parametric initial state
667+
if (
668+
fill_fixed_parameters
669+
and petab_problem.parameter_df is not None
670+
and value in petab_problem.parameter_df.index
671+
and petab_problem.parameter_df.loc[value, ESTIMATE] == 0
672+
):
673+
par_map[init_par_id] = petab_problem.parameter_df.loc[
674+
value, NOMINAL_VALUE
675+
]
625676
scale_map[init_par_id] = petab_problem.parameter_df[
626677
PARAMETER_SCALE
627678
].get(value, petab.LIN)
@@ -638,7 +689,7 @@ def _subset_dict(
638689
Collections of keys to be contained in the different subsets
639690
640691
:return:
641-
subsetted dictionary
692+
Subsetted dictionary
642693
"""
643694
for keys in args:
644695
yield {key: val for (key, val) in full.items() if key in keys}
@@ -647,6 +698,11 @@ def _subset_dict(
647698
def _get_initial_state_sbml(
648699
petab_problem: petab.Problem, element_id: str
649700
) -> float | sp.Basic:
701+
"""Get the initial value of an SBML model entity.
702+
703+
Get the initial value of an SBML model entity (species, parameter, ...) as
704+
defined in the model (not considering any condition table overrides).
705+
"""
650706
import libsbml
651707

652708
element = petab_problem.sbml_model.getElementBySId(element_id)
@@ -688,9 +744,15 @@ def _get_initial_state_sbml(
688744
def _get_initial_state_pysb(
689745
petab_problem: petab.Problem, element_id: str
690746
) -> float | sp.Symbol:
747+
"""Get the initial value of a PySB model entity.
748+
749+
Get the initial value of an PySB model entity as defined in the model
750+
(not considering any condition table overrides).
751+
"""
752+
from pysb.pattern import match_complex_pattern
753+
691754
species_idx = int(re.match(r"__s(\d+)$", element_id)[1])
692755
species_pattern = petab_problem.model.model.species[species_idx]
693-
from pysb.pattern import match_complex_pattern
694756

695757
value = next(
696758
(

tests/petab_test_suite/test_petab_suite.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,15 @@ def _test_case(case, model_type, version):
6666
)
6767
solver = model.getSolver()
6868
solver.setSteadyStateToleranceFactor(1.0)
69+
problem_parameters = dict(
70+
zip(problem.x_free_ids, problem.x_nominal_free, strict=True)
71+
)
6972

7073
# simulate
7174
ret = simulate_petab(
7275
problem,
7376
model,
77+
problem_parameters=problem_parameters,
7478
solver=solver,
7579
log_level=logging.DEBUG,
7680
)
@@ -138,7 +142,7 @@ def _test_case(case, model_type, version):
138142
f"LLH: simulated: {llh}, expected: {gt_llh}, " f"match = {llhs_match}",
139143
)
140144

141-
check_derivatives(problem, model, solver)
145+
check_derivatives(problem, model, solver, problem_parameters)
142146

143147
if not all([llhs_match, simulations_match]) or not chi2s_match:
144148
logger.error(f"Case {case} failed.")
@@ -150,7 +154,10 @@ def _test_case(case, model_type, version):
150154

151155

152156
def check_derivatives(
153-
problem: petab.Problem, model: amici.Model, solver: amici.Solver
157+
problem: petab.Problem,
158+
model: amici.Model,
159+
solver: amici.Solver,
160+
problem_parameters: dict[str, float],
154161
) -> None:
155162
"""Check derivatives using finite differences for all experimental
156163
conditions
@@ -159,11 +166,8 @@ def check_derivatives(
159166
problem: PEtab problem
160167
model: AMICI model matching ``problem``
161168
solver: AMICI solver
169+
problem_parameters: Dictionary of problem parameters
162170
"""
163-
problem_parameters = {
164-
t.Index: getattr(t, petab.NOMINAL_VALUE)
165-
for t in problem.parameter_df.itertuples()
166-
}
167171
solver.setSensitivityMethod(amici.SensitivityMethod.forward)
168172
solver.setSensitivityOrder(amici.SensitivityOrder.first)
169173
# Required for case 9 to not fail in

0 commit comments

Comments
 (0)