from bs4 import NavigableString
from rate_laws import convert_rate_law
import math  # needed for check_sign_numerically()


def collate_interactions(child_classifications):
    child_classifications = set(child_classifications)

    # all terms of same kind
    if len(child_classifications) == 1:
        return list(child_classifications)[0]

    # both kinds of monotonic terms
    if "monotonic_increasing" in child_classifications and "monotonic_decreasing" in child_classifications:
        return "mixed"

    # constant and one kind of monotonic term
    if len(child_classifications) == 2 and "monotonic_increasing" in child_classifications:
        return "monotonic_increasing"

    if len(child_classifications) == 2 and "monotonic_decreasing" in child_classifications:
        return "monotonic_decreasing"


def invert_classification(classification):
    if classification in ["mixed", "constant"]:
        return classification

    if classification == "monotonic_increasing":
        return "monotonic_decreasing"

    if classification == "monotonic_decreasing":
        return "monotonic_increasing"


def classify_basic_interaction_symbolically(operator, child_classifications):
    # if any children are mixed, so is result
    if "mixed" in child_classifications:
        return "mixed"

    # monotonic function of one input:
    if operator in ["root", "exp", "ln", "log", "floor", "ceiling", "factorial", "delay"]:
        return child_classifications[0]

    if operator == "power":
        return collate_interactions(child_classifications)
        # TODO: check sign of second argument, which is in general hard

    if operator in ["plus", "times"]:
        # plus is an N-ary function
        return collate_interactions(child_classifications)

    # minus is unary or binary
    if operator == "minus":
        # unary case
        if len(child_classifications) == 1:
            invert_classification(child_classifications[0])

        # binary case
        child_classifications[1] = invert_classification(child_classifications[1])
        return collate_interactions(child_classifications)

    if operator == "divide":
        # binary operator
        child_classifications[1] = invert_classification(child_classifications[1])
        return collate_interactions(child_classifications)


def categorise_interaction(kinetic_law, species_id, categorise_symbolically=False):

    if categorise_symbolically:
        for math_expr in kinetic_law.select_one("math"):
            if isinstance(math_expr, NavigableString):
                continue
            return categorise_interaction_inner(math_expr, species_id)
    else:
        for math_expr in kinetic_law.select_one("math"):
            if isinstance(math_expr, NavigableString):
                continue
            return categorise_interaction_inner_numerically(math_expr, species_id)


def categorise_interaction_inner(expression, species_id):
    # We implicitly assume constants and powers are positive.

    if expression.name == "cn":
        return "constant"

    if expression.name == "ci":
        if expression.string.strip() == species_id:
            return "monotonic_increasing"
        else:
            return "constant"

    # from a BS4 object, find the identity of the operator and array of it children
    operator = None
    args = []
    for child in expression.children:
        if not operator:
            operator = child.name
            if child.name == "csymbol" and child.string.strip() == "delay":
                operator = "delay"
        else:
            if isinstance(child, NavigableString):
                continue
            args.append(child)

    # classify each of the children
    child_classifications = []
    for arg in args:
        child_classifications.append(categorise_interaction_inner(arg, species_id))

    # combine these classifications based on identity of function
    return classify_basic_interaction_symbolically(operator, child_classifications)


def categorise_interaction_inner_numerically(expression, species_id):
    # identify all parameters and concentrations in the rate law
    symbols = []
    for ci in expression.findAll("ci"):
        symbols.append(ci.text.strip())
    symbols = set(symbols)

    return check_sign_numerically(expression, symbols, species_id)


def check_sign_numerically(expr, param_names, species_id):
    # This function will fail to report that a kineticLaw is mixed (rather than monotonic) if:
    # - the sign of its gradient depends on value of one of the parameters (eg x^a/x^b)
    # - the sign of its gradient depends on the concentration of the corresponding species

    expr = convert_rate_law(expr, variables_not_to_substitute=[species_id])

    replacement = {'^': '**', 'exp': 'math.exp', 'ln': 'math.log', 'log': 'math.log10', 'ceiling': 'math.ceil',
                   'floor': 'math.floor', 'factorial': 'math.factorial', 'delay': '', 'pi': 'math.pi',
                   'exponentiale': 'math.e', 'infinity': 'float("Inf")'}

    for old in replacement:
        expr = expr.replace(old, replacement[old])

    param_names.remove(species_id)

    rate_change = eval(expr.replace(species_id, '1')) - eval(expr.replace(species_id, '0'))

    if rate_change > 0:
        return "monotonic_increasing"
    elif rate_change == 0:
        return "constant"
    else:
        return "monotonic_decreasing"

