Skip to content

Fix dims coercion to list in model_from_fgraph - #8465

Merged
ricardoV94 merged 1 commit into
pymc-devs:mainfrom
williambdean:use-tuple-fgraph
Oct 6, 2026
Merged

ricardoV94 merged 1 commit into
pymc-devs:mainfrom
williambdean:use-tuple-fgraph

Conversation

@williambdean

Copy link
Copy Markdown
Contributor

add_named_variable is declared to take a tuple and is passed a list

pymc/model/core.py#L2196

def add_named_variable(self, var, dims: tuple[str | None, ...] | None = None):

pymc/model/fgraph.py#L381

-        dims = list(op.dims) if op.dims else None
+        dims = tuple(op.dims) if op.dims else None
         model.add_named_variable(var, dims=dims)

ModelVar coerces on the way in regardless, pymc/model/fgraph.py#L37, so the
graph never held a list:

self.dims = tuple(dims) if dims is not None else ()

Consequence: the round trip stops agreeing with itself

fgraph_from_model and model_from_fgraph are a pair, so every path built on
clone_model inherits the mismatch:

>>> model_from_fgraph(fgraph_from_model(m)[0]).named_vars_to_dims == m.named_vars_to_dims
False   # before
True    # after
Path Before After
Model.copy(), copy.copy, copy.deepcopy False True
pm.do False True
pm.observe False True

Invisible to CI: both files sit in the FAILING mypy baseline, scripts/run_mypy.py#L25.

No consumer breaks on a list

Reference Behaviour
pymc/backends/arviz.py#L226 list(dvals), normalizes
pymc/backends/mcbackend.py#L57 list(...), normalizes
pymc/printing.py#L430 .get(), ignores type
pymc/variational/opvi.py#L1174 .get(), ignores type
pymc/model_graph.py#L355 in guard, then iterates
pymc/testing.py assert_equivalent_model never reads dims

So this is a contract fix, not a crash fix.

The tests were the defect

Assertions hardcoded the round trip's output instead of comparing to the original:

assert m_new.named_vars_to_dims == {"z": ["test_dim"]}
Reference Now
tests/model/test_fgraph.py#L65 == m_old.named_vars_to_dims
tests/model/transform/test_conditioning.py#L136 == m_old.named_vars_to_dims
tests/model/transform/test_conditioning.py#L234 == m.named_vars_to_dims
tests/model/transform/test_conditioning.py#L241 == m.named_vars_to_dims

Added tests/model/test_fgraph.py#L404, #L428, #L451, #L475, #L498,
#L520 for each isinstance branch, each dims spelling, clone_model, ModelVar
subclasses, and the unhandled fallthrough. 13 fail without the one-line change.

Incidental

pymc/model/fgraph.py#L378 raised TypeError naming the Variable, so it
reported a TensorVariable and said nothing about which Op was unrecognised.
Now names the Op.

Not fixed

pymc/model/fgraph.py#L258 reads .get(name, ()) while pymc/model/fgraph.py#L37
defaults dims=(), so omitted and explicit dims=() collapse in the graph.
Strict xfail at tests/model/test_fgraph.py#L545. Needs a sentinel or a second
prop on ModelVar, which touches __props__ and all five model_*_rv
constructors. Separately, else None to else () trades one silent loss for
another.

@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 pymc | 🛠️ Build #34949462 | 📁 Comparing 0e663fc against latest (abaadf3)

  🔍 Preview build  

2 files changed
± glossary.html
± _modules/pymc/model/fgraph.html

@codecov

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 91.95%. Comparing base (abaadf3) to head (f17b181).

Files with missing lines Patch % Lines
pymc/model/fgraph.py 50.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #8465   +/-   ##
=======================================
  Coverage   91.95%   91.95%           
=======================================
  Files         128      128           
  Lines       21276    21276           
=======================================
  Hits        19564    19564           
  Misses       1712     1712           
Files with missing lines Coverage Δ
pymc/model/fgraph.py 97.87% <50.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ricardoV94 ricardoV94 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started and then stopped. My posterior is beta(1, 0), that all new tests are overkill, and changing the assertion on the original tests suffices. We can't add 20 tests everytime we fix something like list -> tuple ...

Comment thread tests/model/test_fgraph.py Outdated
np.testing.assert_allclose(ip["y_cholesky-cov-packed__"], new_ip["y_cholesky-cov-packed__"])


def test_clone_model_preserves_dims():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overkill test

Comment thread tests/model/test_fgraph.py Outdated
],
ids=["omitted", "single_str", "single_tuple", "two_dims"],
)
def test_dims_forms_roundtrip(dims, shape):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overkill test

Comment thread tests/model/test_fgraph.py Outdated
"kind",
["free_rv", "observed_rv", "deterministic", "potential", "data"],
)
def test_dims_preserved_for_every_var_kind(kind):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overkill test

add_named_variable is declared to take a tuple and was passed a list. It
stores dims verbatim, so the list reached named_vars_to_dims and the round
trip stopped agreeing with itself:

>>> model_from_fgraph(fgraph_from_model(m)[0]).named_vars_to_dims == m.named_vars_to_dims
False   # before
True    # after

Every path built on the pair inherits it, so Model.copy(), copy.copy,
copy.deepcopy, pm.do and pm.observe all disagreed with their input too.

The tests were part of the defect. They asserted the round trip's own
output, ["test_dim"], rather than comparing to the original model, which is
why this went unnoticed. They now compare against the original, and that is
the assertion that catches the type.

Invisible to CI because both files sit in the FAILING mypy baseline.

No consumer depends on a list here: backends/arviz.py and
backends/mcbackend.py normalise with list(), printing.py and
variational/opvi.py ignore the type, and testing.py never reads dims. So
this is a contract fix, not a crash fix.
@williambdean

Copy link
Copy Markdown
Contributor Author

Agreed. Cut it down to the fix plus the assertion changes: list(op.dims) -> tuple(op.dims), and the tests now compare to the original model instead of hardcoding ['test_dim']. All 8 new tests are gone, 13 insertions / 9 deletions.

Kept out as unrelated: the dims=() vs omitted dims xfail, since ModelVar cannot tell them apart and that is a different bug. Can open it separately.

Say the word if you want the adjacent type(model_var) -> type(op) change dropped too, for a strict single-character diff.

@ricardoV94 ricardoV94 changed the title Fix dims type in model_from_fgraph, and test dims preservation Fix dims coercion to list in model_from_fgraph Oct 6, 2026
@ricardoV94
ricardoV94 merged commit 19a783f into pymc-devs:main Oct 6, 2026
42 checks passed
@williambdean
williambdean deleted the use-tuple-fgraph branch October 6, 2026 21:56
@williambdean

Copy link
Copy Markdown
Contributor Author

Thanks for the review @ricardoV94 !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants