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

Issue:
validation error for formulas starting with minus
As @yannikschaelte pointed out to me: If a right hand side/formula starts with a minus, the translator/validator throws  `ScannerError: sequence entries are not allowed here` (Since yaml-blocks start with a `-`, and the validator complains...). I don't know, wether there is a direct fix (but doubt this a bit, since this is how yaml are parsed??). 

Possible ways around would be setting the formula either in quotes ("...") or in brackets... Therefore I propose

- [x] Explaining this in Format Docs & evtl. README (known issues)
- [x] catching the `ScannerError` and throw a meaningfull error there 
- [x] Test, if the Model Editor works in this case?


Discussion / hints:
Other points in a similar direction

- [x] specify, that any id needs to be a valid SBML identifier (and what that means...)
- [x] any formula is parsed by libsbmls math parser (and what that means), currently that's the last point of the format docu. Make this better visible!
One could reference the sbml [specification](http://sbml.org/Documents/Specifications#SBML_Level_3), but there may also be constraints from [libsbml](http://model.caltech.edu/software/libsbml/5.18.0/docs/formatted/python-api/libsbml-math.html) or mathml, so eventually only the successful export to sbml may give full validity.

Fix patch:
diff --git a/doc/examples/Lotka_Volterra/Lotka_Volterra_python/Lotka_Volterra.ipynb b/doc/examples/Lotka_Volterra/Lotka_Volterra_python/Lotka_Volterra.ipynb
index d5150a5..ad23c68 100644
--- a/doc/examples/Lotka_Volterra/Lotka_Volterra_python/Lotka_Volterra.ipynb
+++ b/doc/examples/Lotka_Volterra/Lotka_Volterra_python/Lotka_Volterra.ipynb
@@ -82,7 +82,7 @@
     "import yaml2sbml\n",
     "\n",
     "yaml_file_basic = 'Lotka_Volterra_basic.yml'\n",
-    "yaml2sbml.validate_yaml(yaml_file=yaml_file_basic)"
+    "yaml2sbml.validate_yaml(yaml_dir=yaml_file_basic)"
    ]
   },
   {
diff --git a/doc/format_specification.rst b/doc/format_specification.rst
index e09fc9c..e173bf7 100644
--- a/doc/format_specification.rst
+++ b/doc/format_specification.rst
@@ -9,6 +9,12 @@ General scope
 
 *  `yaml2PEtab`: Defines a fitting problem of the form `y(t_i) = h(x(t_i), p) + eps_i` with independent Normal or Laplace distributed error terms `eps`. `h` denotes the mapping from system states to observables. PEtab allows to formulate MLE and MAP based fitting problems.
 
+General remarks
+---------------
+
+* All identifiers of states, parameters etc. need to be valid SBML identifiers. Therefore identifiers must consist of only upper and lower case letters, digits and underscores, and must not start with a digit.
+* Mathematical equations are parsed by `libsbml`s `parseL3Formula`. Hence for correct syntax see its `documentation <http://sbml.org/Special/Software/libSBML/docs/formatted/python-api/namespacelibsbml.html#ae79acc3be958963c55f1d03944add36b>`_ and the corresponding section of the format specification.
+* Equations starting with a minus must be surrounded by brackets or quotation marks, since a leading minus also has a syntactic meaning in YAML and the YAML file will not be valid otherwise.
 
 time \[optional\]
 -----------------
diff --git a/yaml2sbml/YamlModel.py b/yaml2sbml/YamlModel.py
index b66e90b..dce1a06 100644
--- a/yaml2sbml/YamlModel.py
+++ b/yaml2sbml/YamlModel.py
@@ -4,7 +4,7 @@
 from typing import Union
 import copy
 
-from .yaml2sbml import _parse_yaml_dict
+from .yaml2sbml import _parse_yaml_dict, _load_yaml_file
 from .yaml2PEtab import _yaml2petab
 from .yaml_validation import _validate_yaml_from_dict
 
@@ -23,7 +23,7 @@ def __init__(self):
                             'conditions': []}
 
     @staticmethod
-    def load_from_yaml(yaml_dir):
+    def load_from_yaml(yaml_dir: str):
         """
         Create a model instance from a yaml file.
 
@@ -37,10 +37,8 @@ def load_from_yaml(yaml_dir):
         """
         new_model = YamlModel()
 
-        # read in yaml_file
-        with open(yaml_dir, 'r') as f_in:
-            yaml_contents = f_in.read()
-            new_model._yaml_model.update(yaml.full_load(yaml_contents))
+        yaml_contents = _load_yaml_file(yaml_dir)
+        new_model._yaml_model.update(yaml_contents)
 
         # check, if the model is valid
         new_model.validate_model()
