This lab exercise uses the SymPy symbolic math library to model constraints in the problem. To do that, we will use symbols (sympy.Symbol
), functions (sympy.Function
), and expressions (sympy.Expr
) from sympy, and then we'll combine the function and expression classes to make constraints -- evaluatable symbolic functions.
In this warmup, you will be introduced to the syntax and functionality of SymPy:
(See a list of common "gotchas" for sympy in their documentation: http://docs.sympy.org/dev/gotchas.html)
Start by reading and running the example cells, then complete the steps in the warmup cell.
In [ ]:
import matplotlib as mpl
import matplotlib.pyplot as plt
from util import constraint
from IPython.display import display
from sympy import *
init_printing()
In [ ]:
x = Symbol('x')
display(x)
You can also create symbols from an iterable sequence using the symbols()
function.
In [ ]:
i, j, k = symbols(['i', 'j', 'k']) # use implicit unpacking to associate multiple symbols with variables
display((i, j, k))
symbols()
can also create subscripted sequences of symbolic variables.
In [ ]:
X = symbols("X:3")
display(X)
In [ ]:
x = Symbol('x')
x?
display(x)
You can also define expressions with relations between symbols. (However, notice that expressions have no names...)
In [ ]:
x, y = symbols('x y')
or_relation = x | y
or_relation?
display(or_relation)
Also, not all operators can be used in expressions. The equal sign (=) performs assignment in python, so it cannot be used to make expressions. Using =
assigns a new python variable to an existing reference.
In [ ]:
x, y = symbols("x y")
y = x # now y references the same symbolic object as x
display(y) # display(y) == x ??!
Use sympy.Eq
for symbolic equality expressions: (Tip: there are lots of expressions in the sympy docs)
In [ ]:
x, z = symbols("x z")
display(Eq(z, x))
Sympy overloads standard python operators so that arithmetic and logical expressions can be constructed directly between symbolic objects.
In [ ]:
x, y, z = symbols("x y z")
display([x**2, x - y, Ne(x, y), (~x & y & z)])
In [ ]:
x, y, z = symbols("x y z")
relation = Eq(x, y)
display(relation)
Symbolic variables can be replaced by other variables, or by concrete values. (Tip: use positional arguments in the subs()
method to replace one symbol)
In [ ]:
display(relation.subs(x, z)) # Use positional arguments to substitute a single symbol
But keep in mind that substitution returns a copy of the expression -- it doesn't operate in-place. (Tip: as a result, you can use substitution on one expression bound to generic variables to generate new instances bound to specific variables.)
Look at what happens when we bind new variables to our equality relation:
In [ ]:
a = symbols("a:5")
b = symbols("b:5")
display([relation.subs({x: _a, y: _b}) for _a, _b in zip(a, b)])
Symbol substitution returns an expression. (Recall that Symbols are expressions).
In [ ]:
print(type(relation), type(relation.subs(x, z)))
print(type(relation) == type(relation.subs(x, z)))
But substituting values for all symbols returns a value type. (Tip: You can substitute multiple symbols in the subs()
command by providing a mapping (dict) from current symbols to new symbols or values.)
In [ ]:
print(type(relation), type(relation.subs({x: 0, y: 1})))
print(type(relation) != type(relation.subs({x: 0, y: 1})))
In [ ]:
x, y = symbols(['x', 'y'])
sameAs = constraint("SameAs", Eq(x, y))
display(sameAs)
Constraints are evaluated using the .subs method, just like an expression. If the resulting expression has unbound (free) symbols, then the result is a new constraint.
In [ ]:
display(sameAs.subs(x, 0), type(sameAs.subs(x, 0)))
If the resulting expression has no free symbols, then the result is only the evaluated expression.
In [ ]:
display(sameAs.subs({x: 0, y: 0}), type(sameAs.subs({x: 0, y: 0})))
Question 1: Create an array of subscripted symbols A0, A1, A2 stored in a variable named A
In [ ]:
A = None
# test for completion
assert(len(A) == 3)
assert(all([type(v) == Symbol for v in A]))
print("All tests passed!")
Question 2: Create an expression E with two generic symbols (e.g., "a" and "b", etc.) that represents logical XOR
In [ ]:
E = None
# test for completion
_vars = E.free_symbols
assert(len(_vars) == 2)
xor_table = {(0, 0): False, (0, 1): True, (1, 0): True, (1, 1): False}
assert(all(E.subs(zip(_vars, vals)) == truth for vals, truth in xor_table.items()))
print("All tests passed!")
Question 3: Create a constraint MaxAbsDiff with three generic arguments to test abs(a - b) < c, and create a copy of the constraint such that it tests abs(A[0] - A[1]) < A[2] from Q1
In [ ]:
maxAbsDiff = None
maxAbsDiff_copy = None
# test for completion
assert(maxAbsDiff.free_symbols != maxAbsDiff_copy.free_symbols)
assert(len(maxAbsDiff_copy.free_symbols) == len(maxAbsDiff_copy.args))
inputs = {(0, 6, 7): True, (6, 0, 7): True, (7, 6, 0): False}
assert(all(maxAbsDiff_copy.subs(zip(A[:3], vals)) == truth for vals, truth in inputs.items()))
print("All tests passed!")
(Optional) Question 4: Create a constraint AllDiff accepting the symbols in A as arguments, returning True if they are all different, and False if any pair is the same
In [ ]:
allDiff = None
inputs = (([0, 1, 2], True), ([1, 1, 1], False), ([0, 1, 1], False))
assert(all(allDiff.subs(zip(A, vals)) == truth for vals, truth in inputs))
print("All tests passed!")