Repository: sybila/eBCSgen

Issue:
replication rules
replication rules - this feature is quite hardcoded, the first question is whether we want to support it at all, then how to do it in a robust way

Discussion:

Patch:
diff --git a/eBCSgen/Parsing/ParseBCSL.py b/eBCSgen/Parsing/ParseBCSL.py
index 4bb2843..480317f 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
@@ -116,7 +116,7 @@ GRAMMAR = r"""
 
     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,8 +131,10 @@ 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"
@@ -647,18 +649,21 @@ 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 == "<=>":


Test patch:
diff --git a/Testing/objects_testing.py b/Testing/objects_testing.py
index a324d26..a39e1fc 100644
--- a/Testing/objects_testing.py
+++ b/Testing/objects_testing.py
@@ -352,6 +352,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_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