Skip to content

Add HSMM type hierarchy and simulation - #164

Open
rsenne wants to merge 9 commits into
mainfrom
part2-hsmm-types
Open

Add HSMM type hierarchy and simulation#164
rsenne wants to merge 9 commits into
mainfrom
part2-hsmm-types

Conversation

@rsenne

@rsenne rsenne commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Second step in porting Hidden Semi-Markov Models into the package. This PR adds the HSMM type layer and sampling only. Unifies the abstract-type hierarchy so AbstractHMM <: AbstractHSMM, introduces the concrete HSMM type, and implements duration-aware rand.

@rsenne
rsenne requested a review from gdalle June 27, 2026 12:43
@codecov

codecov Bot commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.70115% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.30%. Comparing base (e99a924) to head (ca2c755).

Files with missing lines Patch % Lines
src/types/abstract_hsmm.jl 96.82% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #164      +/-   ##
==========================================
+ Coverage   95.79%   96.30%   +0.50%     
==========================================
  Files          20       22       +2     
  Lines         618      676      +58     
==========================================
+ Hits          592      651      +59     
+ Misses         26       25       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gdalle gdalle left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, this looks good! A few minor nitpicks

Comment thread src/types/abstract_hmm.jl Outdated
Comment thread src/types/abstract_hmm.jl Outdated
Comment thread src/types/abstract_hmm.jl Outdated
Comment thread src/types/abstract_hsmm.jl Outdated
Comment thread src/utils/valid.jl Outdated
Comment thread src/utils/valid.jl Outdated
@rsenne
rsenne requested a review from gdalle July 8, 2026 13:54
@rsenne

rsenne commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

gentle bump on this

Comment thread src/types/abstract_hmm.jl

Simulate `hmm` for `T` time steps, or when the sequence `control_seq` is applied.

Return a named tuple `(; state_seq, obs_seq)`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Removing this guarantee is technically breaking? It does still hold for AbstractHMM, doesn't it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes this is still accurate, I have it in a comment in the AbstractHMM specific rand. I'll re-add to docstring as that's important info

Comment thread src/types/abstract_hsmm.jl Outdated

Return the matrix of state transition probabilities for `model` (possibly when `control` is applied).

For an [`AbstractHSMM`](@ref) the diagonal of this matrix must be zero (no self-transitions).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How can this be true in general since AbstractHMM <: AbstractHSMM?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

you're right -- i'll reword

Comment thread src/types/abstract_hmm.jl Outdated
log_transition_matrix(hmm::AbstractHMM, ::Nothing) = log_transition_matrix(hmm)
obs_distributions(hmm::AbstractHMM, ::Nothing) = obs_distributions(hmm)
# No-op for duration distributions, since HMMs don't have them.
duration_logdensity_type(::AbstractHMM, control) = Union{}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't we rather implement duration_distributions for AbstractHMM? Since it is a subtype, it would makes sense for it to adhere to the API requirements of its supertype

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I mulled on this. In theory we could --- HMMs implicitly have a Geometric sojourn distribution, and we can convert an HSMM to its equivalent HMM without the explicit distributions and vice versa. A few reasons I didn't immediately do this:

  • The point of the subtyping was to eliminate code duplication, so I wanted any effect on HMM inference / learning to be zero-cost, or, for a good enough reason, extremely minimal. The Union{} return here does that: since promote_type(T, Union{}) === T, the duration term drops out of the eltype promotion and HMM forward / forward_backward are unchanged.
  • I wanted to avoid a dep. The only distribution that makes sense here is Geometric, so we'd either have to roll our own (which I'd honestly prefer) or pull in Distributions.jl just for it.
  • We'd need to add machinery in forward / forward_backward to keep the underlying duration_distributions updated when they're present; probably convert_transition at every iteration that finds the equivalent set of Geometric dists.
  • For someone reaching for a plain HMM, it's hard for me to envision duration_distributions being something they'd actually want or need.

So I guess tl;dr: I did not implement it this way because I saw it as minimally helpful for the additional code and possibility of introduced bugs

One thing we could consider, and I think could be worth having, is a small set of utilities: from_hmm / from_hsmm so that if you learn an HSMM with Geometric durations you can convert it to the equivalent HMM and vice versa. Could be handy for model comparison, where you want the parameter counts to line up correctly. This doesn't really address the broader point about not adhering to the subtype interface but could be an alternative

Comment thread src/utils/valid.jl
# One duration distribution per state.
length(durations) == N || return false

# No self-transitions (allowing for numerical noise).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, this seems dubious to me

@rsenne

rsenne commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

@gdalle can I get a ruling on something before I continue?

In this PR I've made it so that AbstractHMM <: AbstractHSMM. After your review, though, I think some amount of semantic dubiousness is going to be unavoidable no matter which way we point the subtyping arrow, and I'd rather get your call on the tradeoff now than keep building on a foundation you might want changed.

The issue is best seen in the transition matrix and the duration distributions (though violating the duration-dist semantics is a bit more defensible, I think), as you flagged. An HSMM traditionally enforces a zero diagonal; self-transitions are forbidden because sojourn lengths come from the explicit duration distributions. An HMM does the opposite: it relies on the diagonal being non-zero, since that's exactly where its (implicit, geometric) sojourn behavior is encoded. So with AbstractHMM <: AbstractHSMM, every HMM is formally an instance of a type whose documented invariant it violates.

It's mathematically defensible, but the zero-diagonal contract doesn't literally hold for the subtype and I think the real reason is that the two transition matrices aren't the same object. For an HSMM, the matrix is best understood as a segment_transition_matrix (governing jumps between segments), whereas the HMM's matrix governs per-timestep transitions.

So I see a couple of ways forward and I'm curious which you'd prefer:

  • Keep AbstractHMM <: AbstractHSMM, but make the distinction explicit. Add a segment_transition_matrix (not sold on the name) as the HSMM-level object; an HMM then additionally carries its own transition_matrix, and we could give it Geometric (but see my caveats above) durations. The upside is it makes it genuinely clear how an HMM is an HSMM, rather than just asserting it. The downside is added complexity---more moving pieces on the HMM type that most users won't need or care about.

  • Drop the subtyping and have both HMM and HSMM subtype a shared AbstractDiscreteLatentModel (or similar). Each relies on the interface it actually needs, we can define separate methods per type where it makes sense, and there's no tension around an HMM violating the semantics of its supertype.

  • A secret third thing that's immediately clear to you that isn't to me lol

@gdalle

gdalle commented Jul 22, 2026

Copy link
Copy Markdown
Member

I would probably go for solution two, after reading your explanation that's what I wanted to do as well

@rsenne
rsenne requested a review from gdalle July 23, 2026 02:40
@rsenne

rsenne commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

gentle bump on this

@gdalle

gdalle commented Aug 10, 2026

Copy link
Copy Markdown
Member

I'll be at JuliaCon this week but I'll try to take a look, sorry!

@rsenne

rsenne commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

oh right that's this week--enjoy the conference! This can wait

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants