Skip to content

ode2sbml.converters.petab_writer

petab_writer

PEtab bundle writer: ODEModel IR → PEtab v1 bundle.

write_petab(ir, outdir, observables=None, measurements=None, conditions=None)

Generate a PEtab bundle from an :class:~ode2sbml.model.ODEModel.

Parameters:

Name Type Description Default
ir ODEModel

The ODE model IR.

required
outdir str

Output directory for the PEtab bundle.

required
observables Optional[Dict[str, Dict[str, Any]]]

Dict mapping observable_id to dict with keys formula, noise_formula, name (all optional except formula).

None
measurements Optional[List[Dict[str, Any]]]

List of measurement row dicts (optional).

None
conditions Optional[List[Dict[str, Any]]]

List of condition row dicts (optional).

None

Returns:

Type Description
str

Path to the generated petab_problem.yaml.

Source code in ode2sbml/converters/petab_writer.py
def write_petab(
    ir: ODEModel,
    outdir: str,
    observables: Optional[Dict[str, Dict[str, Any]]] = None,
    measurements: Optional[List[Dict[str, Any]]] = None,
    conditions: Optional[List[Dict[str, Any]]] = None,
) -> str:
    """
    Generate a PEtab bundle from an :class:`~ode2sbml.model.ODEModel`.

    Parameters
    ----------
    ir:
        The ODE model IR.
    outdir:
        Output directory for the PEtab bundle.
    observables:
        Dict mapping observable_id to dict with keys
        ``formula``, ``noise_formula``, ``name`` (all optional except formula).
    measurements:
        List of measurement row dicts (optional).
    conditions:
        List of condition row dicts (optional).

    Returns
    -------
    str
        Path to the generated ``petab_problem.yaml``.
    """
    os.makedirs(outdir, exist_ok=True)

    # 1. Write SBML model
    sbml_filename = f"{ir.id}.xml"
    sbml_path = os.path.join(outdir, sbml_filename)
    write_sbml(ir, sbml_path)

    # 2. Parameters table
    #    Guard: petab.write_parameter_df rejects an empty DataFrame because
    #    it tries to set 'parameterId' as the index and raises KeyError when
    #    the column does not exist.  Skip entirely when there are no parameters
    #    and pass parameter_file=None to create_problem_yaml below.
    parameters_file: Optional[str] = None
    param_rows = []
    for p in ir.parameters:
        param_rows.append(
            {
                petab.PARAMETER_ID: p.id,
                petab.PARAMETER_NAME: p.id,
                petab.PARAMETER_SCALE: petab.LOG10,
                petab.LOWER_BOUND: 1e-5,
                petab.UPPER_BOUND: 1e5,
                petab.NOMINAL_VALUE: p.value,
                petab.ESTIMATE: 1,
                petab.INITIALIZATION_PRIOR_TYPE: "",
                petab.INITIALIZATION_PRIOR_PARAMETERS: "",
                petab.OBJECTIVE_PRIOR_TYPE: "",
                petab.OBJECTIVE_PRIOR_PARAMETERS: "",
            }
        )
    if param_rows:
        parameters_df = pd.DataFrame(param_rows).set_index(petab.PARAMETER_ID)
        parameters_file = os.path.join(outdir, "parameters.tsv")
        petab.write_parameter_df(parameters_df, parameters_file)

    # 3. Conditions table
    if conditions:
        cond_df = pd.DataFrame(conditions)
        if petab.CONDITION_ID not in cond_df.columns:
            cond_df.insert(
                0, petab.CONDITION_ID, [f"cond_{i}" for i in range(len(cond_df))]
            )
    else:
        cond_df = pd.DataFrame({petab.CONDITION_ID: ["default_condition"]})
    if petab.CONDITION_ID in cond_df.columns:
        cond_df = cond_df.set_index(petab.CONDITION_ID)
    conditions_file = os.path.join(outdir, "conditions.tsv")
    petab.write_condition_df(cond_df, conditions_file)

    # 4. Observables table
    if observables:
        obs_rows = []
        for obs_id, obs_spec in observables.items():
            obs_rows.append(
                {
                    petab.OBSERVABLE_ID: obs_id,
                    petab.OBSERVABLE_NAME: obs_spec.get("name", obs_id),
                    petab.OBSERVABLE_FORMULA: obs_spec.get("formula", obs_id),
                    petab.NOISE_FORMULA: obs_spec.get("noise_formula", "1.0"),
                    petab.OBSERVABLE_TRANSFORMATION: obs_spec.get(
                        "transformation", petab.LIN
                    ),
                }
            )
        obs_df = pd.DataFrame(obs_rows).set_index(petab.OBSERVABLE_ID)
    else:
        # Default: observe all species
        obs_rows = [
            {
                petab.OBSERVABLE_ID: f"obs_{s.id}",
                petab.OBSERVABLE_NAME: s.id,
                petab.OBSERVABLE_FORMULA: s.id,
                petab.NOISE_FORMULA: "1.0",
                petab.OBSERVABLE_TRANSFORMATION: petab.LIN,
            }
            for s in ir.species
        ]
        obs_df = pd.DataFrame(obs_rows).set_index(petab.OBSERVABLE_ID)
    observables_file = os.path.join(outdir, "observables.tsv")
    petab.write_observable_df(obs_df, observables_file)

    # 5. Measurements table
    if measurements:
        meas_df = pd.DataFrame(measurements)
    else:
        # Create a placeholder measurements file
        meas_df = pd.DataFrame(
            columns=[
                petab.OBSERVABLE_ID,
                petab.SIMULATION_CONDITION_ID,
                petab.TIME,
                petab.MEASUREMENT,
            ]
        )
    measurements_file = os.path.join(outdir, "measurements.tsv")
    petab.write_measurement_df(meas_df, measurements_file)

    # 6. YAML manifest
    yaml_file = os.path.join(outdir, "petab_problem.yaml")
    petab.create_problem_yaml(
        sbml_files=[sbml_filename],
        condition_files=["conditions.tsv"],
        measurement_files=["measurements.tsv"],
        parameter_file="parameters.tsv" if parameters_file else None,
        observable_files=["observables.tsv"],
        yaml_file=yaml_file,
        relative_paths=False,
    )

    return yaml_file