Skip to content

Derive Categorical from Argmin/Argmax#8072

Open
ricardoV94 wants to merge 4 commits into
pymc-devs:mainfrom
ricardoV94:categorical_derived_rvs
Open

Derive Categorical from Argmin/Argmax#8072
ricardoV94 wants to merge 4 commits into
pymc-devs:mainfrom
ricardoV94:categorical_derived_rvs

Conversation

@ricardoV94

@ricardoV94 ricardoV94 commented Jan 25, 2026

Copy link
Copy Markdown
Member

Couldn't resist the temptation...

Needed some import shuffling so that I'm allowed to import a SymbolicRandomVariable in logprob rewrites

@codecov

codecov Bot commented Jan 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.75630% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.73%. Comparing base (b3033da) to head (0717876).

Files with missing lines Patch % Lines
pymc/logprob/order.py 90.00% 11 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #8072      +/-   ##
==========================================
- Coverage   91.73%   91.73%   -0.01%     
==========================================
  Files         128      128              
  Lines       20697    20807     +110     
==========================================
+ Hits        18987    19087     +100     
- Misses       1710     1720      +10     
Files with missing lines Coverage Δ
pymc/distributions/custom.py 95.73% <100.00%> (+0.01%) ⬆️
pymc/distributions/distribution.py 94.57% <100.00%> (ø)
pymc/distributions/shape_utils.py 92.38% <100.00%> (ø)
pymc/logprob/transforms.py 95.95% <100.00%> (+0.20%) ⬆️
pymc/logprob/order.py 92.39% <90.00%> (-4.38%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ricardoV94

Copy link
Copy Markdown
Member Author

CC @eclipse1605 if you want to review / ask questions

Comment thread pymc/logprob/order.py Outdated
@ricardoV94 ricardoV94 force-pushed the categorical_derived_rvs branch from edd4bc7 to 36634c0 Compare July 14, 2026 08:07
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@ricardoV94 ricardoV94 force-pushed the categorical_derived_rvs branch from 36634c0 to 8bc8201 Compare July 14, 2026 09:11
@ricardoV94 ricardoV94 changed the base branch from v5 to main July 14, 2026 12:38
@ricardoV94 ricardoV94 force-pushed the categorical_derived_rvs branch 2 times, most recently from 635bd12 to 11eaebb Compare July 14, 2026 12:39
Comment thread pymc/logprob/order.py Outdated
@ricardoV94 ricardoV94 force-pushed the categorical_derived_rvs branch from 11eaebb to 0717876 Compare July 15, 2026 07:54
Comment thread pymc/logprob/order.py
Comment on lines +294 to +306
elif (
len(base_node.inputs) == 2
and isinstance(base_node.op, Elemwise)
and isinstance(base_node.op.scalar_op, Mul)
and ((right_neg := is_minus_1(base_node.inputs[1])) or is_minus_1(base_node.inputs[0]))
):
base_var = base_node.inputs[0 if right_neg else 1]
base_node = base_var.owner
if base_node is None:
return None

if isinstance(base_node.op, ExponentialRV):
# argmin(exponential(rate)) -> categorical(rate)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

wont this break on plain Neg or fused exp * -scale unless lift_loc_scale gets there first? idk...

@ricardoV94 ricardoV94 Jul 15, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Neither is a problem. Neg gets canonicalized to mul by -1, and for the second, bailing is exactly the right thing. The rewrites are recalled until saturation (when a pass changes nothing). So if this comes before the lift_loc_scale, it bails, then lift_loc_scale does its thing, and then the next call is a hit. You can see them happening if you enable config.optimizer_verbose:

import numpy as np
import pytensor
import pytensor.tensor as pt
from pymc.logprob import logp

pytensor.config.optimizer_verbose = True

probs = np.array([0.1, 0.3, 0.6])
x = pt.argmin(pt.random.exponential(scale=1 / probs), axis=-1)

logp(x, x.type())
# rewriting: rewrite local_neg_to_mul replaces Neg.0 of Neg(exponential_rv.out) with Mul.0 of Mul(ExpandDims{axis=0}.0, exponential_rv.out)
# rewriting: rewrite find_measurable_transforms replaces Mul.0 of Mul([-1.], exponential_rv.out) with MeasurableMul.0 of MeasurableMul(exponential_rv.out, [-1.])
# rewriting: rewrite categorical_from_argmax replaces argmax of Argmax{axis=(0,)}(MeasurableMul.0) with categorical_rv.out ...

x = pt.argmax(pt.random.exponential(size=3) * (-1 / probs), axis=-1)

logp(x, x.type())
# rewriting: rewrite lift_loc_scale replaces MeasurableMul.0 of MeasurableMul(exponential_rv.out, [-10. ... .66666667]) with Mul.0 of Mul(exponential_rv.out, ExpandDims{axis=0}.0)   # <- lifted rv * -1
# rewriting: rewrite find_measurable_transforms replaces Mul.0 ... with MeasurableMul.0 ...
# rewriting: rewrite categorical_from_argmax replaces argmax of Argmax{axis=(0,)}(MeasurableMul.0) with categorical_rv.out ...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

on yeah fair my bad

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

no need to apologize, they were good questions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thanks :) but for a bit more clarity, the saturation thing makes sense when lift_loc_scale can actually run

for symbolic outer scale i still dont think that rescue happens because lift_loc_scale bails every pass on constant_fold so graph never changes and hits saturation with neither rewrite matching

import numpy as np
import pytensor
import pytensor.tensor as pt
from pymc.logprob import logp
pytensor.config.optimizer_verbose = True

scale = pt.vector("scale")
x = pt.argmin(pt.random.exponential(scale=1, size=3) * scale, axis=-1)
logp(x, x.type())
# rewriting: rewrite local_neg_to_mul replaces Neg.0 of Neg(Mul.0) with Mul.0 of Mul(ExpandDims{axis=0}.0, Mul.0)
# rewriting: rewrite local_mul_canonizer replaces Mul.0 of Mul(ExpandDims{axis=0}.0, Mul.0) with Mul.0 of Mul(ExpandDims{axis=0}.0, exponential_rv{"()->()"}.out, scale)
# rewriting: rewrite find_measurable_transforms replaces Mul.0 of Mul([-1.], exponential_rv{"()->()"}.out, scale) with MeasurableMul.0 of MeasurableMul(exponential_rv{"()->()"}.out, Mul.0)
# >>>  NotImplementedError: Logprob method not implemented for Argmax{axis=(0,)}

constant outer scale does hit lift + categorical

idk if this makes sense, maybe im missing something

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We reject that because scale could be negative and we can't lift negative scales inside the positive distributions. The inline comment says we could do more work say if scale is abs/exp of a root variable then we'd be safe again

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

gotit makes sense thanks :)

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