From 1bc685ab38e8704f3ac2b83bcd145fbf20fe9589 Mon Sep 17 00:00:00 2001 From: Eric Cousineau Date: Tue, 3 Apr 2018 03:37:53 -0400 Subject: [PATCH 1/5] pydrake containers: Add EqualToDict --- bindings/pydrake/util/BUILD.bazel | 18 +++- bindings/pydrake/util/containers.py | 90 +++++++++++++++++++ bindings/pydrake/util/test/containers_test.py | 73 +++++++++++++++ 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 bindings/pydrake/util/containers.py create mode 100644 bindings/pydrake/util/test/containers_test.py diff --git a/bindings/pydrake/util/BUILD.bazel b/bindings/pydrake/util/BUILD.bazel index 6d423a8b9002..4873fc97c38d 100644 --- a/bindings/pydrake/util/BUILD.bazel +++ b/bindings/pydrake/util/BUILD.bazel @@ -156,16 +156,25 @@ drake_py_library( ], ) +drake_py_library( + name = "containers_py", + srcs = ["containers.py"], + deps = [ + ":module_py", + ], +) + PY_LIBRARIES_WITH_INSTALL = [ ":eigen_geometry_py", ] PY_LIBRARIES = [ + ":containers_py", ":cpp_const_py", ":cpp_param_py", ":cpp_template_py", - ":module_py", ":deprecation_py", + ":module_py", ":pybind11_version_py", ] @@ -289,4 +298,11 @@ drake_py_unittest( ], ) +drake_py_unittest( + name = "containers_test", + deps = [ + ":containers_py", + ], +) + add_lint_tests() diff --git a/bindings/pydrake/util/containers.py b/bindings/pydrake/util/containers.py new file mode 100644 index 000000000000..1b21f67e18ef --- /dev/null +++ b/bindings/pydrake/util/containers.py @@ -0,0 +1,90 @@ +""" +Provides extensions for containers of Drake-related objects. +""" + + +class _EqualityProxyBase(object): + # Wraps an object with a non-compliant `__eq__` operator (returns a + # non-bool convertible expression) with a custom compliant `__eq__` + # operator. + def __init__(self, value): + self._value = value + + def _get_value(self): + return self._value + + def __hash__(self): + return hash(self._value) + + def __eq__(self, other): + raise NotImplemented("Abstract method") + + def __nonzero__(self): + return bool(self._value) + + value = property(_get_value) + + +class _DictKeyWrap(dict): + # Wraps a dictionary's key access. For a key of a type `TOrig`, this + # dictionary will provide a key of type `TProxy`, that should proxy the + # original key. + def __init__(self, dict_in, key_wrap, key_unwrap): + # @param dict_in Dictionary with keys of types TOrig (not necessarily + # homogeneous). + # @param key_wrap Functor that maps from TOrig -> TProxy. + # @param key_unwrap Functor that maps from TProxy -> TOrig. + dict.__init__(self) + # N.B. Passing properties to these will cause an issue. This can be + # sidestepped by storing the properties in a `dict`. + self._key_wrap = key_wrap + self._key_unwrap = key_unwrap + for key, value in dict_in.iteritems(): + self[key] = value + + def __setitem__(self, key, value): + return dict.__setitem__(self, self._key_wrap(key), value) + + def __getitem__(self, key): + return dict.__getitem__(self, self._key_wrap(key)) + + def __delitem__(self, key): + return dict.__delitem__(self, self._key_wrap(key)) + + def __contains__(self, key): + return dict.__contains__(self, self._key_wrap(key)) + + def items(self): + return zip(self.keys(), self.values()) + + def keys(self): + return [self._key_unwrap(key) for key in dict.iterkeys(self)] + + def iterkeys(self): + # Non-performant, but sufficient for now. + return self.keys() + + def iteritems(self): + # Non-performant, but sufficient for now. + return self.items() + + def raw(self): + """Returns a dict with the original keys. + N.B. Copying to a `dict` will maintain the proxy keys.""" + return dict(self.iteritems()) + + +class EqualToDict(_DictKeyWrap): + """Implements a dictionary where keys are compared using type and + `lhs.EqualTo(rhs)`. + """ + def __init__(self, *args, **kwargs): + + class Proxy(_EqualityProxyBase): + def __eq__(self, other): + T = type(self.value) + return (isinstance(other.value, T) + and self.value.EqualTo(other.value)) + + dict_in = dict(*args, **kwargs) + _DictKeyWrap.__init__(self, dict_in, Proxy, Proxy._get_value) diff --git a/bindings/pydrake/util/test/containers_test.py b/bindings/pydrake/util/test/containers_test.py new file mode 100644 index 000000000000..60b773efdcca --- /dev/null +++ b/bindings/pydrake/util/test/containers_test.py @@ -0,0 +1,73 @@ +from pydrake.util.containers import EqualToDict + +import unittest + + +class Comparison(object): + def __init__(self, lhs, rhs): + self.lhs = lhs + self.rhs = rhs + + def __nonzero__(self): + raise ValueError("Should not be called") + + +class Item(object): + equal_to_called = False + + def __init__(self, value): + self.value = value + + def __hash__(self): + return hash(self.value) + + def __eq__(self, other): + # Non-boolean return value. + return Comparison(self.value, other.value) + + def EqualTo(self, other): + Item.equal_to_called = True + return hash(self) == hash(other) + + +# Globals for testing. +a = Item(1) +b = Item(2) + + +class TestEqualToDict(unittest.TestCase): + def test_normal_dict(self): + d = {a: "a", b: "b"} + # TODO(eric.cousineau): Figure out how to reproduce failure when `dict` + # attempts to use `__eq__`, similar to what happens when using + # `Polynomial` as a key in a dictionary. + self.assertEqual(d[a], "a") + with self.assertRaises(ValueError): + value = bool(a == b) + + def test_equal_to_dict(self): + d = EqualToDict({a: "a", b: "b"}) + # Ensure that we call `EqualTo`. + self.assertFalse(Item.equal_to_called) + self.assertEquals(d[a], "a") + self.assertTrue(Item.equal_to_called) + + self.assertEquals(d[b], "b") + self.assertTrue(a in d) + + # Ensure hash collision does not occur. + self.assertEquals(hash(a.value), hash(a)) + self.assertFalse(a.value in d) + + # Obtaining the original representation (e.g. for `pybind11`): + # - Constructing using `dict` will not be what is desired; the keys at + # present are not directly convertible, thus would create an error. + # N.B. At present, this behavior may not be overridable via Python, as + # copying is done via `dict.update`, which has a special case for + # `dict`-inheriting types which does not have any hooks for key + # transformations. + raw_attempt = dict(d) + self.assertFalse(isinstance(raw_attempt.keys()[0], Item)) + # - Calling `raw()` should provide the desired behavior. + raw = d.raw() + self.assertTrue(isinstance(raw.keys()[0], Item)) From b1ccf5e7e9a240e6aea3dcdca134ad5c71afe80c Mon Sep 17 00:00:00 2001 From: Eric Cousineau Date: Tue, 3 Apr 2018 03:40:49 -0400 Subject: [PATCH 2/5] pydrake symbolic: Disable Formula.__nonzero__ --- bindings/pydrake/BUILD.bazel | 1 + bindings/pydrake/symbolic_py.cc | 9 +- bindings/pydrake/test/symbolic_test.py | 159 ++++++++++++++++--------- 3 files changed, 109 insertions(+), 60 deletions(-) diff --git a/bindings/pydrake/BUILD.bazel b/bindings/pydrake/BUILD.bazel index 2e223b5bd314..4ab6958e7136 100644 --- a/bindings/pydrake/BUILD.bazel +++ b/bindings/pydrake/BUILD.bazel @@ -280,6 +280,7 @@ drake_py_unittest( deps = [ ":algebra_test_util_py", ":symbolic_py", + "//bindings/pydrake/util:containers_py", ], ) diff --git a/bindings/pydrake/symbolic_py.cc b/bindings/pydrake/symbolic_py.cc index 3346d3f282d5..fea18b00253f 100644 --- a/bindings/pydrake/symbolic_py.cc +++ b/bindings/pydrake/symbolic_py.cc @@ -343,7 +343,14 @@ PYBIND11_MODULE(_symbolic_py, m) { .def("__hash__", [](const Formula& self) { return std::hash{}(self); }) .def_static("True", &Formula::True) - .def_static("False", &Formula::False); + .def_static("False", &Formula::False) + .def("__nonzero__", [](const Formula&) { + throw std::runtime_error( + "You should not call `__nonzero__` on `Formula`. If you are trying " + "to make a map with `Variable`, `Expression`, or `Polynomial` as " + "keys and access the keys, please use " + "`pydrake.util.containers.EqualToDict`."); + }); // Cannot overload logical operators: http://stackoverflow.com/a/471561 // Defining custom function for clarity. diff --git a/bindings/pydrake/test/symbolic_test.py b/bindings/pydrake/test/symbolic_test.py index f36bd38b3f28..71b7961f4333 100644 --- a/bindings/pydrake/test/symbolic_test.py +++ b/bindings/pydrake/test/symbolic_test.py @@ -5,8 +5,10 @@ import numpy as np import pydrake.symbolic as sym from pydrake.test.algebra_test_util import ScalarAlgebra, VectorizedAlgebra +from pydrake.util.containers import EqualToDict from copy import copy + # TODO(eric.cousineau): Replace usages of `sym` math functions with the # overloads from `pydrake.math`. @@ -21,8 +23,31 @@ e_x = sym.Expression(x) e_y = sym.Expression(y) +TYPES = [ + sym.Variable, + sym.Expression, + sym.Polynomial, + sym.Monomial, +] + +RHS_TYPES = TYPES + [float, np.float64] + + +class SymbolicTestCase(unittest.TestCase): + def _check_operand_types(self, lhs, rhs): + self.assertTrue(type(lhs) in TYPES, type(lhs)) + self.assertTrue(type(rhs) in RHS_TYPES, type(rhs)) + + def assertEqualStructure(self, lhs, rhs): + self._check_operand_types(lhs, rhs) + self.assertTrue(lhs.EqualTo(rhs), "{} != {}".format(lhs, rhs)) + + def assertNotEqualStructure(self, lhs, rhs): + self._check_operand_types(lhs, rhs) + self.assertFalse(lhs.EqualTo(rhs), "{} == {}".format(lhs, rhs)) + -class TestSymbolicVariable(unittest.TestCase): +class TestSymbolicVariable(SymbolicTestCase): def test_addition(self): self.assertEqual(str(x + y), "(x + y)") self.assertEqual(str(x + 1), "(1 + x)") @@ -139,7 +164,7 @@ def test_functions_with_variable(self): "(if (x > y) then x else y)") -class TestSymbolicVariables(unittest.TestCase): +class TestSymbolicVariables(SymbolicTestCase): def test_default_constructor(self): vars = sym.Variables() self.assertEqual(vars.size(), 0) @@ -265,7 +290,7 @@ def test_iterable(self): self.assertEqual(count, 3) -class TestSymbolicExpression(unittest.TestCase): +class TestSymbolicExpression(SymbolicTestCase): def _check_scalar(self, actual, expected): self.assertIsInstance(actual, sym.Expression) # Chain conversion to ensure equivalent treatment. @@ -454,14 +479,27 @@ def test_relational_operators(self): self.assertEqual(str(1 == e_y), "(y = 1)") self.assertEqual(str(1 != e_y), "(y != 1)") - def test_relation_operators_array_8135(self): - # Indication of #8135. + def test_relational_operators_nonzero(self): + # For issues #8135 and #8491. + # Ensure that we throw on `__nonzero__`. + with self.assertRaises(RuntimeError) as cm: + value = bool(e_x == e_x) + message = cm.exception.message + self.assertTrue( + all([s in message for s in ["__nonzero__", "EqualToDict"]]), + message) + # Indication of #8135. Ideally, these would all be arrays of formulas. e_xv = np.array([e_x, e_x]) e_yv = np.array([e_y, e_y]) # N.B. In some versions of NumPy, `!=` for dtype=object implies ID # comparison (e.g. `is`). + # N.B. If `__nonzero__` throws, then NumPy returns a scalar boolean if + # everything's false, vs. an array of `True` otherwise. No errors + # shown? value = (e_xv == e_yv) - # Ideally, this would be an array of formulas. + self.assertIsInstance(value, bool) + self.assertFalse(value) + value = (e_xv == e_xv) self.assertEqual(value.dtype, bool) self.assertFalse(isinstance(value[0], sym.Formula)) self.assertTrue(value.all()) @@ -471,14 +509,8 @@ def test_functions_with_float(self): # supported. v_x = 1.0 v_y = 1.0 - # WARNING: If these math functions have `float` overloads that return - # `float`, then `assertEqual`-like tests are meaningful (current state, - # and before `math` overloads were introduced). - # If these math functions implicitly cast `float` to `Expression`, then - # `assertEqual` tests are meaningless, as it tests `__nonzero__` for - # `Formula`, which will always be True. - self.assertEqual(sym.abs(v_x), 0.5*np.abs(v_x)) - self.assertNotEqual(str(sym.abs(v_x)), str(0.5*np.abs(v_x))) + self.assertEqualStructure(sym.abs(v_x), np.abs(v_x)) + self.assertNotEqualStructure(sym.abs(v_x), 0.5*np.abs(v_x)) self._check_scalar(sym.abs(v_x), np.abs(v_x)) self._check_scalar(sym.abs(v_x), np.abs(v_x)) self._check_scalar(sym.exp(v_x), np.exp(v_x)) @@ -557,20 +589,20 @@ def test_evaluate_exception_python_nan(self): def test_substitute_with_pair(self): e = x + y - self.assertEqual(e.Substitute(x, x + 5), x + y + 5) - self.assertEqual(e.Substitute(y, z), x + z) - self.assertEqual(e.Substitute(y, 3), x + 3) + self.assertEqualStructure(e.Substitute(x, x + 5), x + y + 5) + self.assertEqualStructure(e.Substitute(y, z), x + z) + self.assertEqualStructure(e.Substitute(y, 3), x + 3) def test_substitute_with_dict(self): e = x + y env = {x: x + 2, y: y + 3} - self.assertEqual(e.Substitute(env), x + y + 5) + self.assertEqualStructure(e.Substitute(env), x + y + 5) # See `math_overloads_test` for more comprehensive checks on math # functions. -class TestSymbolicFormula(unittest.TestCase): +class TestSymbolicFormula(SymbolicTestCase): def test_get_free_variables(self): f = x > y self.assertEqual(f.GetFreeVariables(), sym.Variables([x, y])) @@ -628,7 +660,7 @@ def test_evaluate_exception_python_nan(self): (x > 1).Evaluate(env) -class TestSymbolicMonomial(unittest.TestCase): +class TestSymbolicMonomial(SymbolicTestCase): def test_constructor_variable(self): m = sym.Monomial(x) # m = x¹ self.assertEqual(m.degree(x), 1) @@ -642,7 +674,7 @@ def test_constructor_variable_int(self): def test_constructor_map(self): powers_in = {x: 2, y: 3, z: 4} m = sym.Monomial(powers_in) - powers_out = m.get_powers() + powers_out = EqualToDict(m.get_powers()) self.assertEqual(powers_out[x], 2) self.assertEqual(powers_out[y], 3) self.assertEqual(powers_out[z], 4) @@ -654,6 +686,7 @@ def test_comparison(self): m3 = sym.Monomial(x, 1) m4 = sym.Monomial(y, 2) # Test operator== + self.assertIsInstance(m1 == m2, bool) self.assertTrue(m1 == m2) self.assertFalse(m1 == m3) self.assertFalse(m1 == m4) @@ -661,6 +694,7 @@ def test_comparison(self): self.assertFalse(m2 == m4) self.assertFalse(m3 == m4) # Test operator!= + self.assertIsInstance(m1 != m2, bool) self.assertFalse(m1 != m2) self.assertTrue(m1 != m3) self.assertTrue(m1 != m4) @@ -728,7 +762,7 @@ def test_pow_in_place(self): def test_get_powers(self): m = sym.Monomial(x, 2) * sym.Monomial(y) # m = x²y - powers = m.get_powers() + powers = EqualToDict(m.get_powers()) self.assertEqual(powers[x], 2) self.assertEqual(powers[y], 1) @@ -769,22 +803,22 @@ def test_evaluate_exception_python_nan(self): m.Evaluate(env) -class TestSymbolicPolynomial(unittest.TestCase): +class TestSymbolicPolynomial(SymbolicTestCase): def test_default_constructor(self): p = sym.Polynomial() - self.assertEqual(p.ToExpression(), sym.Expression()) + self.assertEqualStructure(p.ToExpression(), sym.Expression()) def test_constructor_maptype(self): m = {sym.Monomial(x): sym.Expression(3), sym.Monomial(y): sym.Expression(2)} # 3x + 2y p = sym.Polynomial(m) expected = 3 * x + 2 * y - self.assertEqual(p.ToExpression(), expected) + self.assertEqualStructure(p.ToExpression(), expected) def test_constructor_expression(self): e = 2 * x + 3 * y p = sym.Polynomial(e) - self.assertEqual(p.ToExpression(), e) + self.assertEqualStructure(p.ToExpression(), e) def test_constructor_expression_indeterminates(self): e = a * x + b * y + c * z @@ -805,24 +839,31 @@ def test_monomial_to_coefficient_map(self): e = a * (x ** 2) p = sym.Polynomial(e, [x]) the_map = p.monomial_to_coefficient_map() - self.assertEqual(the_map[m], a) + self.assertEqualStructure(the_map[m], a) def test_differentiate(self): e = a * (x ** 2) p = sym.Polynomial(e, [x]) # p = ax² result = p.Differentiate(x) # = 2ax - self.assertEqual(result.ToExpression(), 2 * a * x) + self.assertEqualStructure(result.ToExpression(), 2 * a * x) def test_add_product(self): p = sym.Polynomial() m = sym.Monomial(x) p.AddProduct(sym.Expression(3), m) # p += 3 * x - self.assertEqual(p.ToExpression(), 3 * x) + self.assertEqualStructure(p.ToExpression(), 3 * x) def test_comparison(self): p = sym.Polynomial() - self.assertTrue(p == p) + self.assertEqualStructure(p, p) + self.assertIsInstance(p == p, sym.Formula) + self.assertEqual(p == p, sym.Formula.True()) self.assertTrue(p.EqualTo(p)) + q = sym.Polynomial(sym.Expression(10)) + self.assertNotEqualStructure(p, q) + self.assertIsInstance(p != q, sym.Formula) + self.assertEqual(p != q, sym.Formula.True()) + self.assertFalse(p.EqualTo(q)) def test_repr(self): p = sym.Polynomial() @@ -830,63 +871,63 @@ def test_repr(self): def test_addition(self): p = sym.Polynomial() - self.assertEqual(p + p, p) + self.assertEqualStructure(p + p, p) m = sym.Monomial(x) - self.assertEqual(m + p, sym.Polynomial(1 * x)) - self.assertEqual(p + m, sym.Polynomial(1 * x)) - self.assertEqual(p + 0, p) - self.assertEqual(0 + p, p) + self.assertEqualStructure(m + p, sym.Polynomial(1 * x)) + self.assertEqualStructure(p + m, sym.Polynomial(1 * x)) + self.assertEqualStructure(p + 0, p) + self.assertEqualStructure(0 + p, p) def test_subtraction(self): p = sym.Polynomial() - self.assertEqual(p - p, p) + self.assertEqualStructure(p - p, p) m = sym.Monomial(x) - self.assertEqual(m - p, sym.Polynomial(1 * x)) - self.assertEqual(p - m, sym.Polynomial(-1 * x)) - self.assertEqual(p - 0, p) - self.assertEqual(0 - p, -p) + self.assertEqualStructure(m - p, sym.Polynomial(1 * x)) + self.assertEqualStructure(p - m, sym.Polynomial(-1 * x)) + self.assertEqualStructure(p - 0, p) + self.assertEqualStructure(0 - p, -p) def test_multiplication(self): p = sym.Polynomial() - self.assertEqual(p * p, p) + self.assertEqualStructure(p * p, p) m = sym.Monomial(x) - self.assertEqual(m * p, p) - self.assertEqual(p * m, p) - self.assertEqual(p * 0, p) - self.assertEqual(0 * p, p) + self.assertEqualStructure(m * p, p) + self.assertEqualStructure(p * m, p) + self.assertEqualStructure(p * 0, p) + self.assertEqualStructure(0 * p, p) def test_addition_assignment(self): p = sym.Polynomial() p += p - self.assertEqual(p, sym.Polynomial()) + self.assertEqualStructure(p, sym.Polynomial()) p += sym.Monomial(x) - self.assertEqual(p, sym.Polynomial(1 * x)) + self.assertEqualStructure(p, sym.Polynomial(1 * x)) p += 3 - self.assertEqual(p, sym.Polynomial(1 * x + 3)) + self.assertEqualStructure(p, sym.Polynomial(3 + 1 * x)) def test_subtraction_assignment(self): p = sym.Polynomial() p -= p - self.assertEqual(p, sym.Polynomial()) + self.assertEqualStructure(p, sym.Polynomial()) p -= sym.Monomial(x) - self.assertEqual(p, sym.Polynomial(-1 * x)) + self.assertEqualStructure(p, sym.Polynomial(-1 * x)) p -= 3 - self.assertEqual(p, sym.Polynomial(-1 * x - 3)) + self.assertEqualStructure(p, sym.Polynomial(-1 * x - 3)) def test_multiplication_assignment(self): p = sym.Polynomial() p *= p - self.assertEqual(p, sym.Polynomial()) + self.assertEqualStructure(p, sym.Polynomial()) p *= sym.Monomial(x) - self.assertEqual(p, sym.Polynomial()) + self.assertEqualStructure(p, sym.Polynomial()) p *= 3 - self.assertEqual(p, sym.Polynomial()) + self.assertEqualStructure(p, sym.Polynomial()) def test_pow(self): e = a * (x ** 2) p = sym.Polynomial(e, [x]) # p = ax² p = pow(p, 2) # p = a²x⁴ - self.assertEqual(p.ToExpression(), (a ** 2) * (x ** 4)) + self.assertEqualStructure(p.ToExpression(), (a ** 2) * (x ** 4)) def test_jacobian(self): e = 5 * x ** 2 + 4 * y ** 2 + 8 * x * y @@ -895,16 +936,16 @@ def test_jacobian(self): p_dy = sym.Polynomial(8 * y + 8 * x, [x, y]) # ∂p/∂y = 8y + 8x J = p.Jacobian([x, y]) - self.assertEqual(J[0], p_dx) - self.assertEqual(J[1], p_dy) + self.assertEqualStructure(J[0], p_dx) + self.assertEqualStructure(J[1], p_dy) def test_hash(self): p1 = sym.Polynomial(x * x, [x]) p2 = sym.Polynomial(x * x, [x]) - self.assertEqual(p1, p2) + self.assertEqualStructure(p1, p2) self.assertEqual(hash(p1), hash(p2)) p1 += 1 - self.assertNotEqual(p1, p2) + self.assertNotEqualStructure(p1, p2) self.assertNotEqual(hash(p1), hash(p2)) def test_evaluate(self): From a626f74faa53d81f47adae9ca46c72a4090324f5 Mon Sep 17 00:00:00 2001 From: Eric Cousineau Date: Sun, 8 Apr 2018 11:36:32 -0400 Subject: [PATCH 3/5] pydrake symbolic: Ensure compound formulas fail (Resolves #8536) --- bindings/pydrake/test/symbolic_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bindings/pydrake/test/symbolic_test.py b/bindings/pydrake/test/symbolic_test.py index 71b7961f4333..9fef1d03a535 100644 --- a/bindings/pydrake/test/symbolic_test.py +++ b/bindings/pydrake/test/symbolic_test.py @@ -488,6 +488,9 @@ def test_relational_operators_nonzero(self): self.assertTrue( all([s in message for s in ["__nonzero__", "EqualToDict"]]), message) + # Ensure that compound formulas fail (#8536). + with self.assertRaises(RuntimeError): + value = 0 < e_y < e_y # Indication of #8135. Ideally, these would all be arrays of formulas. e_xv = np.array([e_x, e_x]) e_yv = np.array([e_y, e_y]) From 16d0f8894ca918bf072204f75de9e640ed2ac17d Mon Sep 17 00:00:00 2001 From: Eric Cousineau Date: Sun, 8 Apr 2018 18:06:42 -0400 Subject: [PATCH 4/5] pydrake symbolic: Capture deprecation warnings with dtype=object comparisons --- bindings/pydrake/symbolic_py.cc | 6 ++++++ bindings/pydrake/test/symbolic_test.py | 24 ++++++++++++++---------- bindings/pydrake/util/deprecation.py | 22 ++++++++++++++++++++++ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/bindings/pydrake/symbolic_py.cc b/bindings/pydrake/symbolic_py.cc index fea18b00253f..838a231e2f74 100644 --- a/bindings/pydrake/symbolic_py.cc +++ b/bindings/pydrake/symbolic_py.cc @@ -23,6 +23,12 @@ PYBIND11_MODULE(_symbolic_py, m) { // NOLINTNEXTLINE(build/namespaces): Emulate placement in namespace. using namespace drake::symbolic; + // Install NumPy warning filtres. + // N.B. This may interfere with other code, but until that is a confirmed + // issue, we should agressively try to avoid these warnings. + py::module::import("pydrake.util.deprecation") + .attr("install_numpy_warning_filters")(); + m.doc() = "Symbolic variable, variables, monomial, expression, polynomial, and " "formula"; diff --git a/bindings/pydrake/test/symbolic_test.py b/bindings/pydrake/test/symbolic_test.py index 9fef1d03a535..a9615f73f1ba 100644 --- a/bindings/pydrake/test/symbolic_test.py +++ b/bindings/pydrake/test/symbolic_test.py @@ -496,16 +496,20 @@ def test_relational_operators_nonzero(self): e_yv = np.array([e_y, e_y]) # N.B. In some versions of NumPy, `!=` for dtype=object implies ID # comparison (e.g. `is`). - # N.B. If `__nonzero__` throws, then NumPy returns a scalar boolean if - # everything's false, vs. an array of `True` otherwise. No errors - # shown? - value = (e_xv == e_yv) - self.assertIsInstance(value, bool) - self.assertFalse(value) - value = (e_xv == e_xv) - self.assertEqual(value.dtype, bool) - self.assertFalse(isinstance(value[0], sym.Formula)) - self.assertTrue(value.all()) + # N.B. If `__nonzero__` throws, then NumPy swallows the error and + # produces a DeprecationWarning, in addition to effectively garbage + # values. For this reason, `pydrake.symbolic` will automatically + # promote these warnings to errors. + # - All false. + with self.assertRaises(DeprecationWarning): + value = (e_xv == e_yv) + # - True + False. + with self.assertRaises(DeprecationWarning): + e_xyv = np.array([e_x, e_y]) + value = (e_xv == e_xyv) + # - All true. + with self.assertRaises(DeprecationWarning): + value = (e_xv == e_xv) def test_functions_with_float(self): # TODO(eric.cousineau): Use concrete values once vectorized methods are diff --git a/bindings/pydrake/util/deprecation.py b/bindings/pydrake/util/deprecation.py index b35ccbb63b2a..3ab29eea7336 100644 --- a/bindings/pydrake/util/deprecation.py +++ b/bindings/pydrake/util/deprecation.py @@ -134,4 +134,26 @@ def wrapped(original): return wrapped +def install_numpy_warning_filters(force=False): + """Install warnings filters specific to NumPy.""" + global installed_numpy_warning_filters + if installed_numpy_warning_filters and not force: + return + installed_numpy_warning_filters = True + # Warnings specific to comparison with `dtype=object` should be raised to + # errors (#8315, #8491). Without them, NumPy will return effectively + # garbage values (e.g. comparison based on object ID): either a scalar bool + # or an array of bools (based on what objects are present and the NumPy + # version). + # N.B. Using a `module=` regex filter does not work, as the warning is + # raised from C code, and thus inherits the calling module, which may not + # be "numpy\..*" (numpy/numpy#10861). + warnings.filterwarnings( + "error", category=DeprecationWarning, message="numpy equal will not") + warnings.filterwarnings( + "error", category=DeprecationWarning, + message="elementwise == comparison failed") + + warnings.simplefilter('once', DrakeDeprecationWarning) +installed_numpy_warning_filters = False From a4845dd757e8286cbaf995b7c646ea1c6c42a327 Mon Sep 17 00:00:00 2001 From: Eric Cousineau Date: Sun, 8 Apr 2018 18:07:09 -0400 Subject: [PATCH 5/5] pydrake autodiff: Ensure NumPy deprecations are also filtered --- bindings/pydrake/BUILD.bazel | 2 ++ bindings/pydrake/autodiffutils_py.cc | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/bindings/pydrake/BUILD.bazel b/bindings/pydrake/BUILD.bazel index 4ab6958e7136..eed3acceaa60 100644 --- a/bindings/pydrake/BUILD.bazel +++ b/bindings/pydrake/BUILD.bazel @@ -65,6 +65,7 @@ drake_pybind_library( py_deps = [ ":common_py", ":math_py", + "//bindings/pydrake/util:deprecation_py", ], py_srcs = [ "autodiffutils.py", @@ -132,6 +133,7 @@ drake_pybind_library( py_deps = [ ":common_py", ":math_py", + "//bindings/pydrake/util:deprecation_py", ], py_srcs = ["symbolic.py"], ) diff --git a/bindings/pydrake/autodiffutils_py.cc b/bindings/pydrake/autodiffutils_py.cc index f0a678b629af..b8c7b46f0010 100644 --- a/bindings/pydrake/autodiffutils_py.cc +++ b/bindings/pydrake/autodiffutils_py.cc @@ -17,6 +17,12 @@ namespace pydrake { PYBIND11_MODULE(_autodiffutils_py, m) { m.doc() = "Bindings for Eigen AutoDiff Scalars"; + // Install NumPy warning filtres. + // N.B. This may interfere with other code, but until that is a confirmed + // issue, we should agressively try to avoid these warnings. + py::module::import("pydrake.util.deprecation") + .attr("install_numpy_warning_filters")(); + py::class_ autodiff(m, "AutoDiffXd"); autodiff .def(py::init())