Source code for dit.symbolic.distributions

"""
Constructors and helpers for symbolic (sympy-backed) distributions.
"""

from ..distribution import Distribution

__all__ = (
    "simplify",
    "symbolic_distribution",
    "symbolic_min",
    "symbols",
)


def _require_sympy():
    """Import sympy or raise a helpful error."""
    try:
        import sympy
    except ImportError as err:  # pragma: no cover
        raise ImportError("Symbolic distributions require sympy. Install with: pip install sympy") from err
    return sympy


[docs] def symbols(names, positive=True, **assumptions): """ Create sympy symbols suitable for use as probabilities. A thin wrapper around :func:`sympy.symbols` that defaults to ``positive=True`` (appropriate for probabilities, and helpful for simplification). Parameters ---------- names : str Symbol names, e.g. ``'p'`` or ``'a b c'`` (see :func:`sympy.symbols`). positive : bool Whether the symbols are assumed positive. Defaults to ``True``. **assumptions Additional sympy assumptions forwarded to :func:`sympy.symbols`. Returns ------- syms : Symbol or tuple of Symbol """ sympy = _require_sympy() return sympy.symbols(names, positive=positive, **assumptions)
[docs] def symbolic_distribution(outcomes, pmf, rv_names=None, validate=True, **kwargs): """ Construct a :class:`~dit.distribution.Distribution` with symbolic probabilities. This is a convenience wrapper around :class:`Distribution` that sympifies the ``pmf`` entries so that the resulting distribution stores exact, symbolic probabilities. Parameters ---------- outcomes : sequence The outcomes, as accepted by :class:`Distribution` (e.g. a list of strings such as ``['00', '11']``). pmf : sequence The probabilities, as numbers and/or sympy expressions. Each entry is passed through :func:`sympy.sympify`. rv_names : list of str, optional Names for each random variable. validate : bool If True, validate normalisation after construction. For pmfs containing free symbols normalisation is not decidable and is skipped. **kwargs Additional keyword arguments forwarded to :class:`Distribution`. Returns ------- d : Distribution A distribution with ``d.is_symbolic()`` True. """ sympy = _require_sympy() sym_pmf = [sympy.sympify(p) for p in pmf] return Distribution( list(outcomes), sym_pmf, rv_names=rv_names, validate=validate, **kwargs, )
[docs] def simplify(expr, **kwargs): """ Simplify a symbolic measure result. A thin wrapper around :func:`sympy.simplify` for convenience, so callers need not import sympy directly. Parameters ---------- expr : sympy expression The expression to simplify (e.g. the return value of a measure). **kwargs Forwarded to :func:`sympy.simplify`. Returns ------- simplified : sympy expression """ sympy = _require_sympy() return sympy.simplify(expr, **kwargs)
def symbolic_min(args): """Return ``sympy.Min`` over ``args``, robust to unsimplified constants. ``sympy.Min`` raises ``ValueError`` when an argument is a constant it cannot immediately decide is comparable (e.g. ``2 - log(4)/log(2)``, which equals ``0``). Simplifying each argument first resolves such constants while leaving genuinely symbolic arguments intact. Parameters ---------- args : iterable of sympy expressions The values to minimise over. Returns ------- m : sympy expression ``Min`` of the (simplified) arguments. """ sympy = _require_sympy() return sympy.Min(*[sympy.simplify(a) for a in args])
[docs] def evaluate(expr, subs): """Numerically evaluate a symbolic measure result at a point. This is a robust alternative to ``expr.subs(subs)`` for expressions that contain ``Min``/``Max`` (as produced by e.g. ``I_min``/``I_mmi`` or CAEKL). ``sympy``'s ``Min``/``Max`` can raise ``ValueError`` ("not comparable") when a plain ``.subs`` leaves unsimplified constant arguments; evaluating through :func:`sympy.lambdify` sidesteps that by comparing the arguments numerically. Parameters ---------- expr : sympy expression or number The expression to evaluate (e.g. a measure result). subs : dict Mapping of symbols to numeric values. Returns ------- value : float The numeric value of ``expr`` at ``subs``. """ sympy = _require_sympy() if not hasattr(expr, "free_symbols"): return float(expr) symbols_ = tuple(expr.free_symbols) if not symbols_: # No free symbols: still route through lambdify to resolve Min/Max. func = sympy.lambdify((), expr, "math") return float(func()) values = [subs[s] for s in symbols_] func = sympy.lambdify(symbols_, expr, "math") return float(func(*values))