Repository: sybila/eBCSgen

Issue:
define observables
define observables - pools, scaling (basic arithmetics).

Discussion:

Patch:
diff --git a/eBCSgen/Parsing/ParseBCSL.py b/eBCSgen/Parsing/ParseBCSL.py
index 4bb2843..8a3973c 100644
--- a/eBCSgen/Parsing/ParseBCSL.py
+++ b/eBCSgen/Parsing/ParseBCSL.py
@@ -106,13 +106,14 @@ class SideHelper:
 
 GRAMMAR = r"""
     model: (sections)* rules (sections | rules)*
-    sections: inits | definitions | complexes | regulation
+    sections: inits | definitions | complexes | regulation | observables
 
     rules: RULES_START _NL+ (rule _NL+)* rule _NL*
     inits: INITS_START _NL+ (init _NL+)* init _NL*
     definitions: DEFNS_START _NL+ (definition _NL+)* definition _NL*
     complexes: COMPLEXES_START _NL+ (cmplx_dfn _NL+)* cmplx_dfn _NL*
     regulation: REGULATION_START _NL+ regulation_def _NL*
+    observables: OBSERVABLES_START _NL+ (observable _NL+)* observable _NL*
 
     init: const? rate_complex 
     definition: def_param "=" number
@@ -138,6 +139,7 @@ GRAMMAR = r"""
     DEFNS_START: "#! definitions"
     COMPLEXES_START: "#! complexes"
     REGULATION_START: "#! regulation"
+    OBSERVABLES_START: "#! observables"
     _NL: /(\r?\n[\t ]*)+/
 
     !label: CNAME "~"
@@ -239,6 +241,11 @@ REGEX_GRAMMAR = r"""
     REGEX_CHAR: /[^\\^$().*+?{}\[\]|]/
 """
 
+OBSERVABLES_GRAMMAR = """
+    observable: CNAME ":" observable_pattern
+    !observable_pattern: const | complex | observable_pattern "+" observable_pattern | observable_pattern "-" observable_pattern | observable_pattern "*" observable_pattern | observable_pattern "/" observable_pattern | observable_pattern POW const | "(" observable_pattern ")"
+"""
+
 
 class TransformRegulations(Transformer):
     def regulation(self, matches):
@@ -663,15 +670,19 @@ class TreeToObjects(Transformer):
         reversible = False
         if arrow == "<=>":
             reversible = True
-        return reversible, Rule(
-            agents,
-            mid,
-            compartments,
-            complexes,
-            pairs,
-            Rate(rate1) if rate1 else None,
-            label,
-        ), Rate(rate2) if rate2 else None
+        return (
+            reversible,
+            Rule(
+                agents,
+                mid,
+                compartments,
+                complexes,
+                pairs,
+                Rate(rate1) if rate1 else None,
+                label,
+            ),
+            Rate(rate2) if rate2 else None,
+        )
 
     def rules(self, matches):
         rules = []
@@ -703,6 +714,15 @@ class TreeToObjects(Transformer):
                 result[init[0].children[0]] = 1
         return {"inits": result}
 
+    def observable(self, matches):
+        return {str(matches[0]): matches[1].children}
+
+    def observables(self, matches):
+        result = dict()
+        for observable in matches[1:]:
+            result.update(observable)
+        return {"observables": result}
+
     def param(self, matches):
         self.params.add(str(matches[0]))
         return Tree("param", matches)
@@ -712,6 +732,7 @@ class TreeToObjects(Transformer):
         definitions = dict()
         regulation = None
         inits = collections.Counter()
+        observables = dict()
         for match in matches:
             if type(match) == dict:
                 key, value = list(match.items())[0]
@@ -728,6 +749,8 @@ class TreeToObjects(Transformer):
                 inits.update(value)
             elif key == "definitions":
                 definitions.update(value)
+            elif key == "observables":
+                observables.update(value)
             elif key == "regulation":
                 if regulation:
                     raise UnspecifiedParsingError("Multiple regulations")
@@ -749,9 +772,13 @@ class Parser:
             + EXTENDED_GRAMMAR
             + REGULATIONS_GRAMMAR
             + REGEX_GRAMMAR
