Source code for prodigy.distribution.equality

"""
Three-valued equality of SymPy expressions: proven equal, proven different, or unknown.

An engine's ``==`` must never claim equality without proof (#117), so for non-rational expressions it
answers False when it merely cannot prove equality. Checks such as the EVT fixed-point equation need to
tell "provably different" from "not proven" apart (#123); :func:`decide_equal` gives that answer.
"""
from typing import Iterable, Optional

import sympy


[docs] def is_rational_over_q(expr: sympy.Expr) -> bool: """ Whether ``expr`` is built from symbols and rational numbers with ``+``, ``*`` and integer powers only, i.e. it is a rational function with rational coefficients. Constants such as ``sqrt(2)`` or ``pi`` and floating-point numbers are excluded: for them, exact cancellation is not a decision procedure. """ for node in sympy.preorder_traversal(expr): if isinstance(node, (sympy.Symbol, sympy.Rational, sympy.Add, sympy.Mul)): continue if isinstance(node, sympy.Pow) and isinstance(node.exp, sympy.Integer): continue return False return True
[docs] def decide_equal(lhs: sympy.Expr, rhs: sympy.Expr, use_simplify: bool = True) -> Optional[bool]: """ Decides ``lhs == rhs`` as an identity in all symbols. :returns: True if equality is proven, False if inequality is proven, None if neither. * Floating-point numbers make arithmetic inexact (``1e20 + 1 - 1e20`` is 0), so expressions containing them are only equal if they are identical; otherwise the answer is None. * The difference is put over a common denominator and cancelled. This is exact polynomial arithmetic over the rationals in which every non-rational subterm (e.g. ``exp(p*x)``) is an indeterminate, so a zero result proves the difference zero. If the difference is a rational function over the rationals (:func:`is_rational_over_q`), a nonzero result proves it nonzero. * Otherwise (only if ``use_simplify``) SymPy's ``Expr.equals`` may prove equality: it returns True only if ``simplify`` turns the difference into 0 or, for constant differences, by an algebraic proof (minimal polynomials). Its False comes from evaluation at random points and is no proof, so it counts as None. """ if lhs == rhs: return True if lhs.has(sympy.Float) or rhs.has(sympy.Float): return None difference = lhs - rhs try: if sympy.cancel(sympy.together(difference)) == 0: return True except sympy.PolynomialError: pass if is_rational_over_q(difference): return False if use_simplify and difference.equals(0) is True: return True return None
[docs] def decide_equal_strings(lhs: str, rhs: str, names: Iterable[str]) -> Optional[bool]: """ Like :func:`decide_equal` (without ``simplify``) for the string forms of two engine expressions, e.g. of SymEngine or GiNaC. ``names`` are the variables and parameters; they are parsed as plain symbols, so names such as ``E`` or ``S`` keep their meaning. Decimal literals stay floating-point numbers, so expressions containing them are never decided different. Unparsable strings give None. The strings must come from an engine, not from the user: SymPy's parser evaluates Python code. """ local_dict = {name: sympy.Symbol(name) for name in names} transformations = sympy.parsing.sympy_parser.standard_transformations \ + (sympy.parsing.sympy_parser.convert_xor,) try: lhs_expr = sympy.parse_expr(lhs, local_dict=local_dict, transformations=transformations) rhs_expr = sympy.parse_expr(rhs, local_dict=local_dict, transformations=transformations) except (SyntaxError, TypeError, ValueError, sympy.SympifyError): return None if not isinstance(lhs_expr, sympy.Expr) or not isinstance(rhs_expr, sympy.Expr): return None return decide_equal(lhs_expr, rhs_expr, use_simplify=False)