#8407·pymc

ENH: Render full symbolic expressions for Deterministics (and Potentials) in model representations — plain text and LaTeX

Author: drbenvincentCreated Aug 25, 2026Updated Sep 14, 2026

Motivation

The model string/LaTeX representation (str_for_model, pm.model_to_graphviz) is one of the main ways users communicate, review, debug, and document their models. It appears constantly in teaching materials, papers, blog posts, and code review. Today, however, every pm.Deterministic collapses to an opaque function call:

python
with pm.Model() as m:
    x = pm.Data("x", ...)
    sigma = pm.HalfNormal("sigma", 1)
    beta = pm.Normal("beta", 0, 1)
    mu = pm.Deterministic("mu", x * beta + pt.log(sigma**2) + 3)
    y = pm.Normal("y", mu=mu, sigma=sigma)

print(m)  # or str_for_model(m)
sigma ~ HalfNormal(<constant>)
beta  ~ Normal(0, 1)
x     = Data(...)
mu    = f(beta, x, sigma)     <-- what does mu actually *do*?
y     ~ Normal(mu, sigma)

f(beta, x, sigma) tells you the ingredients but not the recipe. The information to display the recipe already exists in memory: PyMC builds a fully symbolic PyTensor graph for every deterministic, and PyTensor ships a general-purpose pretty-printer for exactly these graphs. The two are simply not connected today.

Being able to print

mu = x * beta + log(sigma ** 2) + 3

and — especially — export the whole model as LaTeX,

latex
\begin{align}
\sigma &\sim \operatorname{HalfNormal}(1) \\
\beta &\sim \operatorname{Normal}(0, 1) \\
\mu &= x \cdot \beta + \log(\sigma^{2}) + 3 \\
y &\sim \operatorname{Normal}(\mu, \sigma)
\end{align}

would be a genuinely great feature: papers, docs, and teaching notebooks could show the actual mathematical model instead of a lossy abbreviation of it.

Relation to prior work

  • #7538 proposed richer textual representations, including "use pytensor pretty print to show the content of some of the functions". It was closed by #8205 (@drbenvincent), which implemented the constant-only f() fix (<constant>) and Data-variable visibility — but the "show function contents" half was never implemented.
  • In review of #8205 it was noted that constant-folding at print time is too expensive for a print method. This proposal avoids that entirely: it is a pure traversal of the existing symbolic graph (no rewrites, no compilation). The graph already knows everything needed to write the expression down.

Proposed behavior (opt-in)

Default output stays as-is; a flag (e.g. str_for_model(m, deterministic_exprs=True) or similar) renders deterministic bodies, stopping at any named model variable:

default with flag (plain)
mu f(beta, x, sigma) (x * beta) + Log((sigma ** 2)) + 3
eta = mu / (1 + mu) f(mu) mu / (1 + mu)
lam = softplus(eta * x.sum()) f(eta, x) Softplus((eta * sum(x)))

Nested deterministics reference each other by name (d2 = d1 + 1), keeping output compact — the same convention process_graph in pytensor.printing uses.

LaTeX mode would extend the existing formatting="latex" path so model_to_graphviz / LaTeX exports can include full equations.

Feasibility

A working prototype (~100 lines total, no pytensor changes) demonstrates:

  • Plain text: clone the global pytensor.printing.pprinter with a "stop at variables whose name is a known model variable" rule (the same trick PPrinter.process_graph already uses internally at printing.py:1840-1841, just not exposed).
  • LaTeX: PyTensor has no LaTeX printer, but its printer classes are format-agnostic — PatternPrinter(r"\frac{%(0)s}{%(1)s}") and OperatorPrinter(r"\cdot", prec) give correct precedence-aware parenthesization for free. ~10 op registrations cover arithmetic, exp/log/sqrt, dot, sum; unknown ops degrade gracefully to \operatorname{name}(args).

Prototype outputs:

mu   plain : ((x * beta) + Log((sigma ** 2)) + 3)
     latex : $x \cdot \beta + \log({sigma}^{2}) + 3$
eta  plain : mu / (1 + mu)
     latex : $\frac{mu}{(1 + mu)}$
lam  plain : Softplus((eta * sum(x)))
     latex : $\operatorname{softplus}\left(eta \cdot \sum\left(x\right)\right)$

Open design questions

  1. Multidimensional indexing in LaTeX. Plain text already renders slices readably (X[:, 0], X[:2]), because PyTensor registers plain-text printers for Subtensor. LaTeX has no equivalent yet: it currently degrades to \operatorname{Subtensor}(X, 0). We'd want a Subtensor LaTeX printer (e.g. X_{[:,\,0]}). Prototype covers broadcasting (DimShuffle rendered transparently); explicit slicing needs this new printer.
  2. Semantic vs structural fidelity. Some user-facing operations decompose in the graph — x.mean() is literally sum(x) / shape(sum(x))[0]. Printing shows the structural truth. Acceptable? Should we optionally apply light standard rewrites before printing (still cheap), or special-case common patterns?
  3. Truncation. Long bodies need a policy (... truncation like model_to_graphviz's truncate_deterministic, or multi-line align blocks in LaTeX).
  4. Where LaTeX escaping lives. Variable names should be escaped consistently (existing _latex_escape helpers) and greek names normalized (beta\beta) — currently PyTensor's leaf printer does this inconsistently.

Implementation sketch

All in pymc/printing.py:

  • Extend _str_for_expression / str_for_potential_or_deterministic (currently the literal f(...) site) to optionally emit the body via a cloned PyTensor printer, reusing the named_vars threading introduced in #8205.
  • Add a small registry of LaTeX op printers plus a generic fallback.
  • No model rewrites, no compilation, no evaluation — O(graph size) string building only.