Repository: yaml2sbml-dev/yaml2sbml (language: python)

Issue:
Definition of Model ID for use with pyPESTO and AMICI
Currently when a new model is created after using this wonderful tool the model field read as
`<model>`

This leads (when used together with pyPESTO and AMICI) to the generation of a random name which is used as name for the model in the amici folder, e.g. `amici_models/tmpiouv7fa5`.
The problem here is that every time one will load the model, the random name is changing, therefore the already compiled folder is not detected, and the model is compiled again.

This has a very simple fix. During the model creation you can add the model ID, such that the final sbml model will contain
`<model id="MyModelName" name="MyModelName">`
instead of
`<model>`

I do not know how this behaves for other tools, but for pyPESTO/AMICI this would be a valuable fix.


Discussion / hints:
I agree that filling the model id consistently into the various places it's used (in the sbml, as folder names etc) is a good idea. Currently, the model has no id, so this should be easy to fix.

Fix patch:
diff --git a/doc/examples/Format_Features/simulation_and_plotting.py b/doc/examples/Format_Features/simulation_and_plotting.py
index 6d1e5c1..0618fca 100644
--- a/doc/examples/Format_Features/simulation_and_plotting.py
+++ b/doc/examples/Format_Features/simulation_and_plotting.py
@@ -30,7 +30,8 @@ def simulate_AMICI(sbml_name: str):
     amici_model = model_module.getModel()
     solver = amici_model.getSolver()
 
-    # Define time points ans run simulation using default model parameters/solver options
+    # Define time points ans run simulation using default
+    # model parameters/solver options
     amici_model.setTimepoints(np.linspace(0, 5, 101))
     rdata = amici.runAmiciSimulation(amici_model, solver)
 
diff --git a/doc/format_specification.rst b/doc/format_specification.rst
index e173bf7..6c22938 100644
--- a/doc/format_specification.rst
+++ b/doc/format_specification.rst
@@ -127,7 +127,8 @@ observables \[optional\]
       - observableId: Obs_2
         ...
 
-Defines the **observables**. Observables are not part of the SBML standard and hence only influence the _observable table_ generated in `yaml2PEtab`. The entries are written to the corresponding columns of the PEtab observable table. According to the PEtab standard, an observable table can take the following entries:  `observableId, observableName, observableFormula, observableTransformation, noiseFormula, noiseDistribution`. 
+Defines the **observables**. Observables are not part of the SBML standard. If the SBML is generated via the `yaml2sbml.yaml2sbml` command and the `observables_as_assignments` flag is set to `True`, observables are represented as assignments to parameters of the form observable_<observable_id>.
+If the SBML is created via `yaml2sbml.yaml2petab`, observables are represented in the PEtab observables table. The entries are written to the corresponding columns of the PEtab observable table. According to the PEtab standard, an observable table can take the following entries:  `observableId, observableName, observableFormula, observableTransformation, noiseFormula, noiseDistribution`.
 
 For a detailed discussion see the `corresponding part of the PEtab documentation <https://github.com/PEtab-dev/PEtab/blob/master/doc/documentation_data_format.rst#observables-table>`_.
 
diff --git a/yaml2sbml/YamlModel.py b/yaml2sbml/YamlModel.py
index dce1a06..a22d67b 100644
--- a/yaml2sbml/YamlModel.py
+++ b/yaml2sbml/YamlModel.py
@@ -1,8 +1,9 @@
 """A Model Editor for creating YAML models."""
 import yaml
 import os.path
-from typing import Union
 import copy
+from typing import Union
+from pathlib import Path
 
 from .yaml2sbml import _parse_yaml_dict, _load_yaml_file
 from .yaml2PEtab import _yaml2petab
@@ -107,6 +108,9 @@ def write_to_sbml(self,
             raise ValueError('sbml_dir should contain path to the sbml '
                              'and hence end with .xml or .sbml')
 
+        # model name = sbml name without file extension
+        model_name = Path(sbml_dir).stem
+
         if (not overwrite) and os.path.exists(sbml_dir):
             raise FileExistsError(f'Can not write SBML model. File {sbml_dir}'
                                   f' already exists. Consider to set '
@@ -114,7 +118,8 @@ def write_to_sbml(self,
 
         # generate SBML as string
         reduced_model_dict = self._get_reduced_model_dict()
-        sbml_as_string = _parse_yaml_dict(reduced_model_dict)
+        sbml_as_string = _parse_yaml_dict(reduced_model_dict,
+                                          model_name)
 
         with open(sbml_dir, 'w') as f_out:
             f_out.write(sbml_as_string)
diff --git a/yaml2sbml/yaml2PEtab.py b/yaml2sbml/yaml2PEtab.py
index b74ca30..465a514 100644
--- a/yaml2sbml/yaml2PEtab.py
+++ b/yaml2sbml/yaml2PEtab.py
@@ -7,6 +7,7 @@
 import pandas as pd
 import petab
 import yaml
+from pathlib import Path
 
 
 from .yaml2sbml import _parse_yaml_dict, _load_yaml_file
@@ -77,13 +78,19 @@ def _yaml2petab(yaml_model_dict: dict,
 
     with warnings.catch_warnings():
         warnings.simplefilter("ignore", category=UserWarning)
-        sbml_as_string = _parse_yaml_dict(yaml_model_dict)
+        sbml_as_string = _parse_yaml_dict(yaml_model_dict,
+                                          model_name)
 
     with open(sbml_dir, 'w') as f_out:
         f_out.write(sbml_as_string)
 
     # create petab tsv files:
-    _create_petab_tables_from_yaml(yaml_model_dict, output_dir)
+    if model_name.endswith('.xml') or model_name.endswith('.sbml'):
+        model_name = Path(model_name).stem
+
+    _create_petab_tables_from_yaml(yaml_model_dict,
+                                   output_dir,
+                                   model_name)
 
     # create yaml file, that organizes the petab problem:
     if (petab_yaml_name is None) and (measurement_table_name is not None):
@@ -97,22 +104,30 @@ def _yaml2petab(yaml_model_dict: dict,
                                    output_dir,
                                    sbml_dir,
                                    petab_yaml_name,
+                                   model_name,
                                    measurement_table_name)
     # validate PEtab tables:
     validate_petab_tables(sbml_dir, output_dir)
 
 
 def _create_petab_tables_from_yaml(yaml_dict: dict,
-                                   output_dir: str):
+                                   output_dir: str,
+                                   model_name: str):
     """
     Parse the yaml dict to a PEtab observable/parameter table.
 
+    The table will be named (if the corresponding information is in the YAML):
+    `parameters_<model_name>.tsv`,
+    `observables_<model_name>.tsv` and
+    `experimental_conditions_<model_name>.tsv`.
+
     Arguments:
         yaml_dict: dict, that contains the yaml file.
         output_dir: directory, where the PEtab tables should be written.
     """
     parameter_table = _create_parameter_table(yaml_dict)
-    parameter_table.to_csv(os.path.join(output_dir, 'parameter_table.tsv'),
+    parameter_table.to_csv(os.path.join(output_dir,
+                                        f'parameters_{model_name}.tsv'),
                            sep='\t',
                            index=False)
 
@@ -120,22 +135,25 @@ def _create_petab_tables_from_yaml(yaml_dict: dict,
     if 'observables' in yaml_dict.keys():
         observable_table = _create_observable_table(yaml_dict)
         observable_table.to_csv(os.path.join(output_dir,
-                                             'observable_table.tsv'),
+                                             f'observables_{model_name}.tsv'),
                                 sep='\t',
                                 index=False)
 
     # create PEtab condition table, if conditions occur in the yaml file.
     if 'conditions' in yaml_dict.keys():
         condition_table = _create_condition_table(yaml_dict)
-        condition_table.to_csv(os.path.join(output_dir, 'condition_table.tsv'),
-                               sep='\t',
-                               index=False)
+        condition_table.to_csv(
+            os.path.join(output_dir,
+                         f'experimental_conditions_{model_name}.tsv'),
+            sep='\t',
+            index=False)
 
 
 def _create_petab_problem_yaml(yaml_dict: dict,
                                output_dir: str,
                                sbml_dir: str,
                                petab_yaml_name: str,
+                               model_name: str,
                                measurement_table_name: str = None):
     """
     Create the yaml file, that can be used for structuring a PEtab problem.
@@ -145,10 +163,11 @@ def _create_petab_problem_yaml(yaml_dict: dict,
         output_dir: directory, where the PEtab tables should be written.
         sbml_dir: directory of the SBML model.
         petab_yaml_name: name of file, where PEtab yaml is written.
+        model_name: Name of the model, in order to name the PEtab tables.
         measurement_table_name: directory of the  measurement table.
     """
     petab_yaml_dict = {'format_version': 1,
-                       'parameter_file': 'parameter_table.tsv',
+                       'parameter_file': f'parameters_{model_name}.tsv',
                        'problems': [{'sbml_files':
                                          [os.path.basename(sbml_dir)]}]}
 
@@ -156,11 +175,11 @@ def _create_petab_problem_yaml(yaml_dict: dict,
 
     if 'observables' in yaml_dict.keys():
         petab_yaml_dict['problems'][0]['observable_files'] = \
-            ['observable_table.tsv']
+            [f'observables_{model_name}.tsv']
 
     if 'conditions' in yaml_dict.keys():
         petab_yaml_dict['problems'][0]['condition_files'] = \
-            ['condition_table.tsv']
+            [f'experimental_conditions_{model_name}.tsv']
 
     if measurement_table_name is not None:
         petab_yaml_dict['problems'][0]['measurement_files'] = \
@@ -225,7 +244,8 @@ def _create_condition_table(yaml_dict: dict):
                                optional_id_list)
 
 
-def validate_petab_tables(sbml_dir: str, output_dir: str):
+def validate_petab_tables(sbml_dir: str,
+                          output_dir: str):
     """
     Validate the PEtab tables via petab.lint.
 
@@ -239,10 +259,14 @@ def validate_petab_tables(sbml_dir: str, output_dir: str):
         Errors are raised by lint, if PEtab files are invalid...
     """
     model = sbml.readSBML(sbml_dir).getModel()
+    model_name = model.getId()
 
-    parameter_file_dir = os.path.join(output_dir, 'parameter_table.tsv')
-    observable_file_dir = os.path.join(output_dir, 'observable_table.tsv')
-    condition_table_dir = os.path.join(output_dir, 'condition_table.tsv')
+    parameter_file_dir = \
+        os.path.join(output_dir, f'parameters_{model_name}.tsv')
+    observable_file_dir = \
+        os.path.join(output_dir, f'observables_{model_name}.tsv')
+    condition_table_dir = \
+        os.path.join(output_dir, f'experimental_conditions_{model_name}.tsv')
 
     # check observable table, if the table exists
     if os.path.exists(observable_file_dir):
@@ -265,6 +289,7 @@ def validate_petab_tables(sbml_dir: str, output_dir: str):
     parameter_df = pd.read_csv(parameter_file_dir,
                                sep='\t',
                                index_col='parameterId')
+
     petab.lint.check_parameter_df(parameter_df,
                                   sbml_model=model,
                                   observable_df=observable_df)
diff --git a/yaml2sbml/yaml2sbml.py b/yaml2sbml/yaml2sbml.py
index a9a419b..5aa1a46 100644
--- a/yaml2sbml/yaml2sbml.py
+++ b/yaml2sbml/yaml2sbml.py
@@ -1,6 +1,7 @@
 """Translate ODEs in the YAML format into SBML."""
 import argparse
 import warnings
+from pathlib import Path
 
 import libsbml as sbml
 import yaml
@@ -9,24 +10,40 @@
 from .yaml_validation import _validate_yaml_from_dict
 
 
-def yaml2sbml(yaml_dir: str, sbml_dir: str):
+def yaml2sbml(yaml_dir: str,
+              sbml_dir: str,
+              observables_as_assignments: bool = False):
     """
     Parse a YAML file with the specification of ODEs and write it to SBML.
 
-    SBML is written within this function.
+    SBML is written within this function. If `observables_as_assignments=True`,
+    observables will be translated into parameter assignments of the form
+    `observable_<observable_id>`.
 
     Arguments:
-        yaml_file : path to the yaml file with the ODEs specification
-        sbml_file: path to the SBML file to be written out
+        yaml_dir : directory of the yaml file with the ODEs specification
+        sbml_dir: directory of the SBML file to be written out
+        observables_as_assignments: indicates whether there should be
+            parameter assignments of the form `observable_<observable_id>`.
     """
-    sbml_as_string = _parse_yaml(yaml_dir)
+    # check file extension in sbml_dir
+    if not (sbml_dir.endswith('.xml') or sbml_dir.endswith('.sbml')):
+        raise ValueError('sbml_dir should end with .xml or .sbml.')
+
+    model_name = Path(sbml_dir).stem
+
+    sbml_as_string = _parse_yaml(yaml_dir,
+                                 model_name,
+                                 observables_as_assignments)
 
     # write sbml file
     with open(sbml_dir, 'w') as f_out:
         f_out.write(sbml_as_string)
 
 
-def _parse_yaml(yaml_dir: str) -> str:
+def _parse_yaml(yaml_dir: str,
+                model_name: str,
+                observables_as_assignments: bool = False) -> str:
     """
     Parse a YAML file with the specification of ODEs to SBML.
 
@@ -34,6 +51,9 @@ def _parse_yaml(yaml_dir: str) -> str:
 
     Arguments:
         yaml_dir: path to the yaml file with the ODEs specification
+        model_name: model name as specified in the SBML
+        observables_as_assignments: indicates if observables should be
+            translated into parameter assignments
 
     Returns:
         sbml_string: a string containing the ODEs in SBML format
@@ -43,18 +63,26 @@ def _parse_yaml(yaml_dir: str) -> str:
     """
     yaml_dict = _load_yaml_file(yaml_dir)
     _validate_yaml_from_dict(yaml_dict)
-    sbml_string = _parse_yaml_dict(yaml_dict)
+
+    sbml_string = _parse_yaml_dict(yaml_dict,
+                                   model_name,
+                                   observables_as_assignments)
 
     return sbml_string
 
 
-def _parse_yaml_dict(yaml_dict) -> str:
+def _parse_yaml_dict(yaml_dict: dict,
+                     model_name: str,
+                     observables_as_assignments: bool = False) -> str:
     """
     Generate a string, containing the SBML from a yaml_dict.
 
     Arguments:
         yaml_dict: dictionary, containing to the yaml file with the ODEs
                    specification.
+        model_name: model name as specified in the SBML
+        observables_as_assignments: indicates if observables should be
+            translated into parameter assignments
 
     Returns:
         sbml_string: a string containing the ODEs in SBML format.
@@ -66,9 +94,19 @@ def _parse_yaml_dict(yaml_dict) -> str:
         raise SystemExit('Could not create SBMLDocument object')
 
     model = document.createModel()
+
+    # remove file extension
+    if model_name.endswith('.xml') or model_name.endswith('.sbml'):
+        model_name = Path(model_name).stem
+
+    model.setId(model_name)
+    model.setName(model_name)
+
     model = _create_compartment(model)
 
-    _convert_yaml_blocks_to_sbml(model, yaml_dict)
+    _convert_yaml_blocks_to_sbml(model,
+                                 yaml_dict,
+                                 observables_as_assignments)
 
     # check consistency and give warnings for errors in SBML:
     if document.checkConsistency():
@@ -131,7 +169,9 @@ def _load_yaml_file(yaml_file: str) -> dict:
     return yaml_dict
 
 
-def _convert_yaml_blocks_to_sbml(model: sbml.Model, yaml_dic: dict):
+def _convert_yaml_blocks_to_sbml(model: sbml.Model,
+                                 yaml_dic: dict,
+                                 observables_as_assignments):
     """
     Convert each block in the yaml dictionary to SBML.
 
@@ -142,11 +182,17 @@ def _convert_yaml_blocks_to_sbml(model: sbml.Model, yaml_dic: dict):
     Returns:
         model: SBML model with added entities
     """
+
+    def _read_observables_with_assignments(model, block):
+        return _read_observables_block(model,
+                                       block,
+                                       observables_as_assignments)
+
     function_dict = {'time': _read_time_block,
                      'parameters': _read_parameters_block,
                      'assignments': _read_assignments_block,
                      'functions': _read_functions_block,
-                     'observables': _read_observables_block,
+                     'observables': _read_observables_with_assignments,
                      'odes': _read_odes_block,
                      'conditions': _read_conditions_block}
 
@@ -383,9 +429,11 @@ def _create_rate_rule(model: sbml.Model, species: str, formula: str):
     r.setMath(math_ast)
 
 
-def _read_observables_block(model: sbml.Model, observable_list: list):
+def _read_observables_block(model: sbml.Model,
+                            observable_list: list,
+                            observables_as_assignments: bool):
     """
-    Read an process the observables block in the YAML file.
+    Read and process the observables block in the YAML file.
 
     Since the observables are not represented in the SBML, it only gives
     a warning to inform the user.
@@ -394,10 +442,18 @@ def _read_observables_block(model: sbml.Model, observable_list: list):
         model: SBML model (libsbml)
         observable_list: observables block containing all
                          observable definitions.
-    """
-    warnings.warn(
-        'Observables are not represented in the SBML and therefore only have '
-        'an effect on the output when called via yaml2PEtab')
+        observables_as_assignments: indicates whether there should be
+            parameter assignments of the form `observable_<observable_id>`.
+    """
+    if observables_as_assignments:
+        for observable_def in observable_list:
+            _create_assignment(model,
+                               f'observable_{observable_def["observableId"]}',
+                               observable_def['observableFormula'])
+    else:
+        warnings.warn(
+            'Observables are not represented in the SBML and therefore only '
+            'have an effect on the output when called via yaml2petab')
 
 
 def _read_conditions_block(model: sbml.Model, conditions_list: list):
@@ -414,7 +470,7 @@ def _read_conditions_block(model: sbml.Model, conditions_list: list):
     """
     warnings.warn(
         'Conditions are not represented in the SBML and therefore only have '
-        'an effect on the output when called via yaml2PEtab')
+        'an effect on the output when called via yaml2petab')
 
 
 def main():
@@ -424,6 +480,11 @@ def main():
     parser.add_argument('yaml_file', type=str, help='Path to input YAML file.')
     parser.add_argument('sbml_file', type=str,
                         help='Path to output SBML file.')
+    parser.add_argument('-o', '--observables_as_assignments',
+                        action='store_true',
+                        help='Optional argument, flag, that indicates, if '
+                             'observables should be represented in the SBML'
+                             'as assignments. Potential Values: 1/0 (yes/no).')
 
     args = parser.parse_args()
 
@@ -432,7 +493,9 @@ def main():
 
     print('Converting...')
 
-    yaml2sbml(args.yaml_file, args.sbml_file)
+    yaml2sbml(args.yaml_file,
+              args.sbml_file,
+              args.observables_as_assignments)
 
 
 if __name__ == '__main__':


Test patch:
diff --git a/tests/test_CLI.py b/tests/test_CLI.py
index f97a2f0..bf0c8e1 100644
--- a/tests/test_CLI.py
+++ b/tests/test_CLI.py
@@ -15,13 +15,18 @@ def test_yaml2sbml_cli(script_runner):
     script_runner.run('yaml2sbml', yaml_dir, sbml_dir)
 
     # test if SBML created by the python yaml2sbml command is the same.
-    sbml_from_python = _parse_yaml(yaml_dir)
+    sbml_from_python = _parse_yaml(yaml_dir, 'test_sbml')
 
     with open(sbml_dir, 'r') as f_in:
         sbml_from_cli = f_in.read()
 
-    if not (sbml_from_cli == sbml_from_python):
-        raise AssertionError('SBML from CLI and python interface differ.')
+    for line_cli, line_python in zip(sbml_from_cli.split('\n'),
+                                     sbml_from_python.split('\n')):
+
+        # the line containing name & id will not match...
+        if not line_cli == line_python:
+            raise AssertionError('SBML from CLI and '
+                                 'Python interface differ.')
 
     os.remove(sbml_dir)
 
diff --git a/tests/test_yaml2petab.py b/tests/test_yaml2petab.py
index 6084d6a..94f5103 100644
--- a/tests/test_yaml2petab.py
+++ b/tests/test_yaml2petab.py
@@ -1,4 +1,5 @@
 import os
+import shutil
 import unittest
 
 import yaml2sbml.yaml2PEtab as yaml2PEtab
@@ -12,26 +13,25 @@ class TestYaml2PEtab(unittest.TestCase):
 
     def setUp(self):
         this_dir, _ = os.path.split(__file__)
-        self.test_folder = os.path.join(this_dir, 'test_yaml2sbml')
+        self.input_folder = os.path.join(this_dir, 'test_yaml2sbml')
+        self.output_folder = os.path.join(this_dir, 'test_yaml2sbml_output')
+
+    def tearDown(self):
+        shutil.rmtree(self.output_folder)
 
     def test_petab_export(self):
         """
         Test PEtab export
         """
-        ode_file = os.path.join(self.test_folder, 'ode_input2.yaml')
+        input_yaml_dir = os.path.join(self.input_folder, 'ode_input2.yaml')
 
-        yaml2PEtab.yaml2petab(ode_file,
-                              self.test_folder,
+        yaml2PEtab.yaml2petab(input_yaml_dir,
+                              self.output_folder,
                               'sbml_test.xml')
 
         yaml2PEtab.validate_petab_tables(
-            os.path.join(self.test_folder, 'sbml_test.xml'), self.test_folder)
-
-        for f in ['observable_table.tsv',
-                  'parameter_table.tsv',
-                  'condition_table.tsv',
-                  'sbml_test.xml']:
-            os.remove(os.path.join(self.test_folder, f))
+            os.path.join(self.output_folder, 'sbml_test.xml'),
+            self.output_folder)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_yaml2sbml.py b/tests/test_yaml2sbml.py
index c795895..6348a8c 100644
--- a/tests/test_yaml2sbml.py
+++ b/tests/test_yaml2sbml.py
@@ -1,7 +1,7 @@
 import os
 import unittest
 
-from yaml2sbml.yaml2sbml import _parse_yaml
+from yaml2sbml.yaml2sbml import yaml2sbml
 
 
 class TestYaml2SBML(unittest.TestCase):
@@ -18,46 +18,63 @@ def test_yaml_import(self):
         """
         Test yaml import/SBML generation...
         """
-        ode_file = os.path.join(self.test_folder, 'ode_input1.yaml')
+        yaml_dir = os.path.join(self.test_folder, 'ode_input1.yaml')
         expected_result_file = os.path.join(self.test_folder,
                                             'true_sbml_output.xml')
 
-        sbml_contents = _parse_yaml(ode_file)
+        sbml_test_dir = os.path.join(self.test_folder, 'sbml_test.xml')
+
+        yaml2sbml(yaml_dir, sbml_test_dir)
 
         with open(expected_result_file, 'r') as f_in:
-            expected_sbml_contents = f_in.read()
+            expected_sbml = f_in.read()
 
-        sbml_test_dir = os.path.join(self.test_folder, 'sbml_test.xml')
-        with open(sbml_test_dir, 'w') as f_out:
-            f_out.write(sbml_contents)
+        with open(sbml_test_dir, 'r') as f_out:
+            observed_sbml = f_out.read()
+
+        # check if lines coincide
+        for line_true, line_tested in zip(expected_sbml.split('\n'),
+                                          observed_sbml.split('\n')):
 
-        self.assertEqual(expected_sbml_contents, sbml_contents)
+            # the line containing name & id will not match...
+            if not line_true.startswith('  <model id='):
+                self.assertEqual(line_true, line_tested)
 
         os.remove(sbml_test_dir)
 
     def test_yaml_import_observables(self):
         """
-        Test yaml import/export for a model containing observables
-        (that are not translated).
+        Test yaml import/export for a model containing observables.
         """
-        ode_file = os.path.join(self.test_folder, 'ode_input2.yaml')
+        yaml_dir = os.path.join(self.test_folder, 'ode_input2.yaml')
 
         expected_result_file = \
             os.path.join(self.test_folder, 'true_sbml_output.xml')
 
-        test_sbml_dir = os.path.join(self.test_folder, 'sbml_test.xml')
+        sbml_test_dir = os.path.join(self.test_folder, 'sbml_test.xml')
+
+        # Call yaml2sbml with observables as assignments
+        yaml2sbml(yaml_dir,
+                  sbml_test_dir,
+                  observables_as_assignments=True)
 
-        sbml_contents = _parse_yaml(ode_file)
+        # Call yaml2sbml with observables not translated
+        yaml2sbml(yaml_dir, sbml_test_dir)
 
         with open(expected_result_file, 'r') as f_in:
-            expected_sbml_contents = f_in.read()
+            expected_sbml = f_in.read()
 
-        with open(test_sbml_dir, 'w') as f_out:
-            f_out.write(sbml_contents)
+        with open(sbml_test_dir, 'r') as f_out:
+            observed_sbml = f_out.read()
 
-        self.assertEqual(expected_sbml_contents, sbml_contents)
+        for line_true, line_tested in zip(expected_sbml.split('\n'),
+                                          observed_sbml.split('\n')):
 
-        os.remove(test_sbml_dir)
+            # the line containing name & id will not match...
+            if not line_true.startswith('  <model id='):
+                self.assertEqual(line_true, line_tested)
+
+        os.remove(sbml_test_dir)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_yaml2sbml/true_sbml_output.xml b/tests/test_yaml2sbml/true_sbml_output.xml
index f0485c4..c5fed16 100644
--- a/tests/test_yaml2sbml/true_sbml_output.xml
+++ b/tests/test_yaml2sbml/true_sbml_output.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <sbml xmlns="http://www.sbml.org/sbml/level3/version1/core" level="3" version="1">
-  <model>
+  <model id="true_sbml_output" name="true_sbml_output">
     <listOfFunctionDefinitions>
       <functionDefinition id="hill">
         <math xmlns="http://www.w3.org/1998/Math/MathML">