Repository: PEtab-dev/libpetab-python

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

Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/PEtab-dev/libpetab-python/pull/152?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PEtab-dev) Report
> Merging [#152](https://codecov.io/gh/PEtab-dev/libpetab-python/pull/152?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PEtab-dev) (a2e4493) into [develop](https://codecov.io/gh/PEtab-dev/libpetab-python/commit/7a0b77ef6d968a4600497763df0ca791caa24768?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PEtab-dev) (7a0b77e) will **increase** coverage by `0.24%`.
> The diff coverage is `100.00%`.

```diff
@@             Coverage Diff             @@
##           develop     #152      +/-   ##
===========================================
+ Coverage    77.14%   77.39%   +0.24%     
===========================================
  Files           26       26              
  Lines         2437     2437              
  Branches       579      579              
===========================================
+ Hits          1880     1886       +6     
  Misses         404      404              
+ Partials       153      147       -6     
```


| [Impacted Files](https://codecov.io/gh/PEtab-dev/libpetab-python/pull/152?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PEtab-dev) | Coverage Δ | |
|---|---|---|
| [petab/problem.py](https://codecov.io/gh/PEtab-dev/libpetab-python/pull/152/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PEtab-dev#diff-cGV0YWIvcHJvYmxlbS5weQ==) | `69.83% <100.00%> (+2.06%)` | :arrow_up: |
| [petab/yaml.py](https://codecov.io/gh/PEtab-dev/libpetab-python/pull/152/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PEtab-dev#diff-cGV0YWIveWFtbC5weQ==) | `89.87% <0.00%> (-1.27%)` | :arrow_down: |
| [petab/core.py](https://codecov.io/gh/PEtab-dev/libpetab-python/pull/152/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PEtab-dev#diff-cGV0YWIvY29yZS5weQ==) | `89.51% <0.00%> (+1.61%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PEtab-dev/libpetab-python/pull/152?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PEtab-dev).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PEtab-dev)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PEtab-dev/libpetab-python/pull/152?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PEtab-dev). Last update [7a0b77e...a2e4493](https://codecov.io/gh/PEtab-dev/libpetab-python/pull/152?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PEtab-dev). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PEtab-dev).


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