Repository: sybila/eBCSgen

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

Comments:

Patch:
diff --git a/eBCSgen/Parsing/ParseBCSL.py b/eBCSgen/Parsing/ParseBCSL.py
index 4bb2843..8368f44 100644
--- a/eBCSgen/Parsing/ParseBCSL.py
+++ b/eBCSgen/Parsing/ParseBCSL.py
@@ -3,7 +3,7 @@ import json
 import numpy as np
 from numpy import inf
 from copy import deepcopy
-from lark import Lark, Token, Transformer, Tree
+from lark import Lark, Transformer, Tree
 from lark import UnexpectedCharacters, UnexpectedToken, UnexpectedEOF
 from lark.load_grammar import _TERMINAL_NAMES
 import regex
@@ -106,17 +106,18 @@ 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
-    rule: ((label)? side ARROW side ("@" rate)? (";" variable)?) | ((label)? side BI_ARROW side ("@" rate "|" rate )? (";" variable)?)
+    rule: ((label)? side arrow side ("@" rate)? (";" variable)?) | ((label)? side BI_ARROW side ("@" rate "|" rate )? (";" variable)?)
     cmplx_dfn: cmplx_name "=" value
 
     side: (const? complex "+")* (const? complex)?
@@ -131,13 +132,16 @@ GRAMMAR = r"""
 
     COM: "//"
     POW: "**"
-    ARROW: "=>"
+    arrow: SINGLE_ARROW | REPLICATION_ARROW
+    SINGLE_ARROW: "=>"
     BI_ARROW: "<=>"
+    REPLICATION_ARROW: "=*>"
     RULES_START: "#! rules"
     INITS_START: "#! inits"
     DEFNS_START: "#! definitions"
     COMPLEXES_START: "#! complexes"
     REGULATION_START: "#! regulation"
+    OBSERVABLES_START: "#! observables"
     _NL: /(\r?\n[\t ]*)+/
 
     !label: CNAME "~"
@@ -239,6 +243,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):
@@ -647,31 +656,38 @@ class TreeToObjects(Transformer):
             )
         )
         pairs = [(i, i + lhs.counter) for i in range(min(lhs.counter, rhs.counter))]
-        if lhs.counter > rhs.counter:
+        if type(arrow) is Tree and arrow.children[0].value == "=*>":
+            if lhs.counter >= rhs.counter or lhs.counter != 1 or rhs.counter <= 1:
+                raise UnspecifiedParsingError("Rule does not contain replication")
+            
+            for i in range(lhs.counter, rhs.counter):
+                if lhs.seq[pairs[-1][0]] == rhs.seq[pairs[-1][1] - lhs.counter]:
+                    if rhs.seq[pairs[-1][1] - lhs.counter] == rhs.seq[i]:
+                        pairs += [(pairs[-1][0], i + lhs.counter)]
+                    else:
+                        raise UnspecifiedParsingError("Rule does not contain replication")
+
+        elif lhs.counter > rhs.counter:
             pairs += [(i, None) for i in range(rhs.counter, lhs.counter)]
         elif lhs.counter < rhs.counter:
-            for i in range(lhs.counter, rhs.counter):
-                replication = False
-                if lhs.counter == 1 and rhs.counter > 1:
-                    if lhs.seq[pairs[-1][0]] == rhs.seq[pairs[-1][1] - lhs.counter]:
-                        if rhs.seq[pairs[-1][1] - lhs.counter] == rhs.seq[i]:
-                            pairs += [(pairs[-1][0], i + lhs.counter)]
-                            replication = True
-                if not replication:
-                    pairs += [(None, i + lhs.counter)]
+            pairs += [(None, i + lhs.counter) for i in range(lhs.counter, rhs.counter)]
 
         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 +719,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 +737,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 +754,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 +777,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 +888,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..171ba58 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")
@@ -352,6 +354,35 @@ rule_no_change = Rule(
     sequence_no_change, mid_c1, compartments_c1, complexes_c1, pairs_c1, rate_c1
 )
 
+sequence_repl1 = (s31, s31, s31)
+mid_repl1 = 1
+compartments_repl1 = ["rep"] * 3
+complexes_repl1 = [(0, 0), (1, 1), (2, 2)]
+pairs_repl1 = [(0, 1), (0, 2)]
+rate_repl1 = Rate("3.0*[X()::rep]/2.0*v_1")
+
+rule_repl1 = Rule(
+    sequence_repl1, mid_repl1, compartments_repl1, complexes_repl1, pairs_repl1, None
+)
+rule_repl1_rate = Rule(
+    sequence_repl1,
+    mid_repl1,
+    compartments_repl1,
+    complexes_repl1,
+    pairs_repl1,
+    rate_repl1,
+)
+
+repl_sequence2 = (s31, s31, s31, s31)
+mid_repl2 = 1
+compartments_repl2 = ["rep"] * 4
+complexes_repl2 = [(0, 0), (1, 1), (2, 2), (3, 3)]
+pairs_repl2 = [(0, 1), (0, 2), (0, 3)]
+
+rule_repl2 = Rule(
+    repl_sequence2, mid_repl2, compartments_repl2, complexes_repl2, pairs_repl2, None
+)
+
 # reactions
 
 reaction1 = Reaction(lhs, rhs, rate_5)
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
diff --git a/Testing/parsing/test_rule.py b/Testing/parsing/test_rule.py
index e3294e4..3ce55df 100644
--- a/Testing/parsing/test_rule.py
+++ b/Testing/parsing/test_rule.py
@@ -1,6 +1,7 @@
 import pytest
 
 import Testing.objects_testing as objects
+from eBCSgen.Core.Rate import Rate
 
 
 def test_parser():
@@ -70,3 +71,27 @@ def test_bidirectional():
 
     rule_expr = "#! rules\nK(S{u}).B()::cyt => K(S{p})::cyt + B()::cyt @ 3*[K()::cyt]/2*v_1 | 2*[K()::cyt]/3*v_1"
     assert not objects.rules_parser.parse(rule_expr).success
+
+
+def test_replication():
+    rule_expr = "X()::rep =*> X()::rep + X()::rep"
+    result = objects.rule_parser.parse(rule_expr)
+    assert result.success
+    assert result.data[1] == objects.rule_repl1
+
+    rule_expr = "X()::rep =*> X()::rep + X()::rep @ 3*[X()::rep]/2*v_1"
+    result = objects.rule_parser.parse(rule_expr)
+    assert result.success
+    rate_repl1 = Rate("3.0*[X()::rep]/2*v_1")
+    assert result.data[1] == objects.rule_repl1_rate
+
+    rule_expr = "X()::rep =*> X()::rep + X()::rep + X()::rep"
+    result = objects.rule_parser.parse(rule_expr)
+    assert result.success
+    assert result.data[1] == objects.rule_repl2
+
+    rule_expr = "X()::rep + Y()::rep =*> X()::rep + X()::rep"
+    assert not objects.rule_parser.parse(rule_expr).success
+
+    rule_expr = "X()::rep =*> X()::rep + X()::rep + Y()::rep"
+    assert not objects.rule_parser.parse(rule_expr).success