Skip to content

Commit 36b8672

Browse files
committed
Add test for logccdf with SymbolicRandomVariable extended_signature
Tests that logccdf registration works correctly for custom Distribution subclasses using SymbolicRandomVariable with extended_signature, which exercises the params_idxs code path in DistributionMeta.
1 parent 628e6d5 commit 36b8672

File tree

1 file changed

+67
-0
lines changed

1 file changed

+67
-0
lines changed

tests/distributions/test_distribution.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,73 @@ def rv_op(cls, size=None, rng=None):
237237
resized_rv = change_dist_size(rv, new_size=5, expand=True)
238238
assert resized_rv.type.shape == (5,)
239239

240+
def test_logccdf_with_extended_signature(self):
241+
"""Test logccdf registration for SymbolicRandomVariable with extended_signature.
242+
243+
What: Tests that a custom Distribution subclass using SymbolicRandomVariable
244+
with an extended_signature can define a logccdf method that gets properly
245+
registered and dispatched.
246+
247+
Why: The DistributionMeta metaclass has two code paths for registering
248+
distribution methods like logp, logcdf, logccdf:
249+
1. For standard RandomVariable ops: unpack (rng, size, *params)
250+
2. For SymbolicRandomVariable with extended_signature: use params_idxs
251+
252+
This test specifically exercises path #2 (the params_idxs branch) to ensure
253+
logccdf works for custom distributions that wrap other distributions with
254+
additional graph structure.
255+
256+
How:
257+
1. Creates a custom Distribution (TestDistWithLogccdf) that:
258+
- Uses a SymbolicRandomVariable with extended_signature
259+
- Wraps a Normal distribution internally
260+
- Defines a logccdf method using normal_lccdf
261+
2. Creates an instance with mu=0, sigma=1
262+
3. Evaluates pm.logccdf at value=0.5
263+
4. Compares against scipy.stats.norm.logsf reference
264+
265+
The extended_signature "[rng],[size],(),()->[rng],()" means:
266+
- Inputs: rng, size, and two scalar params (mu, sigma)
267+
- Outputs: next_rng and scalar draws
268+
"""
269+
from pymc.distributions.dist_math import normal_lccdf
270+
from pymc.distributions.distribution import Distribution
271+
272+
class TestDistWithLogccdf(Distribution):
273+
# Create a SymbolicRandomVariable type with extended_signature
274+
rv_type = type(
275+
"TestRVWithLogccdf",
276+
(SymbolicRandomVariable,),
277+
{"extended_signature": "[rng],[size],(),()->[rng],()"},
278+
)
279+
280+
@classmethod
281+
def dist(cls, mu, sigma, **kwargs):
282+
mu = pt.as_tensor(mu)
283+
sigma = pt.as_tensor(sigma)
284+
return super().dist([mu, sigma], **kwargs)
285+
286+
@classmethod
287+
def rv_op(cls, mu, sigma, size=None, rng=None):
288+
rng = normalize_rng_param(rng)
289+
size = normalize_size_param(size)
290+
# Internally uses Normal, but wrapped in SymbolicRandomVariable
291+
next_rng, draws = Normal.dist(mu, sigma, size=size, rng=rng).owner.outputs
292+
return cls.rv_type(
293+
inputs=[rng, size, mu, sigma],
294+
outputs=[next_rng, draws],
295+
ndim_supp=0,
296+
)(rng, size, mu, sigma)
297+
298+
# This logccdf will be registered via params_idxs path
299+
def logccdf(value, mu, sigma):
300+
return normal_lccdf(mu, sigma, value)
301+
302+
rv = TestDistWithLogccdf.dist(0, 1)
303+
result = pm.logccdf(rv, 0.5).eval()
304+
expected = st.norm(0, 1).logsf(0.5) # ≈ -0.994
305+
npt.assert_allclose(result, expected)
306+
240307

241308
def test_distribution_op_registered():
242309
"""Test that returned Ops are registered as virtual subclasses of the respective PyMC distributions."""

0 commit comments

Comments
 (0)