Skip to content

Commit

Permalink
pydrake symbolic: Disable Formula.__nonzero__
Browse files Browse the repository at this point in the history
  • Loading branch information
EricCousineau-TRI committed Apr 3, 2018
1 parent 85b4cbd commit 65121d0
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 56 deletions.
1 change: 1 addition & 0 deletions bindings/pydrake/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ drake_py_unittest(
deps = [
":algebra_test_util_py",
":symbolic_py",
"//bindings/pydrake/util:pure_hash_dict_py",
],
)

Expand Down
8 changes: 7 additions & 1 deletion bindings/pydrake/symbolic_py.cc
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,13 @@ PYBIND11_MODULE(_symbolic_py, m) {
.def("__hash__",
[](const Formula& self) { return std::hash<Formula>{}(self); })
.def_static("True", &Formula::True)
.def_static("False", &Formula::False);
.def_static("False", &Formula::False)
.def("__nonzero__", [](const Formula& self) {
throw std::runtime_error(
"Should not call `__nonzero__` on `Formula`. If you are trying to "
"make a map with `Variable`, `Expression`, or `Polynomial` as "
"keys, please use `PureHashDict`.");
});

// Cannot overload logical operators: http://stackoverflow.com/a/471561
// Defining custom function for clarity.
Expand Down
155 changes: 100 additions & 55 deletions bindings/pydrake/test/symbolic_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.pure_hash_dict import PureHashDict
from copy import copy


# TODO(eric.cousineau): Replace usages of `sym` math functions with the
# overloads from `pydrake.math`.

Expand All @@ -21,8 +23,32 @@
e_x = sym.Expression(x)
e_y = sym.Expression(y)

TYPES = [
sym.Variable,
sym.Expression,
sym.Polynomial,
sym.Monomial,
]


class SymbolicTestCase(unittest.TestCase):
def _coerce_value(self, value):
# Coerce value for string comparison.
for T in TYPES:
if isinstance(value, T):
return value
return sym.Expression(value)

def assertSame(self, lhs, rhs):
rhs = self._coerce_value(rhs)
self.assertEqual(str(lhs), str(rhs))

class TestSymbolicVariable(unittest.TestCase):
def assertNotSame(self, lhs, rhs):
rhs = self._coerce_value(rhs)
self.assertNotEqual(str(lhs), str(rhs))


class TestSymbolicVariable(SymbolicTestCase):
def test_addition(self):
self.assertEqual(str(x + y), "(x + y)")
self.assertEqual(str(x + 1), "(1 + x)")
Expand Down Expand Up @@ -135,7 +161,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)
Expand Down Expand Up @@ -244,7 +270,7 @@ def test_intersect(self):
self.assertEqual(vars3, sym.Variables([y]))


class TestSymbolicExpression(unittest.TestCase):
class TestSymbolicExpression(SymbolicTestCase):
def _check_scalar(self, actual, expected):
self.assertIsInstance(actual, sym.Expression)
# Chain conversion to ensure equivalent treatment.
Expand Down Expand Up @@ -429,14 +455,23 @@ 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):
value = bool(e_x == e_x)
# 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())
Expand All @@ -446,14 +481,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.assertSame(sym.abs(v_x), np.abs(v_x))
self.assertNotSame(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))
Expand Down Expand Up @@ -527,7 +556,7 @@ def test_evaluate_exception_python_nan(self):
# 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]))
Expand Down Expand Up @@ -585,7 +614,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)
Expand All @@ -599,7 +628,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 = PureHashDict(m.get_powers())
self.assertEqual(powers_out[x], 2)
self.assertEqual(powers_out[y], 3)
self.assertEqual(powers_out[z], 4)
Expand All @@ -611,13 +640,15 @@ 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)
self.assertFalse(m2 == m3)
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)
Expand Down Expand Up @@ -678,7 +709,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 = PureHashDict(m.get_powers())
self.assertEqual(powers[x], 2)
self.assertEqual(powers[y], 1)

Expand Down Expand Up @@ -719,22 +750,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.assertSame(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.assertSame(p.ToExpression(), expected)

def test_constructor_expression(self):
e = 2 * x + 3 * y
p = sym.Polynomial(e)
self.assertEqual(p.ToExpression(), e)
self.assertSame(p.ToExpression(), e)

def test_constructor_expression_indeterminates(self):
e = a * x + b * y + c * z
Expand All @@ -755,88 +786,101 @@ 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.assertSame(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.assertSame(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.assertSame(p.ToExpression(), 3 * x)

def test_comparison(self):
p = sym.Polynomial()
self.assertTrue(p == p)
self.assertSame(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.assertNotSame(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()
self.assertEqual(repr(p), '<Polynomial "0">')

def test_addition(self):
p = sym.Polynomial()
self.assertEqual(p + p, p)
self.assertSame(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.assertSame(m + p, sym.Polynomial(1 * x))
self.assertSame(p + m, sym.Polynomial(1 * x))
self.assertSame(p + 0, p)
self.assertSame(0 + p, p)

def test_subtraction(self):
p = sym.Polynomial()
self.assertEqual(p - p, p)
self.assertSame(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.assertSame(m - p, sym.Polynomial(1 * x))
self.assertSame(p - m, sym.Polynomial(-1 * x))
self.assertSame(p - 0, p)
self.assertSame(0 - p, -p)

def test_multiplication(self):
p = sym.Polynomial()
self.assertEqual(p * p, p)
self.assertSame(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.assertSame(m * p, p)
self.assertSame(p * m, p)
self.assertSame(p * 0, p)
self.assertSame(0 * p, p)

def test_addition_assignment(self):
p = sym.Polynomial()
p += p
self.assertEqual(p, sym.Polynomial())
self.assertSame(p, sym.Polynomial())
p += sym.Monomial(x)
self.assertEqual(p, sym.Polynomial(1 * x))
self.assertSame(p, sym.Polynomial(1 * x))
p += 3
self.assertEqual(p, sym.Polynomial(1 * x + 3))
# N.B. For whatever reason, the ordering between these two is not the
# same. Without `ToExpression`, we get an erorr that
# '3*1 + 1*x' != '1*x + 3*1'.
self.assertSame(
p.ToExpression(), sym.Polynomial(3 + 1 * x).ToExpression())

def test_subtraction_assignment(self):
p = sym.Polynomial()
p -= p
self.assertEqual(p, sym.Polynomial())
self.assertSame(p, sym.Polynomial())
p -= sym.Monomial(x)
self.assertEqual(p, sym.Polynomial(-1 * x))
self.assertSame(p, sym.Polynomial(-1 * x))
p -= 3
self.assertEqual(p, sym.Polynomial(-1 * x - 3))
# N.B. Same as above; use `ToExpression` to normalize order.
self.assertSame(
p.ToExpression(), sym.Polynomial(-1 * x - 3).ToExpression())

def test_multiplication_assignment(self):
p = sym.Polynomial()
p *= p
self.assertEqual(p, sym.Polynomial())
self.assertSame(p, sym.Polynomial())
p *= sym.Monomial(x)
self.assertEqual(p, sym.Polynomial())
self.assertSame(p, sym.Polynomial())
p *= 3
self.assertEqual(p, sym.Polynomial())
self.assertSame(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.assertSame(p.ToExpression(), (a ** 2) * (x ** 4))

def test_jacobian(self):
e = 5 * x ** 2 + 4 * y ** 2 + 8 * x * y
Expand All @@ -845,16 +889,17 @@ 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)
# N.B. Same as in other places: Use `ToExpression` to normalize order.
self.assertSame(J[0].ToExpression(), p_dx.ToExpression())
self.assertSame(J[1].ToExpression(), p_dy.ToExpression())

def test_hash(self):
p1 = sym.Polynomial(x * x, [x])
p2 = sym.Polynomial(x * x, [x])
self.assertEqual(p1, p2)
self.assertSame(p1, p2)
self.assertEqual(hash(p1), hash(p2))
p1 += 1
self.assertNotEqual(p1, p2)
self.assertNotSame(p1, p2)
self.assertNotEqual(hash(p1), hash(p2))

def test_evaluate(self):
Expand Down

0 comments on commit 65121d0

Please sign in to comment.