+            + OBSERVABLES_GRAMMAR
         )
         self.parser = Lark(
-            grammar, parser="earley", propagate_positions=False, maybe_placeholders=False
+            grammar,
+            parser="earley",
+            propagate_positions=False,
+            maybe_placeholders=False,
         )
 
         self.terminals = dict((v, k) for k, v in _TERMINAL_NAMES.items())
@@ -856,7 +883,7 @@ class Parser:
             return Result(
                 False,
                 {
-                    "unexpected": str(u.token), 
+                    "unexpected": str(u.token),
                     "expected": self.replace(u.expected),
                     "line": u.line,
                     "column": u.column,


Test patch:
diff --git a/Testing/objects_testing.py b/Testing/objects_testing.py
index a324d26..bd1b9dd 100644
--- a/Testing/objects_testing.py
+++ b/Testing/objects_testing.py
@@ -22,6 +22,8 @@ rate_complex_parser = Parser("rate_complex")
 rule_parser = Parser("rule")
 rules_parser = Parser("rules")
 model_parser = Parser("model")
+observables_parser = Parser("observables")
+observable_parser = Parser("observable")
 
 # atomic
 a1 = AtomicAgent("T", "s")
diff --git a/Testing/parsing/test_observables.py b/Testing/parsing/test_observables.py
new file mode 100644
index 0000000..75340a9
--- /dev/null
+++ b/Testing/parsing/test_observables.py
@@ -0,0 +1,67 @@
+import pytest
+
+import Testing.objects_testing as objects
+
+
+def test_parser():
+    observable_expr1 = "abc: A()::cell"
+    assert objects.observable_parser.parse(observable_expr1)
+
+    observable_expr2 = "efg: E(F{_})::cell"
+    assert objects.observable_parser.parse(observable_expr2)
+
+    observable_expr3 = "hij: H()::cell"
+    assert objects.observable_parser.parse(observable_expr3)
+
+    observable_expr4 = "klm: K()::cyt * L()::cell + M()::cell"
+    assert objects.observable_parser.parse(observable_expr4)
+
+    observable_expr5 = "nop: N()::cell"
+    assert objects.observable_parser.parse(observable_expr5).success
+
+    observable_expr6 = "qrs: Q().R().S()::cell"
+    assert objects.observable_parser.parse(observable_expr6).success
+
+    observable_expr7 = "tuv: T(U{v})::cell "
+    assert objects.observable_parser.parse(observable_expr7).success
+
+    observable_expr8 = "wx: 2 * W{x}::cell"
+    assert objects.observable_parser.parse(observable_expr8).success
+
+    observable_expr9 = "z: Y{z}::cyt + Z{y}::ext"
+    assert objects.observable_parser.parse(observable_expr9).success
+
+    observable_expr10 = "z: 2 * Y{z}::cyt + Z{y}::ext ** 2"
+    assert objects.observable_parser.parse(observable_expr10).success
+
+    observable_expr10 = "z: (Y{z}::cell + Z{y}::cyt) / 2.1 ** 10"
+    assert objects.observable_parser.parse(observable_expr10).success
+
+    observable_expr11 = "scaled_A: 1000 * A{i}::cell"
+    assert objects.observable_parser.parse(observable_expr11).success
+
+    observable_expr12 = "obs_A_all: A{i}::cell + A{a}::cell"
+    assert objects.observable_parser.parse(observable_expr12).success
+
+    observables_expr = (
+        "#! observables\n"
+        + observable_expr1
+        + "\n"
+        + observable_expr2
+        + "\n"
+        + observable_expr3
+        + "\n"
+        + observable_expr4
+        + "\n"
+        + observable_expr5
+        + "\n"
+        + observable_expr6
+    )
+    assert objects.observables_parser.parse(observables_expr).success
+
+    assert not objects.observable_parser.parse("A()::cell > 2").success
+    assert not objects.observable_parser.parse("a: A(::cell").success
+    assert not objects.observable_parser.parse("a: b: A():cell > 2").success
+    assert not objects.observable_parser.parse("a: 2 > A():cell").success
+    assert not objects.observable_parser.parse("a: A()::cell$").success
+    assert not objects.observable_parser.parse("a: A{}::cell").success