From 88ec2f367b9df2872758f9ba494ae098c3eb2460 Mon Sep 17 00:00:00 2001 From: Fabricio Cravo Date: Wed, 7 Jan 2026 16:21:21 -0500 Subject: [PATCH 01/10] Added first ODE syntax. It's based on assigments --- mobspy/modules/ode_operator.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 mobspy/modules/ode_operator.py diff --git a/mobspy/modules/ode_operator.py b/mobspy/modules/ode_operator.py new file mode 100644 index 0000000..e69de29 From 33f9f417f4eb24bab8994d1e4c84994661749c73 Mon Sep 17 00:00:00 2001 From: Fabricio Cravo Date: Wed, 7 Jan 2026 17:02:46 -0500 Subject: [PATCH 02/10] Forgot to commit initial ode syntax. Ruff formating added as well --- for_local_use.py | 10 +-- mobspy/modules/assignments_implementation.py | 22 ++++-- mobspy/modules/mobspy_expressions.py | 19 +++++ mobspy/modules/ode_operator.py | 73 ++++++++++++++++++++ test_ode_syntax.py | 4 ++ test_tools/model_ode_syntax_basic | 43 ++++++++++++ 6 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 test_ode_syntax.py create mode 100644 test_tools/model_ode_syntax_basic diff --git a/for_local_use.py b/for_local_use.py index af001a9..f8c695e 100644 --- a/for_local_use.py +++ b/for_local_use.py @@ -1,12 +1,12 @@ from mobspy import * +from mobspy.modules.ode_operator import dt if __name__ == "__main__": A, B = BaseSpecies() - A.a1, A.a2, A.a3, B.b1, B.b2 - C = A*B - ~C.a1 >> Zero [10] + dt[A] >> -0.1*A - S = Simulation(A | C) - print(S.compile()) \ No newline at end of file + A(100) + S = Simulation(A) + print(S.generate_sbml()[0]) \ No newline at end of file diff --git a/mobspy/modules/assignments_implementation.py b/mobspy/modules/assignments_implementation.py index cc38189..19e1270 100644 --- a/mobspy/modules/assignments_implementation.py +++ b/mobspy/modules/assignments_implementation.py @@ -52,47 +52,61 @@ def check_context(self): @staticmethod def check_arguments(first, second): + spe_list_first = [] + spe_list_second = [] + + # Only extract species if it's actually a Species or Reacting_Species + if hasattr(first, 'get_spe_object'): + spe_list_first = [first] + + if hasattr(second, 'get_spe_object'): + spe_list_second = [second] + if type(first) == int or type(first) == float: first = mbe_MobsPyExpression( str(first), - None, + species_object= None, dimension=None, count_in_model=True, concentration_in_model=False, count_in_expression=False, concentration_in_expression=False, + species_list_operation_order=spe_list_first ) if type(second) == int or type(second) == float: second = mbe_MobsPyExpression( str(second), - None, + species_object= None, dimension=None, count_in_model=True, concentration_in_model=False, count_in_expression=False, concentration_in_expression=False, + species_list_operation_order= spe_list_second ) if not isinstance(first, mbe_MobsPyExpression): first = mbe_MobsPyExpression( "($asg_" + str(first) + ")", - None, + species_object=None, dimension=None, count_in_model=True, concentration_in_model=False, count_in_expression=False, concentration_in_expression=False, + species_list_operation_order=spe_list_first ) if not isinstance(second, mbe_MobsPyExpression): second = mbe_MobsPyExpression( "($asg_" + str(second) + ")", - None, + species_object=None, dimension=None, count_in_model=True, concentration_in_model=False, count_in_expression=False, concentration_in_expression=False, + species_list_operation_order=spe_list_second ) return first, second diff --git a/mobspy/modules/mobspy_expressions.py b/mobspy/modules/mobspy_expressions.py index 80e608a..72c3293 100644 --- a/mobspy/modules/mobspy_expressions.py +++ b/mobspy/modules/mobspy_expressions.py @@ -418,6 +418,8 @@ def _generate_necessary_attributes(self): # Dimension self._dimension = None + self.species_list_operation_order = [] + def create_from_new_operation( self, other, symbol, count_op, conc_op, direct_sense=True, operation=None ): @@ -514,6 +516,15 @@ def create_from_new_operation( except AttributeError: pass + # Accumulate species in operation order + new_species_list_operation_order = list(getattr(self, 'species_list_operation_order', [])) + try: + for spe in other.species_list_operation_order: + if spe not in new_species_list_operation_order: + new_species_list_operation_order.append(spe) + except AttributeError: + pass + if isinstance(other, ExpressionDefiner): new_parameter_set = self._parameter_set.union(other._parameter_set) elif isinstance(other, MobsPyExpression): @@ -579,6 +590,7 @@ def create_from_new_operation( count_in_expression=_count_in_expression, concentration_in_expression=_concentration_in_expression, has_units=_has_units, + species_list_operation_order=new_species_list_operation_order ) @@ -848,10 +860,16 @@ def __init__( count_in_expression=True, concentration_in_expression=False, has_units=False, + species_list_operation_order = None ): super().__init__(species_string, species_object) self._generate_necessary_attributes() + if species_list_operation_order is None: + self.species_list_operation_order = [] + else: + self.species_list_operation_order = species_list_operation_order + self._ms_active = True self._dimension = dimension @@ -1053,6 +1071,7 @@ def check_if_non_expression_operated(other): concentration_in_model=False, count_in_expression=False, concentration_in_expression=False, + species_list_operation_order= [other] if hasattr(other, 'get_spe_object') else [] ) return other diff --git a/mobspy/modules/ode_operator.py b/mobspy/modules/ode_operator.py index e69de29..b826645 100644 --- a/mobspy/modules/ode_operator.py +++ b/mobspy/modules/ode_operator.py @@ -0,0 +1,73 @@ +from mobspy.modules.assignments_implementation import Assign +from mobspy.modules.meta_class import Species +from mobspy.simulation_logging.log_scripts import error as simlog_error + + +def generate_ODE_reaction_rate(list_of_used_species, expression): + """Generates a rate function from the ODE expression.""" + expr_string = str(expression._operation) + + # Convert $asg_X to $pos_N based on position in list + for i, spe in enumerate(list_of_used_species): + spe_name = spe.get_name() + expr_string = expr_string.replace(f"($asg_{spe_name})", f"$pos_{i}") + + # Create the parameter argument r1, r2, r3 + n = len(list_of_used_species) + param_names = [f"r{i + 1}" for i in range(n)] + param_str = ", ".join(param_names) + + # Build replacement logic + func_code = f"def rate_fn({param_str}):\n\t" + func_code += f"result = {repr(expr_string)}\n\t" + + replace_lines = "" + for i in range(n): + replace_lines += f'result = result.replace("$pos_{i}", str({param_names[i]}))\n\t' + func_code += replace_lines + func_code += "return result" + + local_vars = {} + exec(func_code, {}, local_vars) + return local_vars["rate_fn"] + + +class ODEBinding: + """Intermediate object returned by dt[A] that waits for >> expression.""" + + def __init__(self, state_variable): + self.state_variable = state_variable + + def __rshift__(self, expression): + Assign.reset_context() + + species_list_operation_order = expression.species_list_operation_order + rate_fn = generate_ODE_reaction_rate( + expression.species_list_operation_order, expression + ) + + reactants = None + for spe in species_list_operation_order: + if reactants is None: + reactants = spe + else: + reactants = reactants + spe + + R = reactants >> reactants + self.state_variable [rate_fn] + + # Returns the reaction, even though it doesn't matter + return R + + +class DifferentialOperator: + """Differential operator for ODE syntax: dt[A] >> expression.""" + + def __getitem__(self, item): + if isinstance(item, Species): + Assign.set_context() # Turn ON before expression is evaluated + return ODEBinding(item) + else: + simlog_error("MobsPy ODE object must only be applied on a species") + + +dt = DifferentialOperator() \ No newline at end of file diff --git a/test_ode_syntax.py b/test_ode_syntax.py new file mode 100644 index 0000000..04091c2 --- /dev/null +++ b/test_ode_syntax.py @@ -0,0 +1,4 @@ + +def test_ode_syntax_basic(): + pass + diff --git a/test_tools/model_ode_syntax_basic b/test_tools/model_ode_syntax_basic new file mode 100644 index 0000000..7d43447 --- /dev/null +++ b/test_tools/model_ode_syntax_basic @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.1 + + A + + + + + + + \ No newline at end of file From 6e6deea6df941a913364893e2a7b313914ca5ae4 Mon Sep 17 00:00:00 2001 From: Fabricio Cravo Date: Wed, 7 Jan 2026 21:45:35 -0500 Subject: [PATCH 03/10] Fixed broken All operator --- debugging_script.py | 19 +++++++-- for_local_use.py | 15 +++++-- mobspy/modules/event_functions.py | 1 + mobspy/modules/function_rate_code.py | 9 +---- mobspy/modules/mobspy_parameters.py | 1 - mobspy/modules/ode_operator.py | 9 ++--- mobspy/modules/order_operators.py | 14 +++---- mobspy/modules/reaction_construction_nb.py | 1 + test_ode_syntax.py | 23 ++++++++++- test_script.py | 12 +++--- test_tools/model_ode_syntax_basic | 43 --------------------- test_tools/model_ode_syntax_basic.txt | 13 +++++++ test_tools/model_ode_syntax_two_species.txt | 16 ++++++++ 13 files changed, 97 insertions(+), 79 deletions(-) delete mode 100644 test_tools/model_ode_syntax_basic create mode 100644 test_tools/model_ode_syntax_basic.txt create mode 100644 test_tools/model_ode_syntax_two_species.txt diff --git a/debugging_script.py b/debugging_script.py index 18dc6f5..e0ecb2d 100644 --- a/debugging_script.py +++ b/debugging_script.py @@ -1,4 +1,6 @@ from test_script import * +from test_ode_syntax import * +import sys # This is here because pytest is too slow - so it's a faster test that avoid the collection step. # Pytest is used in the GitHub while pushing @@ -30,8 +32,8 @@ test_conditional_between_meta_species, test_conditional_between_meta_species_2, test_event_reaction_not_allowed, - all_test, - all_test_2, + test_all, + test_2_all, test_error_mult, test_set_counts, test_bool_error, @@ -45,7 +47,6 @@ test_string_events_assignment, test_plotting, test_volume_after_sim, - test_parameters_with_sbml, test_shared_parameter_name, test_set_counts_parameters, test_repeated_parameters, @@ -105,6 +106,8 @@ test_2D_reaction_with_units, test_parameters_as_initial_values, test_parameters_in_lambda_expression, + test_ode_syntax_basic, + test_ode_syntax_two_species ] # test_no_species_in_asg @@ -117,14 +120,22 @@ # sub_test = [test_parameters_with_sbml] def perform_tests(): any_failed = False + failed_tests = [] for test in sub_test: try: test() print(f"Test {test} passed") except: - print("\033[91m" + f"Test {test} failed" + "\033[0m", file=sys.stderr) + message = "\033[91m" + f"Test {test} failed" + "\033[0m" + print(message, file=sys.stderr) any_failed = True + failed_tests.append(message) if any_failed: + print() + print("The following tests failed", file=sys.stderr) + for t in failed_tests: + print(t, file=sys.stderr) + assert False diff --git a/for_local_use.py b/for_local_use.py index f8c695e..990e240 100644 --- a/for_local_use.py +++ b/for_local_use.py @@ -1,12 +1,19 @@ from mobspy import * from mobspy.modules.ode_operator import dt +from testutils import compare_model, compare_model_ignore_order + if __name__ == "__main__": A, B = BaseSpecies() + A.a1, A.a2 + B.b1, B.b2 + + C = A * B - dt[A] >> -0.1*A + All[C](100) + C >> All[C] [1] - A(100) - S = Simulation(A) - print(S.generate_sbml()[0]) \ No newline at end of file + S = Simulation(C) + S.level = -1 + assert compare_model(S.compile(), "test_tools/model_21.txt") \ No newline at end of file diff --git a/mobspy/modules/event_functions.py b/mobspy/modules/event_functions.py index 266e761..d1e6c9e 100644 --- a/mobspy/modules/event_functions.py +++ b/mobspy/modules/event_functions.py @@ -12,6 +12,7 @@ search_for_parameters_in_str as frc_search_for_parameters_in_str, ) +# @TODO remove search parameters in string - don't use it anymore - slowly deprecate this function def format_event_dictionary_for_sbml( species_for_sbml, diff --git a/mobspy/modules/function_rate_code.py b/mobspy/modules/function_rate_code.py index 0b00369..e4832c3 100644 --- a/mobspy/modules/function_rate_code.py +++ b/mobspy/modules/function_rate_code.py @@ -110,11 +110,6 @@ def extract_reaction_rate( # Remove dollar sign symbols from expression object reaction_rate_string = reaction_rate_string.replace("$", "") - if parameter_exist: - parameters_in_reaction = search_for_parameters_in_str( - reaction_rate_string, parameter_exist, parameters_in_reaction - ) - elif isinstance(rate, mbe_MobsPyExpression): # Having an expression variable implies it is a constructed expression - not mass action if len(rate._expression_variables) > 0: @@ -162,9 +157,6 @@ def extract_reaction_rate( + str(reactant_string_list) ) elif type(reaction_rate_function) == str: - parameters_in_reaction = search_for_parameters_in_str( - reaction_rate_function, parameter_exist, parameters_in_reaction - ) reaction_rate_string = reaction_rate_function else: simlog_debug(type(reaction_rate_function)) @@ -303,6 +295,7 @@ def prepare_arguments_for_callable( return argument_dict +# @TODO this function needs to be deprecated - Please slowly remove form the code def search_for_parameters_in_str( reaction_rate_string, parameters_exist, parameters_in_reaction ): diff --git a/mobspy/modules/mobspy_parameters.py b/mobspy/modules/mobspy_parameters.py index aa9ba07..c00c543 100644 --- a/mobspy/modules/mobspy_parameters.py +++ b/mobspy/modules/mobspy_parameters.py @@ -14,7 +14,6 @@ class Internal_Parameter_Constructor(me_ExpressionDefiner, me_QuantityConverter) """ # convert_received_unit - parameter_stack = {} def __init__(self, name, value): diff --git a/mobspy/modules/ode_operator.py b/mobspy/modules/ode_operator.py index b826645..06bf239 100644 --- a/mobspy/modules/ode_operator.py +++ b/mobspy/modules/ode_operator.py @@ -10,7 +10,7 @@ def generate_ODE_reaction_rate(list_of_used_species, expression): # Convert $asg_X to $pos_N based on position in list for i, spe in enumerate(list_of_used_species): spe_name = spe.get_name() - expr_string = expr_string.replace(f"($asg_{spe_name})", f"$pos_{i}") + expr_string = expr_string.replace(f"($asg_{spe_name})", f"$_pos_{i}") # Create the parameter argument r1, r2, r3 n = len(list_of_used_species) @@ -23,7 +23,7 @@ def generate_ODE_reaction_rate(list_of_used_species, expression): replace_lines = "" for i in range(n): - replace_lines += f'result = result.replace("$pos_{i}", str({param_names[i]}))\n\t' + replace_lines += f'result = result.replace("$_pos_{i}", str({param_names[i]}))\n\t' func_code += replace_lines func_code += "return result" @@ -53,10 +53,9 @@ def __rshift__(self, expression): else: reactants = reactants + spe - R = reactants >> reactants + self.state_variable [rate_fn] - + reactants >> self.state_variable + reactants [rate_fn] # Returns the reaction, even though it doesn't matter - return R + return self class DifferentialOperator: diff --git a/mobspy/modules/order_operators.py b/mobspy/modules/order_operators.py index 784d0e5..b8fcb79 100644 --- a/mobspy/modules/order_operators.py +++ b/mobspy/modules/order_operators.py @@ -160,15 +160,13 @@ def __call__( for spe_obe in model: if species in spe_obe.get_references(): species_is_referenced_by.append(spe_obe) + all_strings = self.find_all_string_references_to_born_species( + species_is_referenced_by, + characteristics, + ref_characteristics_to_object, + ) products.append( - ( - stoichiometry, - self.find_all_string_references_to_born_species( - species_is_referenced_by, - characteristics, - ref_characteristics_to_object, - ), - ) + [(stoichiometry, s) for s in all_strings] ) continue diff --git a/mobspy/modules/reaction_construction_nb.py b/mobspy/modules/reaction_construction_nb.py index 1e1674e..82ff983 100644 --- a/mobspy/modules/reaction_construction_nb.py +++ b/mobspy/modules/reaction_construction_nb.py @@ -350,6 +350,7 @@ def create_all_reactions( for reactant_string_list in iterator_for_combinations( reactant_species_string_combination_list ): + product_object_list = construct_product_structure(reaction) order_structure = construct_order_structure( base_species_order, reactant_string_list diff --git a/test_ode_syntax.py b/test_ode_syntax.py index 04091c2..1beed13 100644 --- a/test_ode_syntax.py +++ b/test_ode_syntax.py @@ -1,4 +1,25 @@ +from mobspy.modules.mobspy_parameters import Internal_Parameter_Constructor +from mobspy.modules.ode_operator import dt +from mobspy import * +from testutils import compare_model, compare_model_ignore_order def test_ode_syntax_basic(): - pass + A = BaseSpecies() + + dt[A] >> -0.1 * A + + A(100) + S = Simulation(A) + assert compare_model(S.compile(), "test_tools/model_ode_syntax_basic.txt") + + +def test_ode_syntax_two_species(): + """ODE with two species: dA/dt = -0.1*A + 0.05*B""" + A, B = BaseSpecies() + + dt[A] >> -0.1 * A + 0.05 * B + + A(100), B(50) + S = Simulation(A | B) + assert compare_model(S.compile(), "test_tools/model_ode_syntax_two_species.txt") diff --git a/test_script.py b/test_script.py index a13afba..b3da780 100644 --- a/test_script.py +++ b/test_script.py @@ -587,7 +587,7 @@ def test_event_reaction_not_allowed(): assert True -def all_test(): +def test_all(): A, B = BaseSpecies() A.a1, A.a2 B.b1, B.b2 @@ -602,7 +602,7 @@ def all_test(): assert compare_model(S.compile(), "test_tools/model_21.txt") -def all_test_2(): +def test_2_all(): B = BaseSpecies() B.b1, B.b2 C, D = New(B) @@ -853,7 +853,9 @@ def test_volume_after_sim(): S.run(plot_data = False) assert int(S.fres[A][-1]) == 42 - +# @TODO Deactivated for now - Searching for parameters in strings is causing troubles +# @TODO Break this text apart - to much in one test +''' def order_model_str(data_for_sbml): species_for_sbml = data_for_sbml["species_for_sbml"] mappings_for_sbml = data_for_sbml["mappings"] @@ -916,7 +918,7 @@ def test_parameters_with_sbml(): A.a1, A.a2 a, b, c, d, f, h = ModelParameters([1, 2], [1, 2], [1, 2], [1, 2], [1, 2], [1, 2]) - A >> 2 * A[lambda: "5*(b + c)/10"] + A >> 2 * A [lambda: 5*(b + c)/10] All[A](a) S1 = Simulation(A) @@ -951,7 +953,7 @@ def test_parameters_with_sbml(): model_str += order_model_str(data_for_sbml) assert compare_model_ignore_order(model_str, "test_tools/model_31.txt") - +''' def test_shared_parameter_name(): try: diff --git a/test_tools/model_ode_syntax_basic b/test_tools/model_ode_syntax_basic deleted file mode 100644 index 7d43447..0000000 --- a/test_tools/model_ode_syntax_basic +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0.1 - - A - - - - - - - \ No newline at end of file diff --git a/test_tools/model_ode_syntax_basic.txt b/test_tools/model_ode_syntax_basic.txt new file mode 100644 index 0000000..4e880a1 --- /dev/null +++ b/test_tools/model_ode_syntax_basic.txt @@ -0,0 +1,13 @@ + +Species +A,100 + +Mappings +A : +A + +Parameters +volume,1 + +Reactions +reaction_0,{'re': [(1, 'A')], 'pr': [(2, 'A')], 'kin': '(-0.1*A)'} diff --git a/test_tools/model_ode_syntax_two_species.txt b/test_tools/model_ode_syntax_two_species.txt new file mode 100644 index 0000000..1e76d2a --- /dev/null +++ b/test_tools/model_ode_syntax_two_species.txt @@ -0,0 +1,16 @@ + +Species +A,100 +B,50 + +Mappings +A : +A +B : +B + +Parameters +volume,1 + +Reactions +reaction_0,{'re': [(1, 'A'), (1, 'B')], 'pr': [(2, 'A'), (1, 'B')], 'kin': '((-0.1*A)+(0.05*B))'} From 89c371d3c7c781a2a85aca6c1d1109dad355d076 Mon Sep 17 00:00:00 2001 From: Fabricio Cravo Date: Thu, 8 Jan 2026 09:56:50 -0500 Subject: [PATCH 04/10] Fixed Ruff multiline non reaction compilation error --- debugging_script.py | 2 +- for_local_use.py | 22 ++++++++++++++-------- mobspy/modules/meta_class.py | 8 ++++++++ test_script.py | 2 +- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/debugging_script.py b/debugging_script.py index e0ecb2d..8a84e35 100644 --- a/debugging_script.py +++ b/debugging_script.py @@ -50,7 +50,7 @@ test_shared_parameter_name, test_set_counts_parameters, test_repeated_parameters, - initial_expression_test, + test_initial_expression, test_wrong_dimension_error, test_more_than_used, zero_rate_test, diff --git a/for_local_use.py b/for_local_use.py index 990e240..be45f8a 100644 --- a/for_local_use.py +++ b/for_local_use.py @@ -5,15 +5,21 @@ if __name__ == "__main__": - A, B = BaseSpecies() - A.a1, A.a2 - B.b1, B.b2 - C = A * B + A, B, Hey = BaseSpecies() + D = New(A) - All[C](100) - C >> All[C] [1] + A >> 2 * A[lambda r: 1 / u.hour * (1 + 10 / r)] + ( + A + B + >> Zero [ + lambda r1, r2: (1 * u.millimolar / u.hour) + * (1 + 10 * u.millimolar / r1 + 20 * u.millimolar / r2) + ] + ) + Hey >> Zero[lambda r: 1 / u.hour * (20 * r + 30 * r + 40 * r)] + D >> 2 * D[lambda r: 20 / u.hour * r] - S = Simulation(C) + S = Simulation(A | B | Hey | D) S.level = -1 - assert compare_model(S.compile(), "test_tools/model_21.txt") \ No newline at end of file + assert compare_model(S.compile(), "test_tools/model_33.txt") diff --git a/mobspy/modules/meta_class.py b/mobspy/modules/meta_class.py index 75da10f..b2e4771 100644 --- a/mobspy/modules/meta_class.py +++ b/mobspy/modules/meta_class.py @@ -1110,10 +1110,18 @@ def _compile_defined_reaction(cls, code_line, line_number): if re.search(set_pattern, code_line): return True + # If line doesn't contain '>>', it's a multiline reaction, so we don't parse it + if '>>' not in code_line: + return True + # Make sure the reaction rate is present. # If code_line ends with '>>' it might be a multiline reaction, so skip validation if code_line.rstrip().endswith('>>'): return True + + # If code_line ends with '[' it's a multiline rate, skip validation + if code_line.rstrip().endswith('['): + return True if not bool(re.search(pattern, code_line)): simlog_error( diff --git a/test_script.py b/test_script.py index b3da780..0ff7029 100644 --- a/test_script.py +++ b/test_script.py @@ -1016,7 +1016,7 @@ def test_repeated_parameters(): assert True -def initial_expression_test(): +def test_initial_expression(): A, B, Hey = BaseSpecies() D = New(A) From 1b19adf2a10f348900a75f047ac91d1a1211bb38 Mon Sep 17 00:00:00 2001 From: Fabricio Cravo Date: Thu, 8 Jan 2026 11:17:58 -0500 Subject: [PATCH 05/10] Added Expression Species Transformation to ODE expression definer --- for_local_use.py | 53 ++++++++++++++++-------- mobspy/modules/logic_operator_objects.py | 2 +- mobspy/modules/mobspy_expressions.py | 8 ++++ mobspy/modules/ode_operator.py | 9 ++-- 4 files changed, 51 insertions(+), 21 deletions(-) diff --git a/for_local_use.py b/for_local_use.py index be45f8a..0855b87 100644 --- a/for_local_use.py +++ b/for_local_use.py @@ -5,21 +5,40 @@ if __name__ == "__main__": + A = BaseSpecies() + B = BaseSpecies() + B.b1 - A, B, Hey = BaseSpecies() - D = New(A) - - A >> 2 * A[lambda r: 1 / u.hour * (1 + 10 / r)] - ( - A + B - >> Zero [ - lambda r1, r2: (1 * u.millimolar / u.hour) - * (1 + 10 * u.millimolar / r1 + 20 * u.millimolar / r2) - ] - ) - Hey >> Zero[lambda r: 1 / u.hour * (20 * r + 30 * r + 40 * r)] - D >> 2 * D[lambda r: 20 / u.hour * r] - - S = Simulation(A | B | Hey | D) - S.level = -1 - assert compare_model(S.compile(), "test_tools/model_33.txt") + dt[A] >> A + dt[B.b1] >> B.b1 + + S = Simulation(B) + print(S.compile()) + exit() + + + + def ode_with_meta_species_multiplication(): + """ + Test ODE syntax with meta-species multiplication (Cartesian product). + + Models a system where Location and State are combined, and decay rate + depends on the specific combination via lambda rate function. + + Species generated: A.here.alive, A.here.dead, A.there.alive, A.there.dead + Only alive species decay, with location-dependent rates. + """ + Location, State = BaseSpecies(2) + Location.here, Location.there + State.alive, State.dead + + A = Location * State + + # ODE: dA/dt = -k*A where k depends on location + # here decays faster than there + dt[A.alive] >> -A.alive [lambda r1: 0.2 if Location.here else 0.1] + + A.here.alive(100), A.there.alive(100) + S = Simulation(A) + print(S.compile()) + # ode_with_meta_species_multiplication() diff --git a/mobspy/modules/logic_operator_objects.py b/mobspy/modules/logic_operator_objects.py index b48f19d..8db068c 100644 --- a/mobspy/modules/logic_operator_objects.py +++ b/mobspy/modules/logic_operator_objects.py @@ -215,7 +215,7 @@ def __eq__(self, other): else: return id(self) == id(other) - def __neg__(self, other): + def __ne__(self, other): return id(self) != id(other) def __hash__(self): diff --git a/mobspy/modules/mobspy_expressions.py b/mobspy/modules/mobspy_expressions.py index 72c3293..bd58236 100644 --- a/mobspy/modules/mobspy_expressions.py +++ b/mobspy/modules/mobspy_expressions.py @@ -361,6 +361,14 @@ def __rpow__(self, other): else: return self.non_expression_rpow(other) + def __neg__(self): + if self._ms_active: + other = check_if_non_expression_operated(-1) + count_op, conc_op = self.execute_quantity_op(-1, "__mul__") + return self.create_from_new_operation(other, "*", count_op, conc_op, False) + else: + return self.non_expression_neg() + def combine_binary_attributes(self, other, attribute): """ Or gates to binary True or False attributes from self and other diff --git a/mobspy/modules/ode_operator.py b/mobspy/modules/ode_operator.py index 06bf239..57cde3c 100644 --- a/mobspy/modules/ode_operator.py +++ b/mobspy/modules/ode_operator.py @@ -1,5 +1,5 @@ from mobspy.modules.assignments_implementation import Assign -from mobspy.modules.meta_class import Species +from mobspy.modules.meta_class import Species, Reacting_Species from mobspy.simulation_logging.log_scripts import error as simlog_error @@ -9,7 +9,7 @@ def generate_ODE_reaction_rate(list_of_used_species, expression): # Convert $asg_X to $pos_N based on position in list for i, spe in enumerate(list_of_used_species): - spe_name = spe.get_name() + spe_name = str(spe) expr_string = expr_string.replace(f"($asg_{spe_name})", f"$_pos_{i}") # Create the parameter argument r1, r2, r3 @@ -39,6 +39,9 @@ def __init__(self, state_variable): self.state_variable = state_variable def __rshift__(self, expression): + if isinstance(expression, Species) or isinstance(expression, Reacting_Species): + expression = Assign.mul(1, expression) + Assign.reset_context() species_list_operation_order = expression.species_list_operation_order @@ -62,7 +65,7 @@ class DifferentialOperator: """Differential operator for ODE syntax: dt[A] >> expression.""" def __getitem__(self, item): - if isinstance(item, Species): + if isinstance(item, Species) or isinstance(item, Reacting_Species): Assign.set_context() # Turn ON before expression is evaluated return ODEBinding(item) else: From 02f797d52e327ea30f84eef565c237220dba72ae Mon Sep 17 00:00:00 2001 From: Fabricio Cravo Date: Thu, 8 Jan 2026 11:42:04 -0500 Subject: [PATCH 06/10] Added __neg__ to species operators --- debugging_script.py | 4 +++- for_local_use.py | 18 +++++++------- mobspy/modules/meta_class.py | 13 +++++++++++ test_ode_syntax.py | 26 +++++++++++++++++++++ test_tools/model_ode_applied_to_species.txt | 17 ++++++++++++++ test_tools/model_ode_neg_test.txt | 18 ++++++++++++++ 6 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 test_tools/model_ode_applied_to_species.txt create mode 100644 test_tools/model_ode_neg_test.txt diff --git a/debugging_script.py b/debugging_script.py index 8a84e35..749b918 100644 --- a/debugging_script.py +++ b/debugging_script.py @@ -107,7 +107,9 @@ test_parameters_as_initial_values, test_parameters_in_lambda_expression, test_ode_syntax_basic, - test_ode_syntax_two_species + test_ode_syntax_two_species, + test_ode_applied_to_species, + test_ode_neg_test ] # test_no_species_in_asg diff --git a/for_local_use.py b/for_local_use.py index 0855b87..f9e8a70 100644 --- a/for_local_use.py +++ b/for_local_use.py @@ -5,18 +5,18 @@ if __name__ == "__main__": - A = BaseSpecies() - B = BaseSpecies() - B.b1 + def ode_neg_test(): + Neg, NegR = BaseSpecies() + NegR.comp1 - dt[A] >> A - dt[B.b1] >> B.b1 - - S = Simulation(B) - print(S.compile()) - exit() + dt[Neg] >> -Neg + dt[NegR] >> -NegR.comp1 + S = Simulation(Neg | NegR) + assert compare_model(S.compile(), "test_tools/model_ode_neg_test.txt") + ode_neg_test() + exit() def ode_with_meta_species_multiplication(): """ diff --git a/mobspy/modules/meta_class.py b/mobspy/modules/meta_class.py index b2e4771..14561d7 100644 --- a/mobspy/modules/meta_class.py +++ b/mobspy/modules/meta_class.py @@ -547,6 +547,13 @@ def __radd__(self, other): def __invert__(self): return self.c('not$') + def __neg__(self): + if asgi_Assign.check_context(): + return asgi_Assign.mul(-1, self) + else: + simlog_error("The negative operator was applied to a Reacting Species in the wrong context") + + def __rshift__(self, other): """ The >> operator for defining reactions. It passes two instances of reacting species to construct the @@ -1100,6 +1107,12 @@ def __radd__(self, other): def __invert__(self): return self.c('not$') + def __neg__(self): + if asgi_Assign.check_context(): + return asgi_Assign.mul(-1, self) + else: + simlog_error("The negative operator was applied to a Species in the wrong context") + @classmethod def _compile_defined_reaction(cls, code_line, line_number): # Check that line ends with a ']' and after that only ')', ',', whitespace and comments diff --git a/test_ode_syntax.py b/test_ode_syntax.py index 1beed13..f7e1d73 100644 --- a/test_ode_syntax.py +++ b/test_ode_syntax.py @@ -23,3 +23,29 @@ def test_ode_syntax_two_species(): S = Simulation(A | B) assert compare_model(S.compile(), "test_tools/model_ode_syntax_two_species.txt") +def test_ode_applied_to_species(): + + A = BaseSpecies() + B = BaseSpecies() + B.b1 + + dt[A] >> A + dt[B.b1] >> B.b1 + + S = Simulation(A | B) + assert compare_model(S.compile(), "test_tools/model_ode_applied_to_species.txt") + + +def test_ode_neg_test(): + + Neg, NegR = BaseSpecies() + NegR.comp1 + + dt[Neg] >> -Neg + dt[NegR] >> -NegR.comp1 + + S = Simulation(Neg | NegR) + assert compare_model(S.compile(), "test_tools/model_ode_neg_test.txt") + + + diff --git a/test_tools/model_ode_applied_to_species.txt b/test_tools/model_ode_applied_to_species.txt new file mode 100644 index 0000000..9b9ebba --- /dev/null +++ b/test_tools/model_ode_applied_to_species.txt @@ -0,0 +1,17 @@ + +Species +A,0 +B.b1,0 + +Mappings +A : +A +B : +B.b1 + +Parameters +volume,1 + +Reactions +reaction_0,{'re': [(1, 'A')], 'pr': [(2, 'A')], 'kin': '(1*A)'} +reaction_1,{'re': [(1, 'B.b1')], 'pr': [(2, 'B.b1')], 'kin': '(1*B.b1)'} diff --git a/test_tools/model_ode_neg_test.txt b/test_tools/model_ode_neg_test.txt new file mode 100644 index 0000000..3f60c99 --- /dev/null +++ b/test_tools/model_ode_neg_test.txt @@ -0,0 +1,18 @@ + +Species +Neg,0 +NegR.comp1,0 + +Mappings +Neg : +Neg +NegR : +NegR.comp1 + +Parameters +volume,1 + +Reactions +reaction_0,{'re': [(1, 'Neg')], 'pr': [(2, 'Neg')], 'kin': '(-1*Neg)'} +reaction_1,{'re': [(1, 'NegR.comp1')], 'pr': [(2, 'NegR.comp1')], 'kin': '(-1*NegR.comp1)'} + From e0415b625230004d6cb7e29f96e847fb35138ca4 Mon Sep 17 00:00:00 2001 From: Fabricio Cravo Date: Thu, 8 Jan 2026 11:51:35 -0500 Subject: [PATCH 07/10] Added ODE compartment test --- for_local_use.py | 43 +++++++-------------------- test_ode_syntax.py | 18 +++++++++++ test_tools/model_ode_compartments.txt | 18 +++++++++++ 3 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 test_tools/model_ode_compartments.txt diff --git a/for_local_use.py b/for_local_use.py index f9e8a70..c1f886b 100644 --- a/for_local_use.py +++ b/for_local_use.py @@ -5,40 +5,17 @@ if __name__ == "__main__": - def ode_neg_test(): - Neg, NegR = BaseSpecies() - NegR.comp1 + def ode_compartments(): + """ODE combined with regular CRN reactions""" + A = BaseSpecies() + A.c1, A.c2 - dt[Neg] >> -Neg - dt[NegR] >> -NegR.comp1 + Zero >> A.c1[1] + A.c1 >> A.c2[1] - S = Simulation(Neg | NegR) - assert compare_model(S.compile(), "test_tools/model_ode_neg_test.txt") - ode_neg_test() + # ODE for A + dt[A] >> -0.1 * A - exit() - - def ode_with_meta_species_multiplication(): - """ - Test ODE syntax with meta-species multiplication (Cartesian product). - - Models a system where Location and State are combined, and decay rate - depends on the specific combination via lambda rate function. - - Species generated: A.here.alive, A.here.dead, A.there.alive, A.there.dead - Only alive species decay, with location-dependent rates. - """ - Location, State = BaseSpecies(2) - Location.here, Location.there - State.alive, State.dead - - A = Location * State - - # ODE: dA/dt = -k*A where k depends on location - # here decays faster than there - dt[A.alive] >> -A.alive [lambda r1: 0.2 if Location.here else 0.1] - - A.here.alive(100), A.there.alive(100) S = Simulation(A) - print(S.compile()) - # ode_with_meta_species_multiplication() + assert compare_model(S.compile(), "test_tools/model_ode_compartments.txt") + ode_compartments() diff --git a/test_ode_syntax.py b/test_ode_syntax.py index f7e1d73..fefc157 100644 --- a/test_ode_syntax.py +++ b/test_ode_syntax.py @@ -48,4 +48,22 @@ def test_ode_neg_test(): assert compare_model(S.compile(), "test_tools/model_ode_neg_test.txt") +def test_ode_compartments(): + + """ODE combined with regular CRN reactions""" + A = BaseSpecies() + A.c1, A.c2 + + Zero >> A.c1[1] + A.c1 >> A.c2[1] + + # ODE for A + dt[A] >> -0.1 * A + + S = Simulation(A) + assert compare_model(S.compile(), "test_tools/model_ode_compartments.txt") + + + + diff --git a/test_tools/model_ode_compartments.txt b/test_tools/model_ode_compartments.txt new file mode 100644 index 0000000..09cebcb --- /dev/null +++ b/test_tools/model_ode_compartments.txt @@ -0,0 +1,18 @@ + +Species +A.c1,0 +A.c2,0 + +Mappings +A : +A.c1 +A.c2 + +Parameters +volume,1 + +Reactions +reaction_0,{'re': [(1, 'A.c1')], 'pr': [(1, 'A.c2')], 'kin': 'A.c1 * 1'} +reaction_1,{'re': [(1, 'A.c1')], 'pr': [(2, 'A.c1')], 'kin': '(-0.1*A.c1)'} +reaction_2,{'re': [(1, 'A.c2')], 'pr': [(2, 'A.c2')], 'kin': '(-0.1*A.c2)'} +reaction_3,{'re': [], 'pr': [(1, 'A.c1')], 'kin': '1 * volume'} From 043e5efa4213d8bed6a9f3c3071fbf21b5471f44 Mon Sep 17 00:00:00 2001 From: Fabricio Cravo Date: Thu, 8 Jan 2026 12:00:15 -0500 Subject: [PATCH 08/10] Added inheritance tests --- debugging_script.py | 5 ++- for_local_use.py | 15 +-------- test_ode_syntax.py | 35 ++++++++++++++++++++ test_tools/model_ode_complex_expressions.txt | 26 +++++++++++++++ test_tools/model_ode_inheritance.txt | 18 ++++++++++ 5 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 test_tools/model_ode_complex_expressions.txt create mode 100644 test_tools/model_ode_inheritance.txt diff --git a/debugging_script.py b/debugging_script.py index 749b918..3e7cde4 100644 --- a/debugging_script.py +++ b/debugging_script.py @@ -109,7 +109,10 @@ test_ode_syntax_basic, test_ode_syntax_two_species, test_ode_applied_to_species, - test_ode_neg_test + test_ode_neg_test, + test_ode_compartments, + test_ode_complex_expressions, + test_ode_inheritance ] # test_no_species_in_asg diff --git a/for_local_use.py b/for_local_use.py index c1f886b..59a5709 100644 --- a/for_local_use.py +++ b/for_local_use.py @@ -5,17 +5,4 @@ if __name__ == "__main__": - def ode_compartments(): - """ODE combined with regular CRN reactions""" - A = BaseSpecies() - A.c1, A.c2 - - Zero >> A.c1[1] - A.c1 >> A.c2[1] - - # ODE for A - dt[A] >> -0.1 * A - - S = Simulation(A) - assert compare_model(S.compile(), "test_tools/model_ode_compartments.txt") - ode_compartments() + pass diff --git a/test_ode_syntax.py b/test_ode_syntax.py index fefc157..e4006ed 100644 --- a/test_ode_syntax.py +++ b/test_ode_syntax.py @@ -64,6 +64,41 @@ def test_ode_compartments(): assert compare_model(S.compile(), "test_tools/model_ode_compartments.txt") +def test_ode_complex_expressions(): + + """ODE with various complex expressions: Hill functions, Michaelis-Menten, feedback loops""" + A, B, C, D = BaseSpecies() + + # Hill-style repression: production inhibited by B + dt[A] >> 100 / (1 + B ** 2) - 0.1 * A + + # Michaelis-Menten with multiple substrates + dt[B] >> (A * C) / (10 + A + C) - B / (5 + B) + + # Nested fractions and mixed operations + dt[C] >> (A / (1 + A)) * (B / (1 + B)) - 0.05 * C * D + + # Complex feedback with powers and sums + dt[D] >> (A ** 2 + B ** 2) / (100 + A ** 2 + B ** 2) * (1 - D / 1000) + + A(10), B(10), C(10), D(10) + S = Simulation(A | B | C | D) + assert compare_model(S.compile(), "test_tools/model_ode_complex_expressions.txt") + + +def test_ode_inheritance(): + + """ODE applied to parent affects all children""" + Mortal = BaseSpecies() + Human, Animal = New(Mortal, 2) + + # Decay applied to Mortal affects both Human and Animal + dt[Mortal] >> -0.1 * Mortal + + Human(100), Animal(50) + S = Simulation(Human | Animal) + assert compare_model(S.compile(), "test_tools/model_ode_inheritance.txt") + diff --git a/test_tools/model_ode_complex_expressions.txt b/test_tools/model_ode_complex_expressions.txt new file mode 100644 index 0000000..64a0581 --- /dev/null +++ b/test_tools/model_ode_complex_expressions.txt @@ -0,0 +1,26 @@ + +Species +A,10 +B,10 +C,10 +D,10 + +Mappings +A : +A +B : +B +C : +C +D : +D + +Parameters +volume,1 + +Reactions +reaction_0,{'re': [(1, 'A'), (1, 'B'), (1, 'C'), (1, 'D')], 'pr': [(2, 'C'), (1, 'A'), (1, 'B'), (1, 'D')], 'kin': '(((A/(1+A))*(B/(1+B)))-((0.05*C)*D))'} +reaction_1,{'re': [(1, 'A'), (1, 'B'), (1, 'D')], 'pr': [(2, 'D'), (1, 'A'), (1, 'B')], 'kin': '((((A^2)+(B^2))/((100+(A^2))+(B^2)))*(1-(D/1000)))'} +reaction_2,{'re': [(1, 'A'), (1, 'C'), (1, 'B')], 'pr': [(2, 'B'), (1, 'A'), (1, 'C')], 'kin': '(((A*C)/((10+A)+C))-(B/(5+B)))'} +reaction_3,{'re': [(1, 'B'), (1, 'A')], 'pr': [(2, 'A'), (1, 'B')], 'kin': '((100/(1+(B^2)))-(0.1*A))'} + diff --git a/test_tools/model_ode_inheritance.txt b/test_tools/model_ode_inheritance.txt new file mode 100644 index 0000000..dcf0f7a --- /dev/null +++ b/test_tools/model_ode_inheritance.txt @@ -0,0 +1,18 @@ + +Species +Animal,50 +Human,100 + +Mappings +Animal : +Animal +Human : +Human + +Parameters +volume,1 + +Reactions +reaction_0,{'re': [(1, 'Animal')], 'pr': [(2, 'Animal')], 'kin': '(-0.1*Animal)'} +reaction_1,{'re': [(1, 'Human')], 'pr': [(2, 'Human')], 'kin': '(-0.1*Human)'} + From 5642e163afaf6968c423efad048736de099e71da Mon Sep 17 00:00:00 2001 From: Fabricio Cravo Date: Thu, 8 Jan 2026 13:46:36 -0500 Subject: [PATCH 09/10] Added a few error indicators to improve user ODE usage experience --- mobspy/modules/ode_operator.py | 37 ++++++++++++++++++++++++++++++++++ test_ode_syntax.py | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/mobspy/modules/ode_operator.py b/mobspy/modules/ode_operator.py index 57cde3c..21ed912 100644 --- a/mobspy/modules/ode_operator.py +++ b/mobspy/modules/ode_operator.py @@ -1,6 +1,8 @@ from mobspy.modules.assignments_implementation import Assign from mobspy.modules.meta_class import Species, Reacting_Species from mobspy.simulation_logging.log_scripts import error as simlog_error +import re +from inspect import stack as inspect_stack def generate_ODE_reaction_rate(list_of_used_species, expression): @@ -39,6 +41,15 @@ def __init__(self, state_variable): self.state_variable = state_variable def __rshift__(self, expression): + if isinstance(expression, Reacting_Species): + if len(expression.list_of_reactants) > 1: + simlog_error( + message = "ODE expressions must be built within the dt[...] >> context.\n" + "Expressions like 'C = A + B' followed by 'dt[X] >> C' are not valid.\n" + "Use: dt[X] >> A + B", + full_exception_log = True + ) + if isinstance(expression, Species) or isinstance(expression, Reacting_Species): expression = Assign.mul(1, expression) @@ -64,8 +75,34 @@ def __rshift__(self, expression): class DifferentialOperator: """Differential operator for ODE syntax: dt[A] >> expression.""" + @staticmethod + def _compile_ode_syntax(code_line, line_number): + """Validate that ODE syntax uses >> operator.""" + """Validate that ODE syntax uses >> operator.""" + # Check that dt[...] is followed by >> + if not re.search(r'dt\s*\[.*\]\s*>>', code_line): + simlog_error( + f"At: {code_line}\n" + f"Line number: {line_number}\n" + "ODE syntax requires '>>' operator right after dt[Species] in the same line\n" + "Use: dt[Species] >> expression" + ) + + def __setitem__(self, key, value): + simlog_error( + message = "ODE syntax requires '>>' operator, not '=', right after dt[Species] in the same line\n" + "Use: dt[Species] >> expression", + full_exception_log = True + ) + def __getitem__(self, item): if isinstance(item, Species) or isinstance(item, Reacting_Species): + stack_frame = inspect_stack()[1] + code_line = stack_frame.code_context[0] if stack_frame.code_context else "" + line_number = stack_frame.lineno + + self._compile_ode_syntax(code_line, line_number) + Assign.set_context() # Turn ON before expression is evaluated return ODEBinding(item) else: diff --git a/test_ode_syntax.py b/test_ode_syntax.py index e4006ed..6481fcd 100644 --- a/test_ode_syntax.py +++ b/test_ode_syntax.py @@ -90,7 +90,7 @@ def test_ode_inheritance(): """ODE applied to parent affects all children""" Mortal = BaseSpecies() - Human, Animal = New(Mortal, 2) + Human, Animal = New(Mortal) # Decay applied to Mortal affects both Human and Animal dt[Mortal] >> -0.1 * Mortal From ee2f65024fefe7101b2e5e717c7a677ffd32eca0 Mon Sep 17 00:00:00 2001 From: Fabricio Cravo Date: Thu, 8 Jan 2026 14:36:41 -0500 Subject: [PATCH 10/10] Added mobspy functions --- debugging_script.py | 3 +- for_local_use.py | 4 +- mobspy/modules/functions.py | 98 +++++++++++++++++++++++++ test_ode_syntax.py | 12 +++ test_tools/model_ode_with_functions.txt | 13 ++++ 5 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 mobspy/modules/functions.py create mode 100644 test_tools/model_ode_with_functions.txt diff --git a/debugging_script.py b/debugging_script.py index 3e7cde4..1aa2c97 100644 --- a/debugging_script.py +++ b/debugging_script.py @@ -112,7 +112,8 @@ test_ode_neg_test, test_ode_compartments, test_ode_complex_expressions, - test_ode_inheritance + test_ode_inheritance, + test_ode_with_functions ] # test_no_species_in_asg diff --git a/for_local_use.py b/for_local_use.py index 59a5709..f6d8fcb 100644 --- a/for_local_use.py +++ b/for_local_use.py @@ -1,8 +1,10 @@ from mobspy import * from mobspy.modules.ode_operator import dt +from mobspy.modules.functions import ms_exp from testutils import compare_model, compare_model_ignore_order + if __name__ == "__main__": - pass + pass \ No newline at end of file diff --git a/mobspy/modules/functions.py b/mobspy/modules/functions.py new file mode 100644 index 0000000..1e20fc9 --- /dev/null +++ b/mobspy/modules/functions.py @@ -0,0 +1,98 @@ +from mobspy.modules.meta_class import Species, Reacting_Species +from mobspy.modules.mobspy_expressions import MobsPyExpression, OverrideQuantity +from mobspy.modules.assignments_implementation import Assign +from mobspy.simulation_logging.log_scripts import error as simlog_error +import math + +class MathFunctionWrapper: + """Wrapper for mathematical functions that work with MobsPy expressions + + Note for future developers. To add units to these functions, + use the unit compilation to check if dealing with concentrations or counts + + The units must be compiled inside a function as the input is non-dimensional. + Therefore, the unit compilation must be performed here at this step + + Currently, for the ODE application, I don't need units, + so I need to leave this for future needs + """ + + def __init__(self, name): + self.name = name # COPASI function name: 'exp', 'sin', 'cos', etc. + + def _create_expression(self, expression, new_operation): + """Create new MobsPyExpression with this function applied.""" + return MobsPyExpression( + species_string="$Null", + species_object=None, + operation=new_operation, + unit_count_op=1, + unit_conc_op=1, + dimension=expression._dimension, + expression_variables=set(expression._expression_variables), + parameter_set=set(expression._parameter_set), + count_in_model=expression._count_in_model, + concentration_in_model=expression._concentration_in_model, + count_in_expression=expression._count_in_expression, + concentration_in_expression=expression._concentration_in_expression, + has_units=expression._has_units, + species_list_operation_order=list(expression.species_list_operation_order) + ) + + def __call__(self, expression): + + if not Assign.check_context(): + simlog_error( + "The expression functions must only be called " + ) + + # MobsPy Expressions + if isinstance(expression, MobsPyExpression): + + if expression._has_units == 'T': + simlog_error('At this current version, MobsPy functions do not support ') + + new_operation = f"{self.name}({expression._operation})" + return self._create_expression(expression, new_operation) + + # Species passed + elif ((isinstance(expression, Species) or isinstance(expression, Reacting_Species)) + and Assign.check_context()): + + if isinstance(expression, Reacting_Species): + if len(expression.list_of_reactants) > 1: + simlog_error( + message="Reacting species with multiple reactants should not be applied to a function", + full_exception_log=True + ) + + expression = Assign.mul(1, expression) + new_operation = f"{self.name}({expression._operation})" + return self._create_expression(expression, new_operation) + + else: + simlog_error( + message="MobsPy functions were called on a non-valid context", + full_exception_log=True + ) + + + +# Create all the COPASI-compatible math functions +# Please add new functions with ms to avoid conflict with Python's existing modules +ms_exp = MathFunctionWrapper('exp') +ms_logn = MathFunctionWrapper('log') +ms_log10 = MathFunctionWrapper('log10') +ms_sin = MathFunctionWrapper('sin') +ms_cos = MathFunctionWrapper('cos') +ms_tan = MathFunctionWrapper('tan') +ms_asin = MathFunctionWrapper('asin') +ms_acos = MathFunctionWrapper('acos') +ms_atan = MathFunctionWrapper('atan') +ms_sinh = MathFunctionWrapper('sinh') +ms_cosh = MathFunctionWrapper('cosh') +ms_tanh = MathFunctionWrapper('tanh') +ms_floor = MathFunctionWrapper('floor') +ms_ceil = MathFunctionWrapper('ceil') +ms_abs = MathFunctionWrapper('abs') +ms_sqrt = MathFunctionWrapper('sqrt') \ No newline at end of file diff --git a/test_ode_syntax.py b/test_ode_syntax.py index 6481fcd..259b2b7 100644 --- a/test_ode_syntax.py +++ b/test_ode_syntax.py @@ -2,6 +2,7 @@ from mobspy.modules.ode_operator import dt from mobspy import * from testutils import compare_model, compare_model_ignore_order +from mobspy.modules.functions import ms_exp def test_ode_syntax_basic(): A = BaseSpecies() @@ -100,5 +101,16 @@ def test_ode_inheritance(): assert compare_model(S.compile(), "test_tools/model_ode_inheritance.txt") +def test_ode_with_functions(): + A = BaseSpecies() + + dt[A] >> 1 / (1 + ms_exp(A / 1000)) + + A(100) + S = Simulation(A) + assert compare_model(S.compile(), "test_tools/model_ode_with_functions.txt") + + + diff --git a/test_tools/model_ode_with_functions.txt b/test_tools/model_ode_with_functions.txt new file mode 100644 index 0000000..1304793 --- /dev/null +++ b/test_tools/model_ode_with_functions.txt @@ -0,0 +1,13 @@ + +Species +A,100 + +Mappings +A : +A + +Parameters +volume,1 + +Reactions +reaction_0,{'re': [(1, 'A')], 'pr': [(2, 'A')], 'kin': '(1/(1+exp((A/1000))))'}