diff --git a/yaml2sbml/yaml2sbml.py b/yaml2sbml/yaml2sbml.py
index 44dc201..a9a419b 100644
--- a/yaml2sbml/yaml2sbml.py
+++ b/yaml2sbml/yaml2sbml.py
@@ -4,6 +4,7 @@
 
 import libsbml as sbml
 import yaml
+from yaml.scanner import ScannerError
 
 from .yaml_validation import _validate_yaml_from_dict
 
@@ -105,19 +106,29 @@ def _create_compartment(model: sbml.Model):
 
 def _load_yaml_file(yaml_file: str) -> dict:
     """
-    Load YAML file to a dictionary.
+    Load YAML file from a dictionary.
 
     Arguments:
-        yaml_file: SBML model
+        yaml_file: directory YAML model
 
     Returns:
         yaml_dic: dictionary with parsed yaml file contents
+    Raises:
+        RuntimeError, if yaml can not be parsed, e.g. to incorrectly
+            formatted entries
     """
-    with open(yaml_file, 'r') as f_in:
-        yaml_contents = f_in.read()
-        yaml_dic = yaml.full_load(yaml_contents)
+    try:
+
+        with open(yaml_file, 'r') as f_in:
+            yaml_contents = f_in.read()
+            yaml_dict = yaml.full_load(yaml_contents)
 
-    return yaml_dic
+    except ScannerError:
+        raise RuntimeError('YAML file can not be parsed due to a Scanner '
+                           'Error. This commonly happens if formulas are '
+                           'starting with a minus. Please set them inside of '
+                           'brackets "(...)" or quotation marks.')
+    return yaml_dict
 
 
 def _convert_yaml_blocks_to_sbml(model: sbml.Model, yaml_dic: dict):
diff --git a/yaml2sbml/yaml_validation.py b/yaml2sbml/yaml_validation.py
index 3ee9c6e..2ead125 100644
--- a/yaml2sbml/yaml_validation.py
+++ b/yaml2sbml/yaml_validation.py
@@ -2,28 +2,37 @@
 import os
 import yaml
 import jsonschema
+from yaml.scanner import ScannerError
 import argparse
 
+
 SCHEMA = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                       "yaml_schema.yaml")
 
 
-def validate_yaml(yaml_file: str):
+def validate_yaml(yaml_dir: str):
     """
     Validate the syntax of the yaml file.
 
     Arguments:
-        yaml_file: path to yaml file to be validated
+        yaml_dir: path to yaml file to be validated
 
     Returns:
         jsonschema.validate
     """
-    # read in yaml_file
-    with open(yaml_file, 'r') as f_in:
-        yaml_contents = f_in.read()
-        yaml_in = yaml.full_load(yaml_contents)
+    try:
+
+        with open(yaml_dir, 'r') as f_in:
+            yaml_contents = f_in.read()
+            yaml_dict = yaml.full_load(yaml_contents)
+
+    except ScannerError:
+        raise RuntimeError('YAML file can not be parsed due to a Scanner '
+                           'Error. This commonly happens if formulas are '
+                           'starting with a minus. Please set them inside of '
+                           'brackets "(...)" or quotation marks.')
 
-    _validate_yaml_from_dict(yaml_in)
+    _validate_yaml_from_dict(yaml_dict)
     print('YAML file is valid ✅')
 
 


Test patch:
diff --git a/tests/test_YamlModel.py b/tests/test_YamlModel.py
index 7c26f1e..a076df8 100644
--- a/tests/test_YamlModel.py
+++ b/tests/test_YamlModel.py
@@ -5,6 +5,7 @@
 import tempfile
 
 from yaml2sbml.YamlModel import YamlModel
+from yaml2sbml.yaml_validation import validate_yaml
 
 
 class TestYamlModel(unittest.TestCase):
@@ -268,6 +269,21 @@ def test_valid_model(self):
 
         model.validate_model()
 
+    def test_minus_in_formula(self):
+        """Test writing a model for a right hand side starting with a minus."""
+        model = YamlModel()
+
+        model.add_ode(state_id='x',
+                      right_hand_side='-x',
+                      initial_value=1)
+
+        model.validate_model()
+
+        yaml_dir = os.path.join(self.test_dir, 'test_minus_sign.yaml')
+        model.write_to_yaml(yaml_dir)
+
+        validate_yaml(yaml_dir)
+
 
 if __name__ == '__main__':
     suite = unittest.TestSuite()