diff --git a/cana/utils.py b/cana/utils.py index ec9252c..5d1bc7f 100644 --- a/cana/utils.py +++ b/cana/utils.py @@ -142,11 +142,17 @@ def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): def output_transitions(eval_line, input_list): - """Returns an output list from combinatorically trying all input values + """Returns an output list from combinatorically trying all input values. + + Each input variable is assigned every possible binary combination (0/1) + via a namespace dict, and the boolean expression is evaluated with + ``eval()`` (builtins disabled, but this is not a security boundary). + Expressions must be trusted boolean rules using ``and``, ``or``, + ``not``, and parentheses, as produced by CANA model files. Args: - eval_line (string) : logic or arithmetic line to evaluate - input_list (list) : list of input variables + eval_line (string) : boolean expression to evaluate (e.g. "A and not B") + input_list (list) : list of input variable names Returns: list of all possible output transitions (list) @@ -168,19 +174,25 @@ def output_transitions(eval_line, input_list): 110 111 - A variable is dynamically created for each member of the input list - and assigned the corresponding value from each trail string. - The original eval_line is then evaluated with each assignment + Each input variable is assigned the corresponding value from each + trial string via a namespace dict, and the expression is evaluated which results in the output list [0, 0, 1, 0, 1, 0, 1, 0] """ total = 2 ** len(input_list) # Total combinations to try output_list = [] + # Use an explicit namespace dict for exec/eval so that dynamically + # created variables are visible across calls. In Python 3.13+ + # (PEP 667) bare exec() inside a function writes to a snapshot of + # locals that eval() cannot see. + ns = {} + safe_globals = {"__builtins__": {}} + code = compile(eval_line.strip(), "", "eval") for i in range(total): trial_string = statenum_to_binstate(i, len(input_list)) # Evaluate trial_string by assigning value to each input variable - for j, input in enumerate(input_list): - exec(input + "=" + trial_string[j]) - output_list.append(int(eval(eval_line))) + for j, input_name in enumerate(input_list): + ns[input_name] = int(trial_string[j]) + output_list.append(int(eval(code, safe_globals, ns))) return output_list diff --git a/tests/test_boolean_network.py b/tests/test_boolean_network.py index 90f2b94..f92d5df 100644 --- a/tests/test_boolean_network.py +++ b/tests/test_boolean_network.py @@ -1,6 +1,6 @@ from cana.boolean_network import BooleanNetwork import networkx as nx -from cana.datasets.bio import THALIANA +from cana.datasets.bio import THALIANA, LEUKEMIA def test_EG_weight_THALIANA(): """Test that effective graph in-degree edge weights are computed correctly.""" @@ -13,3 +13,39 @@ def test_EG_weight_THALIANA(): edgews = {edge: network._eg.edges[edge]["weight"] for edge in network._eg.edges if edge[1]==i} true.append(sum(edgews.values())) assert network.effective_indegrees() == sorted(true, reverse=True) + + +def test_output_transitions(): + """Test output_transitions produces correct truth tables.""" + from cana.utils import output_transitions + + assert output_transitions('A', ['A']) == [0, 1] + assert output_transitions('A and B', ['A', 'B']) == [0, 0, 0, 1] + assert output_transitions('A or B', ['A', 'B']) == [0, 1, 1, 1] + assert output_transitions('(A or B) and not C', ['A', 'B', 'C']) == [0, 0, 1, 0, 1, 0, 1, 0] + + +def test_from_string_boolean(): + """Test that BooleanNetwork.from_string_boolean parses logical rules.""" + rules = "\n".join([ + "A *= B", + "B *= A and C", + "C *= A or B", + ]) + network = BooleanNetwork.from_string_boolean(rules, keep_constants=True) + assert network.Nnodes == 3 + # A = copy of B: output transitions [0, 1] + assert network.nodes[0].outputs == ['0', '1'] + # B = A AND C: output transitions [0, 0, 0, 1] + assert network.nodes[1].outputs == ['0', '0', '0', '1'] + # C = A OR B: output transitions [0, 1, 1, 1] + assert network.nodes[2].outputs == ['0', '1', '1', '1'] + + +def test_load_logical_LEUKEMIA(): + """Test loading a real model in 'logical' boolean-rule format.""" + network = LEUKEMIA() + assert network.Nnodes == 60 + # Spot-check first rule: CTLA4* = TCR (identity) + assert network.nodes[0].name == 'CTLA4' + assert network.nodes[0].outputs == ['0', '1']