From 7e66e2add624911350fae536d801448811ba803c Mon Sep 17 00:00:00 2001 From: Bill Hlavacek Date: Sun, 10 May 2026 16:00:05 -0600 Subject: [PATCH] Render FunctionProduct rate-law type in resolve_ratelaw `RuleBlockXML.resolve_ratelaw` and `PopulationMapBlockXML.resolve_ratelaw` recognize Ele/Function/MM/Sat/Hill/Arrhenius but fall through to a silent `print("don't recognize rate law type")` for FunctionProduct, which then surfaces as the wrong rate constant in the regenerated BNGL. FunctionProduct is a real BNG2.pl rate-law modifier (`Perl2/RateLaw.pm:105`, `:137`, `:670`) used when a rule's rate is the product of two local functions evaluated in different reactant-pattern contexts (e.g. `mobility_factor(x) * mobility_factor(y)` in the BLBR_immobilization model family). Vanilla NFsim already supports it (`NFinput/NFinput.cpp:2251`), so the only thing standing between these models and a successful round-trip is the parser refusing the shape. Fix: add a FunctionProduct branch that emits FunctionProduct("name1(args1)","name2(args2)") mirroring BNG2.pl's own serializer at `Perl2/RateLaw.pm:670-677`. New helper `_ratelaw_arg_ids` accepts both single-Argument (dict) and multi-Argument (list) shapes the same way the existing MM/Sat handler does. Patched in both `RuleBlockXML` and `PopulationMapBlockXML` since both classes carry their own copy of `resolve_ratelaw`. --- bionetgen/modelapi/xmlparsers.py | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/bionetgen/modelapi/xmlparsers.py b/bionetgen/modelapi/xmlparsers.py index bf529821..5fc41e5f 100644 --- a/bionetgen/modelapi/xmlparsers.py +++ b/bionetgen/modelapi/xmlparsers.py @@ -600,6 +600,17 @@ def resolve_ratelaw(self, xml): rate_cts = rate_cts_xml["RateConstant"]["@value"] elif rate_type == "Function": rate_cts = xml["@name"] + elif rate_type == "FunctionProduct": + # Mirror BNG2.pl/Perl2/RateLaw.pm:670-677 — emit + # FunctionProduct("name1(args1)","name2(args2)") so the + # regenerated BNGL round-trips through BNG2.pl's parser + # and reaches NFsim, which supports FunctionProduct + # natively (NFinput.cpp:2251). + name1 = xml["@name1"] + name2 = xml["@name2"] + a1 = self._ratelaw_arg_ids(xml.get("ListOfArguments1")) + a2 = self._ratelaw_arg_ids(xml.get("ListOfArguments2")) + rate_cts = f'FunctionProduct("{name1}({a1})","{name2}({a2})")' elif ( rate_type == "MM" or rate_type == "Sat" @@ -621,6 +632,22 @@ def resolve_ratelaw(self, xml): print("don't recognize rate law type") return rate_cts + def _ratelaw_arg_ids(self, args_xml): + """Join the ``@id`` of each Argument in a ListOfArguments[N] element. + + BNG-XML packs a single Argument as a dict and multiple as a list, + so we accept both shapes. Returns "" when ``args_xml`` is None + or empty so callers can render zero-arg ``f()`` consistently. + """ + if not args_xml: + return "" + args = args_xml.get("Argument") if hasattr(args_xml, "get") else None + if args is None: + return "" + if isinstance(args, list): + return ",".join(str(a["@id"]) for a in args) + return str(args["@id"]) + def resolve_rxn_side(self, xml): # this is either reactant or product if xml is None: @@ -849,6 +876,17 @@ def resolve_ratelaw(self, xml): rate_cts = rate_cts_xml["RateConstant"]["@value"] elif rate_type == "Function": rate_cts = xml["@name"] + elif rate_type == "FunctionProduct": + # Mirror BNG2.pl/Perl2/RateLaw.pm:670-677 — emit + # FunctionProduct("name1(args1)","name2(args2)") so the + # regenerated BNGL round-trips through BNG2.pl's parser + # and reaches NFsim, which supports FunctionProduct + # natively (NFinput.cpp:2251). + name1 = xml["@name1"] + name2 = xml["@name2"] + a1 = self._ratelaw_arg_ids(xml.get("ListOfArguments1")) + a2 = self._ratelaw_arg_ids(xml.get("ListOfArguments2")) + rate_cts = f'FunctionProduct("{name1}({a1})","{name2}({a2})")' elif ( rate_type == "MM" or rate_type == "Sat" @@ -870,6 +908,22 @@ def resolve_ratelaw(self, xml): print("don't recognize rate law type") return rate_cts + def _ratelaw_arg_ids(self, args_xml): + """Join the ``@id`` of each Argument in a ListOfArguments[N] element. + + BNG-XML packs a single Argument as a dict and multiple as a list, + so we accept both shapes. Returns "" when ``args_xml`` is None + or empty so callers can render zero-arg ``f()`` consistently. + """ + if not args_xml: + return "" + args = args_xml.get("Argument") if hasattr(args_xml, "get") else None + if args is None: + return "" + if isinstance(args, list): + return ",".join(str(a["@id"]) for a in args) + return str(args["@id"]) + # TODO: Store operations! class Operation: