@@ -354,7 +354,7 @@ def estimate_stationary_state_transition(
354354 acausal_posterior : np .ndarray ,
355355 stickiness : float = 0.0 ,
356356 concentration : float = 1.0 ,
357- prior_weight : float = 0.0 ,
357+ prior_weight : float | np . ndarray = 0.0 ,
358358) -> np .ndarray :
359359 """Estimate the stationary state transition model.
360360
@@ -366,20 +366,38 @@ def estimate_stationary_state_transition(
366366 acausal_posterior : np.ndarray, shape (n_time, n_states)
367367 stickiness : float, optional
368368 concentration : float, optional
369- prior_weight : float, optional
369+ prior_weight : float or np.ndarray, shape (n_states,) , optional
370370 Dimensionless weight for data-adaptive prior scaling. When > 0,
371- the effective prior pseudo-counts are scaled by the expected
372- transition count per state, making the prior influence approximately
373- invariant to the number of time bins. When 0.0 (default), the
374- fixed-count prior from ``concentration`` and ``stickiness`` is used
375- directly (legacy behavior).
371+ the effective prior pseudo-counts for that row are scaled by the
372+ expected transition count N_i, making the prior influence
373+ approximately invariant to the number of time bins.
374+
375+ Can be a scalar (same weight for all rows) or an array with one
376+ value per state. Rows with ``prior_weight[i] == 0`` use the legacy
377+ fixed-count prior from ``concentration`` and ``stickiness``
378+ directly. This allows mixing frozen rows (e.g., structural states
379+ with very high stickiness) with data-adaptive rows in the same
380+ call. When 0.0 (default), all rows use legacy behavior.
376381
377382 Returns
378383 -------
379384 new_transition_matrix : np.ndarray, shape (n_states, n_states)
380385 """
381- if prior_weight < 0 :
382- raise ValueError (f"prior_weight must be non-negative, got { prior_weight } " )
386+ n_states = acausal_posterior .shape [1 ]
387+
388+ # Normalize prior_weight to shape (n_states,)
389+ prior_weight_arr = np .atleast_1d (np .asarray (prior_weight , dtype = float ))
390+ if prior_weight_arr .size == 1 :
391+ prior_weight_arr = np .full (n_states , prior_weight_arr .item ())
392+ if prior_weight_arr .shape != (n_states ,):
393+ raise ValueError (
394+ f"prior_weight must be a scalar or array of shape ({ n_states } ,), "
395+ f"got shape { prior_weight_arr .shape } "
396+ )
397+ if np .any (prior_weight_arr < 0 ):
398+ raise ValueError (
399+ f"prior_weight must be non-negative, got { prior_weight_arr } "
400+ )
383401
384402 # p(x_t, x_{t+1} | O_{1:T})
385403 joint_distribution = estimate_joint_distribution (
@@ -389,12 +407,14 @@ def estimate_stationary_state_transition(
389407 acausal_posterior ,
390408 )
391409
392- n_states = acausal_posterior .shape [1 ]
393410 joint_sum = joint_distribution .sum (axis = 0 ) # (n_states, n_states)
394411
395412 alpha = get_transition_prior (concentration , stickiness , n_states )
396413
397- if prior_weight > 0.0 :
414+ # Legacy prior for rows with prior_weight[i] == 0
415+ legacy_prior = alpha - 1.0 # (n_states, n_states)
416+
417+ if np .any (prior_weight_arr > 0 ):
398418 # Data-adaptive prior: scale pseudo-counts by expected transitions
399419 # so regularization strength is invariant to temporal resolution.
400420 alpha_shape = alpha - 1.0 # (n_states, n_states)
@@ -407,12 +427,21 @@ def estimate_stationary_state_transition(
407427 # N_i = expected transitions from state i
408428 N_i = joint_sum .sum (axis = - 1 , keepdims = True ) # (n_states, 1)
409429
410- # alpha_eff - 1 = prior_weight * N_i * tilde_alpha
411- effective_prior = prior_weight * N_i * tilde_alpha
412- new_transition_matrix = joint_sum + effective_prior
430+ # Per-row adaptive prior: prior_weight[i] * N_i * tilde_alpha[i]
431+ adaptive_prior = prior_weight_arr [:, np .newaxis ] * N_i * tilde_alpha
432+
433+ # Use adaptive prior for rows where prior_weight > 0, legacy otherwise
434+ use_adaptive = prior_weight_arr > 0 # (n_states,)
435+ effective_prior = np .where (
436+ use_adaptive [:, np .newaxis ],
437+ adaptive_prior ,
438+ legacy_prior ,
439+ )
413440 else :
414- # Legacy fixed-count prior
415- new_transition_matrix = joint_sum + alpha - 1.0
441+ # All rows use legacy fixed-count prior
442+ effective_prior = legacy_prior
443+
444+ new_transition_matrix = joint_sum + effective_prior
416445
417446 # Normalize rows to get transition probabilities.
418447 # Guard against all-zero rows (e.g., unvisited states) to avoid NaN.
@@ -607,7 +636,7 @@ def _estimate_discrete_transition(
607636 transition_concentration : float ,
608637 transition_stickiness : float | np .ndarray ,
609638 transition_regularization : float ,
610- transition_prior_weight : float = 0.0 ,
639+ transition_prior_weight : float | np . ndarray = 0.0 ,
611640) -> tuple [np .ndarray , np .ndarray ]:
612641 """Estimate the discrete transition matrix (stationary or non-stationary).
613642
0 commit comments