@@ -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
241308def test_distribution_op_registered ():
242309 """Test that returned Ops are registered as virtual subclasses of the respective PyMC distributions."""
0 commit comments