Source code for prodigy.distribution.roots_of_unity
"""
Soundness check for substitutions with roots of unity.
The residue classes of a variable x modulo q are extracted by evaluating the generating function f
at w x for the q-th roots of unity w (modulo guards `x % q = r`, `Distribution._arithmetic_progression`
and `x % q` in an assignment with an infinite marginal of x). The engines evaluate f(w x) with the
principal branches of roots and logarithms, which need not agree with the power series of f: for
example, `sqrt(x^2*y^2)` denotes the series x*y, but at -x it evaluates to `sqrt(x^2*y^2)` again
instead of -x*y.
The substitution is exact if the closed form of f is single-valued in x, i.e. built from x and
subexpressions without x by `+`, `*`, integer powers and entire functions (`exp`, `sin`, `cos`,
`sinh`, `cosh`); this covers all rational generating functions. Such an f is holomorphic in x on a
punctured disk around 0 except for isolated singularities. It agrees with its power series on a
positive interval, hence by the identity theorem on that whole domain, so f(w x) and the series at
w x agree there as well and have the same power series. All other generating functions (roots,
logarithms, absolute values, or anything else) are rejected with a :class:`NotSingleValuedError`.
The check is engine independent and works on SymPy expressions.
"""
from __future__ import annotations
import sympy
_ENTIRE = (sympy.exp, sympy.sin, sympy.cos, sympy.sinh, sympy.cosh)
[docs]
class NotSingleValuedError(ValueError):
"""A substitution with roots of unity was requested on a generating function that is not
provably single-valued in the substituted variable."""
def _single_valued(expression: sympy.Basic, variable: sympy.Symbol) -> bool:
if not expression.has(variable) or expression == variable:
return True
if isinstance(expression, (sympy.Add, sympy.Mul)):
return all(_single_valued(argument, variable) for argument in expression.args)
if isinstance(expression, sympy.Pow):
return isinstance(expression.exp, sympy.Integer) and _single_valued(expression.base, variable)
if isinstance(expression, _ENTIRE):
return _single_valued(expression.args[0], variable)
return False
[docs]
def is_single_valued_in(function: sympy.Expr, variable: str) -> bool:
"""
Whether `function` is provably single-valued in the symbol called `variable` (see the module
documentation). Other symbols may occur in arbitrary form, e.g. under a root, as long as these
subexpressions do not contain `variable`.
"""
symbols = [s for s in function.free_symbols if isinstance(s, sympy.Symbol) and s.name == variable]
return all(_single_valued(function, symbol) for symbol in symbols)
[docs]
def require_single_valued(function: sympy.Expr, variable: str, operation: str) -> None:
"""
Raises a :class:`NotSingleValuedError` unless `function` is provably single-valued in
`variable`. `operation` names the instruction for the error message, e.g. `x % 2`.
"""
if not is_single_valued_in(function, variable):
raise NotSingleValuedError(
f"Cannot compute '{operation}' exactly: the generating function is not provably "
f"single-valued in '{variable}' (built from rational functions and exp). The residue "
f"classes modulo a number are extracted with complex roots of unity, which can pick the "
f"wrong branch of roots or logarithms.")