5. Boolean expressions and conditionals#

5.1. Relationals#

In addition to scalar-valued expressions, we can also construct boolean-valued expressions using relationals:

>>> x, y = sym.symbols('x, y')
>>> condition = x < y
>>> type(condition)
pywrenfold.sym.BooleanExpr
>>> print(condition.expression_tree_str())
Relational (<)
├─ Variable (x, unknown)
└─ Variable (y, unknown)

Warning

The __eq__ method in python corresponds to strict equality, not mathematical equivalence. Hence the == operator will directly return True if the two expression trees are completely identical, and False otherwise. To construct an equivalence relational, use wrenfold.sym.eq() instead.

>>> x == y
False
>>> sym.eq(x, y)
x == y

5.2. Conditional logic#

Boolean-valued expressions are relevant when creating conditional expressions. A ternary conditional can be constructed using wrenfold.sym.where():

>>> f = sym.where(x > 0, x * y, -x / 5)
>>> f
where(0 < x, x*y, -x/5)  # Note that `x > 0` was canonicalized as `0 < x`.

In the example above, \(f\left(x\right)\) is a piecewise function with a discontinuity in the first derivative at \(x = 0\). At code-generation time, sym.where will be converted to an if-else branch. This can serve a number of purposes:

  • Skipping irrelevant computations.

  • Avoiding singularities or calculations that would introduce NaN values.

  • Implementing piecewise continuous functions.

The result of the conditional is a scalar-valued expression, so it can be differentiated:

# Take the derivative of both branches.
>>> f.diff(x)
where(0 < x, y, -1/5)
# Take the derivative of both branches, twice.
# Since both sides evaluate to zero, the conditional is immediately simplified:
>>> f.diff(x, 2)
0

Note

When taking the derivative of sym.where expressions, the contribution of the condition itself is ignored and the two branches are differentiated to create a new piecewise function. In the example above, the true derivative of the function \(f\left(x\right)\) is infinite at \(x = 0\). We could insert the Dirac delta function to capture this, but this result would not be very useful in a numerical evaluation context.

5.3. Casting booleans#

Boolean expressions cannot be multiplied or added like scalars, but they can be “casted” to scalars using the Iverson bracket. The iverson bracket evaluates to 1 if the boolean condition is true, and 0 if the condition is false.

>>> condition = x < y
>>> g = sym.iverson(condition)
>>> g
iverson(x < y)
# Substitute values that make the statement true:
>>> g.subs(x, 0.23).subs(y, 4)
1

This can be useful if you want to mask out a value without inserting a conditional:

# We can't multiply (x < 0) by sin(x) directly, but we can do this:
>>> h = sym.iverson(x < 0) * sym.sin(x)
>>> h
sin(x)*iverson(x < 0)
# Will be zero for all positive `x`:
>>> h.subs(x, 0.7)
0