@@ -126,2324 +126,56 @@
 ef c
-ollate_interactions(child_classifications):%0A    child_classifications = set(child_classifications)%0A%0A    # all terms of same kind%0A    if len(child_classifications) == 1:%0A        return list(child_classifications)%5B0%5D%0A%0A    # both kinds of monotonic terms%0A    if %22monotonic_increasing%22 in child_classifications and %22monotonic_decreasing%22 in child_classifications:%0A        return %22mixed%22%0A%0A    # constant and one kind of monotonic term%0A    if len(child_classifications) == 2 and %22monotonic_increasing%22 in child_classifications:%0A        return %22monotonic_increasing%22%0A%0A    if len(child_classifications) == 2 and %22monotonic_decreasing%22 in child_classifications:%0A        return %22monotonic_decreasing%22%0A%0A%0Adef invert_classification(classification):%0A    if classification in %5B%22mixed%22, %22constant%22%5D:%0A        return classification%0A%0A    if classification == %22monotonic_increasing%22:%0A        return %22monotonic_decreasing%22%0A%0A    if classification == %22monotonic_decreasing%22:%0A        return %22monotonic_increasing%22%0A%0A%0Adef classify_basic_interaction_symbolically(operator, child_classifications):%0A    # if any children are mixed, so is result%0A    if %22mixed%22 in child_classifications:%0A        return %22mixed%22%0A%0A    # monotonic function of one input:%0A    if operator in %5B%22root%22, %22exp%22, %22ln%22, %22log%22, %22floor%22, %22ceiling%22, %22factorial%22, %22delay%22%5D:%0A        return child_classifications%5B0%5D%0A%0A    if operator == %22power%22:%0A        return collate_interactions(child_classifications)%0A        # TODO: check sign of second argument, which is in general hard%0A%0A    if operator in %5B%22plus%22, %22times%22%5D:%0A        # plus is an N-ary function%0A        return collate_interactions(child_classifications)%0A%0A    # minus is unary or binary%0A    if operator == %22minus%22:%0A        # unary case%0A        if len(child_classifications) == 1:%0A            invert_classification(child_classifications%5B0%5D)%0A%0A        # binary case%0A        child_classifications%5B1%5D = invert_classification(child_classifications%5B1%5D)%0A        return collate_interactions(child_classifications)%0A%0A    if operator == %22divide%22:%0A        # binary operator%0A        child_classifications%5B1%5D = invert_classification(child_classifications%5B1%5D)%0A        return collate_interactions(child_classifications)%0A%0A%0Adef categorise_interaction(kinetic_law, species_id, categorise_symbolically=False):%0A%0A    if categorise_symbolically:%0A    
+ategorise_interaction(kinetic_law, species_id):%0A
     
@@ -215,36 +215,32 @@
 ct_one(%22math%22):%0A
-    
         if isins
@@ -278,36 +278,32 @@
 g):%0A            
-    
 continue%0A       
@@ -307,1423 +307,77 @@
     
-    return categorise_interaction_inner(math_expr, species_id)%0A    else:%0A        for math_expr in kinetic_law.select_one(%22math%22):%0A            if isinstance(math_expr, NavigableString):%0A                continue%0A            return categorise_interaction_inner_numerically(math_expr, species_id)%0A%0A%0Adef categorise_interaction_inner(expression, species_id):%0A    # We implicitly assume constants and powers are positive.%0A%0A    if expression.name == %22cn%22:%0A        return %22constant%22%0A%0A    if expression.name == %22ci%22:%0A        if expression.string.strip() == species_id:%0A            return %22monotonic_increasing%22%0A        else:%0A            return %22constant%22%0A%0A    # from a BS4 object, find the identity of the operator and array of it children%0A    operator = None%0A    args = %5B%5D%0A    for child in expression.children:%0A        if not operator:%0A            operator = child.name%0A            if child.name == %22csymbol%22 and child.string.strip() == %22delay%22:%0A                operator = %22delay%22%0A        else:%0A            if isinstance(child, NavigableString):%0A                continue%0A            args.append(child)%0A%0A    # classify each of the children%0A    child_classifications = %5B%5D%0A    for arg in args:%0A        child_classifications.append(categorise_interaction_inner(arg, species_id))%0A%0A    # combine these classifications based on identity of function%0A    return classify_basic_interaction_symbolically(operator, child_classifications
+return categorise_interaction_inner_numerically(math_expr, species_id
 )%0A%0A%0A

Remove symbolic heuristic for categorising interactions (replaced by numerical approach in commit 27f9c028f)