Custom functions and extending SymPy

SymPy · Expert · 14 min

Domain-specific functions that differentiate, simplify and print correctly make SymPy fit your field.

Function("f") creates an undefined function with no properties. To give a function real behaviour, subclass sympy.Function and override hooks:

  • eval(cls, *args) (a classmethod) runs on construction. Return a simplified value for special inputs, for example a number for numeric arguments, or None to leave the call unevaluated. Keep it cheap: it runs every time the function is created.
  • fdiff(self, argindex=1) returns the derivative with respect to the given argument (1-based). With it, diff, the chain rule and series work automatically.
  • _eval_is_real, _eval_is_positive and similar methods answer assumption queries, returning True, False or None.
  • _eval_evalf(self, prec) provides numeric evaluation, and _eval_rewrite_as_<Name> enables expr.rewrite(Name).
  • Printing is customised with methods such as _latex(self, printer).

Arguments are always sympified, so inside eval you work with SymPy objects: test numbers with arg.is_Number or arg.is_zero, not with isinstance(arg, int).

The design rule is: automatic evaluation in eval should only do things that are always correct and cheap. Expensive or context-dependent rewrites belong in methods users call explicitly, such as doit, rewrite or an _eval_simplify hook, which is also how built-in functions keep construction fast.

Example

from sympy import Function, symbols, diff, exp, series, S

class softplus(Function):
    """softplus(x) = log(1 + exp(x))."""
    @classmethod
    def eval(cls, x):
        if x.is_zero:
            return S("log(2)")
        return None                      # stay unevaluated

    def fdiff(self, argindex=1):
        x = self.args[0]
        return 1 / (1 + exp(-x))         # the logistic sigmoid

    def _eval_is_positive(self):
        return self.args[0].is_extended_real

x = symbols("x", real=True)
print(softplus(0))                       # log(2)
print(diff(softplus(x**2), x))           # 2*x/(1 + exp(-x**2))
print(softplus(x).is_positive)           # True
print(series(softplus(x), x, 0, 3))      # log(2) + x/2 + x**2/8 + O(x**3)

Remember

  • Subclass Function; eval runs on construction and returns None to stay unevaluated
  • fdiff makes diff, the chain rule and series work
  • _eval_is_* methods plug into the assumptions system

Watch out for

  • Doing expensive simplification inside eval, which slows every construction.

Check yourself

  1. What should eval return when no simplification applies?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.sympy.org/latest/guides/custom-functions.html