Repository: PEtab-dev/libpetab-python

Issue:
Add support for multiple condition files per petab.Problem
Allowed in YAML file, but not implemented in library.

Comments:

Patch:
diff --git a/petab/problem.py b/petab/problem.py
index 517b91c..f523803 100644
--- a/petab/problem.py
+++ b/petab/problem.py
@@ -87,8 +87,9 @@ class Problem:
 
     @staticmethod
     def from_files(
-            sbml_file: Union[str, Path, None] = None,
-            condition_file: Union[str, Path, None] = None,
+            sbml_file: Union[str, Path] = None,
+            condition_file:
+            Union[str, Path, Iterable[Union[str, Path]]] = None,
             measurement_file: Union[str, Path,
                                     Iterable[Union[str, Path]]] = None,
             parameter_file: Union[str, Path,
@@ -115,7 +116,8 @@ class Problem:
         observable_df = None
 
         if condition_file:
-            condition_df = conditions.get_condition_df(condition_file)
+            condition_df = core.concat_tables(condition_file,
+                                              conditions.get_condition_df)
 
         if measurement_file:
             # If there are multiple tables, we will merge them
@@ -198,7 +200,10 @@ class Problem:
 
         problem0 = yaml_config['problems'][0]
 
-        yaml.assert_single_condition_and_sbml_file(problem0)
+        if len(problem0[SBML_FILES]) > 1:
+            # TODO https://github.com/PEtab-dev/libpetab-python/issues/6
+            raise NotImplementedError(
+                'Support for multiple models is not yet implemented.')
 
         if isinstance(yaml_config[PARAMETER_FILE], list):
             parameter_file = [
@@ -213,7 +218,8 @@ class Problem:
             if problem0[SBML_FILES] else None,
             measurement_file=[get_path(f)
                               for f in problem0[MEASUREMENT_FILES]],
-            condition_file=get_path(problem0[CONDITION_FILES][0]),
+            condition_file=[get_path(f)
+                            for f in problem0[CONDITION_FILES]],
             parameter_file=parameter_file,
             visualization_files=[
                 get_path(f) for f in problem0.get(VISUALIZATION_FILES, [])],


Test patch:
diff --git a/tests/test_petab.py b/tests/test_petab.py
index 62e74e8..0e0c4e0 100644
--- a/tests/test_petab.py
+++ b/tests/test_petab.py
@@ -1,8 +1,10 @@
 import copy
 import pickle
 import tempfile
+from io import StringIO
 from math import nan
 from pathlib import Path
+from tempfile import TemporaryDirectory
 
 import libsbml
 import numpy as np
@@ -10,6 +12,7 @@ import pandas as pd
 import petab
 import pytest
 from petab.C import *
+from yaml import safe_load
 
 
 @pytest.fixture
@@ -452,6 +455,33 @@ def test_concat_measurements():
                                 petab.measurements.get_measurement_df))
 
 
+def test_concat_condition_df():
+    df1 = pd.DataFrame(data={
+        CONDITION_ID: ['condition1', 'condition2'],
+        'par1': [1.1, 1.2],
+        'par2': [2.1, 2.2],
+        'par3': [3.1, 3.2]
+    }).set_index(CONDITION_ID)
+
+    assert df1.equals(petab.concat_tables(df1, petab.get_condition_df))
+
+    df2 = pd.DataFrame(data={
+        CONDITION_ID: ['condition3'],
+        'par1': [1.3],
+        'par2': [2.3],
+    }).set_index(CONDITION_ID)
+
+    df_expected = pd.DataFrame(data={
+        CONDITION_ID: ['condition1', 'condition2', 'condition3'],
+        'par1': [1.1, 1.2, 1.3],
+        'par2': [2.1, 2.2, 2.3],
+        'par3': [3.1, 3.2, np.nan],
+    }).set_index(CONDITION_ID)
+    assert df_expected.equals(
+        petab.concat_tables((df1, df2), petab.get_condition_df)
+    )
+
+
 def test_get_observable_ids(petab_problem):  # pylint: disable=W0621
     """Test if observable ids functions returns correct value."""
     assert set(petab_problem.get_observable_ids()) == {'observable_1'}
@@ -535,3 +565,68 @@ def test_load_remote():
     assert petab_problem.sbml_model is not None
     assert petab_problem.measurement_df is not None \
            and not petab_problem.measurement_df.empty
+
+
+def test_problem_from_yaml_v1_empty():
+    """Test loading PEtab version 1 yaml without any files"""
+    yaml_config = """
+    format_version: 1
+    parameter_file:
+    problems:
+    - condition_files: []
+      measurement_files: []
+      observable_files: []
+      sbml_files: []
+    """
+    yaml_config = safe_load(StringIO(yaml_config))
+    petab.Problem.from_yaml(yaml_config)
+
+
+def test_problem_from_yaml_v1_multiple_files():
+    """Test loading PEtab version 1 yaml with multiple condition / measurement
+    / observable files"""
+    yaml_config = """
+    format_version: 1
+    parameter_file:
+    problems:
+    - condition_files: [conditions1.tsv, conditions2.tsv]
+      measurement_files: [measurements1.tsv, measurements2.tsv]
+      observable_files: [observables1.tsv, observables2.tsv]
+      sbml_files: []
+    """
+
+    with TemporaryDirectory() as tmpdir:
+        yaml_path = Path(tmpdir, "problem.yaml")
+        with open(yaml_path, 'w') as f:
+            f.write(yaml_config)
+
+        for i in (1, 2):
+            condition_df = pd.DataFrame({
+                CONDITION_ID: [f"condition{i}"],
+            })
+            condition_df.set_index([CONDITION_ID], inplace=True)
+            petab.write_condition_df(condition_df,
+                                     Path(tmpdir, f"conditions{i}.tsv"))
+
+            measurement_df = pd.DataFrame({
+                SIMULATION_CONDITION_ID: [f"condition{i}"],
+                OBSERVABLE_ID: [f"observable{i}"],
+                TIME: [i],
+                MEASUREMENT: [1]
+            })
+            petab.write_measurement_df(measurement_df,
+                                       Path(tmpdir, f"measurements{i}.tsv"))
+
+            observables_df = pd.DataFrame({
+                OBSERVABLE_ID: [f"observable{i}"],
+                OBSERVABLE_FORMULA: [1],
+                NOISE_FORMULA: [1],
+            })
+            petab.write_observable_df(observables_df,
+                                      Path(tmpdir, f"observables{i}.tsv"))
+
+        petab_problem = petab.Problem.from_yaml(yaml_path)
+
+    assert petab_problem.measurement_df.shape[0] == 2
+    assert petab_problem.observable_df.shape[0] == 2
+    assert petab_problem.condition_df.shape[0] == 2