From 90d9fa8cb18bd5ef1faa55eed1b5133e415d109b Mon Sep 17 00:00:00 2001 From: Levi Naden Date: Thu, 12 Oct 2017 19:11:46 -0400 Subject: [PATCH 1/5] Add logical and bitwise operators for math_eval Adds the ability to provide additional operators to the math_eval function. Only adds the `and`, `&`, `or`, and `|` operators. This is a feature needed in an upcoming version of YANK --- openmmtools/utils.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openmmtools/utils.py b/openmmtools/utils.py index f57709ab8..f63b449ad 100644 --- a/openmmtools/utils.py +++ b/openmmtools/utils.py @@ -259,7 +259,10 @@ def math_eval(expression, variables=None, functions=None): # Supported operators. operators = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, - ast.Pow: operator.pow, ast.USub: operator.neg} + ast.Pow: operator.pow, ast.USub: operator.neg, + ast.BitAnd: operator.and_, ast.And: operator.and_, + ast.BitOr: operator.or_, ast.Or: operator.or_ + } # Supported functions, not defined in math. stock_functions = {'step': lambda x: 1 * (x >= 0), @@ -278,6 +281,19 @@ def _math_eval(node): elif isinstance(node, ast.BinOp): return operators[type(node.op)](_math_eval(node.left), _math_eval(node.right)) + elif isinstance(node, ast.BoolOp): + # Handle Ternary Boolean Operator which has no operator equivalent + def common_operator(values): + return operators[type(node.op)](*values) + # Pre-processes all nodes, + # This does remove the small "A and B" operation which skips evaluating B if A is False + processed_values = [*map(_math_eval, node.values)] + # Run through each value and apply the pairwise operations + while len(processed_values) > 1: + replacement_value = common_operator(processed_values[:2]) + processed_values.pop(0) + processed_values[0] = replacement_value + return processed_values[0] elif isinstance(node, ast.Name): try: return variables[node.id] From e0954ba863e4611812c54159ebf679fb52f9187a Mon Sep 17 00:00:00 2001 From: Levi Naden Date: Fri, 13 Oct 2017 07:50:11 -0400 Subject: [PATCH 2/5] Fix a python 2 compatibility thing by casting the map to list even if its a generator --- openmmtools/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openmmtools/utils.py b/openmmtools/utils.py index f63b449ad..afc8e8f9d 100644 --- a/openmmtools/utils.py +++ b/openmmtools/utils.py @@ -233,6 +233,9 @@ def math_eval(expression, variables=None, functions=None): - step_hm(x) : Heaviside step function with half-maximum convention. - sign(x) : sign function (0.0 for x=0.0) + Available operators are `+`, `-`, `*`, `/`, `**`, `-x` (negative), + `&`, `and`, `|`, and `or` + Parameters ---------- expression : str @@ -287,7 +290,7 @@ def common_operator(values): return operators[type(node.op)](*values) # Pre-processes all nodes, # This does remove the small "A and B" operation which skips evaluating B if A is False - processed_values = [*map(_math_eval, node.values)] + processed_values = list(map(_math_eval, node.values)) # Run through each value and apply the pairwise operations while len(processed_values) > 1: replacement_value = common_operator(processed_values[:2]) From 02e1713b3c732f2f26c22443b7707220d3d5bc75 Mon Sep 17 00:00:00 2001 From: Levi Naden Date: Fri, 13 Oct 2017 10:53:41 -0400 Subject: [PATCH 3/5] Added tests and additional docs saying how the `and` and `or` ops are bitwise, not logical --- openmmtools/tests/test_utils.py | 5 ++++- openmmtools/utils.py | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/openmmtools/tests/test_utils.py b/openmmtools/tests/test_utils.py index e39b2c3b1..1bc640683 100644 --- a/openmmtools/tests/test_utils.py +++ b/openmmtools/tests/test_utils.py @@ -57,7 +57,10 @@ def test_math_eval(): ('(x + lambda) / z * 4', {'x': 1, 'lambda': 2, 'z': 3}, 4.0), ('-((x + y) / z * 4)**2', {'x': 1, 'y': 2, 'z': 3}, -16.0), ('ceil(0.8) + acos(x) + step(0.5 - x) + step(0.5)', {'x': 1}, 2), - ('step_hm(x)', {'x': 0}, 0.5)] + ('step_hm(x)', {'x': 0}, 0.5), + ('{1,2,3} & {2,3,4}', None, {2, 3}), + ('myset or {2,3,4}', {'myset': {1, 2, 3}}, {1, 2, 3, 4}), + ('(myset or my2set) & {2, 3}', {'myset': {1, 2}, 'my2set': {3, 4}}, {2, 3})] for expression, variables, result in test_cases: evaluated_expression = math_eval(expression, variables) assert evaluated_expression == result, '{}, {}, {}'.format( diff --git a/openmmtools/utils.py b/openmmtools/utils.py index afc8e8f9d..9dac49f12 100644 --- a/openmmtools/utils.py +++ b/openmmtools/utils.py @@ -233,8 +233,11 @@ def math_eval(expression, variables=None, functions=None): - step_hm(x) : Heaviside step function with half-maximum convention. - sign(x) : sign function (0.0 for x=0.0) - Available operators are `+`, `-`, `*`, `/`, `**`, `-x` (negative), - `&`, `and`, `|`, and `or` + Available operators are ``+``, ``-``, ``*``, ``/``, ``**``, ``-x`` (negative), + ``&``, ``and``, ``|``, and ``or`` + + **The operators ``and`` and ``or`` operate BITWISE and behave the same as ``&`` and ``|`` respectively as this + function is not designed to handle logical operations.** Parameters ---------- @@ -248,7 +251,7 @@ def math_eval(expression, variables=None, functions=None): Returns ------- - float + result The result of the evaluated expression. Examples From eb858e0d765796637146a43c23f340d762e6da8f Mon Sep 17 00:00:00 2001 From: Levi Naden Date: Fri, 13 Oct 2017 13:33:04 -0400 Subject: [PATCH 4/5] Fixed tests, decided not to parse sets literally, but instead as variables --- openmmtools/tests/test_utils.py | 6 +++--- openmmtools/utils.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openmmtools/tests/test_utils.py b/openmmtools/tests/test_utils.py index 1bc640683..c8f4131bb 100644 --- a/openmmtools/tests/test_utils.py +++ b/openmmtools/tests/test_utils.py @@ -58,9 +58,9 @@ def test_math_eval(): ('-((x + y) / z * 4)**2', {'x': 1, 'y': 2, 'z': 3}, -16.0), ('ceil(0.8) + acos(x) + step(0.5 - x) + step(0.5)', {'x': 1}, 2), ('step_hm(x)', {'x': 0}, 0.5), - ('{1,2,3} & {2,3,4}', None, {2, 3}), - ('myset or {2,3,4}', {'myset': {1, 2, 3}}, {1, 2, 3, 4}), - ('(myset or my2set) & {2, 3}', {'myset': {1, 2}, 'my2set': {3, 4}}, {2, 3})] + ('myset & myset2', {'myset': {1,2,3}, 'myset2': {2,3,4}}, {2, 3}), + ('myset or myset2', {'myset': {1,2,3}, 'myset2': {2,3,4}}, {1, 2, 3, 4}), + ('(myset or my2set) & myset3', {'myset': {1, 2}, 'my2set': {3, 4}, 'myset3': {2, 3}}, {2, 3})] for expression, variables, result in test_cases: evaluated_expression = math_eval(expression, variables) assert evaluated_expression == result, '{}, {}, {}'.format( diff --git a/openmmtools/utils.py b/openmmtools/utils.py index 9dac49f12..3a3fb0ff7 100644 --- a/openmmtools/utils.py +++ b/openmmtools/utils.py @@ -237,7 +237,7 @@ def math_eval(expression, variables=None, functions=None): ``&``, ``and``, ``|``, and ``or`` **The operators ``and`` and ``or`` operate BITWISE and behave the same as ``&`` and ``|`` respectively as this - function is not designed to handle logical operations.** + function is not designed to handle logical operations.** If you provide sets, they must be as variables. Parameters ---------- From 38204d24f81eba6dc3c2ebb4ca0f0fa640cb6cbb Mon Sep 17 00:00:00 2001 From: Levi Naden Date: Fri, 13 Oct 2017 16:45:52 -0400 Subject: [PATCH 5/5] Better BoolOp method by Andrea, keeps with recusion instead of manual handling --- openmmtools/utils.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/openmmtools/utils.py b/openmmtools/utils.py index 3a3fb0ff7..f47ad9ce7 100644 --- a/openmmtools/utils.py +++ b/openmmtools/utils.py @@ -288,18 +288,14 @@ def _math_eval(node): return operators[type(node.op)](_math_eval(node.left), _math_eval(node.right)) elif isinstance(node, ast.BoolOp): - # Handle Ternary Boolean Operator which has no operator equivalent - def common_operator(values): - return operators[type(node.op)](*values) - # Pre-processes all nodes, - # This does remove the small "A and B" operation which skips evaluating B if A is False - processed_values = list(map(_math_eval, node.values)) - # Run through each value and apply the pairwise operations - while len(processed_values) > 1: - replacement_value = common_operator(processed_values[:2]) - processed_values.pop(0) - processed_values[0] = replacement_value - return processed_values[0] + # Parse ternary operator + if len(node.values) > 2: + # Left-to-right precedence. + left_value = copy.deepcopy(node) + left_value.values.pop(-1) + else: + left_value = node.values[0] + return operators[type(node.op)](_math_eval(left_value), _math_eval(node.values[-1])) elif isinstance(node, ast.Name): try: return variables[node.id]