@@ -273,6 +273,43 @@ def test_marginalized_discrete_markov_chain_time_varying_P():
273273 np .testing .assert_allclose (marginal_m .compile_logp ()({}), expected )
274274
275275
276+ def test_marginalized_discrete_markov_chain_batched_time_varying_P ():
277+ """Batched chains sharing one time-varying P. The batch axes of the chain must not collide
278+ with P's core (time, from, to) axes in the forward filter."""
279+ pi0 = np .array ([0.6 , 0.4 ])
280+ sigma = 0.8
281+ obs = np .array ([[0.1 , 1.2 , 0.8 , - 0.2 ], [0.9 , - 0.1 , 0.2 , 1.1 ], [0.5 , 0.7 , 1.0 , 0.3 ]])
282+ n_chains , T , k = * obs .shape , 2
283+ A_t = np .array (
284+ [
285+ [[0.9 , 0.1 ], [0.2 , 0.8 ]],
286+ [[0.4 , 0.6 ], [0.3 , 0.7 ]],
287+ [[0.1 , 0.9 ], [0.5 , 0.5 ]],
288+ ]
289+ ) # (T - 1, k, k), no batch axis: every chain shares the same per-step kernels
290+
291+ with pm .Model () as m :
292+ init = pm .Categorical .dist (p = pi0 , shape = (n_chains ,))
293+ states = DiscreteMarkovChain (
294+ "states" , P = A_t , init_dist = init , time_varying_P = True , shape = (n_chains , T )
295+ )
296+ pm .Normal ("emission" , mu = states , sigma = sigma , observed = obs )
297+
298+ marginal_m = marginalize (m , [states ])
299+
300+ # The chains are independent, so brute force each one over its k**T paths and sum.
301+ expected = 0.0
302+ for b in range (n_chains ):
303+ log_joint = []
304+ for s in itertools .product (range (k ), repeat = T ):
305+ lp = np .log (pi0 [s [0 ]]) + sum (np .log (A_t [t - 1 , s [t - 1 ], s [t ]]) for t in range (1 , T ))
306+ lp += norm .logpdf (obs [b ], loc = np .array (s ), scale = sigma ).sum ()
307+ log_joint .append (lp )
308+ expected += logsumexp (log_joint )
309+
310+ np .testing .assert_allclose (marginal_m .compile_logp ()({}), expected )
311+
312+
276313# The higher-order (n_lags > 1) forward scan currently OOMs under the default numba backend
277314# (a numba fast_run scan-lowering bug, not a logic issue; see numba_nlags2_repro.py). Compile the
278315# logp under the C VM so these still verify correctness; drop the override once numba is fixed.
0 commit comments