From e0ff322d5cabf81159f735d09c59618ac0bd2d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Mon, 9 Mar 2026 11:21:19 +0100 Subject: [PATCH 01/28] Move project skill into .agent --- {.claude/skills => .agent}/python-release-workflow/SKILL.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {.claude/skills => .agent}/python-release-workflow/SKILL.md (100%) diff --git a/.claude/skills/python-release-workflow/SKILL.md b/.agent/python-release-workflow/SKILL.md similarity index 100% rename from .claude/skills/python-release-workflow/SKILL.md rename to .agent/python-release-workflow/SKILL.md From 4f0dcb6434f38eac032443d17dd74d38c9849ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Mon, 9 Mar 2026 11:31:45 +0100 Subject: [PATCH 02/28] Document HMM backend refactor direction --- .../development/hmm_backend_refactor.md | 298 ++++++++++++++++++ docs/source/development/index.rst | 1 + 2 files changed, 299 insertions(+) create mode 100644 docs/source/development/hmm_backend_refactor.md diff --git a/docs/source/development/hmm_backend_refactor.md b/docs/source/development/hmm_backend_refactor.md new file mode 100644 index 00000000..a1b887e1 --- /dev/null +++ b/docs/source/development/hmm_backend_refactor.md @@ -0,0 +1,298 @@ +# HMM Backend Refactor Investigation + +This note captures the first refactor step for the HMM code in `gaitmap_mad`. +The immediate goal is not to replace `pomegranate` yet. +The goal is to separate the public gaitmap/tpcp model API from the current `pomegranate` implementation so that: + +- we can keep reproducing the current models and outputs, +- we can remove the Python 3.9 dependency bottleneck later, +- and we can swap the HMM implementation backend in a controlled follow-up step. + +## Summary + +The current design mixes three concerns in the same objects: + +1. topology/configuration of the HMM, +2. training/prediction primitives, +3. backend-native trained model state. + +This is the main reason `pomegranate` leaks into the public parameter surface. +Today, `SimpleHmm` and `RothSegmentationHmm` both carry raw `pomegranate` models as init parameters, and the codebase contains custom serialization and clone hacks to keep these objects tpcp-compatible enough for `clone()`, hashing, and JSON export. + +The recommended intermediate step is: + +- keep exactly one trained `model` parameter on the public HMM algorithm, +- move all submodel configuration into pure config objects, +- introduce a backend object with stateless primitives, +- keep intermediate submodels as results only when needed for debugging. + +## Current Surface + +The current `pomegranate` dependency is visible in multiple layers: + +- `gaitmap/base.py` has special JSON encoding/decoding for `pomegranate.hmm.HiddenMarkovModel`. +- `SimpleHmm` stores a trained `pomegranate` model in `model`. +- `RothSegmentationHmm` stores three trained `pomegranate` models: + - `stride_model.model` + - `transition_model.model` + - `model` +- `packages/gaitmap_mad/.../hmm/_utils.py` contains `_HackyClonableHMMFix` and `_clone_model()` only to make backend-native models clonable and hash-stable enough for tpcp. + +This leads to two design problems: + +1. `stride_model` and `transition_model` are not configuration objects. They are trainable model holders. +2. The public algorithm surface depends on backend-native model objects instead of backend-neutral model state. + +## tpcp Constraints + +The replacement needs to stay compatible with the tpcp object model: + +- all learned state that must survive `clone()` needs to be an init parameter, +- `self_optimize()` may only update exposed optimizable parameters, +- nested configuration should stay as parameter objects and not become ad-hoc internal state, +- debug-only artifacts should be results with a trailing `_`, not parameters. + +For the HMM code this implies: + +- `model` must stay an optimizable parameter, +- submodel topology/training options should be pure parameters, +- trained stride/transition submodels should not remain nested optimizable parameters unless they are required for inference. + +For `RothSegmentationHmm`, only the final combined model is required for prediction. +Therefore the stride and transition submodels should become temporary training artifacts, not persistent parameters. + +## Recommended Public Object Model + +### 1. Replace model-like subobjects with config objects + +`SimpleHmm` should not remain a trainable wrapper in the new design. +It should become a pure configuration object, for example: + +```python +class SimpleHmmConfig(_BaseSerializable): + n_states: int + n_gmm_components: int + architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"] + algo_train: Literal["viterbi", "baum-welch", "labeled"] + stop_threshold: float + max_iterations: int + name: str +``` + +This keeps the existing semantics needed to reproduce the current models, but removes the trained backend-native model from the config object. + +### 2. Introduce a top-level model definition object + +For the Roth model, pass topology/training configuration as a dedicated nested parameter instead of nested trainable submodels: + +```python +class RothHmmModelConfig(_BaseSerializable): + stride: SimpleHmmConfig + transition: SimpleHmmConfig + initialization: Literal["labels", "fully-connected"] +``` + +Then the public `RothSegmentationHmm` surface becomes: + +```python +class RothSegmentationHmm(BaseSegmentationHmm): + model_config: RothHmmModelConfig + feature_transform: BaseHmmFeatureTransformer + backend: BaseHmmBackend + algo_predict: Literal["viterbi", "map"] + algo_train: Literal["viterbi", "baum-welch"] + stop_threshold: float + max_iterations: int + verbose: bool + n_jobs: int + name: str + model: OptiPara[Optional[HmmModelState]] + data_columns: OptiPara[Optional[tuple[str, ...]]] +``` + +This is the main recommended interface change. +It satisfies the requirement that there is only one trained `model` parameter on the public object. + +### 3. Keep intermediate models as results, not parameters + +If intermediate stride and transition models are still useful for debugging, expose them as: + +- `stride_model_` +- `transition_model_` + +These are results. +They should not be part of the stable parameter surface. + +## Recommended Backend Surface + +The backend should be a small stateless object with primitives. +The backend should not know about `SingleSensorData`, stride lists, or feature transforms. +Those remain the job of the gaitmap/tpcp algorithm layer. + +The backend should operate on already prepared feature-space arrays and labels. + +Recommended minimal surface: + +```python +class BaseHmmBackend(_BaseSerializable): + def initialize_model( + self, + *, + data_sequence: Sequence[np.ndarray], + labels_sequence: Sequence[Optional[np.ndarray]], + config: SimpleHmmConfig, + ) -> HmmModelState: ... + + def fit_model( + self, + *, + model: HmmModelState, + data_sequence: Sequence[np.ndarray], + labels_sequence: Sequence[Optional[np.ndarray]], + algorithm: str, + stop_threshold: float, + max_iterations: int, + verbose: bool, + n_jobs: int, + ) -> tuple[HmmModelState, Any]: ... + + def predict_hidden_state_sequence( + self, + *, + model: HmmModelState, + data: np.ndarray, + algorithm: Literal["viterbi", "map"], + ) -> np.ndarray: ... + + def extract_model_definition(self, *, model: HmmModelState) -> HmmModelDefinition: ... + + def build_model( + self, + *, + definition: HmmModelDefinition, + name: str, + freeze_distributions: bool = False, + ) -> HmmModelState: ... +``` + +### Why this surface + +`RothSegmentationHmm.self_optimize_with_info()` currently needs exactly these primitives: + +- initialize and fit a stride submodel, +- initialize and fit a transition submodel, +- predict labeled state sequences from trained submodels, +- extract fitted distributions and transition matrices from trained submodels, +- build a combined model from explicit topology plus fitted emissions, +- optionally freeze emissions, +- fit the combined model. + +Everything else is glue code and should stay outside the backend. + +## Recommended Model State Representation + +The trained `model` parameter should not be a raw `pomegranate` object. +It should be a backend-owned serializable wrapper, for example: + +```python +class HmmModelState(_BaseSerializable): + backend_name: str + payload: dict[str, Any] +``` + +or, if a stronger type split is preferred: + +```python +class BaseHmmModelState(_BaseSerializable): + pass + + +class PomegranateHmmModelState(BaseHmmModelState): + payload: dict[str, Any] +``` + +The important part is that the public gaitmap object graph only sees `HmmModelState`, not `pomegranate.hmm.HiddenMarkovModel`. + +This lets us remove the special `HiddenMarkovModel` handling from `gaitmap/base.py` once the refactor is complete. + +## How Topology Should Be Passed + +The topology should not be passed as a pre-built backend model. +It should be passed as explicit configuration. + +For the immediate refactor, the smallest useful split is: + +- `SimpleHmmConfig` + - state count + - emission count + - submodel architecture + - submodel-local training settings +- `RothHmmModelConfig` + - stride submodel config + - transition submodel config + - combined-model initialization mode + +This is preferable to passing raw transition matrices directly because: + +- it preserves the current high-level API, +- it remains easy to serialize and compare, +- and it still allows a backend to derive exactly the current model topology. + +Direct matrix-based topology objects can be added later if we want a lower-level custom model builder API. + +## What Should Stay Outside the Backend + +The following should remain in gaitmap-level code: + +- feature extraction and inverse transformation, +- converting stride lists into stride/transition training sequences, +- deriving the fully labeled combined training sequence, +- deciding whether the combined model uses `labels` or `fully-connected` initialization, +- validating gaitmap-specific input datatypes and feature column names. + +These steps are not backend-specific. +They are part of the algorithm definition. + +## Migration Plan + +Recommended order: + +1. Introduce `BaseHmmBackend`, `SimpleHmmConfig`, `RothHmmModelConfig`, and a backend-owned `HmmModelState`. +2. Add a `PomegranateHmmBackend` that reproduces the current behavior. +3. Refactor `RothSegmentationHmm` to use `model_config + backend + model`. +4. Keep intermediate submodels as results only, if needed. +5. Remove `_HackyClonableHMMFix` from the public HMM classes once `model` no longer stores raw `pomegranate` objects. +6. Remove the `HiddenMarkovModel` special cases from `gaitmap/base.py` after all serialized HMM paths use backend-owned model state objects. +7. Add a second backend only after the public surface is stable and regression tests confirm matching outputs. + +## Non-Goals for This Step + +This step should not: + +- introduce a second HMM backend yet, +- optimize or simplify the Roth training logic, +- change feature extraction, +- change the meaning of the current architecture choices, +- or change the expected model outputs. + +The purpose is decoupling, not behavioral change. + +## Open Questions + +These decisions are still open and should be resolved before implementation: + +1. Should `SimpleHmmConfig` keep the current `algo_train` field, or should training strategy move entirely into backend fit calls? +2. Should `HmmModelState` be a single generic wrapper with `backend_name`, or should each backend get its own model-state subclass? +3. Do we want to preserve intermediate stride/transition models as serialized debug artifacts, or are result attributes sufficient? +4. Should the future low-level API expose an explicit `HmmModelDefinition` with transition matrices and emission descriptors, or is that only needed internally by the backend? + +## Recommended Decision + +For the next implementation step, use this combination: + +- `model_config: RothHmmModelConfig` +- `backend: PomegranateHmmBackend` +- `model: HmmModelState` +- `stride_model_` and `transition_model_` as optional result attributes only + +That is the narrowest change that removes backend-native objects from the public parameter surface while preserving the current Roth model behavior. diff --git a/docs/source/development/index.rst b/docs/source/development/index.rst index 5683a57a..ce1392a1 100644 --- a/docs/source/development/index.rst +++ b/docs/source/development/index.rst @@ -11,3 +11,4 @@ Development project_structure development_guide caching_guide + hmm_backend_refactor From 8ca5d8c71a67ca78252b3947707e3c80bec5198f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Mon, 9 Mar 2026 11:32:18 +0100 Subject: [PATCH 03/28] Move HMM refactor note out of docs --- docs/source/development/index.rst | 1 - {docs/source/development => notes}/hmm_backend_refactor.md | 0 2 files changed, 1 deletion(-) rename {docs/source/development => notes}/hmm_backend_refactor.md (100%) diff --git a/docs/source/development/index.rst b/docs/source/development/index.rst index ce1392a1..5683a57a 100644 --- a/docs/source/development/index.rst +++ b/docs/source/development/index.rst @@ -11,4 +11,3 @@ Development project_structure development_guide caching_guide - hmm_backend_refactor diff --git a/docs/source/development/hmm_backend_refactor.md b/notes/hmm_backend_refactor.md similarity index 100% rename from docs/source/development/hmm_backend_refactor.md rename to notes/hmm_backend_refactor.md From def5317ff15c2df91dadbfeba9fc9ed8e2929d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Mon, 9 Mar 2026 12:37:42 +0100 Subject: [PATCH 04/28] Refactor HMM config input surface --- .../segmentation_hmm_training.py | 52 +-- gaitmap/stride_segmentation/hmm.py | 6 + notes/hmm_backend_refactor.md | 298 --------------- notes/hmm_refactor_plan_overview.md | 17 + notes/hmm_refactor_step1_config.md | 84 ++++ notes/hmm_refactor_step2_state.md | 103 +++++ ...m_refactor_step3_pomegranate014_backend.md | 67 ++++ .../stride_segmentation/hmm/__init__.py | 3 + .../stride_segmentation/hmm/_config.py | 108 ++++++ .../hmm/_segmentation_model.py | 358 ++++++++++-------- .../stride_segmentation/hmm/_utils.py | 116 ++++-- .../test_stride_segmentation/test_roth_hmm.py | 103 +++-- 12 files changed, 783 insertions(+), 532 deletions(-) delete mode 100644 notes/hmm_backend_refactor.md create mode 100644 notes/hmm_refactor_plan_overview.md create mode 100644 notes/hmm_refactor_step1_config.md create mode 100644 notes/hmm_refactor_step2_state.md create mode 100644 notes/hmm_refactor_step3_pomegranate014_backend.md create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_config.py diff --git a/examples/stride_segmentation/segmentation_hmm_training.py b/examples/stride_segmentation/segmentation_hmm_training.py index 55296aca..62872720 100644 --- a/examples/stride_segmentation/segmentation_hmm_training.py +++ b/examples/stride_segmentation/segmentation_hmm_training.py @@ -84,28 +84,33 @@ # different in architecture, number of states or number of gaussian mixture model (GMM) components. # In this example all configurable parameters are exposed. # These parameters might require optimization for your specific type of dataset! -from gaitmap.stride_segmentation.hmm import SimpleHmm - -stride_model = SimpleHmm( - n_states=20, - n_gmm_components=6, - algo_train="baum-welch", - stop_threshold=1e-9, - max_iterations=5, - architecture="left-right-strict", - verbose=True, - name="stride_model", -) - -transition_model = SimpleHmm( - n_states=5, - n_gmm_components=3, - algo_train="baum-welch", - stop_threshold=1e-9, - max_iterations=5, - architecture="left-right-loose", - verbose=True, - name="transition_model", +from gaitmap.stride_segmentation.hmm import CompositeHmmConfig, HmmSubModelConfig + +model_config = CompositeHmmConfig( + modules={ + "transition": HmmSubModelConfig( + name="transition", + role="transition", + n_states=5, + n_gmm_components=3, + algo_train="baum-welch", + stop_threshold=1e-9, + max_iterations=5, + architecture="left-right-loose", + verbose=True, + ), + "stride": HmmSubModelConfig( + name="stride", + role="stride", + n_states=20, + n_gmm_components=6, + algo_train="baum-welch", + stop_threshold=1e-9, + max_iterations=5, + architecture="left-right-strict", + verbose=True, + ), + } ) # %% @@ -119,8 +124,7 @@ from gaitmap.stride_segmentation.hmm import RothSegmentationHmm segmentation_model = RothSegmentationHmm( - stride_model=stride_model, - transition_model=transition_model, + model_config=model_config, feature_transform=feature_transform, algo_predict="viterbi", algo_train="baum-welch", diff --git a/gaitmap/stride_segmentation/hmm.py b/gaitmap/stride_segmentation/hmm.py index 65f52e09..a01d63c2 100644 --- a/gaitmap/stride_segmentation/hmm.py +++ b/gaitmap/stride_segmentation/hmm.py @@ -10,9 +10,11 @@ from gaitmap.utils._gaitmap_mad import patch_gaitmap_mad_import _gaitmap_mad_modules = { + "CompositeHmmConfig", "BaseHmmFeatureTransformer", "RothHmmFeatureTransformer", "HmmStrideSegmentation", + "HmmSubModelConfig", "SimpleHmm", "RothSegmentationHmm", "PreTrainedRothSegmentationModel", @@ -24,7 +26,9 @@ from gaitmap_mad.stride_segmentation.hmm import ( BaseHmmFeatureTransformer, BaseSegmentationHmm, + CompositeHmmConfig, HmmStrideSegmentation, + HmmSubModelConfig, PreTrainedRothSegmentationModel, RothHmmFeatureTransformer, RothSegmentationHmm, @@ -33,9 +37,11 @@ __all__ = [ + "CompositeHmmConfig", "BaseHmmFeatureTransformer", "BaseSegmentationHmm", "HmmStrideSegmentation", + "HmmSubModelConfig", "PreTrainedRothSegmentationModel", "RothHmmFeatureTransformer", "RothSegmentationHmm", diff --git a/notes/hmm_backend_refactor.md b/notes/hmm_backend_refactor.md deleted file mode 100644 index a1b887e1..00000000 --- a/notes/hmm_backend_refactor.md +++ /dev/null @@ -1,298 +0,0 @@ -# HMM Backend Refactor Investigation - -This note captures the first refactor step for the HMM code in `gaitmap_mad`. -The immediate goal is not to replace `pomegranate` yet. -The goal is to separate the public gaitmap/tpcp model API from the current `pomegranate` implementation so that: - -- we can keep reproducing the current models and outputs, -- we can remove the Python 3.9 dependency bottleneck later, -- and we can swap the HMM implementation backend in a controlled follow-up step. - -## Summary - -The current design mixes three concerns in the same objects: - -1. topology/configuration of the HMM, -2. training/prediction primitives, -3. backend-native trained model state. - -This is the main reason `pomegranate` leaks into the public parameter surface. -Today, `SimpleHmm` and `RothSegmentationHmm` both carry raw `pomegranate` models as init parameters, and the codebase contains custom serialization and clone hacks to keep these objects tpcp-compatible enough for `clone()`, hashing, and JSON export. - -The recommended intermediate step is: - -- keep exactly one trained `model` parameter on the public HMM algorithm, -- move all submodel configuration into pure config objects, -- introduce a backend object with stateless primitives, -- keep intermediate submodels as results only when needed for debugging. - -## Current Surface - -The current `pomegranate` dependency is visible in multiple layers: - -- `gaitmap/base.py` has special JSON encoding/decoding for `pomegranate.hmm.HiddenMarkovModel`. -- `SimpleHmm` stores a trained `pomegranate` model in `model`. -- `RothSegmentationHmm` stores three trained `pomegranate` models: - - `stride_model.model` - - `transition_model.model` - - `model` -- `packages/gaitmap_mad/.../hmm/_utils.py` contains `_HackyClonableHMMFix` and `_clone_model()` only to make backend-native models clonable and hash-stable enough for tpcp. - -This leads to two design problems: - -1. `stride_model` and `transition_model` are not configuration objects. They are trainable model holders. -2. The public algorithm surface depends on backend-native model objects instead of backend-neutral model state. - -## tpcp Constraints - -The replacement needs to stay compatible with the tpcp object model: - -- all learned state that must survive `clone()` needs to be an init parameter, -- `self_optimize()` may only update exposed optimizable parameters, -- nested configuration should stay as parameter objects and not become ad-hoc internal state, -- debug-only artifacts should be results with a trailing `_`, not parameters. - -For the HMM code this implies: - -- `model` must stay an optimizable parameter, -- submodel topology/training options should be pure parameters, -- trained stride/transition submodels should not remain nested optimizable parameters unless they are required for inference. - -For `RothSegmentationHmm`, only the final combined model is required for prediction. -Therefore the stride and transition submodels should become temporary training artifacts, not persistent parameters. - -## Recommended Public Object Model - -### 1. Replace model-like subobjects with config objects - -`SimpleHmm` should not remain a trainable wrapper in the new design. -It should become a pure configuration object, for example: - -```python -class SimpleHmmConfig(_BaseSerializable): - n_states: int - n_gmm_components: int - architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"] - algo_train: Literal["viterbi", "baum-welch", "labeled"] - stop_threshold: float - max_iterations: int - name: str -``` - -This keeps the existing semantics needed to reproduce the current models, but removes the trained backend-native model from the config object. - -### 2. Introduce a top-level model definition object - -For the Roth model, pass topology/training configuration as a dedicated nested parameter instead of nested trainable submodels: - -```python -class RothHmmModelConfig(_BaseSerializable): - stride: SimpleHmmConfig - transition: SimpleHmmConfig - initialization: Literal["labels", "fully-connected"] -``` - -Then the public `RothSegmentationHmm` surface becomes: - -```python -class RothSegmentationHmm(BaseSegmentationHmm): - model_config: RothHmmModelConfig - feature_transform: BaseHmmFeatureTransformer - backend: BaseHmmBackend - algo_predict: Literal["viterbi", "map"] - algo_train: Literal["viterbi", "baum-welch"] - stop_threshold: float - max_iterations: int - verbose: bool - n_jobs: int - name: str - model: OptiPara[Optional[HmmModelState]] - data_columns: OptiPara[Optional[tuple[str, ...]]] -``` - -This is the main recommended interface change. -It satisfies the requirement that there is only one trained `model` parameter on the public object. - -### 3. Keep intermediate models as results, not parameters - -If intermediate stride and transition models are still useful for debugging, expose them as: - -- `stride_model_` -- `transition_model_` - -These are results. -They should not be part of the stable parameter surface. - -## Recommended Backend Surface - -The backend should be a small stateless object with primitives. -The backend should not know about `SingleSensorData`, stride lists, or feature transforms. -Those remain the job of the gaitmap/tpcp algorithm layer. - -The backend should operate on already prepared feature-space arrays and labels. - -Recommended minimal surface: - -```python -class BaseHmmBackend(_BaseSerializable): - def initialize_model( - self, - *, - data_sequence: Sequence[np.ndarray], - labels_sequence: Sequence[Optional[np.ndarray]], - config: SimpleHmmConfig, - ) -> HmmModelState: ... - - def fit_model( - self, - *, - model: HmmModelState, - data_sequence: Sequence[np.ndarray], - labels_sequence: Sequence[Optional[np.ndarray]], - algorithm: str, - stop_threshold: float, - max_iterations: int, - verbose: bool, - n_jobs: int, - ) -> tuple[HmmModelState, Any]: ... - - def predict_hidden_state_sequence( - self, - *, - model: HmmModelState, - data: np.ndarray, - algorithm: Literal["viterbi", "map"], - ) -> np.ndarray: ... - - def extract_model_definition(self, *, model: HmmModelState) -> HmmModelDefinition: ... - - def build_model( - self, - *, - definition: HmmModelDefinition, - name: str, - freeze_distributions: bool = False, - ) -> HmmModelState: ... -``` - -### Why this surface - -`RothSegmentationHmm.self_optimize_with_info()` currently needs exactly these primitives: - -- initialize and fit a stride submodel, -- initialize and fit a transition submodel, -- predict labeled state sequences from trained submodels, -- extract fitted distributions and transition matrices from trained submodels, -- build a combined model from explicit topology plus fitted emissions, -- optionally freeze emissions, -- fit the combined model. - -Everything else is glue code and should stay outside the backend. - -## Recommended Model State Representation - -The trained `model` parameter should not be a raw `pomegranate` object. -It should be a backend-owned serializable wrapper, for example: - -```python -class HmmModelState(_BaseSerializable): - backend_name: str - payload: dict[str, Any] -``` - -or, if a stronger type split is preferred: - -```python -class BaseHmmModelState(_BaseSerializable): - pass - - -class PomegranateHmmModelState(BaseHmmModelState): - payload: dict[str, Any] -``` - -The important part is that the public gaitmap object graph only sees `HmmModelState`, not `pomegranate.hmm.HiddenMarkovModel`. - -This lets us remove the special `HiddenMarkovModel` handling from `gaitmap/base.py` once the refactor is complete. - -## How Topology Should Be Passed - -The topology should not be passed as a pre-built backend model. -It should be passed as explicit configuration. - -For the immediate refactor, the smallest useful split is: - -- `SimpleHmmConfig` - - state count - - emission count - - submodel architecture - - submodel-local training settings -- `RothHmmModelConfig` - - stride submodel config - - transition submodel config - - combined-model initialization mode - -This is preferable to passing raw transition matrices directly because: - -- it preserves the current high-level API, -- it remains easy to serialize and compare, -- and it still allows a backend to derive exactly the current model topology. - -Direct matrix-based topology objects can be added later if we want a lower-level custom model builder API. - -## What Should Stay Outside the Backend - -The following should remain in gaitmap-level code: - -- feature extraction and inverse transformation, -- converting stride lists into stride/transition training sequences, -- deriving the fully labeled combined training sequence, -- deciding whether the combined model uses `labels` or `fully-connected` initialization, -- validating gaitmap-specific input datatypes and feature column names. - -These steps are not backend-specific. -They are part of the algorithm definition. - -## Migration Plan - -Recommended order: - -1. Introduce `BaseHmmBackend`, `SimpleHmmConfig`, `RothHmmModelConfig`, and a backend-owned `HmmModelState`. -2. Add a `PomegranateHmmBackend` that reproduces the current behavior. -3. Refactor `RothSegmentationHmm` to use `model_config + backend + model`. -4. Keep intermediate submodels as results only, if needed. -5. Remove `_HackyClonableHMMFix` from the public HMM classes once `model` no longer stores raw `pomegranate` objects. -6. Remove the `HiddenMarkovModel` special cases from `gaitmap/base.py` after all serialized HMM paths use backend-owned model state objects. -7. Add a second backend only after the public surface is stable and regression tests confirm matching outputs. - -## Non-Goals for This Step - -This step should not: - -- introduce a second HMM backend yet, -- optimize or simplify the Roth training logic, -- change feature extraction, -- change the meaning of the current architecture choices, -- or change the expected model outputs. - -The purpose is decoupling, not behavioral change. - -## Open Questions - -These decisions are still open and should be resolved before implementation: - -1. Should `SimpleHmmConfig` keep the current `algo_train` field, or should training strategy move entirely into backend fit calls? -2. Should `HmmModelState` be a single generic wrapper with `backend_name`, or should each backend get its own model-state subclass? -3. Do we want to preserve intermediate stride/transition models as serialized debug artifacts, or are result attributes sufficient? -4. Should the future low-level API expose an explicit `HmmModelDefinition` with transition matrices and emission descriptors, or is that only needed internally by the backend? - -## Recommended Decision - -For the next implementation step, use this combination: - -- `model_config: RothHmmModelConfig` -- `backend: PomegranateHmmBackend` -- `model: HmmModelState` -- `stride_model_` and `transition_model_` as optional result attributes only - -That is the narrowest change that removes backend-native objects from the public parameter surface while preserving the current Roth model behavior. diff --git a/notes/hmm_refactor_plan_overview.md b/notes/hmm_refactor_plan_overview.md new file mode 100644 index 00000000..39a4b147 --- /dev/null +++ b/notes/hmm_refactor_plan_overview.md @@ -0,0 +1,17 @@ +# HMM Refactor Plan Overview + +This note replaces the earlier single investigation note with a staged plan. + +The refactor should happen in three steps: + +1. Move the current HMM system to a unified input config while still storing and operating on `pomegranate` models. +2. Introduce a generic serializable `HMMState` and change `RothSegmentationHmm.model` to use that state. +3. Add a dedicated `pomegranate 0.14` backend abstraction that converts between backend-native models and `HMMState`. + +Why this order: + +- Step 1 changes the public construction/configuration surface without changing the trained-model representation. +- Step 2 changes the trained-model representation while still keeping the current implementation behavior. +- Step 3 isolates the legacy backend after the public config and model-state surfaces are already stable. + +This order reduces risk and keeps regressions easier to localize. diff --git a/notes/hmm_refactor_step1_config.md b/notes/hmm_refactor_step1_config.md new file mode 100644 index 00000000..8fa11fb6 --- /dev/null +++ b/notes/hmm_refactor_step1_config.md @@ -0,0 +1,84 @@ +# Step 1: Unified Config Surface + +## Goal + +Replace the dedicated `stride_model` and `transition_model` init parameters with a single composite model config while +still using the current `pomegranate` model objects internally. + +## Target API + +The HMM class should accept one nested config object that describes: + +- the available submodules, +- each submodule's local HMM settings, +- how submodules are connected, +- and how the combined model is trained. + +Example target surface: + +```python +class HmmSubModelConfig(_BaseSerializable): + n_states: int + n_gmm_components: int + architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"] + algo_train: Literal["viterbi", "baum-welch", "labeled"] + stop_threshold: float + max_iterations: int + name: str + + +class HmmConnectionConfig(_BaseSerializable): + from_module: str + from_state: int + to_module: str + to_state: int + initial_probability: float | None = None + + +class CompositeHmmConfig(_BaseSerializable): + modules: dict[str, HmmSubModelConfig] + connections: tuple[HmmConnectionConfig, ...] + initialization: Literal["labels", "fully-connected"] + combined_algo_train: Literal["viterbi", "baum-welch", "labeled"] + combined_stop_threshold: float + combined_max_iterations: int + name: str +``` + +## Scope + +This step should only change configuration flow. +It should not yet change: + +- `RothSegmentationHmm.model` +- the stored pretrained model format +- the use of raw `pomegranate` models internally +- `_HackyClonableHMMFix` +- `gaitmap/base.py` HMM serialization + +## Implementation Notes + +- Keep the current Roth behavior by providing a default two-module config with `transition` and `stride`. +- Move the current submodel-specific parameters into default config values. +- Keep the current training loop structure: + - train each configured submodule independently + - combine them by building a final `pomegranate` model + - run the final refinement pass on the combined model +- The training/data-splitting logic can still be Roth-specific in this step. + The point is to stabilize the input surface first. + +## Compatibility Strategy + +- Backwards compatibility for init parameters is optional. +- If needed, provide a thin compatibility shim that maps legacy `stride_model` / `transition_model` inputs to the new + config internally. +- The stored model format should remain unchanged in this step so that the pretrained artifact continues to load + without migration work. + +## Expected Outcome + +After this step: + +- the public constructor uses a single config object, +- custom multi-module setups become expressible, +- but the trained model is still a `pomegranate` model and behavior should be unchanged. diff --git a/notes/hmm_refactor_step2_state.md b/notes/hmm_refactor_step2_state.md new file mode 100644 index 00000000..fce4a41a --- /dev/null +++ b/notes/hmm_refactor_step2_state.md @@ -0,0 +1,103 @@ +# Step 2: Introduce Generic HMMState + +## Goal + +Introduce a backend-neutral serializable `HMMState` and change `RothSegmentationHmm.model` to store this state instead +of a raw `pomegranate` model. + +## Core Idea + +The model state should support: + +- one compiled flat HMM for inference, +- optional hierarchical/composite structure, +- backend provenance for compatibility warnings, +- and fully explicit emission and transition parameters. + +## Target Shape + +The state should not be hardcoded to Roth's current `transition` and `stride` modules. +It must allow arbitrary named submodules. + +Suggested split: + +```python +class BackendInfo(_BaseSerializable): + backend_id: str + backend_version: str | None = None + state_schema_version: int = 1 + + +class HmmGraphState(_BaseSerializable): + start_probs: np.ndarray + end_probs: np.ndarray + transition_probs: np.ndarray + + +class GaussianEmissionState(_BaseSerializable): + kind: Literal["gaussian"] + mean: np.ndarray + covariance: np.ndarray + covariance_type: Literal["full", "diag", "sphere"] + frozen: bool = False + + +class GaussianMixtureEmissionState(_BaseSerializable): + kind: Literal["gaussian_mixture"] + weights: np.ndarray + components: tuple[GaussianEmissionState, ...] + frozen: bool = False + + +class FlatHmmState(_BaseSerializable): + graph: HmmGraphState + emissions: tuple[GaussianEmissionState | GaussianMixtureEmissionState, ...] + state_names: tuple[str, ...] | None = None + + +class CompositionEdge(_BaseSerializable): + from_module: str + from_state: int + to_module: str + to_state: int + weight: float | None = None + + +class CompositeHmmState(_BaseSerializable): + submodels: dict[str, FlatHmmState] + combined: FlatHmmState + cross_module_edges: tuple[CompositionEdge, ...] + metadata: dict[str, Any] | None = None + + +class HmmModelState(_BaseSerializable): + trained_with: BackendInfo + compiled: FlatHmmState + composite: CompositeHmmState | None = None +``` + +## Scope + +This step should change: + +- `RothSegmentationHmm.model` +- the serialization format for trained HMMs +- loading of the existing pretrained model via migration into `HmmModelState` + +This step should not yet require a full backend abstraction. +The current implementation can still directly use `pomegranate` internally and only convert at the boundary where the +public parameter is read or written. + +## Migration Work + +- Add a loader that converts the existing pretrained JSON artifact into `HmmModelState`. +- Preserve enough information to warn when a state was produced by the legacy backend. +- Update tests so cloning, hashing, and JSON roundtrips work without raw `pomegranate` objects in public parameters. + +## Expected Outcome + +After this step: + +- the public trained-model parameter is backend-neutral, +- legacy pretrained artifacts still load through migration, +- and the public API no longer depends on `pomegranate` object serialization. diff --git a/notes/hmm_refactor_step3_pomegranate014_backend.md b/notes/hmm_refactor_step3_pomegranate014_backend.md new file mode 100644 index 00000000..e604beed --- /dev/null +++ b/notes/hmm_refactor_step3_pomegranate014_backend.md @@ -0,0 +1,67 @@ +# Step 3: Add Pomegranate 0.14 Backend Abstraction + +## Goal + +Introduce a dedicated `pomegranate 0.14` backend that is responsible for converting between backend-native models and +the generic `HMMState`, and for executing flat-model training/inference primitives. + +## Backend Responsibility + +The backend should own: + +- initialization of a flat HMM from config and initial labels, +- fitting a flat model, +- prediction of hidden state sequences, +- conversion from backend-native model to `FlatHmmState`, +- conversion from `FlatHmmState` to backend-native model. + +The algorithm layer should still own: + +- feature transformation, +- extraction of training sequences, +- composition of multiple named submodules, +- derivation of cross-module transitions, +- orchestration of the final combined-model refinement pass. + +## Suggested Surface + +```python +class BaseHmmBackend(_BaseSerializable): + def initialize(self, config, data, labels) -> FlatHmmState: ... + def fit(self, model, data, labels, *, fit_mode) -> tuple[FlatHmmState, Any]: ... + def predict(self, model, data, *, algorithm) -> np.ndarray: ... + def to_backend_model(self, model: FlatHmmState) -> Any: ... + def from_backend_model(self, model: Any) -> FlatHmmState: ... +``` + +For `pomegranate 0.14`, `fit_mode` likely needs at least: + +- `"full"` +- `"transitions_only"` + +## Scope + +This step should: + +- move the remaining direct `pomegranate` calls out of the public algorithm classes, +- isolate legacy backend details such as cloning quirks and state-name quirks, +- and establish the adapter pattern needed for later backends. + +This is the step where `_HackyClonableHMMFix` should start disappearing from the public HMM classes, because the +public parameter should already be `HmmModelState` from step 2. + +## Follow-Up + +Once this step is complete, future work becomes much simpler: + +- add a `pomegranate 1.x` backend if desired, +- add a SciPy inference-only backend using the compiled flat state, +- and add compatibility warnings based on `HmmModelState.trained_with`. + +## Expected Outcome + +After this step: + +- the current legacy implementation is isolated behind a backend adapter, +- the public HMM API is config-driven and backend-neutral, +- and future backend replacements no longer require another public API redesign. diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py index eb079c66..69a4926a 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py @@ -3,6 +3,7 @@ import multiprocessing import warnings +from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import ( BaseHmmFeatureTransformer, RothHmmFeatureTransformer, @@ -24,9 +25,11 @@ ) __all__ = [ + "CompositeHmmConfig", "BaseHmmFeatureTransformer", "BaseSegmentationHmm", "HmmStrideSegmentation", + "HmmSubModelConfig", "PreTrainedRothSegmentationModel", "RothHmmFeatureTransformer", "RothSegmentationHmm", diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_config.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_config.py new file mode 100644 index 00000000..be1c427a --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_config.py @@ -0,0 +1,108 @@ +"""Configuration objects for composite HMM segmentation models.""" + +from typing import Literal + +from tpcp import cf + +from gaitmap.base import _BaseSerializable + + +def _default_modules() -> dict[str, "HmmSubModelConfig"]: + return { + "transition": HmmSubModelConfig( + name="transition", + role="transition", + n_states=5, + n_gmm_components=3, + architecture="left-right-loose", + algo_train="baum-welch", + stop_threshold=1e-9, + max_iterations=10, + ), + "stride": HmmSubModelConfig( + name="stride", + role="stride", + n_states=20, + n_gmm_components=6, + architecture="left-right-strict", + algo_train="baum-welch", + stop_threshold=1e-9, + max_iterations=10, + ), + } + + +class HmmSubModelConfig(_BaseSerializable): + """Configuration of a single trainable HMM submodule.""" + + name: str + role: Literal["transition", "stride", "other"] + n_states: int + n_gmm_components: int + architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"] + algo_train: Literal["viterbi", "baum-welch", "labeled"] + stop_threshold: float + max_iterations: int + verbose: bool + n_jobs: int + + def __init__( + self, + name: str, + role: Literal["transition", "stride", "other"], + n_states: int, + n_gmm_components: int, + *, + architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"] = "left-right-strict", + algo_train: Literal["viterbi", "baum-welch", "labeled"] = "baum-welch", + stop_threshold: float = 1e-9, + max_iterations: int = 10, + verbose: bool = True, + n_jobs: int = 1, + ) -> None: + self.name = name + self.role = role + self.n_states = n_states + self.n_gmm_components = n_gmm_components + self.architecture = architecture + self.algo_train = algo_train + self.stop_threshold = stop_threshold + self.max_iterations = max_iterations + self.verbose = verbose + self.n_jobs = n_jobs + + def __eq__(self, other: object) -> bool: + if not isinstance(other, HmmSubModelConfig): + return False + return self.get_params(deep=False) == other.get_params(deep=False) + + +class CompositeHmmConfig(_BaseSerializable): + """Configuration of a composite HMM with named submodules.""" + + modules: dict[str, HmmSubModelConfig] + transition_model_name: str + + def __init__( + self, + modules: dict[str, HmmSubModelConfig] = cf(_default_modules()), + *, + transition_model_name: str = "transition", + ) -> None: + self.modules = modules + self.transition_model_name = transition_model_name + + @property + def explicit_region_model_names(self) -> tuple[str, ...]: + """Return all module names except the implicit transition module.""" + return tuple(name for name in self.modules if name != self.transition_model_name) + + @property + def transition_model(self) -> HmmSubModelConfig: + """Return the configuration of the implicit transition module.""" + return self.modules[self.transition_model_name] + + @property + def stride_model_names(self) -> tuple[str, ...]: + """Return module names that should be interpreted as stride-like states.""" + return tuple(name for name, module in self.modules.items() if module.role == "stride") diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py index 247f6602..068e915e 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py @@ -7,13 +7,19 @@ import numpy as np import pandas as pd import pomegranate as pg +import tpcp from pomegranate import HiddenMarkovModel as pgHMM from pomegranate.hmm import History from tpcp import OptiPara, cf, make_optimize_safe from typing_extensions import Self from gaitmap.base import _BaseSerializable -from gaitmap.utils.datatype_helper import SingleSensorData, SingleSensorStrideList +from gaitmap.utils.datatype_helper import ( + SingleSensorData, + SingleSensorRegionsOfInterestList, + SingleSensorStrideList, +) +from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import ( BaseHmmFeatureTransformer, RothHmmFeatureTransformer, @@ -26,39 +32,35 @@ _HackyClonableHMMFix, add_transition, check_history_for_training_failure, - convert_stride_list_to_transition_list, + convert_region_list_to_transition_list, create_transition_matrix_fully_connected, extract_transitions_starts_stops_from_hidden_state_sequence, fix_model_names, get_model_distributions, - get_train_data_sequences_strides, + get_train_data_sequences_regions, get_train_data_sequences_transitions, labels_to_strings, predict, + validate_trainable_region_list, ) def create_fully_labeled_gait_sequences( - data_train_sequence, stride_list_sequence, transition_model, stride_model, algo_predict + data_train_sequence: Sequence[pd.DataFrame], + region_list_sequence: Sequence[SingleSensorRegionsOfInterestList], + module_models: dict[str, SimpleHmm], + module_offsets: dict[str, int], + transition_model_name: str, + algo_predict: Literal["viterbi", "map"], ): - """Create fully labeled gait sequence. - - To find the "actual" hidden-state labels for "labeled-training" with the given training data set, we will again - split everything into strides and transitions based on our initial stride borders and then predict the labels with - the respective already learned models. - - To rephrase it again: We want to create a fully labeled dataset with already optimal hidden-state labels, but as - these lables are hidden, we need to predict them with our already trained models... - """ + """Create fully labeled gait sequences from typed regions and trained submodels.""" labels_train_sequence = [] - for data, stride_list in zip(data_train_sequence, stride_list_sequence): + transition_model = module_models[transition_model_name] + for data, region_list in zip(data_train_sequence, region_list_sequence): labels_train = np.zeros(len(data)) - # predict hidden-state sequence for each transition using "transition model" - transition_start_end_list = convert_stride_list_to_transition_list(stride_list, data.shape[0]) - - # for each transition, get data and create some naive labels for initialization + transition_start_end_list = convert_region_list_to_transition_list(region_list, data.shape[0]) for start, end in transition_start_end_list[["start", "end"]].to_numpy(): transition_data_train = data[start:end] try: @@ -66,27 +68,57 @@ def create_fully_labeled_gait_sequences( transition_data_train, algorithm=algo_predict ) except _DataToShortError: - # This happens if a transition is too short to be predicted by the transition model continue - # predict hidden-state sequence for each stride using "stride model" - for start, end in stride_list[["start", "end"]].to_numpy(): - stride_data_train = data[start:end] + for start, end, region_type in region_list[["start", "end", "type"]].to_numpy(): + region_model = module_models[region_type] + region_data_train = data[start:end] try: labels_train[start:end] = ( - stride_model.predict_hidden_state_sequence(stride_data_train, algorithm=algo_predict) - + transition_model.n_states + region_model.predict_hidden_state_sequence(region_data_train, algorithm=algo_predict) + + module_offsets[region_type] ) except _DataToShortError: - # This happens if a stride is too short to be predicted by the stride model continue - # append cleaned sequences to train_sequence labels_train_sequence.append(labels_train) return labels_train_sequence +def _create_simple_hmm_from_config(config: HmmSubModelConfig) -> SimpleHmm: + return SimpleHmm( + n_states=config.n_states, + n_gmm_components=config.n_gmm_components, + architecture=config.architecture, + algo_train=config.algo_train, + stop_threshold=config.stop_threshold, + max_iterations=config.max_iterations, + verbose=config.verbose, + n_jobs=config.n_jobs, + name=config.name, + ) + + +def _coerce_region_list_input( + region_list: pd.DataFrame, explicit_region_model_names: tuple[str, ...] +) -> SingleSensorRegionsOfInterestList: + """Normalize legacy stride-list input to the new typed-region format when possible.""" + if "type" in region_list.reset_index().columns: + return region_list + if len(explicit_region_model_names) != 1: + raise ValueError( + "The provided training regions do not contain a `type` column. " + "Automatic conversion from the legacy stride-list format is only possible when exactly one explicit " + f"region module exists. Got explicit modules: {list(explicit_region_model_names)}" + ) + coerced_region_list = region_list.reset_index().copy() + if not {"roi_id", "gs_id"} & set(coerced_region_list.columns): + coerced_region_list.insert(0, "roi_id", np.arange(len(coerced_region_list))) + coerced_region_list["type"] = explicit_region_model_names[0] + return coerced_region_list + + class BaseSegmentationHmm(_BaseSerializable): """Base class for HMM segmentation models. @@ -128,7 +160,7 @@ def predict(self, data: SingleSensorData, sampling_rate_hz: float) -> Self: def self_optimize( self, data_sequence: Sequence[SingleSensorData], - stride_list_sequence: Sequence[SingleSensorStrideList], + region_list_sequence: Sequence[SingleSensorRegionsOfInterestList], sampling_rate_hz: float, ) -> Self: """Create and train the HMM model based on the given data and labels. @@ -137,9 +169,9 @@ def self_optimize( ---------- data_sequence Sequence of gaitmap sensordata objects. - stride_list_sequence - Sequence of gaitmap stride lists. - The number of stride lists must match the number of sensordata objects (i.e. they must belong together). + region_list_sequence + Sequence of typed region lists. + The number of region lists must match the number of sensordata objects (i.e. they must belong together). sampling_rate_hz Sampling frequency of the data. @@ -154,7 +186,7 @@ def self_optimize( def self_optimize_with_info( self, data_sequence: Sequence[SingleSensorData], - stride_list_sequence: Sequence[SingleSensorStrideList], + region_list_sequence: Sequence[SingleSensorRegionsOfInterestList], sampling_rate_hz: float, ) -> tuple[Self, Any]: """Create and train the HMM model based on the given data and labels. @@ -165,9 +197,9 @@ def self_optimize_with_info( ---------- data_sequence Sequence of gaitmap sensordata objects. - stride_list_sequence - Sequence of gaitmap stride lists. - The number of stride lists must match the number of sensordata objects (i.e. they must belong together). + region_list_sequence + Sequence of typed region lists. + The number of region lists must match the number of sensordata objects (i.e. they must belong together). sampling_rate_hz Sampling frequency of the data. @@ -192,12 +224,10 @@ class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHM Parameters ---------- - stride_model - The (untrained) hmm representing the strides in the data. - This will be updated during the optimization step (see notes). - transition_model - The (untrained) hmm representing the transitions in the data. - This will be updated during the optimization step (see notes). + model_config + The configuration of the named HMM submodules that are trained and combined into the final model. + One module is interpreted as the implicit transition model and all remaining modules are expected to be covered + by typed input regions during optimization. feature_transform An instance of a :class:`~gaitmap.stride_segmentation.hmm.FeatureTransformHMM` that can transform the data (and the labeled stride list) into the feature space required by the HMM. @@ -210,16 +240,14 @@ class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHM The loss threshold to stop the optimization. Note, that is the threshold for the "combined" training of the final model. This is less important, as we recommend to train the combined model only for a single iteration anyway. - If you want to adjust the stop threshold for the individual models, you can do so by adjusting the respective - parameters of the stride and transition model (`stride_model.stop_threshold` and - `transition_model.stop_threshold`). + If you want to adjust the stop threshold for the individual submodels, do so via the corresponding entries in + `model_config.modules`. max_iterations The maximum number of iterations to perform during the optimization. Note, that this is the value for the "combined" training of the final model. We recommend keeping this value at 1, as the combined model training only adjusts the transition matrix. - If you want to adjust the max iterations for the individual models, you can do so by adjusting the respective - parameters of the stride and transition model (`stride_model.max_iterations` and - `transition_model.max_iterations`). + If you want to adjust the max iterations for the individual submodels, do so via the corresponding entries in + `model_config.modules`. initialization The initialization method to use for the HMM during optimization. `fully-connected` assumes that all states are reachable from any other state with the same probability. @@ -269,10 +297,8 @@ class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHM Notes ----- - Note, that we are also store the trained stride and transition model during the optimization step. - These are not required for prediction, as all there information is also contained in the fused model. - However, inspecting the trained models might provide further inside into possible training issues. - As the models are generally small, this should not impact RAM (or disk usage during export) in a relevant way. + The final model is still stored as a raw `pomegranate` HMM in this step of the refactor. + The `model_config` only replaces the dedicated submodel constructor inputs. References ---------- @@ -282,12 +308,7 @@ class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHM """ - stride_model: SimpleHmm - stride_model__model: OptiPara - stride_model__data_columns: OptiPara - transition_model: SimpleHmm - transition_model__model: OptiPara - transition_model__data_columns: OptiPara + model_config: CompositeHmmConfig feature_transform: BaseHmmFeatureTransformer algo_predict: Literal["viterbi", "baum-welch"] algo_train: Literal["viterbi", "baum-welch"] @@ -303,30 +324,52 @@ class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHM feature_space_data_: pd.DataFrame hidden_state_sequence_feature_space_: np.ndarray - def __init__( - self, - stride_model: SimpleHmm = cf( - SimpleHmm( - n_states=20, - n_gmm_components=6, - algo_train="baum-welch", - stop_threshold=1e-9, - max_iterations=10, - architecture="left-right-strict", - name="stride_model", + @classmethod + def _from_json_dict(cls, json_dict: dict) -> Self: + params = json_dict["params"].copy() + if "model_config" not in params and {"stride_model", "transition_model"} <= set(params): + stride_model = params.pop("stride_model") + transition_model = params.pop("transition_model") + stride_params = stride_model.get_params(deep=False) if isinstance(stride_model, SimpleHmm) else stride_model["params"] + transition_params = ( + transition_model.get_params(deep=False) + if isinstance(transition_model, SimpleHmm) + else transition_model["params"] ) - ), - transition_model: SimpleHmm = cf( - SimpleHmm( - n_states=5, - n_gmm_components=3, - algo_train="baum-welch", - stop_threshold=1e-9, - max_iterations=10, - architecture="left-right-loose", - name="transition_model", + params["model_config"] = CompositeHmmConfig( + modules={ + "transition": HmmSubModelConfig( + name="transition", + role="transition", + n_states=transition_params["n_states"], + n_gmm_components=transition_params["n_gmm_components"], + architecture=transition_params["architecture"], + algo_train=transition_params["algo_train"], + stop_threshold=transition_params["stop_threshold"], + max_iterations=transition_params["max_iterations"], + verbose=transition_params.get("verbose", True), + n_jobs=transition_params.get("n_jobs", 1), + ), + "stride": HmmSubModelConfig( + name="stride", + role="stride", + n_states=stride_params["n_states"], + n_gmm_components=stride_params["n_gmm_components"], + architecture=stride_params["architecture"], + algo_train=stride_params["algo_train"], + stop_threshold=stride_params["stop_threshold"], + max_iterations=stride_params["max_iterations"], + verbose=stride_params.get("verbose", True), + n_jobs=stride_params.get("n_jobs", 1), + ), + } ) - ), + input_data = {k: params[k] for k in tpcp.get_param_names(cls) if k in params} + return cls(**input_data) + + def __init__( + self, + model_config: CompositeHmmConfig = cf(CompositeHmmConfig()), feature_transform: RothHmmFeatureTransformer = cf(RothHmmFeatureTransformer()), *, algo_predict: Literal["viterbi", "map"] = "viterbi", @@ -340,8 +383,7 @@ def __init__( model: Optional[pgHMM] = None, data_columns: Optional[tuple[str, ...]] = None, ) -> None: - self.stride_model = stride_model - self.transition_model = transition_model + self.model_config = model_config self.feature_transform = feature_transform self.algo_predict = algo_predict self.algo_train = algo_train @@ -357,17 +399,34 @@ def __init__( @property def n_states(self) -> int: """Return the number of states of the final model.""" - return self.transition_model.n_states + self.stride_model.n_states + return sum(module.n_states for module in self.model_config.modules.values()) + + @property + def module_offsets(self) -> dict[str, int]: + """Return the state offsets of each configured submodule in the combined model.""" + offsets = {} + current_offset = 0 + for name, module in self.model_config.modules.items(): + offsets[name] = current_offset + current_offset += module.n_states + return offsets @property def stride_states(self) -> list[int]: - """Return the ids of the stride states.""" - return (np.arange(self.stride_model.n_states) + self.transition_model.n_states).tolist() + """Return the ids of all stride-like states.""" + stride_states = [] + for name, module in self.model_config.modules.items(): + if module.role != "stride": + continue + stride_states.extend((np.arange(module.n_states) + self.module_offsets[name]).tolist()) + return stride_states @property def transition_states(self) -> list[int]: """Return the ids of the transition states.""" - return np.arange(self.transition_model.n_states).tolist() + transition_module = self.model_config.transition_model + transition_offset = self.module_offsets[self.model_config.transition_model_name] + return (np.arange(transition_module.n_states) + transition_offset).tolist() def predict(self, data: SingleSensorData, sampling_rate_hz: float) -> Self: """Perform prediction based on given data and given model. @@ -418,7 +477,7 @@ def predict(self, data: SingleSensorData, sampling_rate_hz: float) -> Self: def _transform( self, data_sequence: Sequence[pd.DataFrame], - stride_list_sequence: Optional[Sequence[pd.DataFrame]], + region_list_sequence: Optional[Sequence[pd.DataFrame]], sampling_rate_hz: float, ): """Perform feature transformation.""" @@ -429,20 +488,20 @@ def _transform( for dataset in data_sequence ] - stride_list_feature_space = None - if stride_list_sequence: - stride_list_feature_space = [ + region_list_feature_space = None + if region_list_sequence: + region_list_feature_space = [ feature_transform.transform( - roi_list=stride_list, sampling_rate_hz=sampling_rate_hz + roi_list=region_list, sampling_rate_hz=sampling_rate_hz ).transformed_roi_list_ - for stride_list in stride_list_sequence + for region_list in region_list_sequence ] - return data_sequence_feature_space, stride_list_feature_space + return data_sequence_feature_space, region_list_feature_space def self_optimize( self, data_sequence: Sequence[SingleSensorData], - stride_list_sequence: Sequence[SingleSensorStrideList], + region_list_sequence: Sequence[SingleSensorRegionsOfInterestList], sampling_rate_hz: float, ) -> Self: """Create and train the HMM model based on the given data and labels. @@ -458,9 +517,9 @@ def self_optimize( ---------- data_sequence Sequence of gaitmap sensordata objects. - stride_list_sequence - Sequence of gaitmap stride lists. - The number of stride lists must match the number of sensordata objects (i.e. they must belong together). + region_list_sequence + Sequence of typed region lists with `start`, `end`, and `type`. + The number of region lists must match the number of sensordata objects (i.e. they must belong together). sampling_rate_hz Sampling frequency of the data. @@ -470,15 +529,15 @@ def self_optimize( The trained model instance. """ - return self.self_optimize_with_info(data_sequence, stride_list_sequence, sampling_rate_hz=sampling_rate_hz)[0] + return self.self_optimize_with_info(data_sequence, region_list_sequence, sampling_rate_hz=sampling_rate_hz)[0] @make_optimize_safe def self_optimize_with_info( self, data_sequence: Sequence[SingleSensorData], - stride_list_sequence: Sequence[SingleSensorStrideList], + region_list_sequence: Sequence[SingleSensorRegionsOfInterestList], sampling_rate_hz: float, - ) -> tuple[Self, dict[Literal["self", "transition_model", "stride_model"], History]]: + ) -> tuple[Self, dict[str, History]]: """Create and train the HMM model based on the given data and labels. This is identical to `self_optimize`, but returns additional information about the training process. @@ -489,9 +548,9 @@ def self_optimize_with_info( ---------- data_sequence Sequence of gaitmap sensordata objects. - stride_list_sequence - Sequence of gaitmap stride lists. - The number of stride lists must match the number of sensordata objects (i.e. they must belong together). + region_list_sequence + Sequence of typed region lists with `start`, `end`, and `type`. + The number of region lists must match the number of sensordata objects (i.e. they must belong together). sampling_rate_hz Sampling frequency of the data. @@ -506,48 +565,57 @@ def self_optimize_with_info( if self.initialization not in ["labels", "fully-connected"]: raise ValueError("Invalid value for initialization! Must be one of `labels` or `fully-connected`.") - # perform feature transformation - data_sequence_feature_space, stride_list_feature_space = self._transform( - data_sequence, stride_list_sequence, sampling_rate_hz - ) - - # train sub stride model - strides_sequence, init_stride_state_labels = get_train_data_sequences_strides( - data_sequence_feature_space, stride_list_feature_space, self.stride_model.n_states - ) - - stride_model_trained, stride_model_history = self.stride_model.self_optimize_with_info( - strides_sequence, init_stride_state_labels - ) + validated_region_list_sequence = [ + validate_trainable_region_list( + _coerce_region_list_input(region_list, self.model_config.explicit_region_model_names), + self.model_config.explicit_region_model_names, + ) + for region_list in region_list_sequence + ] - # train sub transition _model - transition_sequence, init_trans_state_labels = get_train_data_sequences_transitions( - data_sequence_feature_space, stride_list_feature_space, self.transition_model.n_states + # perform feature transformation + data_sequence_feature_space, region_list_feature_space = self._transform( + data_sequence, validated_region_list_sequence, sampling_rate_hz ) + if region_list_feature_space is None: + raise RuntimeError("The feature transform did not produce region lists for optimization.") + + trained_models: dict[str, SimpleHmm] = {} + histories: dict[str, History] = {} + for module_name, module_config in self.model_config.modules.items(): + if module_name == self.model_config.transition_model_name: + train_sequence, init_state_labels = get_train_data_sequences_transitions( + data_sequence_feature_space, region_list_feature_space, module_config.n_states + ) + else: + train_sequence, init_state_labels = get_train_data_sequences_regions( + data_sequence_feature_space, + region_list_feature_space, + region_type=module_name, + n_states=module_config.n_states, + ) - transition_model_trained, transition_model_history = self.transition_model.self_optimize_with_info( - transition_sequence, init_trans_state_labels - ) + trained_model, history = _create_simple_hmm_from_config(module_config).self_optimize_with_info( + train_sequence, init_state_labels + ) + trained_models[module_name] = trained_model + histories[module_name] = history # For model combination actually only the transition probabilities will be updated, while keeping the already # learned distributions for all states. This can be achieved by "labeled" training, where basically just the # number of transitions will be counted. - - # some initialization stuff... - n_states_transition = transition_model_trained.n_states - n_states_stride = stride_model_trained.n_states - - # extract fitted distributions from both separate trained models - distributions = get_model_distributions(transition_model_trained.model) + get_model_distributions( - stride_model_trained.model - ) + distributions = [] + for module_name in self.model_config.modules: + distributions.extend(get_model_distributions(trained_models[module_name].model)) # predict hidden state labels for complete walking bouts + module_offsets = self.module_offsets labels_train_sequence = create_fully_labeled_gait_sequences( data_sequence_feature_space, - stride_list_feature_space, - transition_model_trained, - stride_model_trained, + region_list_feature_space, + trained_models, + module_offsets, + self.model_config.transition_model_name, self.algo_predict, ) @@ -565,18 +633,14 @@ def self_optimize_with_info( ) elif self.initialization == "labels": - # combine already trained transition matrices -> zero pad "stride" transition matrix to the left - trans_mat_stride = stride_model_trained.model.dense_transition_matrix()[:-2, :-2] - transmat_stride = np.pad( - trans_mat_stride, [(n_states_transition, 0), (n_states_transition, 0)], mode="constant" - ) - - # zero-pad "transition" transition matrix to the right - trans_mat_transition = transition_model_trained.model.dense_transition_matrix()[:-2, :-2] - transmat_trans = np.pad(trans_mat_transition, [(0, n_states_stride), (0, n_states_stride)], mode="constant") - - # after correct zero padding we can combine both transition matrices just by "adding" them together! - trans_mat = transmat_trans + transmat_stride + trans_mat = np.zeros((self.n_states, self.n_states)) + for module_name, module_config in self.model_config.modules.items(): + module_transition_matrix = trained_models[module_name].model.dense_transition_matrix()[:-2, :-2] + offset = module_offsets[module_name] + trans_mat[ + offset : offset + module_config.n_states, + offset : offset + module_config.n_states, + ] = module_transition_matrix # find missing transitions from labels transitions, starts, ends = extract_transitions_starts_stops_from_hidden_state_sequence( @@ -647,7 +711,5 @@ def self_optimize_with_info( self.model = new_model - return ( - self, - {"self": history, "transition_model": transition_model_history, "stride_model": stride_model_history}, - ) + histories["self"] = history + return self, histories diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py index d01f527c..fc1321b3 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py @@ -11,7 +11,13 @@ from tpcp import BaseTpcpObject, CloneFactory from tpcp._hash import custom_hash -from gaitmap.utils.datatype_helper import SingleSensorData, SingleSensorRegionsOfInterestList, SingleSensorStrideList +from gaitmap.utils.datatype_helper import ( + SingleSensorData, + SingleSensorRegionsOfInterestList, + SingleSensorStrideList, + is_single_sensor_regions_of_interest_list, +) +from gaitmap.utils.exceptions import ValidationError def _add_transition(model, a, b, probability, pseudocount, group) -> None: @@ -499,8 +505,64 @@ def convert_stride_list_to_transition_list( ) +def validate_trainable_region_list( + region_list: SingleSensorRegionsOfInterestList, explicit_region_types: tuple[str, ...] +) -> SingleSensorRegionsOfInterestList: + """Validate and normalize a typed region list for HMM training. + + The passed region list must be a valid ROI list with an additional `type` column. + The `type` values must map to the explicit region modules of the HMM config. + Regions must not overlap. + """ + try: + is_single_sensor_regions_of_interest_list(region_list, region_type="any", raise_exception=True) + normalized_region_list = region_list.reset_index() + if "type" not in normalized_region_list.columns: + raise ValidationError("The region list is expected to have a `type` column.") + if normalized_region_list["type"].isna().any(): + raise ValidationError("The `type` column of the region list is not allowed to contain missing values.") + invalid_types = sorted(set(normalized_region_list["type"]) - set(explicit_region_types)) + if invalid_types: + raise ValidationError( + f"The region list contains unknown region types {invalid_types}. " + f"Expected only the following explicit region types: {list(explicit_region_types)}" + ) + normalized_region_list = normalized_region_list.sort_values(["start", "end"]).reset_index(drop=True) + previous_end = None + for start, end in normalized_region_list[["start", "end"]].to_numpy(): + if end <= start: + raise ValidationError("All regions must satisfy `end > start`.") + if previous_end is not None and start < previous_end: + raise ValidationError("The region list must not contain overlapping regions.") + previous_end = end + except ValidationError as e: + raise ValidationError( + "The passed object does not seem to be a valid typed region list for HMM training. " + f"The validation failed with the following error:\n\n{e!s}" + ) from e + return normalized_region_list + + +def convert_region_list_to_transition_list( + region_list: SingleSensorRegionsOfInterestList, last_end: int +) -> SingleSensorRegionsOfInterestList: + """Return the implicit transition regions, i.e. everything not covered by explicit regions.""" + regions = region_list[["start", "end"]].sort_values(["start", "end"]).to_numpy() + transition_regions = [] + current_start = 0 + for start, end in regions: + if start > current_start: + transition_regions.append((current_start, start)) + current_start = end + if current_start < last_end: + transition_regions.append((current_start, last_end)) + return pd.DataFrame(transition_regions, columns=["start", "end"]) + + def get_train_data_sequences_transitions( - data_train_sequence: list[SingleSensorData], stride_list_sequence: list[SingleSensorStrideList], n_states: int + data_train_sequence: list[SingleSensorData], + region_list_sequence: list[SingleSensorRegionsOfInterestList], + n_states: int, ) -> tuple[list[np.ndarray], list[np.ndarray]]: """Extract Transition Training set. @@ -513,11 +575,9 @@ def get_train_data_sequences_transitions( n_too_short_transitions = 0 - for data, stride_list in zip(data_train_sequence, stride_list_sequence): + for data, region_list in zip(data_train_sequence, region_list_sequence): # for each transition, get data and create some naive labels for initialization - for start, end in convert_stride_list_to_transition_list(stride_list, data.shape[0])[ - ["start", "end"] - ].to_numpy(): + for start, end in convert_region_list_to_transition_list(region_list, data.shape[0])[["start", "end"]].to_numpy(): # append extracted sequences and corresponding label set to results list try: labels = create_equidistant_label_sequence(end - start, n_states).astype("int64") @@ -531,7 +591,7 @@ def get_train_data_sequences_transitions( warnings.warn( f"{n_too_short_transitions} transitions (out of " f"{len(trans_labels_train_sequence) + n_too_short_transitions}) were ignored, because they were shorter " - "than the expected number of transition states ({n_states}). " + f"than the expected number of transition states ({n_states}). " "This warning can usually be ignored, if the number of remaining transitions is still large " "enough to train a model." ) @@ -539,40 +599,42 @@ def get_train_data_sequences_transitions( return trans_data_train_sequence, trans_labels_train_sequence -def get_train_data_sequences_strides( - data_train_sequence: list[SingleSensorData], stride_list_sequence: list[SingleSensorStrideList], n_states: int +def get_train_data_sequences_regions( + data_train_sequence: list[SingleSensorData], + region_list_sequence: list[SingleSensorRegionsOfInterestList], + region_type: str, + n_states: int, ) -> tuple[list[np.ndarray], list[np.ndarray]]: - """Extract Transition Training set. + """Extract training sequences for one explicit region type. - - data_train_sequence: list of datasets in feature space - - stride_list_sequence: list of gaitmap stride-lists - - n_states: number of labels. + The region list is expected to have `start`, `end`, and `type` columns. """ - stride_data_train_sequence = [] - stride_labels_train_sequence = [] + region_data_train_sequence = [] + region_labels_train_sequence = [] - n_too_short_strides = 0 + n_too_short_regions = 0 - for data, stride_list in zip(data_train_sequence, stride_list_sequence): - # extract strides directly from stride_list - for start, end in stride_list[["start", "end"]].to_numpy(): + for data, region_list in zip(data_train_sequence, region_list_sequence): + matching_regions = region_list[region_list["type"] == region_type] + for start, end in matching_regions[["start", "end"]].to_numpy(): try: labels = create_equidistant_label_sequence(end - start, n_states).astype("int64") except ValueError: - n_too_short_strides += 1 + n_too_short_regions += 1 continue - stride_labels_train_sequence.append(labels) - stride_data_train_sequence.append(data[start:end]) + region_labels_train_sequence.append(labels) + region_data_train_sequence.append(data[start:end]) - if n_too_short_strides > 0: + if n_too_short_regions > 0: warnings.warn( - f"{n_too_short_strides} strides (out of {len(stride_data_train_sequence) + n_too_short_strides}) " - f"were ignored, because they were shorter than the expected number of stride states ({n_states}). " - "This warning can usually be ignored, if the number of remaining strides is still large " + f"{n_too_short_regions} regions of type `{region_type}` " + f"(out of {len(region_data_train_sequence) + n_too_short_regions}) were ignored, because they were " + f"shorter than the expected number of hidden states ({n_states}). " + "This warning can usually be ignored, if the number of remaining regions is still large " "enough to train a model." ) - return stride_data_train_sequence, stride_labels_train_sequence + return region_data_train_sequence, region_labels_train_sequence class _DataToShortError(ValueError): diff --git a/tests/test_stride_segmentation/test_roth_hmm.py b/tests/test_stride_segmentation/test_roth_hmm.py index ca24eee4..f93d8e1b 100644 --- a/tests/test_stride_segmentation/test_roth_hmm.py +++ b/tests/test_stride_segmentation/test_roth_hmm.py @@ -21,7 +21,9 @@ is_single_sensor_stride_list, ) from gaitmap_mad.stride_segmentation.hmm import ( + CompositeHmmConfig, HmmStrideSegmentation, + HmmSubModelConfig, PreTrainedRothSegmentationModel, RothHmmFeatureTransformer, RothSegmentationHmm, @@ -34,6 +36,40 @@ np.random.seed(1) +def _create_roth_model_config(*, stride_n_states=20, stride_n_gmm_components=6, transition_n_gmm_components=3): + return CompositeHmmConfig( + modules={ + "transition": HmmSubModelConfig( + name="transition", + role="transition", + n_states=5, + n_gmm_components=transition_n_gmm_components, + algo_train="baum-welch", + stop_threshold=1e-9, + max_iterations=10, + architecture="left-right-loose", + ), + "stride": HmmSubModelConfig( + name="stride", + role="stride", + n_states=stride_n_states, + n_gmm_components=stride_n_gmm_components, + algo_train="baum-welch", + stop_threshold=1e-9, + max_iterations=10, + architecture="left-right-strict", + ), + } + ) + + +def _stride_list_to_region_list(stride_list: pd.DataFrame, region_type: str = "stride") -> pd.DataFrame: + region_list = stride_list[["start", "end"]].copy() + region_list.insert(0, "roi_id", np.arange(len(region_list))) + region_list["type"] = region_type + return region_list.set_index("roi_id") + + class TestMetaFunctionalityRothSegmentationHmm(TestAlgorithmMixin): __test__ = True @@ -341,7 +377,9 @@ def test_predict_without_model_raises_error(self) -> None: assert "No trained model for prediction available!" in str(e.value) def test_self_optimize_calls_self_optimize_with_info(self) -> None: - data, labels = [pd.DataFrame(np.random.rand(100, 3))], [pd.DataFrame({"start": [0], "end": [100]})] + data, labels = [pd.DataFrame(np.random.rand(100, 3))], [ + _stride_list_to_region_list(pd.DataFrame({"start": [0], "end": [100]})) + ] with patch.object(RothSegmentationHmm, "self_optimize_with_info") as mock: instance = RothSegmentationHmm() @@ -353,50 +391,46 @@ def test_self_optimize_calls_self_optimize_with_info(self) -> None: def test_self_optimize_with_info_returns_history(self) -> None: data, labels = ( [pd.DataFrame(np.random.rand(120, 6), columns=BF_COLS)], - [pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]})], + [_stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]}))], ) - instance = RothSegmentationHmm().set_params( + instance = RothSegmentationHmm(model_config=_create_roth_model_config(stride_n_states=3, stride_n_gmm_components=3)).set_params( feature_transform__sampling_rate_feature_space_hz=100, - stride_model__n_states=3, - stride_model__n_gmm_components=3, ) trained_instance, history = instance.self_optimize_with_info(data, labels, sampling_rate_hz=100) assert instance is trained_instance for v in history.values(): assert isinstance(v, History) - assert set(history.keys()) == {"stride_model", "transition_model", "self"} + assert set(history.keys()) == {"stride", "transition", "self"} def test_short_strides_raise_warning(self) -> None: data, labels = ( [pd.DataFrame(np.random.rand(130, 6), columns=BF_COLS)], - [pd.DataFrame({"start": [0, 40, 70, 110], "end": [30, 70, 100, 114]})], + [_stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70, 110], "end": [30, 70, 100, 114]}))], ) - instance = RothSegmentationHmm().set_params( + instance = RothSegmentationHmm(model_config=_create_roth_model_config(stride_n_states=5, stride_n_gmm_components=3)).set_params( feature_transform__sampling_rate_feature_space_hz=100, - stride_model__n_states=5, - stride_model__n_gmm_components=3, ) with pytest.warns(UserWarning) as w: instance.self_optimize(data, labels, sampling_rate_hz=100) - assert "1 strides (out of 4)" in str(w[0].message) + assert any("regions of type `stride`" in str(warning.message) for warning in w) def test_short_transitions_raise_warning(self) -> None: data, labels = ( [pd.DataFrame(np.random.rand(250, 6), columns=BF_COLS)], - [pd.DataFrame({"start": [0, 70, 102, 125, 170], "end": [30, 100, 125, 170, 200]})], + [_stride_list_to_region_list(pd.DataFrame({"start": [0, 70, 102, 125, 170], "end": [30, 100, 125, 170, 200]}))], ) - instance = RothSegmentationHmm().set_params( + instance = RothSegmentationHmm( + model_config=_create_roth_model_config( + stride_n_states=5, stride_n_gmm_components=3, transition_n_gmm_components=3 + ) + ).set_params( feature_transform__sampling_rate_feature_space_hz=100, - transition_model__n_gmm_components=3, - stride_model__n_gmm_components=3, - stride_model__n_states=5, ) with pytest.warns(UserWarning) as w: instance.self_optimize(data, labels, sampling_rate_hz=100) - # The first warning is the warning about negative improvements during training - assert "1 transitions (out of 3)" in str(w[1].message) + assert any("1 transitions (out of 3)" in str(warning.message) for warning in w) def test_strange_inputs_trigger_nan_error(self) -> None: # XXXX: We test the skip at the moment because it is not deteministic... @@ -406,14 +440,15 @@ def test_strange_inputs_trigger_nan_error(self) -> None: # So we use it to test, that the error is raised. data, labels = ( [pd.DataFrame(np.random.rand(200, 6), columns=BF_COLS)], - [pd.DataFrame({"start": [0, 70, 102, 125, 170], "end": [30, 100, 125, 170, 200]})], + [_stride_list_to_region_list(pd.DataFrame({"start": [0, 70, 102, 125, 170], "end": [30, 100, 125, 170, 200]}))], ) - instance = RothSegmentationHmm().set_params( + instance = RothSegmentationHmm( + model_config=_create_roth_model_config( + stride_n_states=5, stride_n_gmm_components=3, transition_n_gmm_components=3 + ) + ).set_params( feature_transform__sampling_rate_feature_space_hz=100, - transition_model__n_gmm_components=3, - stride_model__n_gmm_components=3, - stride_model__n_states=5, ) with pytest.warns(UserWarning) as w, pytest.raises(ValueError) as e: @@ -422,27 +457,25 @@ def test_strange_inputs_trigger_nan_error(self) -> None: assert "During training the improvement per epoch became NaN/infinite or negative!" in str(w[0].message) assert "the provided pomegranate model has non-finite/NaN parameters." in str(e.value) - def test_training_updates_all_models(self) -> None: - """Training should modify the stride, the transition model and the model itself.""" + def test_training_updates_final_model(self) -> None: + """Training should modify the final fused model while leaving the config untouched.""" data, labels = ( [pd.DataFrame(np.random.rand(250, 6), columns=BF_COLS)], - [pd.DataFrame({"start": [0, 70, 102, 125, 170], "end": [30, 100, 125, 170, 200]})], + [_stride_list_to_region_list(pd.DataFrame({"start": [0, 70, 102, 125, 170], "end": [30, 100, 125, 170, 200]}))], ) - instance = RothSegmentationHmm().set_params( + instance = RothSegmentationHmm( + model_config=_create_roth_model_config( + stride_n_states=5, stride_n_gmm_components=3, transition_n_gmm_components=3 + ) + ).set_params( feature_transform__sampling_rate_feature_space_hz=100, - transition_model__n_gmm_components=3, - stride_model__n_gmm_components=3, - stride_model__n_states=5, ) - # We can not properly test this, so we get the hash before and after and compare them. - hash_stride_model = custom_hash(instance.stride_model) - hash_transition_model = custom_hash(instance.transition_model) + hash_model_config = custom_hash(instance.model_config) hash_model = custom_hash(instance.model) instance.self_optimize(data, labels, sampling_rate_hz=100) - assert hash_stride_model != custom_hash(instance.stride_model) - assert hash_transition_model != custom_hash(instance.transition_model) + assert hash_model_config == custom_hash(instance.model_config) assert hash_model != custom_hash(instance.model) From 5bb0e61680ca5810ac3c4d0c6e7a26ad4c9d094e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Mon, 9 Mar 2026 13:03:12 +0100 Subject: [PATCH 05/28] Address HMM config review feedback --- .../segmentation_hmm_training.py | 26 ++- .../stride_segmentation/hmm/_config.py | 65 ++++-- .../hmm/_segmentation_model.py | 189 +++++++++++++----- .../stride_segmentation/hmm/_utils.py | 15 +- .../test_stride_segmentation/test_roth_hmm.py | 99 +++++++-- 5 files changed, 291 insertions(+), 103 deletions(-) diff --git a/examples/stride_segmentation/segmentation_hmm_training.py b/examples/stride_segmentation/segmentation_hmm_training.py index 62872720..de36e8ae 100644 --- a/examples/stride_segmentation/segmentation_hmm_training.py +++ b/examples/stride_segmentation/segmentation_hmm_training.py @@ -5,7 +5,7 @@ ========================== This example illustrates how a Hidden Markov Model (HMM) implemented by the -:class:`~gaitmap.stride_segmentation.hmm.RothSegmentationHmm` can be trained from IMU data and presegmented stride lists. +:class:`~gaitmap.stride_segmentation.hmm.RothSegmentationHmm` can be trained from IMU data and typed region lists. The used implementation is based on the work of Roth et al [1]_ .. [1] Roth, N., Küderle, A., Ullrich, M., Gladow, T., Marxreiter F., Klucken, J., Eskofier, B. & Kluge F. (2021). @@ -87,8 +87,8 @@ from gaitmap.stride_segmentation.hmm import CompositeHmmConfig, HmmSubModelConfig model_config = CompositeHmmConfig( - modules={ - "transition": HmmSubModelConfig( + modules=( + HmmSubModelConfig( name="transition", role="transition", n_states=5, @@ -99,7 +99,7 @@ architecture="left-right-loose", verbose=True, ), - "stride": HmmSubModelConfig( + HmmSubModelConfig( name="stride", role="stride", n_states=20, @@ -110,7 +110,7 @@ architecture="left-right-strict", verbose=True, ), - } + ) ) # %% @@ -143,12 +143,18 @@ # convention!). # The main input format for the training process are gait sequences which include transitions as well as valid strides. # To train on multiple sequences, we can just feed a list of gaitsequences into the model for training. -# For each gait sequence we also need to have a valid stride list. In this example we handle the data from the left and -# right foot as separate gait sequences and add them to a simple list. -# We have to do the same for the stride lists. +# For each gait sequence we also need typed training regions with `start`, `end`, and `type`. +# In this example the stride regions are all of type `"stride"` and transitions are defined implicitly as everything +# not covered by a region. +# We handle the data from the left and right foot as separate gait sequences and add them to a simple list. data_train_sequence = [bf_data["left_sensor"], bf_data["right_sensor"]] -stride_list_sequence = [stride_list["left_sensor"], stride_list["right_sensor"]] +region_list_sequence = [] +for sensor in ["left_sensor", "right_sensor"]: + region_list = stride_list[sensor][["start", "end"]].copy() + region_list.insert(0, "roi_id", np.arange(len(region_list))) + region_list["type"] = "stride" + region_list_sequence.append(region_list.set_index("roi_id")) # %% # Training @@ -161,7 +167,7 @@ # finally combine them to a flatted segmentation model. segmentation_model = segmentation_model.self_optimize( - data_train_sequence, stride_list_sequence, sampling_rate_hz=sampling_rate_hz + data_train_sequence, region_list_sequence, sampling_rate_hz=sampling_rate_hz ) # %% diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_config.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_config.py index be1c427a..ae36f7e3 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_config.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_config.py @@ -1,15 +1,16 @@ """Configuration objects for composite HMM segmentation models.""" -from typing import Literal +from typing import Union from tpcp import cf +from typing_extensions import Literal from gaitmap.base import _BaseSerializable -def _default_modules() -> dict[str, "HmmSubModelConfig"]: - return { - "transition": HmmSubModelConfig( +def _default_modules() -> tuple["HmmSubModelConfig", ...]: + return ( + HmmSubModelConfig( name="transition", role="transition", n_states=5, @@ -19,7 +20,7 @@ def _default_modules() -> dict[str, "HmmSubModelConfig"]: stop_threshold=1e-9, max_iterations=10, ), - "stride": HmmSubModelConfig( + HmmSubModelConfig( name="stride", role="stride", n_states=20, @@ -29,14 +30,14 @@ def _default_modules() -> dict[str, "HmmSubModelConfig"]: stop_threshold=1e-9, max_iterations=10, ), - } + ) class HmmSubModelConfig(_BaseSerializable): """Configuration of a single trainable HMM submodule.""" name: str - role: Literal["transition", "stride", "other"] + role: Union[Literal["transition", "stride"], str] n_states: int n_gmm_components: int architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"] @@ -49,7 +50,7 @@ class HmmSubModelConfig(_BaseSerializable): def __init__( self, name: str, - role: Literal["transition", "stride", "other"], + role: Union[Literal["transition", "stride"], str], n_states: int, n_gmm_components: int, *, @@ -71,38 +72,62 @@ def __init__( self.verbose = verbose self.n_jobs = n_jobs - def __eq__(self, other: object) -> bool: - if not isinstance(other, HmmSubModelConfig): - return False - return self.get_params(deep=False) == other.get_params(deep=False) - class CompositeHmmConfig(_BaseSerializable): """Configuration of a composite HMM with named submodules.""" - modules: dict[str, HmmSubModelConfig] + modules: tuple[HmmSubModelConfig, ...] transition_model_name: str def __init__( self, - modules: dict[str, HmmSubModelConfig] = cf(_default_modules()), + modules: tuple[HmmSubModelConfig, ...] = cf(_default_modules()), *, transition_model_name: str = "transition", ) -> None: self.modules = modules self.transition_model_name = transition_model_name + @property + def _module_configs_by_name(self) -> dict[str, HmmSubModelConfig]: + modules_by_name = {module.name: module for module in self.modules} + if len(modules_by_name) != len(self.modules): + raise ValueError("All configured HMM submodule names must be unique.") + return modules_by_name + + def get_module(self, module_name: str) -> HmmSubModelConfig: + """Return the config of a single named submodule.""" + try: + return self._module_configs_by_name[module_name] + except KeyError as e: + raise ValueError(f"No HMM submodule with the name `{module_name}` exists in the model config.") from e + @property def explicit_region_model_names(self) -> tuple[str, ...]: - """Return all module names except the implicit transition module.""" - return tuple(name for name in self.modules if name != self.transition_model_name) + """Return all explicit region model names except the implicit transition module.""" + return tuple(module.name for module in self.modules if module.name != self.transition_model_name) @property def transition_model(self) -> HmmSubModelConfig: """Return the configuration of the implicit transition module.""" - return self.modules[self.transition_model_name] + transition_model = self.get_module(self.transition_model_name) + if transition_model.role != "transition": + raise ValueError( + "The configured transition model is expected to have the role `transition`, " + f"but `{transition_model.name}` has the role `{transition_model.role}`." + ) + return transition_model @property def stride_model_names(self) -> tuple[str, ...]: - """Return module names that should be interpreted as stride-like states.""" - return tuple(name for name, module in self.modules.items() if module.role == "stride") + """Return the names of all modules that should be interpreted as stride-like states.""" + return tuple(module.name for module in self.modules if module.role == "stride") + + @property + def custom_model_names(self) -> tuple[str, ...]: + """Return the names of all modules with custom non-built-in roles.""" + return tuple(module.name for module in self.modules if module.role not in {"transition", "stride"}) + + def get_module_role(self, module_name: str) -> str: + """Return the configured role of a named module.""" + return self.get_module(module_name).role diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py index 068e915e..991e5679 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py @@ -17,7 +17,6 @@ from gaitmap.utils.datatype_helper import ( SingleSensorData, SingleSensorRegionsOfInterestList, - SingleSensorStrideList, ) from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import ( @@ -45,15 +44,39 @@ ) -def create_fully_labeled_gait_sequences( +def create_fully_labeled_hidden_state_sequences( data_train_sequence: Sequence[pd.DataFrame], region_list_sequence: Sequence[SingleSensorRegionsOfInterestList], module_models: dict[str, SimpleHmm], - module_offsets: dict[str, int], + state_offsets: dict[str, int], transition_model_name: str, algo_predict: Literal["viterbi", "map"], ): - """Create fully labeled gait sequences from typed regions and trained submodels.""" + """Create fully labeled hidden-state sequences from typed regions and trained submodels. + + Parameters + ---------- + data_train_sequence + Sequence of feature-space datasets. + region_list_sequence + Sequence of typed region lists with `start`, `end`, and `type`. + `type` values are expected to refer to the configured explicit module names. + Regions covered by no explicit module are treated as belonging to the implicit transition module. + module_models + Trained per-module models keyed by module name. + state_offsets + State-index offsets of each module in the combined final model. + transition_model_name + Name of the module that models the implicit transition regions. + algo_predict + Prediction algorithm used to create state labels from the trained submodels. + + Returns + ------- + list of np.ndarray + Fully labeled hidden-state sequences aligned with `data_train_sequence`. + + """ labels_train_sequence = [] transition_model = module_models[transition_model_name] @@ -76,7 +99,7 @@ def create_fully_labeled_gait_sequences( try: labels_train[start:end] = ( region_model.predict_hidden_state_sequence(region_data_train, algorithm=algo_predict) - + module_offsets[region_type] + + state_offsets[region_type] ) except _DataToShortError: continue @@ -100,6 +123,67 @@ def _create_simple_hmm_from_config(config: HmmSubModelConfig) -> SimpleHmm: ) +def _get_training_sequences_for_module( + module_config: HmmSubModelConfig, + model_config: CompositeHmmConfig, + data_sequence_feature_space: list[pd.DataFrame], + region_list_feature_space: list[SingleSensorRegionsOfInterestList], +) -> tuple[list[np.ndarray], list[np.ndarray]]: + module_name = module_config.name + if module_name == model_config.transition_model_name: + train_sequence, init_state_labels = get_train_data_sequences_transitions( + data_sequence_feature_space, region_list_feature_space, module_config.n_states + ) + if len(train_sequence) == 0: + raise ValueError( + "The configured transition module did not receive any trainable data. " + "Either no implicit transition regions were found or all transition regions became too short after " + "feature transformation." + ) + return train_sequence, init_state_labels + + train_sequence, init_state_labels = get_train_data_sequences_regions( + data_sequence_feature_space, + region_list_feature_space, + region_type=module_name, + n_states=module_config.n_states, + ) + if len(train_sequence) == 0: + raise ValueError( + f"The configured submodule `{module_name}` did not receive any trainable regions. " + "Ensure that the region lists contain this type and that the regions remain long enough after feature " + "transformation." + ) + return train_sequence, init_state_labels + + +def _collect_trained_module_distributions( + modules: tuple[HmmSubModelConfig, ...], trained_models: dict[str, SimpleHmm] +) -> list[Any]: + distributions = [] + for module in modules: + distributions.extend(get_model_distributions(trained_models[module.name].model)) + return distributions + + +def _predict_labeled_training_sequences( + data_sequence_feature_space: list[pd.DataFrame], + region_list_feature_space: list[SingleSensorRegionsOfInterestList], + trained_models: dict[str, SimpleHmm], + module_offsets: dict[str, int], + transition_model_name: str, + algo_predict: Literal["viterbi", "map"], +) -> list[np.ndarray]: + return create_fully_labeled_hidden_state_sequences( + data_sequence_feature_space, + region_list_feature_space, + trained_models, + module_offsets, + transition_model_name, + algo_predict, + ) + + def _coerce_region_list_input( region_list: pd.DataFrame, explicit_region_model_names: tuple[str, ...] ) -> SingleSensorRegionsOfInterestList: @@ -217,10 +301,11 @@ def self_optimize_with_info( class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHMMPrint): """A hierarchical HMM model for stride segmentation proposed by Roth et al. [1]_. - This model differentiates between strides and transitions. - Both data sections are modeled by individual HMMs and are trained separately. - A final model is created by combining the transition matrices of the two models and allowing for transitions between - these higher level states at the start or end of a stride. + This model uses individually trained HMM submodules that are combined into one final segmentation HMM. + One submodule is reserved for the implicit transition regions, while all other submodules are trained on explicit + typed regions. + A final model is created by combining the transition matrices of the trained submodules and allowing transitions + between the higher-level states where they occur in the labeled data. Parameters ---------- @@ -328,17 +413,20 @@ class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHM def _from_json_dict(cls, json_dict: dict) -> Self: params = json_dict["params"].copy() if "model_config" not in params and {"stride_model", "transition_model"} <= set(params): + # TODO: Remove this compatibility shim once legacy serialized Roth models have been migrated. stride_model = params.pop("stride_model") transition_model = params.pop("transition_model") - stride_params = stride_model.get_params(deep=False) if isinstance(stride_model, SimpleHmm) else stride_model["params"] + stride_params = ( + stride_model.get_params(deep=False) if isinstance(stride_model, SimpleHmm) else stride_model["params"] + ) transition_params = ( transition_model.get_params(deep=False) if isinstance(transition_model, SimpleHmm) else transition_model["params"] ) params["model_config"] = CompositeHmmConfig( - modules={ - "transition": HmmSubModelConfig( + modules=( + HmmSubModelConfig( name="transition", role="transition", n_states=transition_params["n_states"], @@ -350,7 +438,7 @@ def _from_json_dict(cls, json_dict: dict) -> Self: verbose=transition_params.get("verbose", True), n_jobs=transition_params.get("n_jobs", 1), ), - "stride": HmmSubModelConfig( + HmmSubModelConfig( name="stride", role="stride", n_states=stride_params["n_states"], @@ -362,7 +450,7 @@ def _from_json_dict(cls, json_dict: dict) -> Self: verbose=stride_params.get("verbose", True), n_jobs=stride_params.get("n_jobs", 1), ), - } + ) ) input_data = {k: params[k] for k in tpcp.get_param_names(cls) if k in params} return cls(**input_data) @@ -399,15 +487,15 @@ def __init__( @property def n_states(self) -> int: """Return the number of states of the final model.""" - return sum(module.n_states for module in self.model_config.modules.values()) + return sum(module.n_states for module in self.model_config.modules) @property - def module_offsets(self) -> dict[str, int]: + def _module_offsets(self) -> dict[str, int]: """Return the state offsets of each configured submodule in the combined model.""" offsets = {} current_offset = 0 - for name, module in self.model_config.modules.items(): - offsets[name] = current_offset + for module in self.model_config.modules: + offsets[module.name] = current_offset current_offset += module.n_states return offsets @@ -415,17 +503,17 @@ def module_offsets(self) -> dict[str, int]: def stride_states(self) -> list[int]: """Return the ids of all stride-like states.""" stride_states = [] - for name, module in self.model_config.modules.items(): + for module in self.model_config.modules: if module.role != "stride": continue - stride_states.extend((np.arange(module.n_states) + self.module_offsets[name]).tolist()) + stride_states.extend((np.arange(module.n_states) + self._module_offsets[module.name]).tolist()) return stride_states @property def transition_states(self) -> list[int]: """Return the ids of the transition states.""" transition_module = self.model_config.transition_model - transition_offset = self.module_offsets[self.model_config.transition_model_name] + transition_offset = self._module_offsets[self.model_config.transition_model_name] return (np.arange(transition_module.n_states) + transition_offset).tolist() def predict(self, data: SingleSensorData, sampling_rate_hz: float) -> Self: @@ -506,19 +594,23 @@ def self_optimize( ) -> Self: """Create and train the HMM model based on the given data and labels. - This will first apply the feature transformation to the given data and then train the HMM model in three steps: + This will first apply the feature transformation to the given data and then train the HMM model in three + stages: - 1. Train the stride model on the stride data - 2. Train the transition model on the transition data - 3. Assemble the final model by combining the stride and transition model and train it for a couple further - iterations + 1. Train each explicit typed-region submodule on its corresponding training regions. + 2. Train the implicit transition module on all uncovered regions between explicit regions. + 3. Assemble the final model by combining all trained submodules and train it for a couple further iterations. Parameters ---------- data_sequence Sequence of gaitmap sensordata objects. region_list_sequence - Sequence of typed region lists with `start`, `end`, and `type`. + Sequence of typed region lists. Each list must have `start`, `end`, and `type` columns and a valid ROI/GS + id column or index (`roi_id`/`gs_id`). + `type` must only contain names of explicit modules configured in `model_config`. + Regions must not overlap. Samples not covered by an explicit region are treated as the implicit transition + region. The number of region lists must match the number of sensordata objects (i.e. they must belong together). sampling_rate_hz Sampling frequency of the data. @@ -541,15 +633,19 @@ def self_optimize_with_info( """Create and train the HMM model based on the given data and labels. This is identical to `self_optimize`, but returns additional information about the training process. - The dictionary returned as second parameter contains the training history for each of the three models ( - stride-model, transition-model, and the combined final model "self"). + The dictionary returned as second parameter contains the training history for each trained submodule and the + combined final model `"self"`. Parameters ---------- data_sequence Sequence of gaitmap sensordata objects. region_list_sequence - Sequence of typed region lists with `start`, `end`, and `type`. + Sequence of typed region lists. Each list must have `start`, `end`, and `type` columns and a valid ROI/GS + id column or index (`roi_id`/`gs_id`). + `type` must only contain names of explicit modules configured in `model_config`. + Regions must not overlap. Samples not covered by an explicit region are treated as the implicit transition + region. The number of region lists must match the number of sensordata objects (i.e. they must belong together). sampling_rate_hz Sampling frequency of the data. @@ -559,7 +655,7 @@ def self_optimize_with_info( self The trained model instance. history - Dictionary containing the training history for each of the three models + Dictionary containing the training history for each trained submodule and the final combined model. """ if self.initialization not in ["labels", "fully-connected"]: @@ -582,18 +678,14 @@ def self_optimize_with_info( trained_models: dict[str, SimpleHmm] = {} histories: dict[str, History] = {} - for module_name, module_config in self.model_config.modules.items(): - if module_name == self.model_config.transition_model_name: - train_sequence, init_state_labels = get_train_data_sequences_transitions( - data_sequence_feature_space, region_list_feature_space, module_config.n_states - ) - else: - train_sequence, init_state_labels = get_train_data_sequences_regions( - data_sequence_feature_space, - region_list_feature_space, - region_type=module_name, - n_states=module_config.n_states, - ) + for module_config in self.model_config.modules: + module_name = module_config.name + train_sequence, init_state_labels = _get_training_sequences_for_module( + module_config, + self.model_config, + data_sequence_feature_space, + region_list_feature_space, + ) trained_model, history = _create_simple_hmm_from_config(module_config).self_optimize_with_info( train_sequence, init_state_labels @@ -604,13 +696,11 @@ def self_optimize_with_info( # For model combination actually only the transition probabilities will be updated, while keeping the already # learned distributions for all states. This can be achieved by "labeled" training, where basically just the # number of transitions will be counted. - distributions = [] - for module_name in self.model_config.modules: - distributions.extend(get_model_distributions(trained_models[module_name].model)) + distributions = _collect_trained_module_distributions(self.model_config.modules, trained_models) # predict hidden state labels for complete walking bouts - module_offsets = self.module_offsets - labels_train_sequence = create_fully_labeled_gait_sequences( + module_offsets = self._module_offsets + labels_train_sequence = _predict_labeled_training_sequences( data_sequence_feature_space, region_list_feature_space, trained_models, @@ -634,7 +724,8 @@ def self_optimize_with_info( elif self.initialization == "labels": trans_mat = np.zeros((self.n_states, self.n_states)) - for module_name, module_config in self.model_config.modules.items(): + for module_config in self.model_config.modules: + module_name = module_config.name module_transition_matrix = trained_models[module_name].model.dense_transition_matrix()[:-2, :-2] offset = module_offsets[module_name] trans_mat[ diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py index fc1321b3..0d97712d 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py @@ -528,13 +528,11 @@ def validate_trainable_region_list( f"Expected only the following explicit region types: {list(explicit_region_types)}" ) normalized_region_list = normalized_region_list.sort_values(["start", "end"]).reset_index(drop=True) - previous_end = None - for start, end in normalized_region_list[["start", "end"]].to_numpy(): - if end <= start: - raise ValidationError("All regions must satisfy `end > start`.") - if previous_end is not None and start < previous_end: - raise ValidationError("The region list must not contain overlapping regions.") - previous_end = end + regions = normalized_region_list[["start", "end"]].to_numpy() + if np.any(regions[:, 1] <= regions[:, 0]): + raise ValidationError("All regions must satisfy `end > start`.") + if len(regions) > 1 and np.any(regions[1:, 0] < regions[:-1, 1]): + raise ValidationError("The region list must not contain overlapping regions.") except ValidationError as e: raise ValidationError( "The passed object does not seem to be a valid typed region list for HMM training. " @@ -577,7 +575,8 @@ def get_train_data_sequences_transitions( for data, region_list in zip(data_train_sequence, region_list_sequence): # for each transition, get data and create some naive labels for initialization - for start, end in convert_region_list_to_transition_list(region_list, data.shape[0])[["start", "end"]].to_numpy(): + transition_regions = convert_region_list_to_transition_list(region_list, data.shape[0]) + for start, end in transition_regions[["start", "end"]].to_numpy(): # append extracted sequences and corresponding label set to results list try: labels = create_equidistant_label_sequence(end - start, n_states).astype("int64") diff --git a/tests/test_stride_segmentation/test_roth_hmm.py b/tests/test_stride_segmentation/test_roth_hmm.py index f93d8e1b..6bf1f0e4 100644 --- a/tests/test_stride_segmentation/test_roth_hmm.py +++ b/tests/test_stride_segmentation/test_roth_hmm.py @@ -1,13 +1,12 @@ from unittest.mock import patch -import pytest - -pytest.importorskip("pomegranate") - import numpy as np import pandas as pd import pytest from numpy.testing import assert_almost_equal, assert_array_equal + +pytest.importorskip("pomegranate") + from pomegranate import GeneralMixtureModel from pomegranate.hmm import History from tpcp._hash import custom_hash @@ -20,6 +19,7 @@ is_multi_sensor_stride_list, is_single_sensor_stride_list, ) +from gaitmap.utils.exceptions import ValidationError from gaitmap_mad.stride_segmentation.hmm import ( CompositeHmmConfig, HmmStrideSegmentation, @@ -38,8 +38,8 @@ def _create_roth_model_config(*, stride_n_states=20, stride_n_gmm_components=6, transition_n_gmm_components=3): return CompositeHmmConfig( - modules={ - "transition": HmmSubModelConfig( + modules=( + HmmSubModelConfig( name="transition", role="transition", n_states=5, @@ -49,7 +49,7 @@ def _create_roth_model_config(*, stride_n_states=20, stride_n_gmm_components=6, max_iterations=10, architecture="left-right-loose", ), - "stride": HmmSubModelConfig( + HmmSubModelConfig( name="stride", role="stride", n_states=stride_n_states, @@ -59,7 +59,7 @@ def _create_roth_model_config(*, stride_n_states=20, stride_n_gmm_components=6, max_iterations=10, architecture="left-right-strict", ), - } + ) ) @@ -377,9 +377,10 @@ def test_predict_without_model_raises_error(self) -> None: assert "No trained model for prediction available!" in str(e.value) def test_self_optimize_calls_self_optimize_with_info(self) -> None: - data, labels = [pd.DataFrame(np.random.rand(100, 3))], [ - _stride_list_to_region_list(pd.DataFrame({"start": [0], "end": [100]})) - ] + data, labels = ( + [pd.DataFrame(np.random.rand(100, 3))], + [_stride_list_to_region_list(pd.DataFrame({"start": [0], "end": [100]}))], + ) with patch.object(RothSegmentationHmm, "self_optimize_with_info") as mock: instance = RothSegmentationHmm() @@ -393,7 +394,9 @@ def test_self_optimize_with_info_returns_history(self) -> None: [pd.DataFrame(np.random.rand(120, 6), columns=BF_COLS)], [_stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]}))], ) - instance = RothSegmentationHmm(model_config=_create_roth_model_config(stride_n_states=3, stride_n_gmm_components=3)).set_params( + instance = RothSegmentationHmm( + model_config=_create_roth_model_config(stride_n_states=3, stride_n_gmm_components=3) + ).set_params( feature_transform__sampling_rate_feature_space_hz=100, ) trained_instance, history = instance.self_optimize_with_info(data, labels, sampling_rate_hz=100) @@ -407,7 +410,9 @@ def test_short_strides_raise_warning(self) -> None: [pd.DataFrame(np.random.rand(130, 6), columns=BF_COLS)], [_stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70, 110], "end": [30, 70, 100, 114]}))], ) - instance = RothSegmentationHmm(model_config=_create_roth_model_config(stride_n_states=5, stride_n_gmm_components=3)).set_params( + instance = RothSegmentationHmm( + model_config=_create_roth_model_config(stride_n_states=5, stride_n_gmm_components=3) + ).set_params( feature_transform__sampling_rate_feature_space_hz=100, ) with pytest.warns(UserWarning) as w: @@ -415,10 +420,64 @@ def test_short_strides_raise_warning(self) -> None: assert any("regions of type `stride`" in str(warning.message) for warning in w) + def test_unknown_region_type_raises_error(self) -> None: + data = [pd.DataFrame(np.random.rand(120, 6), columns=BF_COLS)] + labels = [ + _stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]}), "stair_stride") + ] + instance = RothSegmentationHmm(model_config=_create_roth_model_config()).set_params( + feature_transform__sampling_rate_feature_space_hz=100, + ) + + with pytest.raises(ValidationError) as exc: + instance.self_optimize(data, labels, sampling_rate_hz=100) + + assert "unknown region types" in str(exc.value) + + def test_missing_configured_module_data_raises_error(self) -> None: + data = [pd.DataFrame(np.random.rand(120, 6), columns=BF_COLS)] + labels = [_stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]}), "stride")] + config = CompositeHmmConfig( + modules=( + HmmSubModelConfig( + name="transition", + role="transition", + n_states=5, + n_gmm_components=3, + architecture="left-right-loose", + ), + HmmSubModelConfig( + name="stride", + role="stride", + n_states=3, + n_gmm_components=3, + ), + HmmSubModelConfig( + name="stair_stride", + role="stride", + n_states=3, + n_gmm_components=3, + ), + ) + ) + instance = RothSegmentationHmm(model_config=config).set_params( + feature_transform__sampling_rate_feature_space_hz=100, + ) + + with pytest.raises(ValueError) as exc: + instance.self_optimize(data, labels, sampling_rate_hz=100) + + assert "stair_stride" in str(exc.value) + assert "did not receive any trainable regions" in str(exc.value) + def test_short_transitions_raise_warning(self) -> None: data, labels = ( [pd.DataFrame(np.random.rand(250, 6), columns=BF_COLS)], - [_stride_list_to_region_list(pd.DataFrame({"start": [0, 70, 102, 125, 170], "end": [30, 100, 125, 170, 200]}))], + [ + _stride_list_to_region_list( + pd.DataFrame({"start": [0, 70, 102, 125, 170], "end": [30, 100, 125, 170, 200]}) + ) + ], ) instance = RothSegmentationHmm( model_config=_create_roth_model_config( @@ -440,7 +499,11 @@ def test_strange_inputs_trigger_nan_error(self) -> None: # So we use it to test, that the error is raised. data, labels = ( [pd.DataFrame(np.random.rand(200, 6), columns=BF_COLS)], - [_stride_list_to_region_list(pd.DataFrame({"start": [0, 70, 102, 125, 170], "end": [30, 100, 125, 170, 200]}))], + [ + _stride_list_to_region_list( + pd.DataFrame({"start": [0, 70, 102, 125, 170], "end": [30, 100, 125, 170, 200]}) + ) + ], ) instance = RothSegmentationHmm( @@ -461,7 +524,11 @@ def test_training_updates_final_model(self) -> None: """Training should modify the final fused model while leaving the config untouched.""" data, labels = ( [pd.DataFrame(np.random.rand(250, 6), columns=BF_COLS)], - [_stride_list_to_region_list(pd.DataFrame({"start": [0, 70, 102, 125, 170], "end": [30, 100, 125, 170, 200]}))], + [ + _stride_list_to_region_list( + pd.DataFrame({"start": [0, 70, 102, 125, 170], "end": [30, 100, 125, 170, 200]}) + ) + ], ) instance = RothSegmentationHmm( model_config=_create_roth_model_config( From 8fb574d9b65ab56f9b20eae2826b8523132280d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Mon, 9 Mar 2026 13:42:01 +0100 Subject: [PATCH 06/28] Add HMM state and pomegranate backend abstraction --- .../segmentation_hmm_training.py | 14 +- gaitmap/stride_segmentation/hmm.py | 34 +- .../stride_segmentation/hmm/__init__.py | 23 +- .../stride_segmentation/hmm/_backend.py | 280 ++++++++++++++++ .../hmm/_segmentation_model.py | 213 +++++------- .../stride_segmentation/hmm/_state.py | 313 ++++++++++++++++++ .../stride_segmentation/hmm/_utils.py | 15 + ...segmentation_hmm_training_left_sensor.json | 6 +- ...egmentation_hmm_training_right_sensor.json | 6 +- .../test_stride_segmentation/test_roth_hmm.py | 87 +++++ 10 files changed, 841 insertions(+), 150 deletions(-) create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_state.py diff --git a/examples/stride_segmentation/segmentation_hmm_training.py b/examples/stride_segmentation/segmentation_hmm_training.py index de36e8ae..9f6848a7 100644 --- a/examples/stride_segmentation/segmentation_hmm_training.py +++ b/examples/stride_segmentation/segmentation_hmm_training.py @@ -121,11 +121,12 @@ # invoke the training process. # Again, all configurable parameters are exposed for demonstration purpose. # These parameters should again work for most usecases. -from gaitmap.stride_segmentation.hmm import RothSegmentationHmm +from gaitmap.stride_segmentation.hmm import PomegranateHmmBackend, RothSegmentationHmm segmentation_model = RothSegmentationHmm( model_config=model_config, feature_transform=feature_transform, + backend=PomegranateHmmBackend(), algo_predict="viterbi", algo_train="baum-welch", stop_threshold=1e-9, @@ -174,16 +175,19 @@ # Inspecting the Results # -------------------------------------- # -# Now all internal models which were initialized as "None" should be populated by pomegranate models. -# We can now have a look at the final transition matrix or the trained distributions (GMMs). +# Now the trained model is stored as a serializable HMM state. +# We can now have a look at the final transition matrix, the backend provenance, or one of the trained emission +# distributions. # You could now either use the model to predict stride borders on an unseen sequence or save it to a json file for later # use. np.set_printoptions(precision=3, linewidth=180, suppress=True) -print(segmentation_model.model.dense_transition_matrix()[0:-2, 0:-2]) +print(segmentation_model.model.trained_with) -print(segmentation_model.model.states[10]) +print(segmentation_model.model.compiled.graph.transition_probs) + +print(segmentation_model.model.compiled.emissions[10]) # %% # Applying the Model to a Sequence diff --git a/gaitmap/stride_segmentation/hmm.py b/gaitmap/stride_segmentation/hmm.py index a01d63c2..120fa45d 100644 --- a/gaitmap/stride_segmentation/hmm.py +++ b/gaitmap/stride_segmentation/hmm.py @@ -1,6 +1,6 @@ """Hidden-Markov based stride segmentation developed by Roth et al.. -All HMM implementations are based on pomegranate [1]_. +The default training and inference backend is currently based on pomegranate [1]_. .. [1] Schreiber, J. (2018). Pomegranate: fast and flexible probabilistic modeling in python. Journal of Machine Learning Research, 18(164), 1-6. @@ -10,11 +10,21 @@ from gaitmap.utils._gaitmap_mad import patch_gaitmap_mad_import _gaitmap_mad_modules = { + "BackendInfo", + "BaseHmmBackend", "CompositeHmmConfig", "BaseHmmFeatureTransformer", + "CrossModuleTransition", + "FlatHmmState", + "GaussianEmissionState", + "GaussianMixtureEmissionState", + "HMMState", "RothHmmFeatureTransformer", "HmmStrideSegmentation", + "HmmGraphState", "HmmSubModelConfig", + "HmmSubModelState", + "PomegranateHmmBackend", "SimpleHmm", "RothSegmentationHmm", "PreTrainedRothSegmentationModel", @@ -24,11 +34,21 @@ if not (__getattr__ := patch_gaitmap_mad_import(_gaitmap_mad_modules, __name__)): del __getattr__ from gaitmap_mad.stride_segmentation.hmm import ( + BackendInfo, + BaseHmmBackend, BaseHmmFeatureTransformer, BaseSegmentationHmm, CompositeHmmConfig, + CrossModuleTransition, + FlatHmmState, + GaussianEmissionState, + GaussianMixtureEmissionState, + HmmGraphState, + HMMState, HmmStrideSegmentation, HmmSubModelConfig, + HmmSubModelState, + PomegranateHmmBackend, PreTrainedRothSegmentationModel, RothHmmFeatureTransformer, RothSegmentationHmm, @@ -37,11 +57,21 @@ __all__ = [ - "CompositeHmmConfig", + "BackendInfo", + "BaseHmmBackend", "BaseHmmFeatureTransformer", "BaseSegmentationHmm", + "CompositeHmmConfig", + "CrossModuleTransition", + "FlatHmmState", + "GaussianEmissionState", + "GaussianMixtureEmissionState", + "HMMState", + "HmmGraphState", "HmmStrideSegmentation", "HmmSubModelConfig", + "HmmSubModelState", + "PomegranateHmmBackend", "PreTrainedRothSegmentationModel", "RothHmmFeatureTransformer", "RothSegmentationHmm", diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py index 69a4926a..1b40d39f 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py @@ -3,6 +3,7 @@ import multiprocessing import warnings +from gaitmap_mad.stride_segmentation.hmm._backend import BaseHmmBackend, PomegranateHmmBackend from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import ( BaseHmmFeatureTransformer, @@ -14,6 +15,16 @@ ) from gaitmap_mad.stride_segmentation.hmm._segmentation_model import BaseSegmentationHmm, RothSegmentationHmm from gaitmap_mad.stride_segmentation.hmm._simple_model import SimpleHmm +from gaitmap_mad.stride_segmentation.hmm._state import ( + BackendInfo, + CrossModuleTransition, + FlatHmmState, + GaussianEmissionState, + GaussianMixtureEmissionState, + HmmGraphState, + HMMState, + HmmSubModelState, +) if multiprocessing.parent_process() is None: warnings.warn( @@ -25,11 +36,21 @@ ) __all__ = [ - "CompositeHmmConfig", + "BackendInfo", + "BaseHmmBackend", "BaseHmmFeatureTransformer", "BaseSegmentationHmm", + "CompositeHmmConfig", + "CrossModuleTransition", + "FlatHmmState", + "GaussianEmissionState", + "GaussianMixtureEmissionState", + "HMMState", + "HmmGraphState", "HmmStrideSegmentation", "HmmSubModelConfig", + "HmmSubModelState", + "PomegranateHmmBackend", "PreTrainedRothSegmentationModel", "RothHmmFeatureTransformer", "RothSegmentationHmm", diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py new file mode 100644 index 00000000..9e0624c8 --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py @@ -0,0 +1,280 @@ +"""Backend abstractions for HMM training and inference.""" + +from __future__ import annotations + +import copy +from typing import Literal + +import numpy as np +import pandas as pd +import pomegranate as pg +from pomegranate.hmm import History + +from gaitmap.base import _BaseSerializable +from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig +from gaitmap_mad.stride_segmentation.hmm._simple_model import SimpleHmm +from gaitmap_mad.stride_segmentation.hmm._state import ( + BackendInfo, + CrossModuleTransition, + HMMState, + HmmSubModelState, + hmm_state_to_pomegranate_model, + pomegranate_model_to_flat_hmm_state, + pomegranate_model_to_hmm_state, +) +from gaitmap_mad.stride_segmentation.hmm._utils import ( + _clone_model, + add_transition, + check_history_for_training_failure, + create_transition_matrix_fully_connected, + extract_transitions_starts_stops_from_hidden_state_sequence, + fix_model_names, + get_model_distributions, + labels_to_strings, + predict, +) + + +class BaseHmmBackend(_BaseSerializable): + """Base abstraction for backend-specific HMM primitives.""" + + backend_id: str + + def __init__(self, backend_id: str) -> None: + self.backend_id = backend_id + + def create_submodel(self, config: HmmSubModelConfig) -> SimpleHmm: + """Create a backend-specific trainable flat HMM wrapper.""" + raise NotImplementedError + + def predict( + self, + model: HMMState, + data: pd.DataFrame, + *, + expected_columns: tuple[str, ...], + algorithm: Literal["viterbi", "map"], + verbose: bool, + ) -> np.ndarray: + """Predict hidden states with a serialized model.""" + raise NotImplementedError + + def finalize_model( + self, + *, + trained_models: dict[str, SimpleHmm], + labels_train_sequence: list[np.ndarray], + data_sequence_feature_space: list[pd.DataFrame], + data_columns: tuple[str, ...], + model_config: CompositeHmmConfig, + module_offsets: dict[str, int], + initialization: Literal["labels", "fully-connected"], + algo_train: Literal["viterbi", "baum-welch"], + stop_threshold: float, + max_iterations: int, + verbose: bool, + n_jobs: int, + name: str, + ) -> tuple[HMMState, History]: + """Create, train, and serialize the final combined HMM.""" + raise NotImplementedError + + +def _create_simple_hmm_from_config(config: HmmSubModelConfig) -> SimpleHmm: + return SimpleHmm( + n_states=config.n_states, + n_gmm_components=config.n_gmm_components, + architecture=config.architecture, + algo_train=config.algo_train, + stop_threshold=config.stop_threshold, + max_iterations=config.max_iterations, + verbose=config.verbose, + n_jobs=config.n_jobs, + name=config.name, + ) + + +def _build_submodel_states( + model_config: CompositeHmmConfig, trained_models: dict[str, SimpleHmm] +) -> tuple[HmmSubModelState, ...]: + return tuple( + HmmSubModelState( + name=module.name, + role=module.role, + model=pomegranate_model_to_flat_hmm_state(trained_models[module.name].model), + ) + for module in model_config.modules + ) + + +def _extract_cross_module_transitions( + compiled_state: HMMState, + module_offsets: dict[str, int], +) -> tuple[CrossModuleTransition, ...]: + transitions = [] + transition_matrix = compiled_state.compiled.graph.transition_probs + module_sizes = {submodel.name: len(submodel.model.state_names) for submodel in compiled_state.submodels} + ordered_modules = tuple(submodel.name for submodel in compiled_state.submodels) + for from_module in ordered_modules: + from_offset = module_offsets[from_module] + from_size = module_sizes[from_module] + for to_module in ordered_modules: + if from_module == to_module: + continue + to_offset = module_offsets[to_module] + to_size = module_sizes[to_module] + for from_state in range(from_size): + for to_state in range(to_size): + probability = transition_matrix[from_offset + from_state, to_offset + to_state] + if probability <= 0: + continue + transitions.append( + CrossModuleTransition( + from_module=from_module, + from_state=from_state, + to_module=to_module, + to_state=to_state, + probability=float(probability), + ) + ) + return tuple(transitions) + + +class PomegranateHmmBackend(BaseHmmBackend): + """`pomegranate 0.14` backend for HMM training and inference.""" + + def __init__(self, backend_id: str = "pomegranate-legacy") -> None: + super().__init__(backend_id=backend_id) + + def create_submodel(self, config: HmmSubModelConfig) -> SimpleHmm: + """Create a pomegranate-backed trainable submodel.""" + return _create_simple_hmm_from_config(config) + + def predict( + self, + model: HMMState, + data: pd.DataFrame, + *, + expected_columns: tuple[str, ...], + algorithm: Literal["viterbi", "map"], + verbose: bool, + ) -> np.ndarray: + """Compile the serialized state and predict hidden states.""" + runtime_model = hmm_state_to_pomegranate_model(model, verbose=verbose) + return predict(runtime_model, data, expected_columns=expected_columns, algorithm=algorithm) + + def finalize_model( + self, + *, + trained_models: dict[str, SimpleHmm], + labels_train_sequence: list[np.ndarray], + data_sequence_feature_space: list[pd.DataFrame], + data_columns: tuple[str, ...], + model_config: CompositeHmmConfig, + module_offsets: dict[str, int], + initialization: Literal["labels", "fully-connected"], + algo_train: Literal["viterbi", "baum-welch"], + stop_threshold: float, + max_iterations: int, + verbose: bool, + n_jobs: int, + name: str, + ) -> tuple[HMMState, History]: + """Build the final pomegranate model, train it, and convert it to `HMMState`.""" + distributions = [] + for module in model_config.modules: + distributions.extend(get_model_distributions(trained_models[module.name].model)) + + model = self._create_combined_model( + trained_models=trained_models, + labels_train_sequence=labels_train_sequence, + distributions=distributions, + model_config=model_config, + module_offsets=module_offsets, + initialization=initialization, + verbose=verbose, + ) + + labels_train_sequence_str = labels_to_strings(labels_train_sequence) + data_train_sequence = [ + np.ascontiguousarray(feature_data[list(data_columns)].to_numpy().copy()) + for feature_data in data_sequence_feature_space + ] + + _, history = model.fit( + sequences=np.array(data_train_sequence, dtype=object), + labels=np.array(labels_train_sequence_str, dtype=object).copy(), + algorithm=algo_train, + stop_threshold=stop_threshold, + max_iterations=max_iterations, + return_history=True, + verbose=verbose, + n_jobs=n_jobs, + multiple_check_input=False, + ) + check_history_for_training_failure(history) + model.name = name + + submodel_states = _build_submodel_states(model_config, trained_models) + model_state = pomegranate_model_to_hmm_state( + model, + submodels=submodel_states, + backend_info=BackendInfo(backend_id=self.backend_id), + ) + model_state.cross_module_transitions = _extract_cross_module_transitions(model_state, module_offsets) + return model_state, history + + def _create_combined_model( + self, + *, + trained_models: dict[str, SimpleHmm], + labels_train_sequence: list[np.ndarray], + distributions: list[pg.Distribution], + model_config: CompositeHmmConfig, + module_offsets: dict[str, int], + initialization: Literal["labels", "fully-connected"], + verbose: bool, + ) -> pg.HiddenMarkovModel: + n_states = sum(module.n_states for module in model_config.modules) + if initialization == "fully-connected": + trans_mat, start_probs, _end_probs = create_transition_matrix_fully_connected(n_states) + model = pg.HiddenMarkovModel.from_matrix( + transition_probabilities=copy.deepcopy(trans_mat), + distributions=copy.deepcopy(distributions), + starts=start_probs, + ends=None, + state_names=None, + verbose=verbose, + ) + else: + trans_mat = np.zeros((n_states, n_states)) + for module in model_config.modules: + module_transition_matrix = trained_models[module.name].model.dense_transition_matrix()[:-2, :-2] + offset = module_offsets[module.name] + trans_mat[ + offset : offset + module.n_states, + offset : offset + module.n_states, + ] = module_transition_matrix + + transitions, starts, _ = extract_transitions_starts_stops_from_hidden_state_sequence(labels_train_sequence) + + start_probs = np.zeros(n_states) + start_probs[starts] = 1.0 + + model = pg.HiddenMarkovModel.from_matrix( + transition_probabilities=copy.deepcopy(trans_mat), + distributions=copy.deepcopy(distributions), + starts=start_probs, + ends=None, + state_names=None, + verbose=verbose, + ) + + existing_transitions = {(start.name, end.name) for start, end in model.graph.edges()} + for transition in sorted(transitions - existing_transitions): + add_transition(model, transition, 0.1) + + model = fix_model_names(model) + model.bake() + model.freeze_distributions() + return _clone_model(model, assert_correct=False) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py index 991e5679..b0b078de 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py @@ -1,6 +1,6 @@ """Segmentation _model base classes and helper.""" -import copy +import warnings from collections.abc import Sequence from typing import Any, Literal, Optional @@ -8,7 +8,6 @@ import pandas as pd import pomegranate as pg import tpcp -from pomegranate import HiddenMarkovModel as pgHMM from pomegranate.hmm import History from tpcp import OptiPara, cf, make_optimize_safe from typing_extensions import Self @@ -18,28 +17,27 @@ SingleSensorData, SingleSensorRegionsOfInterestList, ) +from gaitmap_mad.stride_segmentation.hmm._backend import BaseHmmBackend, PomegranateHmmBackend from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import ( BaseHmmFeatureTransformer, RothHmmFeatureTransformer, ) from gaitmap_mad.stride_segmentation.hmm._simple_model import SimpleHmm +from gaitmap_mad.stride_segmentation.hmm._state import ( + BackendInfo, + HMMState, + HmmSubModelState, + pomegranate_model_to_flat_hmm_state, + pomegranate_model_to_hmm_state, +) from gaitmap_mad.stride_segmentation.hmm._utils import ( ShortenedHMMPrint, - _clone_model, _DataToShortError, _HackyClonableHMMFix, - add_transition, - check_history_for_training_failure, convert_region_list_to_transition_list, - create_transition_matrix_fully_connected, - extract_transitions_starts_stops_from_hidden_state_sequence, - fix_model_names, - get_model_distributions, get_train_data_sequences_regions, get_train_data_sequences_transitions, - labels_to_strings, - predict, validate_trainable_region_list, ) @@ -109,20 +107,6 @@ def create_fully_labeled_hidden_state_sequences( return labels_train_sequence -def _create_simple_hmm_from_config(config: HmmSubModelConfig) -> SimpleHmm: - return SimpleHmm( - n_states=config.n_states, - n_gmm_components=config.n_gmm_components, - architecture=config.architecture, - algo_train=config.algo_train, - stop_threshold=config.stop_threshold, - max_iterations=config.max_iterations, - verbose=config.verbose, - n_jobs=config.n_jobs, - name=config.name, - ) - - def _get_training_sequences_for_module( module_config: HmmSubModelConfig, model_config: CompositeHmmConfig, @@ -157,15 +141,6 @@ def _get_training_sequences_for_module( return train_sequence, init_state_labels -def _collect_trained_module_distributions( - modules: tuple[HmmSubModelConfig, ...], trained_models: dict[str, SimpleHmm] -) -> list[Any]: - distributions = [] - for module in modules: - distributions.extend(get_model_distributions(trained_models[module.name].model)) - return distributions - - def _predict_labeled_training_sequences( data_sequence_feature_space: list[pd.DataFrame], region_list_feature_space: list[SingleSensorRegionsOfInterestList], @@ -348,11 +323,11 @@ class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHM The number of parallel jobs to use during optimization. If set to -1, all available cores will be used. name - The name of the final pomegranate model. + The name of the final compiled model. model - The actual pomegranate HMM model. + The serialized trained HMM state. This can be set to `None` initially. - A model will then be created during the optimization step. + A trained state will then be created during the optimization step. If you want to use a pre-trained model, you can set this parameter to the respective model. However, we recommend to ideally export this entire class instead of just the model to make sure that things like the feature transform are also exported/stored. @@ -361,6 +336,9 @@ class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHM This will be automatically set based on the feature transform output during the optimization step. This does not affect the output, but is used as a sanity check to ensure that valid input data is provided and that the column order is correct. + backend + Backend implementation that provides the backend-specific HMM primitives used for prediction and the final + combined-model training step. Attributes ---------- @@ -382,8 +360,8 @@ class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHM Notes ----- - The final model is still stored as a raw `pomegranate` HMM in this step of the refactor. - The `model_config` only replaces the dedicated submodel constructor inputs. + The public trained-model parameter is stored as a serializable `HMMState`. + The default backend in this refactor step is `PomegranateHmmBackend`. References ---------- @@ -403,8 +381,9 @@ class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHM verbose: bool n_jobs: int name: Optional[str] - model: OptiPara[Optional[pgHMM]] + model: OptiPara[Optional[HMMState]] data_columns: OptiPara[Optional[tuple[str, ...]]] + backend: BaseHmmBackend feature_space_data_: pd.DataFrame hidden_state_sequence_feature_space_: np.ndarray @@ -414,6 +393,11 @@ def _from_json_dict(cls, json_dict: dict) -> Self: params = json_dict["params"].copy() if "model_config" not in params and {"stride_model", "transition_model"} <= set(params): # TODO: Remove this compatibility shim once legacy serialized Roth models have been migrated. + warnings.warn( + "Loading a legacy RothSegmentationHmm serialization with raw pomegranate models. " + "The model is migrated to the new HMMState representation during loading.", + UserWarning, + ) stride_model = params.pop("stride_model") transition_model = params.pop("transition_model") stride_params = ( @@ -452,6 +436,39 @@ def _from_json_dict(cls, json_dict: dict) -> Self: ), ) ) + legacy_submodels = [] + if getattr(transition_model, "model", None) is not None: + legacy_submodels.append( + HmmSubModelState( + name="transition", + role="transition", + model=pomegranate_model_to_flat_hmm_state(transition_model.model), + ) + ) + if getattr(stride_model, "model", None) is not None: + legacy_submodels.append( + HmmSubModelState( + name="stride", + role="stride", + model=pomegranate_model_to_flat_hmm_state(stride_model.model), + ) + ) + if params.get("model") is not None: + params["model"] = pomegranate_model_to_hmm_state( + params["model"], + submodels=tuple(legacy_submodels), + backend_info=BackendInfo(backend_id="pomegranate-legacy-migrated"), + ) + elif isinstance(params.get("model"), pg.HiddenMarkovModel): + warnings.warn( + "Loading a RothSegmentationHmm with a raw pomegranate model parameter. " + "The model is migrated to the new HMMState representation during loading.", + UserWarning, + ) + params["model"] = pomegranate_model_to_hmm_state( + params["model"], + backend_info=BackendInfo(backend_id="pomegranate-legacy-migrated"), + ) input_data = {k: params[k] for k in tpcp.get_param_names(cls) if k in params} return cls(**input_data) @@ -468,8 +485,9 @@ def __init__( verbose: bool = True, n_jobs: int = 1, name: str = "segmentation_model", - model: Optional[pgHMM] = None, + model: Optional[HMMState] = None, data_columns: Optional[tuple[str, ...]] = None, + backend: BaseHmmBackend = cf(PomegranateHmmBackend()), ) -> None: self.model_config = model_config self.feature_transform = feature_transform @@ -483,6 +501,7 @@ def __init__( self.name = name self.model = model self.data_columns = data_columns + self.backend = backend @property def n_states(self) -> int: @@ -553,9 +572,12 @@ def predict(self, data: SingleSensorData, sampling_rate_hz: float) -> Self: feature_data = feature_data[0] self.feature_space_data_ = feature_data - # pomegranate always adds a label for the start- and end-state, which can be ignored here! - self.hidden_state_sequence_feature_space_ = predict( - self.model, feature_data, expected_columns=self.data_columns, algorithm=self.algo_predict + self.hidden_state_sequence_feature_space_ = self.backend.predict( + self.model, + feature_data, + expected_columns=self.data_columns, + algorithm=self.algo_predict, + verbose=self.verbose, ) self.hidden_state_sequence_ = self.feature_transform.inverse_transform_state_sequence( self.hidden_state_sequence_feature_space_, data=data @@ -687,17 +709,12 @@ def self_optimize_with_info( region_list_feature_space, ) - trained_model, history = _create_simple_hmm_from_config(module_config).self_optimize_with_info( + trained_model, history = self.backend.create_submodel(module_config).self_optimize_with_info( train_sequence, init_state_labels ) trained_models[module_name] = trained_model histories[module_name] = history - # For model combination actually only the transition probabilities will be updated, while keeping the already - # learned distributions for all states. This can be achieved by "labeled" training, where basically just the - # number of transitions will be counted. - distributions = _collect_trained_module_distributions(self.model_config.modules, trained_models) - # predict hidden state labels for complete walking bouts module_offsets = self._module_offsets labels_train_sequence = _predict_labeled_training_sequences( @@ -709,98 +726,22 @@ def self_optimize_with_info( self.algo_predict, ) - # Now that we have a fully labeled dataset, we use our already fitted distributions as input for the new model - if self.initialization == "fully-connected": - trans_mat, start_probs, end_probs = create_transition_matrix_fully_connected(self.n_states) - - new_model = pg.HiddenMarkovModel.from_matrix( - transition_probabilities=copy.deepcopy(trans_mat), - distributions=copy.deepcopy(distributions), - starts=start_probs, - ends=None, - state_names=None, - verbose=self.verbose, - ) - - elif self.initialization == "labels": - trans_mat = np.zeros((self.n_states, self.n_states)) - for module_config in self.model_config.modules: - module_name = module_config.name - module_transition_matrix = trained_models[module_name].model.dense_transition_matrix()[:-2, :-2] - offset = module_offsets[module_name] - trans_mat[ - offset : offset + module_config.n_states, - offset : offset + module_config.n_states, - ] = module_transition_matrix - - # find missing transitions from labels - transitions, starts, ends = extract_transitions_starts_stops_from_hidden_state_sequence( - labels_train_sequence - ) - - start_probs = np.zeros(self.n_states) - start_probs[starts] = 1.0 - end_probs = np.zeros(self.n_states) - end_probs[ends] = 1.0 - - new_model = pg.HiddenMarkovModel.from_matrix( - transition_probabilities=copy.deepcopy(trans_mat), - distributions=copy.deepcopy(distributions), - starts=start_probs, - ends=None, - state_names=None, - verbose=self.verbose, - ) - - existing_transitions = {(start.name, end.name) for start, end in new_model.graph.edges()} - missing_transitions = transitions - existing_transitions - # Add missing transitions which will "connect" transition-hmm and stride-hmm - # We initialize with a very small probability, so that the model can learn the correct values in the next - # step. - # Note: We sort the transitions to enforce consistent order and reproducibility. - for trans in sorted(missing_transitions): - add_transition(new_model, trans, 0.1) - else: - # Can not be reached, as we perform the check beforehand, but just to be sure and make the linter happy - raise RuntimeError() - # pomegranate seems to have a strange sorting bug where state names >= 10 (e.g. s10 get sorted in a bad order - # like s0, s1, s10, s2 usw..) - new_model = fix_model_names(new_model) - new_model.bake() - - # make sure we do not change our distributions anymore! - new_model.freeze_distributions() - - # We clone the model here, as this changes the order of edges to be sorted somehow... - new_model = _clone_model(new_model, assert_correct=False) - - # convert labels to state-names - labels_train_sequence_str = labels_to_strings(labels_train_sequence) - self.data_columns = tuple(data_sequence_feature_space[0].columns) - - # make sure data is in an pomegranate compatible format! - data_train_sequence = [ - np.ascontiguousarray(feature_data[list(self.data_columns)].to_numpy().copy()) - for feature_data in data_sequence_feature_space - ] - - _, history = new_model.fit( - sequences=np.array(data_train_sequence, dtype=object), - labels=np.array(labels_train_sequence_str, dtype=object).copy(), - algorithm=self.algo_train, + self.model, history = self.backend.finalize_model( + trained_models=trained_models, + labels_train_sequence=labels_train_sequence, + data_sequence_feature_space=data_sequence_feature_space, + data_columns=self.data_columns, + model_config=self.model_config, + module_offsets=module_offsets, + initialization=self.initialization, + algo_train=self.algo_train, stop_threshold=self.stop_threshold, max_iterations=self.max_iterations, - return_history=True, verbose=self.verbose, n_jobs=self.n_jobs, - multiple_check_input=False, + name=self.name, ) - check_history_for_training_failure(history) - - new_model.name = self.name - - self.model = new_model histories["self"] = history return self, histories diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_state.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_state.py new file mode 100644 index 00000000..47a7b391 --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_state.py @@ -0,0 +1,313 @@ +"""Serializable HMM state objects and pomegranate conversion helpers.""" + +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, version +from typing import Union + +import numpy as np +import pomegranate as pg +from pomegranate import HiddenMarkovModel as pgHMM +from typing_extensions import Literal + +from gaitmap.base import _BaseSerializable +from gaitmap_mad.stride_segmentation.hmm._utils import add_transition + + +def _get_pomegranate_version() -> str | None: + try: + return version("pomegranate") + except PackageNotFoundError: + return None + + +class BackendInfo(_BaseSerializable): + """Provenance information for a serialized HMM state.""" + + backend_id: str + backend_version: str | None + state_schema_version: int + + def __init__( + self, + backend_id: str, + *, + backend_version: str | None = None, + state_schema_version: int = 1, + ) -> None: + self.backend_id = backend_id + self.backend_version = backend_version + self.state_schema_version = state_schema_version + + +class HmmGraphState(_BaseSerializable): + """Dense graph representation of one flat HMM.""" + + transition_probs: np.ndarray + start_probs: np.ndarray + end_probs: np.ndarray + + def __init__(self, transition_probs: np.ndarray, start_probs: np.ndarray, end_probs: np.ndarray) -> None: + self.transition_probs = transition_probs + self.start_probs = start_probs + self.end_probs = end_probs + + +class GaussianEmissionState(_BaseSerializable): + """Serializable multivariate Gaussian emission.""" + + kind: Literal["gaussian"] + mean: np.ndarray + covariance: np.ndarray + covariance_type: Literal["full"] + frozen: bool + + def __init__( + self, + mean: np.ndarray, + covariance: np.ndarray, + *, + covariance_type: Literal["full"] = "full", + frozen: bool = False, + ) -> None: + self.kind = "gaussian" + self.mean = mean + self.covariance = covariance + self.covariance_type = covariance_type + self.frozen = frozen + + +class GaussianMixtureEmissionState(_BaseSerializable): + """Serializable Gaussian-mixture emission.""" + + kind: Literal["gaussian_mixture"] + weights: np.ndarray + components: tuple[GaussianEmissionState, ...] + frozen: bool + + def __init__( + self, + weights: np.ndarray, + components: tuple[GaussianEmissionState, ...], + *, + frozen: bool = False, + ) -> None: + self.kind = "gaussian_mixture" + self.weights = weights + self.components = components + self.frozen = frozen + + +EmissionState = Union[GaussianEmissionState, GaussianMixtureEmissionState] + + +class FlatHmmState(_BaseSerializable): + """Serializable flat HMM representation used for compiled and submodule states.""" + + name: str | None + graph: HmmGraphState + emissions: tuple[EmissionState, ...] + state_names: tuple[str, ...] + + def __init__( + self, + graph: HmmGraphState, + emissions: tuple[EmissionState, ...], + *, + state_names: tuple[str, ...], + name: str | None = None, + ) -> None: + self.name = name + self.graph = graph + self.emissions = emissions + self.state_names = state_names + + +class HmmSubModelState(_BaseSerializable): + """Serializable named submodule state.""" + + name: str + role: str + model: FlatHmmState + + def __init__(self, name: str, role: str, model: FlatHmmState) -> None: + self.name = name + self.role = role + self.model = model + + +class CrossModuleTransition(_BaseSerializable): + """Serializable transition between two named submodules.""" + + from_module: str + from_state: int + to_module: str + to_state: int + probability: float + + def __init__(self, from_module: str, from_state: int, to_module: str, to_state: int, probability: float) -> None: + self.from_module = from_module + self.from_state = from_state + self.to_module = to_module + self.to_state = to_state + self.probability = probability + + +class HMMState(_BaseSerializable): + """Serializable, backend-neutral trained HMM state.""" + + trained_with: BackendInfo + compiled: FlatHmmState + submodels: tuple[HmmSubModelState, ...] + cross_module_transitions: tuple[CrossModuleTransition, ...] + + def __init__( + self, + trained_with: BackendInfo, + compiled: FlatHmmState, + *, + submodels: tuple[HmmSubModelState, ...] = (), + cross_module_transitions: tuple[CrossModuleTransition, ...] = (), + ) -> None: + self.trained_with = trained_with + self.compiled = compiled + self.submodels = submodels + self.cross_module_transitions = cross_module_transitions + + def __getstate__(self) -> str: + """Use the JSON serialization as stable pickle state for hashing and cloning.""" + return self.to_json() + + def __setstate__(self, state: str) -> None: + restored = type(self).from_json(state) + self.__dict__.update(restored.__dict__) + + +def _normalize_mixture_weights(log_weights: np.ndarray) -> np.ndarray: + shifted = np.exp(log_weights - np.max(log_weights)) + return shifted / np.sum(shifted) + + +def _distribution_to_state(distribution: pg.Distribution) -> EmissionState: + if isinstance(distribution, pg.GeneralMixtureModel): + return GaussianMixtureEmissionState( + weights=_normalize_mixture_weights(np.asarray(distribution.weights, dtype=float)), + components=tuple(_distribution_to_state(component) for component in distribution.distributions), # type: ignore[arg-type] + frozen=bool(getattr(distribution, "frozen", False)), + ) + if isinstance(distribution, pg.MultivariateGaussianDistribution): + mean, covariance = distribution.parameters + return GaussianEmissionState( + mean=np.asarray(mean, dtype=float), + covariance=np.asarray(covariance, dtype=float), + frozen=bool(getattr(distribution, "frozen", False)), + ) + raise TypeError( + f"Unsupported pomegranate emission distribution `{type(distribution).__name__}`. " + "Only multivariate Gaussian and Gaussian mixture emissions are supported in the serialized HMM state." + ) + + +def _state_to_distribution(state: EmissionState) -> pg.Distribution: + if isinstance(state, GaussianEmissionState): + distribution = pg.MultivariateGaussianDistribution(state.mean.tolist(), state.covariance.tolist()) + distribution.frozen = state.frozen + return distribution + if isinstance(state, GaussianMixtureEmissionState): + weights = np.asarray(state.weights, dtype=float) + weights = np.clip(weights, np.finfo(float).tiny, None) + weights = weights / np.sum(weights) + distribution = pg.GeneralMixtureModel( + [_state_to_distribution(component) for component in state.components], + weights=weights.tolist(), + ) + distribution.frozen = state.frozen + return distribution + raise TypeError(f"Unsupported serialized emission state `{type(state).__name__}`.") + + +def pomegranate_model_to_flat_hmm_state(model: pgHMM) -> FlatHmmState: + """Convert a pomegranate HMM into a serializable flat state.""" + dense_transition_matrix = model.dense_transition_matrix() + graph_state = HmmGraphState( + transition_probs=np.asarray(dense_transition_matrix[:-2, :-2], dtype=float), + start_probs=np.asarray(dense_transition_matrix[-2, :-2], dtype=float), + end_probs=np.asarray(dense_transition_matrix[:-2, -1], dtype=float), + ) + hidden_states = [state for state in model.states if state.distribution is not None] + return FlatHmmState( + graph=graph_state, + emissions=tuple(_distribution_to_state(state.distribution) for state in hidden_states), + state_names=tuple(state.name for state in hidden_states), + name=model.name, + ) + + +def flat_hmm_state_to_pomegranate_model(state: FlatHmmState, *, verbose: bool = False) -> pgHMM: + """Compile a serializable flat state into a pomegranate HMM.""" + model = pg.HiddenMarkovModel.from_matrix( + transition_probabilities=np.asarray(state.graph.transition_probs, dtype=float), + distributions=[_state_to_distribution(distribution) for distribution in state.emissions], + starts=np.asarray(state.graph.start_probs, dtype=float), + ends=np.asarray(state.graph.end_probs, dtype=float), + state_names=list(state.state_names), + verbose=verbose, + ) + model.bake() + if state.name is not None: + model.name = state.name + return model + + +def pomegranate_model_to_hmm_state( + compiled_model: pgHMM, + *, + submodels: tuple[HmmSubModelState, ...] = (), + cross_module_transitions: tuple[CrossModuleTransition, ...] = (), + backend_info: BackendInfo | None = None, +) -> HMMState: + """Convert a compiled pomegranate HMM and optional hierarchy into a serializable HMM state.""" + if backend_info is None: + backend_info = BackendInfo(backend_id="pomegranate-legacy", backend_version=_get_pomegranate_version()) + elif backend_info.backend_version is None and backend_info.backend_id.startswith("pomegranate"): + backend_info = BackendInfo( + backend_id=backend_info.backend_id, + backend_version=_get_pomegranate_version(), + state_schema_version=backend_info.state_schema_version, + ) + return HMMState( + trained_with=backend_info, + compiled=pomegranate_model_to_flat_hmm_state(compiled_model), + submodels=submodels, + cross_module_transitions=cross_module_transitions, + ) + + +def hmm_state_to_pomegranate_model(state: HMMState, *, verbose: bool = False) -> pgHMM: + """Compile a serializable HMM state into a pomegranate HMM.""" + model = flat_hmm_state_to_pomegranate_model(state.compiled, verbose=verbose) + existing_transitions = {(start.name, end.name) for start, end in model.graph.edges()} + for transition in state.cross_module_transitions: + from_state = state.compiled.state_names[_find_state_index(state, transition.from_module, transition.from_state)] + to_state = state.compiled.state_names[_find_state_index(state, transition.to_module, transition.to_state)] + if (from_state, to_state) in existing_transitions: + continue + add_transition(model, (from_state, to_state), transition.probability) + existing_transitions.add((from_state, to_state)) + model.bake() + return model + + +def _find_state_index(state: HMMState, module_name: str, state_idx: int) -> int: + offset = 0 + for submodel in state.submodels: + n_states = len(submodel.model.state_names) + if submodel.name == module_name: + if state_idx >= n_states: + raise ValueError( + f"Cross-module transition refers to state {state_idx} of module `{module_name}`, " + f"but the module only has {n_states} states." + ) + return offset + state_idx + offset += n_states + raise ValueError(f"No submodel named `{module_name}` exists in the serialized HMM state.") diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py index 0d97712d..e15c89bc 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py @@ -131,6 +131,15 @@ class _HackyClonableHMMFix(BaseTpcpObject): """ + @staticmethod + def _is_serialized_hmm_state(value: Any) -> bool: + return ( + hasattr(value, "compiled") + and hasattr(value, "trained_with") + and callable(getattr(value, "to_json", None)) + and callable(getattr(type(value), "from_json", None)) + ) + @classmethod def __clone_param__(cls, param_name: str, value: Any) -> Any: """Overwrite cloning for HMM models. @@ -143,6 +152,8 @@ def __clone_param__(cls, param_name: str, value: Any) -> Any: """ if isinstance(value, pg.HiddenMarkovModel): return _clone_model(value) + if cls._is_serialized_hmm_state(value): + return type(value).from_json(value.to_json()) return super().__clone_param__(param_name, value) @@ -152,6 +163,10 @@ class ShortenedHMMPrint(BaseTpcpObject): def __repr_parameter__(self, name: str, value: Any) -> str: """Representation with specific care for HMM models.""" if name == "model": + if self._is_serialized_hmm_state(value): + n_states = len(value.compiled.state_names) if getattr(value, "compiled", None) is not None else "?" + backend = getattr(getattr(value, "trained_with", None), "backend_id", "?") + return f"{name}=HMMState[backend={backend}, states={n_states}](...)" if isinstance(value, pg.HiddenMarkovModel): return f"{name}=HiddenMarkovModel[name={value.name}](...)" if isinstance(value, CloneFactory) and isinstance(value.default_value, pg.HiddenMarkovModel): diff --git a/tests/test_examples/snapshot/test_segmentation_hmm_training_left_sensor.json b/tests/test_examples/snapshot/test_segmentation_hmm_training_left_sensor.json index ed298d69..17bf770a 100644 --- a/tests/test_examples/snapshot/test_segmentation_hmm_training_left_sensor.json +++ b/tests/test_examples/snapshot/test_segmentation_hmm_training_left_sensor.json @@ -88,7 +88,7 @@ { "s_id":13, "start":3231, - "end":3466 + "end":3453 }, { "s_id":14, @@ -158,7 +158,7 @@ { "s_id":27, "start":6858, - "end":7107 + "end":7091 } ] -} \ No newline at end of file +} diff --git a/tests/test_examples/snapshot/test_segmentation_hmm_training_right_sensor.json b/tests/test_examples/snapshot/test_segmentation_hmm_training_right_sensor.json index fc6d5c7e..6348c470 100644 --- a/tests/test_examples/snapshot/test_segmentation_hmm_training_right_sensor.json +++ b/tests/test_examples/snapshot/test_segmentation_hmm_training_right_sensor.json @@ -93,7 +93,7 @@ { "s_id":14, "start":3567, - "end":3816 + "end":3802 }, { "s_id":15, @@ -167,8 +167,8 @@ }, { "s_id":29, - "start":6966, + "start":6977, "end":7246 } ] -} \ No newline at end of file +} diff --git a/tests/test_stride_segmentation/test_roth_hmm.py b/tests/test_stride_segmentation/test_roth_hmm.py index 6bf1f0e4..1f16d4ec 100644 --- a/tests/test_stride_segmentation/test_roth_hmm.py +++ b/tests/test_stride_segmentation/test_roth_hmm.py @@ -1,3 +1,5 @@ +import json +from pathlib import Path from unittest.mock import patch import numpy as np @@ -11,6 +13,7 @@ from pomegranate.hmm import History from tpcp._hash import custom_hash +from gaitmap.base import _custom_deserialize from gaitmap.data_transform import SlidingWindowMean from gaitmap.utils.consts import BF_COLS from gaitmap.utils.coordinate_conversion import convert_left_foot_to_fbf, convert_to_fbf @@ -22,14 +25,19 @@ from gaitmap.utils.exceptions import ValidationError from gaitmap_mad.stride_segmentation.hmm import ( CompositeHmmConfig, + HMMState, HmmStrideSegmentation, + PomegranateHmmBackend, HmmSubModelConfig, PreTrainedRothSegmentationModel, RothHmmFeatureTransformer, RothSegmentationHmm, SimpleHmm, ) +from gaitmap_mad.stride_segmentation.hmm import _backend as backend_module from gaitmap_mad.stride_segmentation.hmm._simple_model import initialize_hmm +from gaitmap_mad.stride_segmentation.hmm._state import hmm_state_to_pomegranate_model, pomegranate_model_to_hmm_state +from gaitmap_mad.stride_segmentation.hmm._utils import predict from tests.mixins.test_algorithm_mixin import TestAlgorithmMixin # Fix random seed for reproducibility @@ -70,6 +78,15 @@ def _stride_list_to_region_list(stride_list: pd.DataFrame, region_type: str = "s return region_list.set_index("roi_id") +def _load_raw_pretrained_pomegranate_model(): + raw = json.loads( + Path( + "packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_pre_trained_models/fallriskpd_at_lab_model.json" + ).read_text() + ) + return _custom_deserialize(raw["params"]["model"]) + + class TestMetaFunctionalityRothSegmentationHmm(TestAlgorithmMixin): __test__ = True @@ -401,6 +418,7 @@ def test_self_optimize_with_info_returns_history(self) -> None: ) trained_instance, history = instance.self_optimize_with_info(data, labels, sampling_rate_hz=100) assert instance is trained_instance + assert isinstance(instance.model, HMMState) for v in history.values(): assert isinstance(v, History) assert set(history.keys()) == {"stride", "transition", "self"} @@ -545,6 +563,75 @@ def test_training_updates_final_model(self) -> None: assert hash_model_config == custom_hash(instance.model_config) assert hash_model != custom_hash(instance.model) + def test_pretrained_model_is_migrated_to_hmm_state(self) -> None: + with pytest.warns(UserWarning, match="migrated to the new HMMState representation"): + model = PreTrainedRothSegmentationModel() + + assert isinstance(model.model, HMMState) + assert model.model.trained_with.backend_id == "pomegranate-legacy-migrated" + assert model.model.trained_with.backend_version is not None + assert len(model.model.submodels) == 2 + assert isinstance(model.backend, PomegranateHmmBackend) + + def test_pretrained_model_roundtrip_matches_legacy_hidden_states(self, healthy_example_imu_data) -> None: + raw_model = _load_raw_pretrained_pomegranate_model() + model = PreTrainedRothSegmentationModel() + runtime_model = hmm_state_to_pomegranate_model(model.model) + data = convert_left_foot_to_fbf(healthy_example_imu_data["left_sensor"]) + feature_data, _ = model._transform([data], None, sampling_rate_hz=100) + feature_data = feature_data[0] + + raw_sequence = predict(raw_model, feature_data, expected_columns=model.data_columns, algorithm=model.algo_predict) + roundtrip_sequence = predict( + runtime_model, + feature_data, + expected_columns=model.data_columns, + algorithm=model.algo_predict, + ) + + assert_array_equal(raw_sequence, roundtrip_sequence) + + def test_trained_model_roundtrip_matches_original_hidden_states(self) -> None: + data_sequence = [pd.DataFrame(np.random.rand(120, 6), columns=BF_COLS)] + region_list_sequence = [ + _stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]})) + ] + instance = RothSegmentationHmm( + model_config=_create_roth_model_config(stride_n_states=3, stride_n_gmm_components=3) + ).set_params( + feature_transform__sampling_rate_feature_space_hz=100, + ) + captured_model = {} + original_converter = backend_module.pomegranate_model_to_hmm_state + + def _capture_and_convert(model, *args, **kwargs): + captured_model["raw_model"] = model + return original_converter(model, *args, **kwargs) + + with patch.object(backend_module, "pomegranate_model_to_hmm_state", side_effect=_capture_and_convert): + instance.self_optimize(data_sequence, region_list_sequence, sampling_rate_hz=100) + + runtime_model = hmm_state_to_pomegranate_model(instance.model) + feature_data, _ = instance._transform(data_sequence, None, sampling_rate_hz=100) + feature_data = feature_data[0] + + raw_sequence = predict( + captured_model["raw_model"], + feature_data, + expected_columns=instance.data_columns, + algorithm=instance.algo_predict, + ) + roundtrip_sequence = predict( + runtime_model, + feature_data, + expected_columns=instance.data_columns, + algorithm=instance.algo_predict, + ) + + assert_array_equal(raw_sequence, roundtrip_sequence) + assert instance.model.trained_with.backend_id == "pomegranate-legacy" + assert instance.model.trained_with.backend_version is not None + class TestHmmStrideSegmentation: def test_segment_with_single_dataset(self, healthy_example_imu_data) -> None: From 6101b6d4f5cf7fb681878d70fd1d58d10e42563c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Mon, 9 Mar 2026 14:37:52 +0100 Subject: [PATCH 07/28] Bundle Roth HMM preprocessing into config --- .../segmentation_hmm_training.py | 21 +- gaitmap/stride_segmentation/hmm.py | 3 + .../stride_segmentation/hmm/__init__.py | 3 +- .../stride_segmentation/hmm/_config.py | 41 ++++ .../hmm/_hmm_feature_transform.py | 14 +- .../hmm/_segmentation_model.py | 230 +++++++++--------- .../test_stride_segmentation/test_roth_hmm.py | 58 +++-- 7 files changed, 223 insertions(+), 147 deletions(-) diff --git a/examples/stride_segmentation/segmentation_hmm_training.py b/examples/stride_segmentation/segmentation_hmm_training.py index 9f6848a7..cf9eb286 100644 --- a/examples/stride_segmentation/segmentation_hmm_training.py +++ b/examples/stride_segmentation/segmentation_hmm_training.py @@ -84,7 +84,7 @@ # different in architecture, number of states or number of gaussian mixture model (GMM) components. # In this example all configurable parameters are exposed. # These parameters might require optimization for your specific type of dataset! -from gaitmap.stride_segmentation.hmm import CompositeHmmConfig, HmmSubModelConfig +from gaitmap.stride_segmentation.hmm import CompositeHmmConfig, HmmSubModelConfig, RothHmmConfig model_config = CompositeHmmConfig( modules=( @@ -124,16 +124,17 @@ from gaitmap.stride_segmentation.hmm import PomegranateHmmBackend, RothSegmentationHmm segmentation_model = RothSegmentationHmm( - model_config=model_config, - feature_transform=feature_transform, + hmm_config=RothHmmConfig( + model_config=model_config, + feature_transform=feature_transform, + algo_predict="viterbi", + algo_train="baum-welch", + stop_threshold=1e-9, + max_iterations=1, + initialization="labels", + name="segmentation_model", + ), backend=PomegranateHmmBackend(), - algo_predict="viterbi", - algo_train="baum-welch", - stop_threshold=1e-9, - max_iterations=1, - initialization="labels", - verbose=True, - name="segmentation_model", ) # %% diff --git a/gaitmap/stride_segmentation/hmm.py b/gaitmap/stride_segmentation/hmm.py index 120fa45d..36bb1c05 100644 --- a/gaitmap/stride_segmentation/hmm.py +++ b/gaitmap/stride_segmentation/hmm.py @@ -26,6 +26,7 @@ "HmmSubModelState", "PomegranateHmmBackend", "SimpleHmm", + "RothHmmConfig", "RothSegmentationHmm", "PreTrainedRothSegmentationModel", "BaseSegmentationHmm", @@ -50,6 +51,7 @@ HmmSubModelState, PomegranateHmmBackend, PreTrainedRothSegmentationModel, + RothHmmConfig, RothHmmFeatureTransformer, RothSegmentationHmm, SimpleHmm, @@ -73,6 +75,7 @@ "HmmSubModelState", "PomegranateHmmBackend", "PreTrainedRothSegmentationModel", + "RothHmmConfig", "RothHmmFeatureTransformer", "RothSegmentationHmm", "SimpleHmm", diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py index 1b40d39f..b05222cb 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py @@ -4,7 +4,7 @@ import warnings from gaitmap_mad.stride_segmentation.hmm._backend import BaseHmmBackend, PomegranateHmmBackend -from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig +from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig, RothHmmConfig from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import ( BaseHmmFeatureTransformer, RothHmmFeatureTransformer, @@ -52,6 +52,7 @@ "HmmSubModelState", "PomegranateHmmBackend", "PreTrainedRothSegmentationModel", + "RothHmmConfig", "RothHmmFeatureTransformer", "RothSegmentationHmm", "SimpleHmm", diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_config.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_config.py index ae36f7e3..16f9e0c1 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_config.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_config.py @@ -6,6 +6,7 @@ from typing_extensions import Literal from gaitmap.base import _BaseSerializable +from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import RothHmmFeatureTransformer def _default_modules() -> tuple["HmmSubModelConfig", ...]: @@ -131,3 +132,43 @@ def custom_model_names(self) -> tuple[str, ...]: def get_module_role(self, module_name: str) -> str: """Return the configured role of a named module.""" return self.get_module(module_name).role + + +class RothHmmConfig(_BaseSerializable): + """Serializable configuration bundle for `RothSegmentationHmm`.""" + + model_config: CompositeHmmConfig + feature_transform: RothHmmFeatureTransformer + algo_predict: Literal["viterbi", "map"] + algo_train: Literal["viterbi", "baum-welch"] + stop_threshold: float + max_iterations: int + initialization: Literal["labels", "fully-connected"] + verbose: bool + n_jobs: int + name: str + + def __init__( + self, + model_config: CompositeHmmConfig = cf(CompositeHmmConfig()), + feature_transform: RothHmmFeatureTransformer = cf(RothHmmFeatureTransformer()), + *, + algo_predict: Literal["viterbi", "map"] = "viterbi", + algo_train: Literal["viterbi", "baum-welch"] = "baum-welch", + stop_threshold: float = 1e-9, + max_iterations: int = 1, + initialization: Literal["labels", "fully-connected"] = "labels", + verbose: bool = True, + n_jobs: int = 1, + name: str = "segmentation_model", + ) -> None: + self.model_config = model_config + self.feature_transform = feature_transform + self.algo_predict = algo_predict + self.algo_train = algo_train + self.stop_threshold = stop_threshold + self.max_iterations = max_iterations + self.initialization = initialization + self.verbose = verbose + self.n_jobs = n_jobs + self.name = name diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_hmm_feature_transform.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_hmm_feature_transform.py index 5e1c4b42..65fef7ce 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_hmm_feature_transform.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_hmm_feature_transform.py @@ -38,7 +38,7 @@ class BaseHmmFeatureTransformer(BaseTransformer): This is only required if :class:`gaitmap.stride_segmentation.hmm.RothHMMFeatureTransformer` is not sufficient for your use case, when using :class:`~gaitmap.stride_segmentation.hmm.RothSegmentationHmm`. - In this case implement a custom subclass and pass it to the `feature_transform` parameter of + In this case implement a custom subclass and pass it via `RothHmmConfig.feature_transform` to `RothSegmentationHmm`. Note, that you need to implement the `transform` and `inverse_transform_state_sequence` methods. """ @@ -185,6 +185,18 @@ def n_features(self) -> int: """Get the number of features in the transformed data.""" return len(self.axes) * len(self.features) + @property + def transformed_feature_columns(self) -> tuple[str, ...]: + """Return the expected feature-space column order for the configured axes/features.""" + feature_prefixes = { + "raw": "", + "gradient": "__gradient", + "mean": "__mean", + "std": "__std", + "var": "__var", + } + return tuple(f"{feature}{feature_prefixes[feature]}__{axis}" for feature in self.features for axis in self.axes) + def transform( self, data: Optional[SingleSensorData] = None, diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py index b0b078de..0726c394 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py @@ -18,11 +18,8 @@ SingleSensorRegionsOfInterestList, ) from gaitmap_mad.stride_segmentation.hmm._backend import BaseHmmBackend, PomegranateHmmBackend -from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig -from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import ( - BaseHmmFeatureTransformer, - RothHmmFeatureTransformer, -) +from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig, RothHmmConfig +from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import RothHmmFeatureTransformer from gaitmap_mad.stride_segmentation.hmm._simple_model import SimpleHmm from gaitmap_mad.stride_segmentation.hmm._state import ( BackendInfo, @@ -284,58 +281,14 @@ class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHM Parameters ---------- - model_config - The configuration of the named HMM submodules that are trained and combined into the final model. - One module is interpreted as the implicit transition model and all remaining modules are expected to be covered - by typed input regions during optimization. - feature_transform - An instance of a :class:`~gaitmap.stride_segmentation.hmm.FeatureTransformHMM` that can transform the data - (and the labeled stride list) into the feature space required by the HMM. - If you want to use custome feature extraction, - algo_train - The algorithm to use for training the HMM. - algo_predict - The algorithm to use for prediction with the HMM. - stop_threshold - The loss threshold to stop the optimization. - Note, that is the threshold for the "combined" training of the final model. - This is less important, as we recommend to train the combined model only for a single iteration anyway. - If you want to adjust the stop threshold for the individual submodels, do so via the corresponding entries in - `model_config.modules`. - max_iterations - The maximum number of iterations to perform during the optimization. - Note, that this is the value for the "combined" training of the final model. - We recommend keeping this value at 1, as the combined model training only adjusts the transition matrix. - If you want to adjust the max iterations for the individual submodels, do so via the corresponding entries in - `model_config.modules`. - initialization - The initialization method to use for the HMM during optimization. - `fully-connected` assumes that all states are reachable from any other state with the same probability. - `labels` will derive the allowed transitions and probabilities from the given labels. - In both cases, distributions will be derived from the data during optimization. - We recommend using `labels` here, as this is kind of the default mode we expect these segmentation models to be - used. - If you select `fully-connected`, you might want to increase the `max_iterations` parameter to allow the model to - actually be trained. - verbose - If True, print additional information during optimization. - n_jobs - The number of parallel jobs to use during optimization. - If set to -1, all available cores will be used. - name - The name of the final compiled model. + hmm_config + Serializable configuration bundle containing the HMM submodel topology, feature extraction parameters, and + training/prediction settings. model The serialized trained HMM state. This can be set to `None` initially. A trained state will then be created during the optimization step. - If you want to use a pre-trained model, you can set this parameter to the respective model. - However, we recommend to ideally export this entire class instead of just the model to make sure that things - like the feature transform are also exported/stored. - data_columns - The expected columns of the input data in feature space. - This will be automatically set based on the feature transform output during the optimization step. - This does not affect the output, but is used as a sanity check to ensure that valid input data is provided - and that the column order is correct. + If you want to use a pre-trained model, you can set this parameter to the respective model state. backend Backend implementation that provides the backend-specific HMM primitives used for prediction and the final combined-model training step. @@ -371,18 +324,8 @@ class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHM """ - model_config: CompositeHmmConfig - feature_transform: BaseHmmFeatureTransformer - algo_predict: Literal["viterbi", "baum-welch"] - algo_train: Literal["viterbi", "baum-welch"] - stop_threshold: float - max_iterations: int - initialization: Literal["labels", "fully-connected"] - verbose: bool - n_jobs: int - name: Optional[str] + hmm_config: RothHmmConfig model: OptiPara[Optional[HMMState]] - data_columns: OptiPara[Optional[tuple[str, ...]]] backend: BaseHmmBackend feature_space_data_: pd.DataFrame @@ -391,7 +334,20 @@ class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHM @classmethod def _from_json_dict(cls, json_dict: dict) -> Self: params = json_dict["params"].copy() - if "model_config" not in params and {"stride_model", "transition_model"} <= set(params): + if "hmm_config" not in params and "model_config" in params: + params["hmm_config"] = RothHmmConfig( + model_config=params.pop("model_config"), + feature_transform=params.pop("feature_transform", RothHmmFeatureTransformer()), + algo_predict=params.pop("algo_predict", "viterbi"), + algo_train=params.pop("algo_train", "baum-welch"), + stop_threshold=params.pop("stop_threshold", 1e-9), + max_iterations=params.pop("max_iterations", 1), + initialization=params.pop("initialization", "labels"), + verbose=params.pop("verbose", True), + n_jobs=params.pop("n_jobs", 1), + name=params.pop("name", "segmentation_model"), + ) + if "hmm_config" not in params and {"stride_model", "transition_model"} <= set(params): # TODO: Remove this compatibility shim once legacy serialized Roth models have been migrated. warnings.warn( "Loading a legacy RothSegmentationHmm serialization with raw pomegranate models. " @@ -408,33 +364,44 @@ def _from_json_dict(cls, json_dict: dict) -> Self: if isinstance(transition_model, SimpleHmm) else transition_model["params"] ) - params["model_config"] = CompositeHmmConfig( - modules=( - HmmSubModelConfig( - name="transition", - role="transition", - n_states=transition_params["n_states"], - n_gmm_components=transition_params["n_gmm_components"], - architecture=transition_params["architecture"], - algo_train=transition_params["algo_train"], - stop_threshold=transition_params["stop_threshold"], - max_iterations=transition_params["max_iterations"], - verbose=transition_params.get("verbose", True), - n_jobs=transition_params.get("n_jobs", 1), - ), - HmmSubModelConfig( - name="stride", - role="stride", - n_states=stride_params["n_states"], - n_gmm_components=stride_params["n_gmm_components"], - architecture=stride_params["architecture"], - algo_train=stride_params["algo_train"], - stop_threshold=stride_params["stop_threshold"], - max_iterations=stride_params["max_iterations"], - verbose=stride_params.get("verbose", True), - n_jobs=stride_params.get("n_jobs", 1), - ), - ) + params["hmm_config"] = RothHmmConfig( + model_config=CompositeHmmConfig( + modules=( + HmmSubModelConfig( + name="transition", + role="transition", + n_states=transition_params["n_states"], + n_gmm_components=transition_params["n_gmm_components"], + architecture=transition_params["architecture"], + algo_train=transition_params["algo_train"], + stop_threshold=transition_params["stop_threshold"], + max_iterations=transition_params["max_iterations"], + verbose=transition_params.get("verbose", True), + n_jobs=transition_params.get("n_jobs", 1), + ), + HmmSubModelConfig( + name="stride", + role="stride", + n_states=stride_params["n_states"], + n_gmm_components=stride_params["n_gmm_components"], + architecture=stride_params["architecture"], + algo_train=stride_params["algo_train"], + stop_threshold=stride_params["stop_threshold"], + max_iterations=stride_params["max_iterations"], + verbose=stride_params.get("verbose", True), + n_jobs=stride_params.get("n_jobs", 1), + ), + ) + ), + feature_transform=params.pop("feature_transform", RothHmmFeatureTransformer()), + algo_predict=params.pop("algo_predict", "viterbi"), + algo_train=params.pop("algo_train", "baum-welch"), + stop_threshold=params.pop("stop_threshold", 1e-9), + max_iterations=params.pop("max_iterations", 1), + initialization=params.pop("initialization", "labels"), + verbose=params.pop("verbose", True), + n_jobs=params.pop("n_jobs", 1), + name=params.pop("name", "segmentation_model"), ) legacy_submodels = [] if getattr(transition_model, "model", None) is not None: @@ -472,37 +439,69 @@ def _from_json_dict(cls, json_dict: dict) -> Self: input_data = {k: params[k] for k in tpcp.get_param_names(cls) if k in params} return cls(**input_data) + def _to_json_dict(self) -> dict[str, Any]: + return { + "_gaitmap_obj": self.__class__.__name__, + "params": { + "hmm_config": self.hmm_config, + "model": self.model, + }, + } + def __init__( self, - model_config: CompositeHmmConfig = cf(CompositeHmmConfig()), - feature_transform: RothHmmFeatureTransformer = cf(RothHmmFeatureTransformer()), - *, - algo_predict: Literal["viterbi", "map"] = "viterbi", - algo_train: Literal["viterbi", "baum-welch"] = "baum-welch", - stop_threshold: float = 1e-9, - max_iterations: int = 1, - initialization: Literal["labels", "fully-connected"] = "labels", - verbose: bool = True, - n_jobs: int = 1, - name: str = "segmentation_model", + hmm_config: RothHmmConfig = cf(RothHmmConfig()), model: Optional[HMMState] = None, - data_columns: Optional[tuple[str, ...]] = None, backend: BaseHmmBackend = cf(PomegranateHmmBackend()), ) -> None: - self.model_config = model_config - self.feature_transform = feature_transform - self.algo_predict = algo_predict - self.algo_train = algo_train - self.stop_threshold = stop_threshold - self.max_iterations = max_iterations - self.initialization = initialization - self.verbose = verbose - self.n_jobs = n_jobs - self.name = name + self.hmm_config = hmm_config self.model = model - self.data_columns = data_columns self.backend = backend + @property + def model_config(self) -> CompositeHmmConfig: + return self.hmm_config.model_config + + @property + def feature_transform(self) -> RothHmmFeatureTransformer: + return self.hmm_config.feature_transform + + @property + def algo_predict(self) -> Literal["viterbi", "map"]: + return self.hmm_config.algo_predict + + @property + def algo_train(self) -> Literal["viterbi", "baum-welch"]: + return self.hmm_config.algo_train + + @property + def stop_threshold(self) -> float: + return self.hmm_config.stop_threshold + + @property + def max_iterations(self) -> int: + return self.hmm_config.max_iterations + + @property + def initialization(self) -> Literal["labels", "fully-connected"]: + return self.hmm_config.initialization + + @property + def verbose(self) -> bool: + return self.hmm_config.verbose + + @property + def n_jobs(self) -> int: + return self.hmm_config.n_jobs + + @property + def name(self) -> str: + return self.hmm_config.name + + @property + def data_columns(self) -> tuple[str, ...]: + return self.feature_transform.transformed_feature_columns + @property def n_states(self) -> int: """Return the number of states of the final model.""" @@ -726,7 +725,6 @@ def self_optimize_with_info( self.algo_predict, ) - self.data_columns = tuple(data_sequence_feature_space[0].columns) self.model, history = self.backend.finalize_model( trained_models=trained_models, labels_train_sequence=labels_train_sequence, diff --git a/tests/test_stride_segmentation/test_roth_hmm.py b/tests/test_stride_segmentation/test_roth_hmm.py index 1f16d4ec..d6d3d6b9 100644 --- a/tests/test_stride_segmentation/test_roth_hmm.py +++ b/tests/test_stride_segmentation/test_roth_hmm.py @@ -27,9 +27,10 @@ CompositeHmmConfig, HMMState, HmmStrideSegmentation, - PomegranateHmmBackend, HmmSubModelConfig, + PomegranateHmmBackend, PreTrainedRothSegmentationModel, + RothHmmConfig, RothHmmFeatureTransformer, RothSegmentationHmm, SimpleHmm, @@ -71,6 +72,16 @@ def _create_roth_model_config(*, stride_n_states=20, stride_n_gmm_components=6, ) +def _create_roth_hmm_config(*, stride_n_states=20, stride_n_gmm_components=6, transition_n_gmm_components=3): + return RothHmmConfig( + model_config=_create_roth_model_config( + stride_n_states=stride_n_states, + stride_n_gmm_components=stride_n_gmm_components, + transition_n_gmm_components=transition_n_gmm_components, + ) + ) + + def _stride_list_to_region_list(stride_list: pd.DataFrame, region_type: str = "stride") -> pd.DataFrame: region_list = stride_list[["start", "end"]].copy() region_list.insert(0, "roi_id", np.arange(len(region_list))) @@ -412,9 +423,9 @@ def test_self_optimize_with_info_returns_history(self) -> None: [_stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]}))], ) instance = RothSegmentationHmm( - model_config=_create_roth_model_config(stride_n_states=3, stride_n_gmm_components=3) + hmm_config=_create_roth_hmm_config(stride_n_states=3, stride_n_gmm_components=3) ).set_params( - feature_transform__sampling_rate_feature_space_hz=100, + hmm_config__feature_transform__sampling_rate_feature_space_hz=100, ) trained_instance, history = instance.self_optimize_with_info(data, labels, sampling_rate_hz=100) assert instance is trained_instance @@ -423,15 +434,24 @@ def test_self_optimize_with_info_returns_history(self) -> None: assert isinstance(v, History) assert set(history.keys()) == {"stride", "transition", "self"} + def test_serialization_excludes_backend(self) -> None: + instance = RothSegmentationHmm(hmm_config=_create_roth_hmm_config()) + + payload = json.loads(instance.to_json()) + restored = RothSegmentationHmm.from_json(instance.to_json()) + + assert set(payload["params"]) == {"hmm_config", "model"} + assert isinstance(restored.backend, PomegranateHmmBackend) + def test_short_strides_raise_warning(self) -> None: data, labels = ( [pd.DataFrame(np.random.rand(130, 6), columns=BF_COLS)], [_stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70, 110], "end": [30, 70, 100, 114]}))], ) instance = RothSegmentationHmm( - model_config=_create_roth_model_config(stride_n_states=5, stride_n_gmm_components=3) + hmm_config=_create_roth_hmm_config(stride_n_states=5, stride_n_gmm_components=3) ).set_params( - feature_transform__sampling_rate_feature_space_hz=100, + hmm_config__feature_transform__sampling_rate_feature_space_hz=100, ) with pytest.warns(UserWarning) as w: instance.self_optimize(data, labels, sampling_rate_hz=100) @@ -443,8 +463,8 @@ def test_unknown_region_type_raises_error(self) -> None: labels = [ _stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]}), "stair_stride") ] - instance = RothSegmentationHmm(model_config=_create_roth_model_config()).set_params( - feature_transform__sampling_rate_feature_space_hz=100, + instance = RothSegmentationHmm(hmm_config=_create_roth_hmm_config()).set_params( + hmm_config__feature_transform__sampling_rate_feature_space_hz=100, ) with pytest.raises(ValidationError) as exc: @@ -478,8 +498,8 @@ def test_missing_configured_module_data_raises_error(self) -> None: ), ) ) - instance = RothSegmentationHmm(model_config=config).set_params( - feature_transform__sampling_rate_feature_space_hz=100, + instance = RothSegmentationHmm(hmm_config=RothHmmConfig(model_config=config)).set_params( + hmm_config__feature_transform__sampling_rate_feature_space_hz=100, ) with pytest.raises(ValueError) as exc: @@ -498,11 +518,11 @@ def test_short_transitions_raise_warning(self) -> None: ], ) instance = RothSegmentationHmm( - model_config=_create_roth_model_config( + hmm_config=_create_roth_hmm_config( stride_n_states=5, stride_n_gmm_components=3, transition_n_gmm_components=3 ) ).set_params( - feature_transform__sampling_rate_feature_space_hz=100, + hmm_config__feature_transform__sampling_rate_feature_space_hz=100, ) with pytest.warns(UserWarning) as w: instance.self_optimize(data, labels, sampling_rate_hz=100) @@ -525,11 +545,11 @@ def test_strange_inputs_trigger_nan_error(self) -> None: ) instance = RothSegmentationHmm( - model_config=_create_roth_model_config( + hmm_config=_create_roth_hmm_config( stride_n_states=5, stride_n_gmm_components=3, transition_n_gmm_components=3 ) ).set_params( - feature_transform__sampling_rate_feature_space_hz=100, + hmm_config__feature_transform__sampling_rate_feature_space_hz=100, ) with pytest.warns(UserWarning) as w, pytest.raises(ValueError) as e: @@ -549,18 +569,18 @@ def test_training_updates_final_model(self) -> None: ], ) instance = RothSegmentationHmm( - model_config=_create_roth_model_config( + hmm_config=_create_roth_hmm_config( stride_n_states=5, stride_n_gmm_components=3, transition_n_gmm_components=3 ) ).set_params( - feature_transform__sampling_rate_feature_space_hz=100, + hmm_config__feature_transform__sampling_rate_feature_space_hz=100, ) - hash_model_config = custom_hash(instance.model_config) + hash_model_config = custom_hash(instance.hmm_config) hash_model = custom_hash(instance.model) instance.self_optimize(data, labels, sampling_rate_hz=100) - assert hash_model_config == custom_hash(instance.model_config) + assert hash_model_config == custom_hash(instance.hmm_config) assert hash_model != custom_hash(instance.model) def test_pretrained_model_is_migrated_to_hmm_state(self) -> None: @@ -597,9 +617,9 @@ def test_trained_model_roundtrip_matches_original_hidden_states(self) -> None: _stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]})) ] instance = RothSegmentationHmm( - model_config=_create_roth_model_config(stride_n_states=3, stride_n_gmm_components=3) + hmm_config=_create_roth_hmm_config(stride_n_states=3, stride_n_gmm_components=3) ).set_params( - feature_transform__sampling_rate_feature_space_hz=100, + hmm_config__feature_transform__sampling_rate_feature_space_hz=100, ) captured_model = {} original_converter = backend_module.pomegranate_model_to_hmm_state From db241830fd460df8e08aada0a9dd9c03d49611f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Mon, 9 Mar 2026 14:45:48 +0100 Subject: [PATCH 08/28] Drop legacy clone mixin from Roth HMM --- .../hmm/_segmentation_model.py | 3 +-- .../stride_segmentation/hmm/_utils.py | 22 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py index 0726c394..9754839b 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py @@ -31,7 +31,6 @@ from gaitmap_mad.stride_segmentation.hmm._utils import ( ShortenedHMMPrint, _DataToShortError, - _HackyClonableHMMFix, convert_region_list_to_transition_list, get_train_data_sequences_regions, get_train_data_sequences_transitions, @@ -270,7 +269,7 @@ def self_optimize_with_info( raise NotImplementedError -class RothSegmentationHmm(BaseSegmentationHmm, _HackyClonableHMMFix, ShortenedHMMPrint): +class RothSegmentationHmm(BaseSegmentationHmm, ShortenedHMMPrint): """A hierarchical HMM model for stride segmentation proposed by Roth et al. [1]_. This model uses individually trained HMM submodules that are combined into one final segmentation HMM. diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py index e15c89bc..3916e582 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py @@ -119,6 +119,15 @@ def _clone_model(orig_model: pg.HiddenMarkovModel, assert_correct: bool = True) return model +def _is_serialized_hmm_state(value: Any) -> bool: + return ( + hasattr(value, "compiled") + and hasattr(value, "trained_with") + and callable(getattr(value, "to_json", None)) + and callable(getattr(type(value), "from_json", None)) + ) + + class _HackyClonableHMMFix(BaseTpcpObject): """A hacky implementation to ensure that HMM parameters are actually cloned, when cloning the algorithm. @@ -131,15 +140,6 @@ class _HackyClonableHMMFix(BaseTpcpObject): """ - @staticmethod - def _is_serialized_hmm_state(value: Any) -> bool: - return ( - hasattr(value, "compiled") - and hasattr(value, "trained_with") - and callable(getattr(value, "to_json", None)) - and callable(getattr(type(value), "from_json", None)) - ) - @classmethod def __clone_param__(cls, param_name: str, value: Any) -> Any: """Overwrite cloning for HMM models. @@ -152,7 +152,7 @@ def __clone_param__(cls, param_name: str, value: Any) -> Any: """ if isinstance(value, pg.HiddenMarkovModel): return _clone_model(value) - if cls._is_serialized_hmm_state(value): + if _is_serialized_hmm_state(value): return type(value).from_json(value.to_json()) return super().__clone_param__(param_name, value) @@ -163,7 +163,7 @@ class ShortenedHMMPrint(BaseTpcpObject): def __repr_parameter__(self, name: str, value: Any) -> str: """Representation with specific care for HMM models.""" if name == "model": - if self._is_serialized_hmm_state(value): + if _is_serialized_hmm_state(value): n_states = len(value.compiled.state_names) if getattr(value, "compiled", None) is not None else "?" backend = getattr(getattr(value, "trained_with", None), "backend_id", "?") return f"{name}=HMMState[backend={backend}, states={n_states}](...)" From db3985adb6f70852a75b901db391f155a5b9584c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Mon, 9 Mar 2026 14:54:26 +0100 Subject: [PATCH 09/28] Add SciPy inference backend for HMM states --- gaitmap/stride_segmentation/hmm.py | 3 + .../stride_segmentation/hmm/__init__.py | 3 +- .../stride_segmentation/hmm/_backend.py | 152 ++++++++++++++++++ .../test_stride_segmentation/test_roth_hmm.py | 51 ++++++ 4 files changed, 208 insertions(+), 1 deletion(-) diff --git a/gaitmap/stride_segmentation/hmm.py b/gaitmap/stride_segmentation/hmm.py index 36bb1c05..d6a494d7 100644 --- a/gaitmap/stride_segmentation/hmm.py +++ b/gaitmap/stride_segmentation/hmm.py @@ -28,6 +28,7 @@ "SimpleHmm", "RothHmmConfig", "RothSegmentationHmm", + "ScipyHmmInferenceBackend", "PreTrainedRothSegmentationModel", "BaseSegmentationHmm", } @@ -54,6 +55,7 @@ RothHmmConfig, RothHmmFeatureTransformer, RothSegmentationHmm, + ScipyHmmInferenceBackend, SimpleHmm, ) @@ -78,5 +80,6 @@ "RothHmmConfig", "RothHmmFeatureTransformer", "RothSegmentationHmm", + "ScipyHmmInferenceBackend", "SimpleHmm", ] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py index b05222cb..dcef26d8 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py @@ -3,7 +3,7 @@ import multiprocessing import warnings -from gaitmap_mad.stride_segmentation.hmm._backend import BaseHmmBackend, PomegranateHmmBackend +from gaitmap_mad.stride_segmentation.hmm._backend import BaseHmmBackend, PomegranateHmmBackend, ScipyHmmInferenceBackend from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig, RothHmmConfig from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import ( BaseHmmFeatureTransformer, @@ -55,5 +55,6 @@ "RothHmmConfig", "RothHmmFeatureTransformer", "RothSegmentationHmm", + "ScipyHmmInferenceBackend", "SimpleHmm", ] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py index 9e0624c8..24eb4eed 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py @@ -9,6 +9,8 @@ import pandas as pd import pomegranate as pg from pomegranate.hmm import History +from scipy.special import logsumexp +from scipy.stats import multivariate_normal from gaitmap.base import _BaseSerializable from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig @@ -16,6 +18,8 @@ from gaitmap_mad.stride_segmentation.hmm._state import ( BackendInfo, CrossModuleTransition, + GaussianEmissionState, + GaussianMixtureEmissionState, HMMState, HmmSubModelState, hmm_state_to_pomegranate_model, @@ -24,6 +28,7 @@ ) from gaitmap_mad.stride_segmentation.hmm._utils import ( _clone_model, + _DataToShortError, add_transition, check_history_for_training_failure, create_transition_matrix_fully_connected, @@ -278,3 +283,150 @@ def _create_combined_model( model.bake() model.freeze_distributions() return _clone_model(model, assert_correct=False) + + +def _prepare_predict_data(data: pd.DataFrame, expected_columns: tuple[str, ...], n_states: int) -> np.ndarray: + try: + data = data[list(expected_columns)] + except KeyError as e: + raise ValueError( + "The provided feature data is expected to have the following columns:\n\n" + f"{expected_columns}\n\n" + "But it only has the following columns:\n\n" + f"{data.columns}" + ) from e + + if len(data) < n_states: + raise _DataToShortError( + "The provided feature data is expected to have at least as many samples as the number of states " + f"of the model ({n_states}). " + f"But it only has {len(data)} samples." + ) + return np.ascontiguousarray(data.to_numpy()) + + +def _log_emission_probabilities(model: HMMState, observations: np.ndarray) -> np.ndarray: + log_emissions = np.empty((len(observations), len(model.compiled.emissions)), dtype=float) + for state_idx, emission in enumerate(model.compiled.emissions): + if isinstance(emission, GaussianEmissionState): + log_emissions[:, state_idx] = multivariate_normal.logpdf( + observations, + mean=emission.mean, + cov=emission.covariance, + allow_singular=True, + ) + continue + if isinstance(emission, GaussianMixtureEmissionState): + component_log_probs = np.column_stack([ + multivariate_normal.logpdf( + observations, + mean=component.mean, + cov=component.covariance, + allow_singular=True, + ) + for component in emission.components + ]) + with np.errstate(divide="ignore"): + log_weights = np.log(np.asarray(emission.weights, dtype=float)) + log_emissions[:, state_idx] = logsumexp(component_log_probs + log_weights, axis=1) + continue + raise TypeError(f"Unsupported serialized emission state `{type(emission).__name__}`.") + return log_emissions + + +def _viterbi_decode(model: HMMState, log_emissions: np.ndarray) -> np.ndarray: + with np.errstate(divide="ignore"): + transition_log_probs = np.log(np.asarray(model.compiled.graph.transition_probs, dtype=float)) + start_log_probs = np.log(np.asarray(model.compiled.graph.start_probs, dtype=float)) + + n_samples, n_states = log_emissions.shape + dp = np.full((n_samples, n_states), -np.inf, dtype=float) + pointers = np.zeros((n_samples, n_states), dtype=int) + + dp[0] = start_log_probs + log_emissions[0] + for sample_idx in range(1, n_samples): + scores = dp[sample_idx - 1][:, None] + transition_log_probs + pointers[sample_idx] = np.argmax(scores, axis=0) + dp[sample_idx] = scores[pointers[sample_idx], np.arange(n_states)] + log_emissions[sample_idx] + + # Match the behavior of `pomegranate 0.14`'s `model.predict(..., algorithm="viterbi")`, + # which returns a path that later gets trimmed to `path[1:-1]` in our compatibility wrapper. + last_state = int(np.argmax(dp[-1])) + path = np.zeros(n_samples, dtype=int) + path[-1] = last_state + for sample_idx in range(n_samples - 1, 0, -1): + path[sample_idx - 1] = pointers[sample_idx, path[sample_idx]] + return path[:-1] + + +def _map_decode(model: HMMState, log_emissions: np.ndarray) -> np.ndarray: + with np.errstate(divide="ignore"): + transition_log_probs = np.log(np.asarray(model.compiled.graph.transition_probs, dtype=float)) + start_log_probs = np.log(np.asarray(model.compiled.graph.start_probs, dtype=float)) + end_log_probs = np.log(np.asarray(model.compiled.graph.end_probs, dtype=float)) + + n_samples, n_states = log_emissions.shape + forward = np.full((n_samples, n_states), -np.inf, dtype=float) + backward = np.full((n_samples, n_states), -np.inf, dtype=float) + + forward[0] = start_log_probs + log_emissions[0] + for sample_idx in range(1, n_samples): + forward[sample_idx] = log_emissions[sample_idx] + logsumexp( + forward[sample_idx - 1][:, None] + transition_log_probs, + axis=0, + ) + + backward[-1] = end_log_probs + for sample_idx in range(n_samples - 2, -1, -1): + backward[sample_idx] = logsumexp( + transition_log_probs + log_emissions[sample_idx + 1][None, :] + backward[sample_idx + 1][None, :], + axis=1, + ) + + posterior = forward + backward + return np.argmax(posterior, axis=1) + + +class ScipyHmmInferenceBackend(BaseHmmBackend): + """SciPy-based inference-only backend operating directly on `HMMState`.""" + + def __init__(self, backend_id: str = "scipy-inference") -> None: + super().__init__(backend_id=backend_id) + + def create_submodel(self, config: HmmSubModelConfig) -> SimpleHmm: + raise NotImplementedError("ScipyHmmInferenceBackend is inference-only and can not create trainable submodels.") + + def finalize_model( + self, + *, + trained_models: dict[str, SimpleHmm], + labels_train_sequence: list[np.ndarray], + data_sequence_feature_space: list[pd.DataFrame], + data_columns: tuple[str, ...], + model_config: CompositeHmmConfig, + module_offsets: dict[str, int], + initialization: Literal["labels", "fully-connected"], + algo_train: Literal["viterbi", "baum-welch"], + stop_threshold: float, + max_iterations: int, + verbose: bool, + n_jobs: int, + name: str, + ) -> tuple[HMMState, History]: + raise NotImplementedError("ScipyHmmInferenceBackend is inference-only and can not finalize/train models.") + + def predict( + self, + model: HMMState, + data: pd.DataFrame, + *, + expected_columns: tuple[str, ...], + algorithm: Literal["viterbi", "map"], + verbose: bool, + ) -> np.ndarray: + del verbose + observations = _prepare_predict_data(data, expected_columns, len(model.compiled.state_names)) + log_emissions = _log_emission_probabilities(model, observations) + if algorithm == "viterbi": + return _viterbi_decode(model, log_emissions) + return _map_decode(model, log_emissions) diff --git a/tests/test_stride_segmentation/test_roth_hmm.py b/tests/test_stride_segmentation/test_roth_hmm.py index d6d3d6b9..8dcfa429 100644 --- a/tests/test_stride_segmentation/test_roth_hmm.py +++ b/tests/test_stride_segmentation/test_roth_hmm.py @@ -33,6 +33,7 @@ RothHmmConfig, RothHmmFeatureTransformer, RothSegmentationHmm, + ScipyHmmInferenceBackend, SimpleHmm, ) from gaitmap_mad.stride_segmentation.hmm import _backend as backend_module @@ -611,6 +612,20 @@ def test_pretrained_model_roundtrip_matches_legacy_hidden_states(self, healthy_e assert_array_equal(raw_sequence, roundtrip_sequence) + def test_pretrained_model_scipy_backend_matches_pomegranate_hidden_states(self, healthy_example_imu_data) -> None: + model = PreTrainedRothSegmentationModel() + scipy_model = model.clone().set_params(backend=ScipyHmmInferenceBackend()) + data = convert_left_foot_to_fbf(healthy_example_imu_data["left_sensor"]) + + pomegranate_result = model.predict(data, sampling_rate_hz=100) + scipy_result = scipy_model.predict(data, sampling_rate_hz=100) + + assert_array_equal(pomegranate_result.hidden_state_sequence_, scipy_result.hidden_state_sequence_) + assert_array_equal( + pomegranate_result.hidden_state_sequence_feature_space_, + scipy_result.hidden_state_sequence_feature_space_, + ) + def test_trained_model_roundtrip_matches_original_hidden_states(self) -> None: data_sequence = [pd.DataFrame(np.random.rand(120, 6), columns=BF_COLS)] region_list_sequence = [ @@ -652,6 +667,28 @@ def _capture_and_convert(model, *args, **kwargs): assert instance.model.trained_with.backend_id == "pomegranate-legacy" assert instance.model.trained_with.backend_version is not None + def test_trained_model_scipy_backend_matches_pomegranate_hidden_states(self) -> None: + data_sequence = [pd.DataFrame(np.random.rand(120, 6), columns=BF_COLS)] + region_list_sequence = [ + _stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]})) + ] + instance = RothSegmentationHmm( + hmm_config=_create_roth_hmm_config(stride_n_states=3, stride_n_gmm_components=3) + ).set_params( + hmm_config__feature_transform__sampling_rate_feature_space_hz=100, + ) + instance.self_optimize(data_sequence, region_list_sequence, sampling_rate_hz=100) + + scipy_instance = instance.clone().set_params(backend=ScipyHmmInferenceBackend()) + pomegranate_result = instance.predict(data_sequence[0], sampling_rate_hz=100) + scipy_result = scipy_instance.predict(data_sequence[0], sampling_rate_hz=100) + + assert_array_equal(pomegranate_result.hidden_state_sequence_, scipy_result.hidden_state_sequence_) + assert_array_equal( + pomegranate_result.hidden_state_sequence_feature_space_, + scipy_result.hidden_state_sequence_feature_space_, + ) + class TestHmmStrideSegmentation: def test_segment_with_single_dataset(self, healthy_example_imu_data) -> None: @@ -669,6 +706,20 @@ def test_segment_with_single_dataset(self, healthy_example_imu_data) -> None: assert isinstance(result.hidden_state_sequence_, np.ndarray) assert result.hidden_state_sequence_ is result.result_model_.hidden_state_sequence_ + def test_pretrained_scipy_backend_matches_pomegranate_segmentation(self, healthy_example_imu_data) -> None: + data = convert_left_foot_to_fbf(healthy_example_imu_data["left_sensor"]) + pomegranate_result = HmmStrideSegmentation(model=PreTrainedRothSegmentationModel()).segment(data, 204.8) + scipy_result = HmmStrideSegmentation( + model=PreTrainedRothSegmentationModel().set_params(backend=ScipyHmmInferenceBackend()) + ).segment(data, 204.8) + + assert_array_equal(pomegranate_result.hidden_state_sequence_, scipy_result.hidden_state_sequence_) + assert_array_equal(pomegranate_result.matches_start_end_, scipy_result.matches_start_end_) + assert_array_equal( + pomegranate_result.matches_start_end_original_, + scipy_result.matches_start_end_original_, + ) + def test_segment_with_multi_dataset(self, healthy_example_imu_data) -> None: data = convert_to_fbf(healthy_example_imu_data, left_like="left_", right_like="right_") model = PreTrainedRothSegmentationModel() From a51d326df2b74fb848cdccb92c66faf62bd1c12c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Mon, 9 Mar 2026 20:37:04 +0100 Subject: [PATCH 10/28] Fix Roth HMM example and migration checks --- .../roth_hmm_stride_segmentation.py | 9 ++++++--- .../src/gaitmap_mad/stride_segmentation/hmm/_state.py | 6 +++++- tests/test_stride_segmentation/test_roth_hmm.py | 10 ++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/examples/stride_segmentation/roth_hmm_stride_segmentation.py b/examples/stride_segmentation/roth_hmm_stride_segmentation.py index 5221adea..dafa629c 100644 --- a/examples/stride_segmentation/roth_hmm_stride_segmentation.py +++ b/examples/stride_segmentation/roth_hmm_stride_segmentation.py @@ -55,10 +55,13 @@ roth_hmm_model = PreTrainedRothSegmentationModel() -print(f"Number of states, stride-model: {roth_hmm_model.stride_model.n_states:d}") -print(f"Number of states, transition-model: {roth_hmm_model.transition_model.n_states:d}") +stride_model = roth_hmm_model.model_config.get_module("stride") +transition_model = roth_hmm_model.model_config.transition_model + +print(f"Number of states, stride-model: {stride_model.n_states:d}") +print(f"Number of states, transition-model: {transition_model.n_states:d}") np.set_printoptions(precision=3, linewidth=180, suppress=True) -print(f"Transition matrix:\n{roth_hmm_model.model.dense_transition_matrix()[0:-2, 0:-2]}") +print(f"Transition matrix:\n{roth_hmm_model.model.compiled.graph.transition_probs}") # %% # Predicting hidden states / Stride borders diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_state.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_state.py index 47a7b391..4ba2a1ab 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_state.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_state.py @@ -227,7 +227,11 @@ def _state_to_distribution(state: EmissionState) -> pg.Distribution: def pomegranate_model_to_flat_hmm_state(model: pgHMM) -> FlatHmmState: - """Convert a pomegranate HMM into a serializable flat state.""" + """Convert a pomegranate HMM into a serializable flat state. + + The canonical state only stores emitting states. `pomegranate`'s silent + start/end nodes are folded into explicit `start_probs`/`end_probs`. + """ dense_transition_matrix = model.dense_transition_matrix() graph_state = HmmGraphState( transition_probs=np.asarray(dense_transition_matrix[:-2, :-2], dtype=float), diff --git a/tests/test_stride_segmentation/test_roth_hmm.py b/tests/test_stride_segmentation/test_roth_hmm.py index 8dcfa429..d1268b02 100644 --- a/tests/test_stride_segmentation/test_roth_hmm.py +++ b/tests/test_stride_segmentation/test_roth_hmm.py @@ -594,6 +594,16 @@ def test_pretrained_model_is_migrated_to_hmm_state(self) -> None: assert len(model.model.submodels) == 2 assert isinstance(model.backend, PomegranateHmmBackend) + def test_pretrained_model_migration_removes_silent_backend_states(self) -> None: + model = PreTrainedRothSegmentationModel() + compiled = model.model.compiled + + assert compiled.graph.transition_probs.shape == (len(compiled.state_names), len(compiled.state_names)) + assert compiled.graph.start_probs.shape == (len(compiled.state_names),) + assert compiled.graph.end_probs.shape == (len(compiled.state_names),) + assert len(compiled.emissions) == len(compiled.state_names) + assert all(name not in {"start", "end"} for name in compiled.state_names) + def test_pretrained_model_roundtrip_matches_legacy_hidden_states(self, healthy_example_imu_data) -> None: raw_model = _load_raw_pretrained_pomegranate_model() model = PreTrainedRothSegmentationModel() From 25909f36cee6fa58492c2b839ce9cbe1826026e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 08:48:07 +0100 Subject: [PATCH 11/28] Fix legacy fused HMM end probabilities --- CHANGELOG.md | 9 + .../hmm/_backend_legacy.py | 200 ++++++++++++++++++ .../hmm/_hmm_feature_transform.py | 2 +- .../stride_segmentation/hmm/_utils.py | 28 ++- 4 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_legacy.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e665f5a1..ddb8761d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ For more information see the [Github Releases Page](https://github.com/mad-lab-fau/gaitmap/releases) of this project. +## [Unreleased] + +### Scientific Changes + +- Fixed the legacy `pomegranate 0.14` fused HMM path to preserve sequence-end semantics when composing the final model. + The fused model now estimates global end probabilities from labeled sequence endings and normalizes them together + with outgoing transitions instead of dropping terminal probabilities during composition. This can slightly shift + decoded stride boundaries near sequence tails. (Issue: https://github.com/mad-lab-fau/gaitmap/issues/80) + ## [2.6.0] - 2026-03-05 ### Scientific Changes diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_legacy.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_legacy.py new file mode 100644 index 00000000..6505e937 --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_legacy.py @@ -0,0 +1,200 @@ +"""Legacy pomegranate backend.""" + +from __future__ import annotations + +import copy +from importlib import import_module +from typing import Any, Literal + +import numpy as np +import pandas as pd + +from gaitmap_mad.stride_segmentation.hmm._backend_base import BaseHmmBackend +from gaitmap_mad.stride_segmentation.hmm._backend_common import ( + extract_cross_module_transitions, + normalize_transition_and_end_probs, +) +from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig +from gaitmap_mad.stride_segmentation.hmm._pomegranate import create_state_names +from gaitmap_mad.stride_segmentation.hmm._state import ( + BackendInfo, + HMMState, + HmmSubModelState, + hmm_state_to_pomegranate_model, + pomegranate_model_to_flat_hmm_state, + pomegranate_model_to_hmm_state, +) +from gaitmap_mad.stride_segmentation.hmm._utils import ( + _clone_model, + check_history_for_training_failure, + create_transition_matrix_fully_connected, + estimate_sequence_boundary_probs, + extract_transitions_starts_stops_from_hidden_state_sequence, + fix_model_names, + get_model_distributions, + labels_to_strings, + predict, +) + + +def _create_simple_hmm_from_config(config: HmmSubModelConfig) -> Any: + simple_hmm = import_module("gaitmap_mad.stride_segmentation.hmm._simple_model").SimpleHmm + return simple_hmm( + n_states=config.n_states, + n_gmm_components=config.n_gmm_components, + architecture=config.architecture, + algo_train=config.algo_train, + stop_threshold=config.stop_threshold, + max_iterations=config.max_iterations, + verbose=config.verbose, + n_jobs=config.n_jobs, + name=config.name, + ) + + +class PomegranateLegacyHmmBackend(BaseHmmBackend): + """`pomegranate 0.x` backend for HMM training and inference.""" + + def __init__(self, backend_id: str = "pomegranate-legacy") -> None: + super().__init__(backend_id=backend_id) + + def create_submodel(self, config: HmmSubModelConfig) -> Any: + return _create_simple_hmm_from_config(config) + + def predict( + self, + model: HMMState, + data: pd.DataFrame, + *, + expected_columns: tuple[str, ...], + algorithm: Literal["viterbi", "map"], + verbose: bool, + ) -> np.ndarray: + runtime_model = hmm_state_to_pomegranate_model(model, verbose=verbose) + return predict(runtime_model, data, expected_columns=expected_columns, algorithm=algorithm) + + def finalize_model( + self, + *, + trained_models: dict[str, Any], + labels_train_sequence: list[np.ndarray], + data_sequence_feature_space: list[pd.DataFrame], + data_columns: tuple[str, ...], + model_config: CompositeHmmConfig, + module_offsets: dict[str, int], + initialization: Literal["labels", "fully-connected"], + algo_train: Literal["viterbi", "baum-welch"], + stop_threshold: float, + max_iterations: int, + verbose: bool, + n_jobs: int, + name: str, + ) -> tuple[HMMState, Any]: + + distributions = [] + for module in model_config.modules: + distributions.extend(get_model_distributions(trained_models[module.name].model)) + + model = self._create_combined_model( + trained_models=trained_models, + labels_train_sequence=labels_train_sequence, + distributions=distributions, + model_config=model_config, + module_offsets=module_offsets, + initialization=initialization, + verbose=verbose, + ) + + labels_train_sequence_str = labels_to_strings(labels_train_sequence) + data_train_sequence = [ + np.ascontiguousarray(feature_data[list(data_columns)].to_numpy().copy()) + for feature_data in data_sequence_feature_space + ] + + _, history = model.fit( + sequences=np.array(data_train_sequence, dtype=object), + labels=np.array(labels_train_sequence_str, dtype=object).copy(), + algorithm=algo_train, + stop_threshold=stop_threshold, + max_iterations=max_iterations, + return_history=True, + verbose=verbose, + n_jobs=n_jobs, + multiple_check_input=False, + ) + check_history_for_training_failure(history) + model.name = name + + submodel_states = tuple( + HmmSubModelState( + name=module.name, + role=module.role, + model=pomegranate_model_to_flat_hmm_state(trained_models[module.name].model), + ) + for module in model_config.modules + ) + model_state = pomegranate_model_to_hmm_state( + model, + submodels=submodel_states, + backend_info=BackendInfo(backend_id=self.backend_id), + ) + model_state.cross_module_transitions = extract_cross_module_transitions(model_state, module_offsets) + return model_state, history + + def _create_combined_model( + self, + *, + trained_models: dict[str, Any], + labels_train_sequence: list[np.ndarray], + distributions: list[Any], + model_config: CompositeHmmConfig, + module_offsets: dict[str, int], + initialization: Literal["labels", "fully-connected"], + verbose: bool, + ) -> Any: + pg = import_module("pomegranate") + + n_states = sum(module.n_states for module in model_config.modules) + if initialization == "fully-connected": + trans_mat, start_probs, end_probs = create_transition_matrix_fully_connected(n_states) + trans_mat, end_probs = normalize_transition_and_end_probs(trans_mat, end_probs) + model = pg.HiddenMarkovModel.from_matrix( + transition_probabilities=copy.deepcopy(trans_mat), + distributions=copy.deepcopy(distributions), + starts=start_probs, + ends=end_probs, + state_names=list(create_state_names(n_states)), + verbose=verbose, + ) + else: + trans_mat = np.zeros((n_states, n_states)) + for module in model_config.modules: + module_transition_matrix = trained_models[module.name].model.dense_transition_matrix()[:-2, :-2] + offset = module_offsets[module.name] + trans_mat[ + offset : offset + module.n_states, + offset : offset + module.n_states, + ] = module_transition_matrix + + transitions, _, _ = extract_transitions_starts_stops_from_hidden_state_sequence(labels_train_sequence) + start_probs, end_probs = estimate_sequence_boundary_probs(labels_train_sequence, n_states) + for from_state, to_state in transitions: + trans_mat[int(from_state[1:]), int(to_state[1:])] = max( + trans_mat[int(from_state[1:]), int(to_state[1:])], + 0.1, + ) + trans_mat, end_probs = normalize_transition_and_end_probs(trans_mat, end_probs) + + model = pg.HiddenMarkovModel.from_matrix( + transition_probabilities=copy.deepcopy(trans_mat), + distributions=copy.deepcopy(distributions), + starts=start_probs, + ends=end_probs, + state_names=list(create_state_names(n_states)), + verbose=verbose, + ) + + model = fix_model_names(model) + model.bake() + model.freeze_distributions() + return _clone_model(model, assert_correct=False) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_hmm_feature_transform.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_hmm_feature_transform.py index 65fef7ce..3ce692db 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_hmm_feature_transform.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_hmm_feature_transform.py @@ -294,7 +294,7 @@ def inverse_transform_state_sequence(self, state_sequence: np.ndarray, *, data: The state sequence in the original sampling rate """ - downsampled_x = np.arange(0, len(data) - 1, len(data) / len(state_sequence)) + downsampled_x = np.linspace(0, len(data) - 1, num=len(state_sequence)) new_x = np.arange(0, len(data)) interpolated = interp1d(downsampled_x, state_sequence, kind="nearest", fill_value="extrapolate")(new_x) return interpolated.astype(int) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py index 3916e582..ab0fa8c3 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py @@ -319,7 +319,10 @@ def fix_model_names(model): """ for state in model.states: if state.name[0] == "s": - state_number = int(state.name[1:]) + try: + state_number = int(state.name[1:]) + except ValueError: + continue # replace state numbers >= 10 by characters form the ascii-table :) if state_number >= 10: state.name = "s" + chr(87 + state_number) @@ -448,6 +451,29 @@ def extract_transitions_starts_stops_from_hidden_state_sequence( return transitions, starts, ends +def estimate_sequence_boundary_probs( + hidden_state_sequence: list[np.ndarray], n_states: int +) -> tuple[np.ndarray, np.ndarray]: + """Estimate start and end probabilities from sequence boundary counts.""" + if n_states <= 0: + raise ValueError("`n_states` must be positive.") + + start_counts = np.zeros(n_states, dtype=float) + end_counts = np.zeros(n_states, dtype=float) + for labels in hidden_state_sequence: + start_counts[int(labels[0])] += 1.0 + end_counts[int(labels[-1])] += 1.0 + + start_total = start_counts.sum() + end_total = end_counts.sum() + if start_total <= 0 or end_total <= 0: + raise ValueError( + "At least one non-empty hidden-state sequence is required to estimate boundary probabilities." + ) + + return start_counts / start_total, end_counts / end_total + + def create_equidistant_label_sequence(n_labels: int, n_states: int) -> np.ndarray: """Create equidistant label sequence. From c8a49bc602695e60934e07d56926f0deeba180ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 09:24:33 +0100 Subject: [PATCH 12/28] Repair pretrained Roth HMM terminal probabilities --- CHANGELOG.md | 4 + .../fallriskpd_at_lab_model.json | 16741 +++++++++------- 2 files changed, 9451 insertions(+), 7294 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb8761d..1c48b8c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ project. ### Scientific Changes +- Repaired the shipped pretrained Roth HMM artifact so its compiled global `end_probs` now represent the intended + sequence semantics: uniform stopping probability across the five transition states and zero stopping probability + within stride states. This removes the invalid all-zero terminal distribution from the serialized model and aligns + backend-agnostic inference with the intended model semantics. - Fixed the legacy `pomegranate 0.14` fused HMM path to preserve sequence-end semantics when composing the final model. The fused model now estimates global end probabilities from labeled sequence endings and normalizes them together with outgoing transitions instead of dropping terminal probabilities during composition. This can slightly shift diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_pre_trained_models/fallriskpd_at_lab_model.json b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_pre_trained_models/fallriskpd_at_lab_model.json index e3be5a63..50e5fd8d 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_pre_trained_models/fallriskpd_at_lab_model.json +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_pre_trained_models/fallriskpd_at_lab_model.json @@ -1,7556 +1,9709 @@ { "_gaitmap_obj": "RothSegmentationHmm", "params": { - "algo_predict": "viterbi", - "algo_train": "baum-welch", - "data_columns": ["raw__gyr_ml", "gradient__gradient__gyr_ml"], - "feature_transform": { - "_gaitmap_obj": "RothHmmFeatureTransformer", + "hmm_config": { + "_gaitmap_obj": "RothHmmConfig", "params": { - "axes": [ - "gyr_ml" - ], - "features": [ - "raw", - "gradient" - ], - "low_pass_filter": { - "_gaitmap_obj": "ButterworthFilter", + "algo_predict": "viterbi", + "algo_train": "baum-welch", + "feature_transform": { + "_gaitmap_obj": "RothHmmFeatureTransformer", + "params": { + "axes": [ + "gyr_ml" + ], + "features": [ + "raw", + "gradient" + ], + "low_pass_filter": { + "_gaitmap_obj": "ButterworthFilter", + "params": { + "cutoff_freq_hz": 10, + "filter_type": "lowpass", + "order": 4 + } + }, + "sampling_rate_feature_space_hz": 51.2, + "standardization": true, + "window_size_s": 0.2 + } + }, + "initialization": "labels", + "max_iterations": 1, + "model_config": { + "_gaitmap_obj": "CompositeHmmConfig", "params": { - "cutoff_freq_hz": 10, - "order": 4, - "type": "lowpass" + "modules": [ + { + "_gaitmap_obj": "HmmSubModelConfig", + "params": { + "algo_train": "baum-welch", + "architecture": "left-right-loose", + "max_iterations": 10, + "n_gmm_components": 3, + "n_jobs": 1, + "n_states": 5, + "name": "transition", + "role": "transition", + "stop_threshold": 1e-09, + "verbose": true + } + }, + { + "_gaitmap_obj": "HmmSubModelConfig", + "params": { + "algo_train": "baum-welch", + "architecture": "left-right-strict", + "max_iterations": 10, + "n_gmm_components": 6, + "n_jobs": 1, + "n_states": 20, + "name": "stride", + "role": "stride", + "stop_threshold": 1e-09, + "verbose": true + } + } + ], + "transition_model_name": "transition" } }, - "sampling_frequency_feature_space_hz": 51.2, - "standardization": true, - "window_size_s": 0.2 + "n_jobs": 1, + "name": "segmentation_model", + "stop_threshold": 1e-09, + "verbose": true } }, - "initialization": "labels", - "max_iterations": 1, "model": { - "_obj_type": "HiddenMarkovModel", - "hmm": { - "class": "HiddenMarkovModel", - "name": "segmentation_model", - "start": { - "class": "State", - "distribution": null, - "name": "None-start", - "weight": 1.0 - }, - "end": { - "class": "State", - "distribution": null, - "name": "None-end", - "weight": 1.0 - }, - "states": [ - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.005997599719241142, - -0.007834069598685245 - ], - [ - [ - 0.00010174624438512144, - 1.5548565113115012e-05 - ], - [ - 1.5548565113115012e-05, - 6.943450598374113e-06 - ] - ] + "_gaitmap_obj": "HMMState", + "params": { + "compiled": { + "_gaitmap_obj": "FlatHmmState", + "params": { + "emissions": [ + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.00010174624438512144, + 1.5548565113115012e-05 + ], + [ + 1.5548565113115012e-05, + 6.943450598374113e-06 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.005997599719241142, + -0.007834069598685245 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0203470808271111, + 0.008906126872059989 + ], + [ + 0.008906126872059989, + 0.030380758701502277 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.1402170027695126, + 0.10267748284020042 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.5676506410167194, + 0.04722090407049995 + ], + [ + 0.04722090407049995, + 0.6525839943295144 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.02703357666235144, + 0.057771154949070626 + ] + } + } + } ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.1402170027695126, - 0.10267748284020042 - ], - [ - [ - 0.0203470808271111, - 0.008906126872059989 - ], - [ - 0.008906126872059989, - 0.030380758701502277 - ] + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.030347556963310634, + 0.19558494193403117, + 0.7740675011026582 ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 3.651530143614506e-05, + 1.7957352404200133e-06 + ], + [ + 1.7957352404200133e-06, + 6.8253959087975915e-06 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.005112069321216803, + -0.0003836959192688232 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.00020688479745427268, + 1.4946885646238893e-05 + ], + [ + 1.4946885646238893e-05, + 0.00016590250538747931 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.011017565322037499, + 1.0575560153235358e-05 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0005245625426675409, + -9.046224503575308e-05 + ], + [ + -9.046224503575308e-05, + 0.002039139990806878 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.020254053323335502, + 0.012988598823194142 + ] + } + } + } ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.02703357666235144, - 0.057771154949070626 - ], - [ - [ - 0.5676506410167194, - 0.04722090407049995 - ], - [ - 0.04722090407049995, - 0.6525839943295144 - ] + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.6086414102924766, + 0.39135007979236064, + 8.50991516279216e-06 ] - ], - "frozen": false + } } - ], - "weights": [ - 0.03034755696331064, - 0.1955849419340312, - 0.7740675011026582 - ] - }, - "name": "s0", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.005112069321216803, - -0.0003836959192688232 - ], - [ - [ - 3.651530143614506e-05, - 1.7957352404200133e-06 - ], - [ - 1.7957352404200133e-06, - 6.8253959087975915e-06 - ] - ] + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 2.618236096697655e-05, + -8.041346694983122e-08 + ], + [ + -8.041346694983122e-08, + 3.7606917902623595e-06 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.015358535596446106, + -4.070345261858078e-05 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.00039508085422951543, + -4.079478790447417e-05 + ], + [ + -4.079478790447417e-05, + 0.0008236842831401263 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.02600870323060062, + 0.027054909704841864 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.00027863324927444915, + 0.0002165057509630118 + ], + [ + 0.0002165057509630118, + 0.0005221742732126262 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.009035208880283429, + -0.012292942445827832 + ] + } + } + } ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.011017565322037499, - 1.0575560153235358e-05 - ], - [ - [ - 0.00020688479745427268, - 1.4946885646238893e-05 - ], - [ - 1.4946885646238893e-05, - 0.00016590250538747931 - ] + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.9845357979763119, + 9.399662247969446e-05, + 0.015370205401208429 ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 4.1241714302302386e-05, + 1.4442141290213624e-05 + ], + [ + 1.4442141290213624e-05, + 7.24488624344902e-05 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.010222491305658998, + -0.011752069143448509 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.002567784920812402, + 0.0003314709303588074 + ], + [ + 0.0003314709303588074, + 0.0037115117181637375 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.0013804746618345575, + -0.012200819734238538 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.025091760306063134, + -0.0001494080597895057 + ], + [ + -0.0001494080597895057, + 0.03151005928803442 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.09601306406930554, + -0.039613599968281794 + ] + } + } + } ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.020254053323335502, - 0.012988598823194142 - ], - [ - [ - 0.0005245625426675409, - -9.046224503575308e-05 - ], - [ - -9.046224503575308e-05, - 0.002039139990806878 - ] + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.011568482274080258, + 0.604301396028299, + 0.3841301216976206 ] - ], - "frozen": false + } } - ], - "weights": [ - 0.6086414102924765, - 0.39135007979236064, - 8.509915162792162e-06 - ] - }, - "name": "s1", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.015358535596446106, - -4.070345261858078e-05 - ], - [ - [ - 2.618236096697655e-05, - -8.041346694983122e-08 - ], - [ - -8.041346694983122e-08, - 3.7606917902623595e-06 - ] - ] + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0001255675352906284, + 2.196067402693785e-05 + ], + [ + 2.196067402693785e-05, + 0.00013742773585317318 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.02178742298368039, + -0.0029954952647887776 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.006777372496319222, + 0.009825656102108825 + ], + [ + 0.009825656102108825, + 0.0474293045135804 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.08352157899572946, + -0.30327185867360373 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.04012487033658457, + 0.06263285396730135 + ], + [ + 0.06263285396730135, + 0.18475896916891646 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.07048744497804728, + -0.3226713510902122 + ] + } + } + } ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.02600870323060062, - 0.027054909704841864 - ], - [ - [ - 0.00039508085422951543, - -4.079478790447417e-05 - ], - [ - -4.079478790447417e-05, - 0.0008236842831401263 - ] + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.8971428277389487, + 0.102830269097985, + 2.6903163066219968e-05 ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.2811686502027946, + 0.19529934601289853 + ], + [ + 0.19529934601289853, + 0.36565539992238716 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -2.6671472177437408, + 2.0673173323958016 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.1949695815048765, + 0.008495407199038263 + ], + [ + 0.008495407199038263, + 0.03829829528845152 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.2674616196206828, + 0.4994160419413428 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.18746172523042354, + 0.164600661091996 + ], + [ + 0.164600661091996, + 0.2370348132229869 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -2.4087373140168418, + 1.4546191495823213 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.1666591845749615, + 0.025694838535543593 + ], + [ + 0.025694838535543593, + 0.2308452218156021 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -2.546060501701736, + 0.5617938814946412 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.40483510870772926, + -0.19287804254796195 + ], + [ + -0.19287804254796195, + 0.4182568048646161 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.4386893522632695, + 1.6972076752596261 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 1.3763566933059819, + 0.6693717599308622 + ], + [ + 0.6693717599308622, + 3.445592064213577 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -3.2821708308888096, + 2.6671858464154625 + ] + } + } + } ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.009035208880283429, - -0.012292942445827832 - ], - [ - [ - 0.00027863324927444915, - 0.0002165057509630118 - ], - [ - 0.0002165057509630118, - 0.0005221742732126262 - ] - ] + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.29895586689925907, + 0.03228223692710609, + 0.303803381057093, + 0.12020399150050627, + 0.1506368583662001, + 0.09411766524983546 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.1015205398712131, + 0.06407946446037824 + ], + [ + 0.06407946446037824, + 0.1745918197823862 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.7930667436600324, + 2.77359624178794 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.26358787006526785, + 0.04688619910813957 + ], + [ + 0.04688619910813957, + 0.23530872898913208 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.0625933457962435, + 2.8418908928186415 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 1.735374303252404, + 0.42954280464472855 + ], + [ + 0.42954280464472855, + 0.9521518619411448 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.6412212776209764, + 4.080299035063935 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.19087883694405433, + 0.052885706473014556 + ], + [ + 0.052885706473014556, + 0.044037951163633474 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.1273528088500362, + 2.3131824295318353 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.04753730395355176, + 0.0030231153451804682 + ], + [ + 0.0030231153451804682, + 0.1630505456146342 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.21231291615239337, + 0.966453628808478 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.22363374763308103, + 0.0568083272336948 + ], + [ + 0.0568083272336948, + 0.12851025867554855 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.3925724036753182, + 3.01341422145098 + ] + } + } + } ], - "frozen": false + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.2600130311882213, + 0.35283110709966653, + 0.07784372000853192, + 0.11256751246298377, + 0.07062938835252072, + 0.1261152408880756 + ] + } } - ], - "weights": [ - 0.984535797976312, - 9.399662247969455e-05, - 0.015370205401208427 - ] - }, - "name": "s2", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.010222491305658998, - -0.011752069143448509 - ], - [ - [ - 4.1241714302302386e-05, - 1.4442141290213624e-05 - ], - [ - 1.4442141290213624e-05, - 7.24488624344902e-05 - ] - ] + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.08568985038604215, + 0.04514818594087123 + ], + [ + 0.04514818594087123, + 0.14697435039407214 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.27262440661541, + 2.4962324346441793 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.40081998269138985, + 0.08539365608394457 + ], + [ + 0.08539365608394457, + 0.49915160497359945 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 2.0030472837271676, + 2.436424415879026 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.02455292290176685, + 0.0026353060217543744 + ], + [ + 0.0026353060217543744, + 0.0030384369517548063 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.7575623582373705, + 0.40715990128936763 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0627772074364836, + 0.027048886081947957 + ], + [ + 0.027048886081947957, + 0.056497890681816515 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.752263092713321, + 0.6153454907033118 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.018934248781400084, + 0.0008225467826023848 + ], + [ + 0.0008225467826023848, + 0.04828080771457061 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.2710038945391413, + 1.1363816665328421 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.072137860271559, + 0.008301511357802396 + ], + [ + 0.008301511357802396, + 0.20244102727622662 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.1918084959123356, + 1.757165694805128 + ] + } + } + } ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.0013804746618345575, - -0.012200819734238538 - ], - [ - [ - 0.002567784920812402, - 0.0003314709303588074 - ], - [ - 0.0003314709303588074, - 0.0037115117181637375 - ] - ] + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.1874128386753945, + 0.05905922781413575, + 0.00013180244692742607, + 0.09317384315145891, + 0.14625909619229158, + 0.5139631917197919 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.7157740657354805, + -0.0033605362872233487 + ], + [ + -0.0033605362872233487, + 0.12411280338539885 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 2.6274135819712106, + 0.8940466948126727 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.026120600686184083, + -0.000304412367188788 + ], + [ + -0.000304412367188788, + 0.009898316380517386 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.7919891181981025, + 0.12801766668000156 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.04367195250188803, + 0.03502703922836976 + ], + [ + 0.03502703922836976, + 0.0823795052922783 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.4533614139394757, + 0.6952485209637947 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.03464979548463108, + 0.01480958230690024 + ], + [ + 0.01480958230690024, + 0.03439055213520964 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.7754863182572604, + 0.4837980901136886 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.007876530327825758, + -0.0009003766469068802 + ], + [ + -0.0009003766469068802, + 0.015402042509686707 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.556595631415173, + 0.28168973171370904 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.14637120456846728, + 0.04380872024785654 + ], + [ + 0.04380872024785654, + 0.024315135268371497 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.6435836504587341, + 0.13488359154873725 + ] + } + } + } ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.09601306406930554, - -0.039613599968281794 - ], - [ - [ - 0.025091760306063134, - -0.0001494080597895057 - ], - [ - -0.0001494080597895057, - 0.03151005928803442 - ] - ] + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.05059117833189641, + 0.056413821295499864, + 0.3445381188229942, + 0.3152592500785814, + 0.2279409420134751, + 0.005256689457553029 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.18288591564861567, + -0.01563983547688968 + ], + [ + -0.01563983547688968, + 0.01340968920034488 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.0978232427195225, + -0.19880507306347045 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.009861412477369762, + -0.001605574409422754 + ], + [ + -0.001605574409422754, + 0.02328429968050431 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.585662894700118, + -0.09327176875314531 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.8037290828419877, + -0.13880558760346237 + ], + [ + -0.13880558760346237, + 0.13937669395420002 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 3.220156351950902, + -0.2701336946824086 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.049564436047409684, + 0.027772559158844436 + ], + [ + 0.027772559158844436, + 0.0335403331352617 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.961035154660426, + 0.22845664351539355 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0490882014544778, + 0.015337901174890072 + ], + [ + 0.015337901174890072, + 0.031652684581713414 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.955582777589335, + -0.06772341770708028 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.19762152642034286, + 0.03181128017468199 + ], + [ + 0.03181128017468199, + 0.05054680719283672 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 2.3330379198529037, + 0.020098640935322775 + ] + } + } + } ], - "frozen": false + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.18912509569993793, + 0.19667888523507426, + 0.01938804677755136, + 0.2627898854528598, + 0.2533692766112804, + 0.07864881022329623 + ] + } } - ], - "weights": [ - 0.011568482274080258, - 0.6043013960282991, - 0.3841301216976207 - ] - }, - "name": "s3", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.02178742298368039, - -0.0029954952647887776 - ], - [ - [ - 0.0001255675352906284, - 2.196067402693785e-05 - ], - [ - 2.196067402693785e-05, - 0.00013742773585317318 - ] - ] + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.14046684011661453, + 0.006665339021284342 + ], + [ + 0.006665339021284342, + 0.028269986425225616 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.8351143121194007, + -0.5122638693721864 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.253755626966208, + -0.07545275527056751 + ], + [ + -0.07545275527056751, + 0.06536709260748119 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.4981732066394406, + -0.7660118195597697 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.04390115883703879, + 0.018550354325481903 + ], + [ + 0.018550354325481903, + 0.05918781344903495 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.4870727310188694, + -0.7576810472962574 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0032895563347738826, + -0.0011274229411736593 + ], + [ + -0.0011274229411736593, + 0.004064028822452112 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.5478516646551511, + -0.43853212625144644 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.04639857294609491, + -0.049858780129014554 + ], + [ + -0.049858780129014554, + 0.07786780553576868 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.8285913935278124, + -0.7326833848315257 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.5006816671925979, + -0.20227007543098452 + ], + [ + -0.20227007543098452, + 0.31535710117506477 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 2.6758882845129555, + -1.1938490533506798 + ] + } + } + } ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.08352157899572946, - -0.30327185867360373 - ], - [ - [ - 0.006777372496319222, - 0.009825656102108825 - ], - [ - 0.009825656102108825, - 0.0474293045135804 - ] - ] + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.3175964924895178, + 0.2957417368899987, + 0.290290660051822, + 0.001502920858594204, + 0.056527727855268724, + 0.03834046185479861 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.11392623488450301, + 0.0016209208853717662 + ], + [ + 0.0016209208853717662, + 0.05206638985105943 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.6931587123564085, + -1.4089515331427522 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.41001425585485474, + -0.24827317801275978 + ], + [ + -0.24827317801275978, + 0.2969864605456684 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.4325395296647314, + -2.00125475188884 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.11207160587161284, + -0.035903412273462824 + ], + [ + -0.035903412273462824, + 0.08950711803291501 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.9010756541811548, + -1.6316381324644655 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.16843468537340958, + -0.032265281486372546 + ], + [ + -0.032265281486372546, + 0.10098868594071204 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.3773911707507072, + -1.7815478639979678 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.07381232402838175, + -0.07595253377920648 + ], + [ + -0.07595253377920648, + 0.17047113785871443 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.37740419892813387, + -0.7949121975563709 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.1062464647211127, + 0.05304247528188522 + ], + [ + 0.05304247528188522, + 0.05098708651353123 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.1811267539735342, + -1.3806335752615653 + ] + } + } + } ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.07048744497804728, - -0.3226713510902122 - ], - [ - [ - 0.04012487033658457, - 0.06263285396730135 - ], - [ - 0.06263285396730135, - 0.18475896916891646 - ] - ] + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.14789642405166822, + 0.03182486194421295, + 0.15580254164021448, + 0.1554372162704567, + 0.2792592801869429, + 0.22977967590650475 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.058862337328279085, + -0.02766227065245916 + ], + [ + -0.02766227065245916, + 0.28480563096544564 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.13878271306347267, + -1.5474492576230316 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.03254650338809167, + 0.008127320940059385 + ], + [ + 0.008127320940059385, + 0.045257183881101185 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.2969342862014465, + -0.7278300295113517 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.20362295989732357, + -0.07524949655161875 + ], + [ + -0.07524949655161875, + 0.15674179929419454 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.4588432691504559, + -2.1300692388039946 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0010273772668491572, + 4.7604695764052905e-05 + ], + [ + 4.7604695764052905e-05, + 0.006007980152658993 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.0972098277835148, + -0.22934054837280451 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.024690750530544904, + -0.024275282277841842 + ], + [ + -0.024275282277841842, + 0.07425307474259665 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.5539553691115032, + -1.390437224305981 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.1315486945377525, + -0.04015813429001713 + ], + [ + -0.04015813429001713, + 0.027717186766069064 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.11683982047005993, + -1.7354936026470975 + ] + } + } + } ], - "frozen": false + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.26700471685635363, + 0.12305060819716648, + 0.31015435165902666, + 0.015368776144476341, + 0.16405667202280755, + 0.12036487512016938 + ] + } } - ], - "weights": [ - 0.8971428277389488, - 0.102830269097985, - 2.6903163066219954e-05 - ] - }, - "name": "s4", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -2.6671472177437408, - 2.0673173323958016 - ], - [ - [ - 0.2811686502027946, - 0.19529934601289853 - ], - [ - 0.19529934601289853, - 0.36565539992238716 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.2674616196206828, - 0.4994160419413428 - ], - [ - [ - 0.1949695815048765, - 0.008495407199038263 - ], - [ - 0.008495407199038263, - 0.03829829528845152 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -2.4087373140168418, - 1.4546191495823213 - ], - [ - [ - 0.18746172523042354, - 0.164600661091996 - ], - [ - 0.164600661091996, - 0.2370348132229869 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -2.546060501701736, - 0.5617938814946412 - ], - [ - [ - 0.1666591845749615, - 0.025694838535543593 - ], - [ - 0.025694838535543593, - 0.2308452218156021 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.4386893522632695, - 1.6972076752596261 - ], - [ - [ - 0.40483510870772926, - -0.19287804254796195 - ], - [ - -0.19287804254796195, - 0.4182568048646161 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -3.2821708308888096, - 2.6671858464154625 - ], - [ - [ - 1.3763566933059819, - 0.6693717599308622 - ], - [ - 0.6693717599308622, - 3.445592064213577 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.29895586689925907, - 0.0322822369271061, - 0.303803381057093, - 0.12020399150050629, - 0.15063685836620008, - 0.09411766524983546 - ] - }, - "name": "s5", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.7930667436600324, - 2.77359624178794 - ], - [ - [ - 0.1015205398712131, - 0.06407946446037824 - ], - [ - 0.06407946446037824, - 0.1745918197823862 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.0625933457962435, - 2.8418908928186415 - ], - [ - [ - 0.26358787006526785, - 0.04688619910813957 - ], - [ - 0.04688619910813957, - 0.23530872898913208 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.6412212776209764, - 4.080299035063935 - ], - [ - [ - 1.735374303252404, - 0.42954280464472855 - ], - [ - 0.42954280464472855, - 0.9521518619411448 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.1273528088500362, - 2.3131824295318353 - ], - [ - [ - 0.19087883694405433, - 0.052885706473014556 - ], - [ - 0.052885706473014556, - 0.044037951163633474 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.21231291615239337, - 0.966453628808478 - ], - [ - [ - 0.04753730395355176, - 0.0030231153451804682 - ], - [ - 0.0030231153451804682, - 0.1630505456146342 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.3925724036753182, - 3.01341422145098 - ], - [ - [ - 0.22363374763308103, - 0.0568083272336948 - ], - [ - 0.0568083272336948, - 0.12851025867554855 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.2600130311882213, - 0.35283110709966664, - 0.07784372000853194, - 0.11256751246298381, - 0.07062938835252075, - 0.12611524088807563 - ] - }, - "name": "s6", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.27262440661541, - 2.4962324346441793 - ], - [ - [ - 0.08568985038604215, - 0.04514818594087123 - ], - [ - 0.04514818594087123, - 0.14697435039407214 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 2.0030472837271676, - 2.436424415879026 - ], - [ - [ - 0.40081998269138985, - 0.08539365608394457 - ], - [ - 0.08539365608394457, - 0.49915160497359945 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.7575623582373705, - 0.40715990128936763 - ], - [ - [ - 0.02455292290176685, - 0.0026353060217543744 - ], - [ - 0.0026353060217543744, - 0.0030384369517548063 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.752263092713321, - 0.6153454907033118 - ], - [ - [ - 0.0627772074364836, - 0.027048886081947957 - ], - [ - 0.027048886081947957, - 0.056497890681816515 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.2710038945391413, - 1.1363816665328421 - ], - [ - [ - 0.018934248781400084, - 0.0008225467826023848 - ], - [ - 0.0008225467826023848, - 0.04828080771457061 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.1918084959123356, - 1.757165694805128 - ], - [ - [ - 0.072137860271559, - 0.008301511357802396 - ], - [ - 0.008301511357802396, - 0.20244102727622662 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.18741283867539446, - 0.059059227814135765, - 0.00013180244692742618, - 0.09317384315145892, - 0.14625909619229158, - 0.5139631917197919 - ] - }, - "name": "s7", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 2.6274135819712106, - 0.8940466948126727 - ], - [ - [ - 0.7157740657354805, - -0.0033605362872233487 - ], - [ - -0.0033605362872233487, - 0.12411280338539885 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.7919891181981025, - 0.12801766668000156 - ], - [ - [ - 0.026120600686184083, - -0.000304412367188788 - ], - [ - -0.000304412367188788, - 0.009898316380517386 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.4533614139394757, - 0.6952485209637947 - ], - [ - [ - 0.04367195250188803, - 0.03502703922836976 - ], - [ - 0.03502703922836976, - 0.0823795052922783 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.7754863182572604, - 0.4837980901136886 - ], - [ - [ - 0.03464979548463108, - 0.01480958230690024 - ], - [ - 0.01480958230690024, - 0.03439055213520964 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.556595631415173, - 0.28168973171370904 - ], - [ - [ - 0.007876530327825758, - -0.0009003766469068802 - ], - [ - -0.0009003766469068802, - 0.015402042509686707 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.6435836504587341, - 0.13488359154873725 - ], - [ - [ - 0.14637120456846728, - 0.04380872024785654 - ], - [ - 0.04380872024785654, - 0.024315135268371497 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.050591178331896434, - 0.056413821295499864, - 0.34453811882299423, - 0.31525925007858147, - 0.22794094201347515, - 0.005256689457553028 - ] - }, - "name": "s8", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.0978232427195225, - -0.19880507306347045 - ], - [ - [ - 0.18288591564861567, - -0.01563983547688968 - ], - [ - -0.01563983547688968, - 0.01340968920034488 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.585662894700118, - -0.09327176875314531 - ], - [ - [ - 0.009861412477369762, - -0.001605574409422754 - ], - [ - -0.001605574409422754, - 0.02328429968050431 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 3.220156351950902, - -0.2701336946824086 - ], - [ - [ - 0.8037290828419877, - -0.13880558760346237 - ], - [ - -0.13880558760346237, - 0.13937669395420002 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.961035154660426, - 0.22845664351539355 - ], - [ - [ - 0.049564436047409684, - 0.027772559158844436 - ], - [ - 0.027772559158844436, - 0.0335403331352617 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.955582777589335, - -0.06772341770708028 - ], - [ - [ - 0.0490882014544778, - 0.015337901174890072 - ], - [ - 0.015337901174890072, - 0.031652684581713414 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 2.3330379198529037, - 0.020098640935322775 - ], - [ - [ - 0.19762152642034286, - 0.03181128017468199 - ], - [ - 0.03181128017468199, - 0.05054680719283672 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.18912509569993793, - 0.19667888523507424, - 0.01938804677755137, - 0.2627898854528598, - 0.2533692766112804, - 0.07864881022329624 - ] - }, - "name": "s9", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.8351143121194007, - -0.5122638693721864 - ], - [ - [ - 0.14046684011661453, - 0.006665339021284342 - ], - [ - 0.006665339021284342, - 0.028269986425225616 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.4981732066394406, - -0.7660118195597697 - ], - [ - [ - 0.253755626966208, - -0.07545275527056751 - ], - [ - -0.07545275527056751, - 0.06536709260748119 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.4870727310188694, - -0.7576810472962574 - ], - [ - [ - 0.04390115883703879, - 0.018550354325481903 - ], - [ - 0.018550354325481903, - 0.05918781344903495 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.5478516646551511, - -0.43853212625144644 - ], - [ - [ - 0.0032895563347738826, - -0.0011274229411736593 - ], - [ - -0.0011274229411736593, - 0.004064028822452112 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.8285913935278124, - -0.7326833848315257 - ], - [ - [ - 0.04639857294609491, - -0.049858780129014554 - ], - [ - -0.049858780129014554, - 0.07786780553576868 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 2.6758882845129555, - -1.1938490533506798 - ], - [ - [ - 0.5006816671925979, - -0.20227007543098452 - ], - [ - -0.20227007543098452, - 0.31535710117506477 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.3175964924895178, - 0.2957417368899987, - 0.290290660051822, - 0.0015029208585942034, - 0.05652772785526872, - 0.03834046185479861 - ] - }, - "name": "sa", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.6931587123564085, - -1.4089515331427522 - ], - [ - [ - 0.11392623488450301, - 0.0016209208853717662 - ], - [ - 0.0016209208853717662, - 0.05206638985105943 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.4325395296647314, - -2.00125475188884 - ], - [ - [ - 0.41001425585485474, - -0.24827317801275978 - ], - [ - -0.24827317801275978, - 0.2969864605456684 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.9010756541811548, - -1.6316381324644655 - ], - [ - [ - 0.11207160587161284, - -0.035903412273462824 - ], - [ - -0.035903412273462824, - 0.08950711803291501 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.3773911707507072, - -1.7815478639979678 - ], - [ - [ - 0.16843468537340958, - -0.032265281486372546 - ], - [ - -0.032265281486372546, - 0.10098868594071204 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.37740419892813387, - -0.7949121975563709 - ], - [ - [ - 0.07381232402838175, - -0.07595253377920648 - ], - [ - -0.07595253377920648, - 0.17047113785871443 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.1811267539735342, - -1.3806335752615653 - ], - [ - [ - 0.1062464647211127, - 0.05304247528188522 - ], - [ - 0.05304247528188522, - 0.05098708651353123 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.14789642405166825, - 0.03182486194421295, - 0.15580254164021448, - 0.1554372162704567, - 0.27925928018694296, - 0.22977967590650475 - ] - }, - "name": "sb", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.13878271306347267, - -1.5474492576230316 - ], - [ - [ - 0.058862337328279085, - -0.02766227065245916 - ], - [ - -0.02766227065245916, - 0.28480563096544564 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.2969342862014465, - -0.7278300295113517 - ], - [ - [ - 0.03254650338809167, - 0.008127320940059385 - ], - [ - 0.008127320940059385, - 0.045257183881101185 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.4588432691504559, - -2.1300692388039946 - ], - [ - [ - 0.20362295989732357, - -0.07524949655161875 - ], - [ - -0.07524949655161875, - 0.15674179929419454 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.0972098277835148, - -0.22934054837280451 - ], - [ - [ - 0.0010273772668491572, - 4.7604695764052905e-05 - ], - [ - 4.7604695764052905e-05, - 0.006007980152658993 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.5539553691115032, - -1.390437224305981 - ], - [ - [ - 0.024690750530544904, - -0.024275282277841842 - ], - [ - -0.024275282277841842, - 0.07425307474259665 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.11683982047005993, - -1.7354936026470975 - ], - [ - [ - 0.1315486945377525, - -0.04015813429001713 - ], - [ - -0.04015813429001713, - 0.027717186766069064 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.26700471685635363, - 0.1230506081971665, - 0.31015435165902666, - 0.015368776144476343, - 0.16405667202280758, - 0.1203648751201694 - ] - }, - "name": "sc", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.0176476828946697, - -0.27200633152025705 - ], - [ - [ - 0.16699602024620364, - 0.031817558075169526 - ], - [ - 0.031817558075169526, - 0.053522226407302094 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.0644517466774976, - -2.1991321950535982 - ], - [ - [ - 0.3594060164032232, - -0.2671581379581159 - ], - [ - -0.2671581379581159, - 0.30160115588446085 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.2100961846947191, - -0.32998632569538794 - ], - [ - [ - 0.012388023639425582, - 0.000795594061740385 - ], - [ - 0.000795594061740385, - 0.08412273590215606 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.0189876568501428, - -0.8359772815724409 - ], - [ - [ - 0.10645157428423571, - 0.040557857879518176 - ], - [ - 0.040557857879518176, - 0.13532172610437926 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.8078437980870343, - -1.662671061959941 - ], - [ - [ - 0.06617606076240037, - -0.0771740438505756 - ], - [ - -0.0771740438505756, - 0.16503252732782633 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.21655273772777373, - -0.34104934607275766 - ], - [ - [ - 0.008259635779222625, - 0.0010286115715905952 - ], - [ - 0.0010286115715905952, - 0.0018811618575603263 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.1517450986896751, - 0.06307348153200126, - 0.17094507475178894, - 0.4086994518612988, - 0.20033861771458408, - 0.005198275450651683 - ] - }, - "name": "sd", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.5552432920716407, - 0.5730472173579441 - ], - [ - [ - 0.03954606364565164, - -0.06515626632815194 - ], - [ - -0.06515626632815194, - 0.14995161063496934 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.3791795270345042, - 0.24492076461481646 - ], - [ - [ - 0.056055145811250394, - 0.03929252313334932 - ], - [ - 0.03929252313334932, - 0.14931411279350598 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.053767251322347866, - -0.18158537017572296 - ], - [ - [ - 0.0063017444230055505, - -0.00038404366653848937 - ], - [ - -0.00038404366653848937, - 0.005085173165065696 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.8809947587855619, - 0.5959360914721322 - ], - [ - [ - 0.055410195509785085, - 0.020143760531716383 - ], - [ - 0.020143760531716383, - 0.05446672103114752 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -2.1436418823697503, - 0.3636090351284151 - ], - [ - [ - 0.4686782624746718, - 0.10279949546631781 - ], - [ - 0.10279949546631781, - 0.8541004456870454 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.006597300342228862, - -0.029415132468641525 - ], - [ - [ - 9.405813156506171e-05, - -0.00016152243633298097 - ], - [ - -0.00016152243633298097, - 0.00034643104476464296 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.09809202020858468, - 0.42614327239134175, - 0.005683477957041837, - 0.4090797622416401, - 0.059764009992946114, - 0.0012374572084454965 - ] - }, - "name": "se", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.0760223137379458, - 0.00261115543341902 - ], - [ - [ - 0.015514005612152042, - 0.00435242136870637 - ], - [ - 0.00435242136870637, - 0.007200698423556048 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.5561662482556352, - 1.0733535363617486 - ], - [ - [ - 0.04898558854468965, - 0.018952657457887392 - ], - [ - 0.018952657457887392, - 0.023230936345009442 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.33097620803218303, - 0.8542953635369246 - ], - [ - [ - 0.021743963351466222, - -0.0007388536970455881 - ], - [ - -0.0007388536970455881, - 0.020776681756822728 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.05446041131292501, - 0.683140927392867 - ], - [ - [ - 0.0007979293993163288, - -0.0002658258522799146 - ], - [ - -0.0002658258522799146, - 0.018546129606030744 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.13575596750127816, - 0.7667586063209816 - ], - [ - [ - 0.005857985448356958, - 0.0015872208652658339 - ], - [ - 0.0015872208652658339, - 0.06978267814605611 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.36109168742622083, - 1.6640270594133126 - ], - [ - [ - 0.1539560701453866, - -0.1152465249193895 - ], - [ - -0.1152465249193895, - 0.34023114454628495 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.008937515507038935, - 0.06556513133752329, - 0.28196646146901216, - 0.307712340759529, - 0.3127501232423219, - 0.023068427684574715 - ] - }, - "name": "sf", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.041833307664748624, - 0.1705769569246408 - ], - [ - [ - 0.0006871042297588102, - 0.00013518528303884552 - ], - [ - 0.00013518528303884552, - 0.0038939607277044025 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.026857807622764684, - 0.08873487937793743 - ], - [ - [ - 0.00047550412043388906, - 6.803488627608004e-05 - ], - [ - 6.803488627608004e-05, - 0.00021834172197806893 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.08339982052410588, - 0.242332017680003 - ], - [ - [ - 0.0031436039581924386, - -0.0032240370080416528 - ], - [ - -0.0032240370080416528, - 0.024708397759146874 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.24518560861368674, - 0.2606805845081343 - ], - [ - [ - 0.045990379971836845, - -0.0016290743752791397 - ], - [ - -0.0016290743752791397, - 0.027330904352321026 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -2.2719619768144157, - 2.502586182579559 - ], - [ - [ - 0.0752085979801202, - 0.06448282495522933 - ], - [ - 0.06448282495522933, - 0.05579634529098662 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.0457877259303524, - 0.3806205639337352 - ], - [ - [ - 0.0008488727785275026, - 0.0008470552877879244 - ], - [ - 0.0008470552877879244, - 0.013264068480053043 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.38734486081276204, - 0.13693955969089783, - 0.005873780473782593, - 0.029588087565230313, - 0.0, - 0.4402537114573271 - ] - }, - "name": "sg", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.013242201938851396, - 0.031845925507977775 - ], - [ - [ - 0.0009390615935839225, - 0.0001653813861364287 - ], - [ - 0.0001653813861364287, - 0.0003971604780331884 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.08607979584990627, - -0.05853224594881856 - ], - [ - [ - 0.017310669781983718, - -0.0032851138741958824 - ], - [ - -0.0032851138741958824, - 0.028025374341380447 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.49559332236428194, - -1.4003207695102506 - ], - [ - [ - 0.05082507032632953, - -0.02489997080381228 - ], - [ - -0.02489997080381228, - 0.024879660571573092 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.22880383487895914, - 2.233272904641477 - ], - [ - [ - 0.09272965372035101, - -0.11219290093828721 - ], - [ - -0.11219290093828721, - 0.15912577082362542 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.9360490778396979, - -0.6804093789388136 - ], - [ - [ - 0.017873757721209162, - -0.026395549313954637 - ], - [ - -0.026395549313954637, - 0.040930957947626384 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.04320742109721851, - 0.017722063947934085 - ], - [ - [ - 0.001273042410046457, - -0.000624788885604057 - ], - [ - -0.000624788885604057, - 0.004761234282595781 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.8896901334468728, - 0.029644742547909136, - 0.0, - 1.4296417994723952e-130, - 0.0, - 0.08066512400521812 - ] - }, - "name": "sh", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.008827958881681865, - -0.0015608819531710305 - ], - [ - [ - 0.0006997403911677459, - 0.000197241011394837 - ], - [ - 0.000197241011394837, - 0.00017257673760017698 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.031742160869992694, - 0.010427224672933595 - ], - [ - [ - 0.0050698935923880195, - -0.0020252898314969624 - ], - [ - -0.0020252898314969624, - 0.0014778595924074371 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.0024529527282765636, - -0.016897856998535694 - ], - [ - [ - 0.00035202792732571197, - -7.625771115500745e-05 - ], - [ - -7.625771115500745e-05, - 0.0003118798146958923 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.0059796486520338745, - -0.010538630549651045 - ], - [ - [ - 0.0004567326578116454, - 4.4726963279365465e-05 - ], - [ - 4.4726963279365465e-05, - 0.0002455696041855514 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.032888802555955984, - -0.28205734639824087 - ], - [ - [ - 0.002993239256349607, - 0.003495396492033091 - ], - [ - 0.003495396492033091, - 0.0041630276471643505 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.024975547985649188, - -0.01776568349982063 - ], - [ - [ - 0.0017866950442623649, - 0.0007428793313783671 - ], - [ - 0.0007428793313783671, - 0.0025389327809397784 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.1932621177626548, - 0.0035360955883515627, - 0.03383512892943376, - 0.7455382977987395, - 0.0012524582722173109, - 0.022575901648603028 - ] - }, - "name": "si", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.029258631317761023, - -0.06785963954483779 - ], - [ - [ - 0.0007380047121639887, - -0.000294352812212126 - ], - [ - -0.000294352812212126, - 0.0007622718466989874 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.10681333199205689, - -0.16431574895247184 - ], - [ - [ - 0.0006949909364778672, - 0.00039460183494006165 - ], - [ - 0.00039460183494006165, - 0.004227729978722817 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.0982313136869901, - -0.05999869880340171 - ], - [ - [ - 0.003279943647079228, - -0.0005722779496733451 - ], - [ - -0.0005722779496733451, - 0.004423250902669305 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.23967396096114335, - -0.044176610184626236 - ], - [ - [ - 0.007805912682974319, - -0.002365491335194562 - ], - [ - -0.002365491335194562, - 0.0017640650904148314 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.00706630215268268, - -0.0465621944038333 - ], - [ - [ - 0.0003413497573864167, - -3.679660983239417e-05 - ], - [ - -3.679660983239417e-05, - 0.0001025775666203872 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.02643426799967081, - -0.11778981749989963 - ], - [ - [ - 0.0002389447751020781, - 9.378125589935128e-06 - ], - [ - 9.378125589935128e-06, - 0.0017752863476281002 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.3205850888601075, - 0.004480647679044943, - 0.2391029919382351, - 0.006512589310861102, - 0.22315056355660529, - 0.20616811865514614 - ] - }, - "name": "sj", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.052185394426412235, - -0.1264636712258031 - ], - [ - [ - 3.895041183238874e-05, - 1.5110535804900476e-05 - ], - [ - 1.5110535804900476e-05, - 9.938342079244728e-05 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.0232896748355558, - -0.3131096562787241 - ], - [ - [ - 0.008249500478174077, - -0.001725734835458333 - ], - [ - -0.001725734835458333, - 0.012903869169936433 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.06383665666202674, - -0.4301302922465954 - ], - [ - [ - 0.0018566431450146867, - 0.0029010466417030865 - ], - [ - 0.0029010466417030865, - 0.012389070286510307 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.12427511677783334, - -0.33159599453676286 - ], - [ - [ - 0.0025198006868689493, - -0.003425815281748251 - ], - [ - -0.003425815281748251, - 0.015009296271451917 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.924277366844015, - -2.293939099737936 - ], - [ - [ - 0.3230774852749861, - -0.22932274626399304 - ], - [ - -0.22932274626399304, - 0.20091304830455892 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.049820538605244914, - -0.23405186488447458 - ], - [ - [ - 0.0009102758658479725, - 6.688618538384926e-05 - ], - [ - 6.688618538384926e-05, - 0.005359558467161061 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.007929469644550718, - 0.0028884935280120874, - 0.1676231612565627, - 0.25015820268295313, - 0.0, - 0.5714006728879214 - ] - }, - "name": "sk", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.06650795758039411, - -0.03828225768903544 - ], - [ - [ - 0.0008492927991259382, - 0.00036953053955581856 - ], - [ - 0.00036953053955581856, - 0.0016894558832424625 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.1931965000364244, - -0.5774908509624352 - ], - [ - [ - 0.005424608779422052, - 0.00026721868416634055 - ], - [ - 0.00026721868416634055, - 0.03496650501790332 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.010217170843726848, - -1.782576822454449 - ], - [ - [ - 1e-08, - 0.0 - ], - [ - 0.0, - 1.0000000888192436e-08 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.2552963883542676, - -1.2568871579388492 - ], - [ - [ - 0.015281768031916079, - 0.006727682809622518 - ], - [ - 0.006727682809622518, - 0.004590248401683354 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -4.106099574147729, - 0.9651243360773101 - ], - [ - [ - 0.19726018779377366, - -0.48735363438086454 - ], - [ - -0.48735363438086454, - 1.6647880160509023 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.298875977792634, - -0.9654092605486819 - ], - [ - [ - 0.0101607357320042, - 0.0137489790772033 - ], - [ - 0.0137489790772033, - 0.04203423666629918 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.005781355509645207, - 0.5109441053804828, - 0.0005427067289165128, - 0.014828878313184066, - 0.0, - 0.46790295406777144 - ] - }, - "name": "sl", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.09315740685905076, - -0.009839348242461434 - ], - [ - [ - 0.001962099749312198, - 0.0003631866223520721 - ], - [ - 0.0003631866223520721, - 0.0007249053631467968 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.9265097779492681, - -1.5309388755370257 - ], - [ - [ - 0.031889397321262704, - -0.013916481666086401 - ], - [ - -0.013916481666086401, - 0.014365209337659914 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.6101341393146054, - -1.547727383776736 - ], - [ - [ - 0.06417880305787836, - -0.01566893633616545 - ], - [ - -0.01566893633616545, - 0.08423830135229891 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.39743511415271965, - -0.4890166043279221 - ], - [ - [ - 0.010520970052894667, - 0.024198280392720654 - ], - [ - 0.024198280392720654, - 0.0868262321535988 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.346206593792657, - -1.3038892915133093 - ], - [ - [ - 0.005228228513695661, - -0.004290989066737287 - ], - [ - -0.004290989066737287, - 0.005359314588070637 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.5621829772614868, - -1.4552088705802286 - ], - [ - [ - 0.023339123594084955, - 0.01816728623706648 - ], - [ - 0.01816728623706648, - 0.06091308072369845 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.0062993722507902585, - 0.18429603440788933, - 0.04297926171697384, - 0.10066573873288806, - 0.008198558936925534, - 0.6575610339545329 - ] - }, - "name": "sm", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.06820732869652332, - -0.030739694751622748 - ], - [ - [ - 1.281951268242188e-08, - 2.8411929444230552e-08 - ], - [ - 2.8411929444230552e-08, - 2.9630399138709924e-07 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.8280509668491328, - -2.0233563871055273 - ], - [ - [ - 0.014255315490516243, - 0.00023233428227283082 - ], - [ - 0.00023233428227283082, - 0.023381465280093224 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.8297998414459817, - -0.8305243338114182 - ], - [ - [ - 0.03447413681035377, - -0.03948845510512655 - ], - [ - -0.03948845510512655, - 0.06112960343519014 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.6038224344853582, - -0.4298387970597698 - ], - [ - [ - 0.08238214884867226, - 0.10357156836380109 - ], - [ - 0.10357156836380109, - 0.16509228860251804 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.4207697137501396, - -1.2171099736997577 - ], - [ - [ - 0.07935983585931271, - 0.013070792313596298 - ], - [ - 0.013070792313596298, - 0.1101082506602764 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.2513886217659775, - -1.728297281151107 - ], - [ - [ - 0.06870284941851519, - -0.05812524946160287 - ], - [ - -0.05812524946160287, - 0.1537331894060799 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.0003502807143412273, - 0.09046606834602916, - 0.15047159892933062, - 0.09533880681688435, - 0.14749232460435993, - 0.5158809205890547 - ] - }, - "name": "sn", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -3.0318558205186696, - -0.606131152222811 - ], - [ - [ - 0.7509957355602215, - -0.4483626609922767 - ], - [ - -0.4483626609922767, - 2.1009006610264254 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.1448790682720736, - 0.09941095585911952 - ], - [ - [ - 0.002170498202160566, - -0.0017584741374254158 - ], - [ - -0.0017584741374254158, - 0.005525196937021685 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.9757080528164253, - -0.12635826480896817 - ], - [ - [ - 0.23420446228074726, - 0.08444919858879225 - ], - [ - 0.08444919858879225, - 0.07718096700671238 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.2346438738431826, - -2.744229681274007 - ], - [ - [ - 0.012770246266524739, - 0.03540284455523518 - ], - [ - 0.03540284455523518, - 0.10952888518397509 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.8111897167794737, - -1.8064922746353294 - ], - [ - [ - 0.07857390614309127, - -0.09378174561727863 - ], - [ - -0.09378174561727863, - 0.18071050305601855 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -2.4813301038305045, - -0.35623254814678684 - ], - [ - [ - 0.16246567201820478, - -0.16476229714755336 - ], - [ - -0.16476229714755336, - 0.6751434856877658 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.14273815443538637, - 0.0070964626528938176, - 0.06166599941429371, - 0.006511363797569338, - 0.10848278234144827, - 0.6735052373584085 - ] - }, - "name": "so", - "weight": 1.0 - }, - { - "class": "State", - "distribution": null, - "name": "None-start", - "weight": 1.0 - }, - { - "class": "State", - "distribution": null, - "name": "None-end", - "weight": 1.0 - } - ], - "end_index": 26, - "start_index": 25, - "silent_index": 25, - "edges": [ - [ - 0, - 0, - 0.8821640907352049, - 0.9074951827010481, - null - ], - [ - 0, - 1, - 0.035963397665353634, - 0.07193546848162154, - null - ], - [ - 0, - 3, - 0.05396554318488152, - 0.1, - null - ], - [ - 0, - 5, - 0.027906968414560033, - 0.1, - null - ], - [ - 1, - 1, - 0.9748197391098997, - 0.94909416209142, - null - ], - [ - 1, - 2, - 0.025180260890100305, - 0.1, - null - ], - [ - 2, - 2, - 0.974725530359632, - 0.9423970523876716, - null - ], - [ - 2, - 3, - 0.02527446964036792, - 0.055367092475447056, - null - ], - [ - 3, - 3, - 0.9415015080833038, - 0.9417576875514561, - null - ], - [ - 3, - 4, - 0.058498491916696295, - 0.05734130349585611, - null - ], - [ - 4, - 0, - 0.08484630449340673, - 0.1, - null - ], - [ - 4, - 4, - 0.9151536955065933, - 0.9128453395944705, - null - ], - [ - 5, - 5, - 0.5888317954975275, - 0.6954172648012702, - null - ], - [ - 5, - 6, - 0.4111682045024725, - 0.30458273519872986, - null - ], - [ - 6, - 6, - 0.6665756708574827, - 0.6844013426420876, - null - ], - [ - 6, - 7, - 0.3334243291425172, - 0.31559865735791237, - null - ], - [ - 7, - 7, - 0.6800610372080454, - 0.6843798839610127, - null - ], - [ - 7, - 8, - 0.3199389627919546, - 0.3156201160389874, - null - ], - [ - 8, - 8, - 0.6940311667096045, - 0.699144506945995, - null - ], - [ - 8, - 9, - 0.3059688332903955, - 0.3008554930540052, - null - ], - [ - 9, - 9, - 0.6960613747358361, - 0.7021252947139494, - null - ], - [ - 9, - 10, - 0.30393862526416404, - 0.2978747052860507, - null - ], - [ - 10, - 10, - 0.6800290288208687, - 0.68846903198653, - null - ], - [ - 10, - 11, - 0.31997097117913126, - 0.31153096801346997, - null - ], - [ - 11, - 11, - 0.6908660524108106, - 0.6827534790694575, - null - ], - [ - 11, - 12, - 0.3091339475891894, - 0.1, - null - ], - [ - 12, - 12, - 0.6824646638708582, - 0.6792167535169966, - null - ], - [ - 12, - 13, - 0.3175353361291417, - 0.1, - null - ], - [ - 13, - 13, - 0.6529268521083446, - 0.6593498187398975, - null - ], - [ - 13, - 14, - 0.3470731478916555, - 0.3406501812601025, - null - ], - [ - 14, - 14, - 0.6742615053779222, - 0.6767793585516855, - null - ], - [ - 14, - 15, - 0.3257384946220778, - 0.32322064144831447, - null - ], - [ - 15, - 15, - 0.6899877046925306, - 0.694512552877693, - null - ], - [ - 15, - 16, - 0.3100122953074694, - 0.305487447122307, - null - ], - [ - 16, - 16, - 0.6682986866590126, - 0.6650337670131573, - null - ], - [ - 16, - 17, - 0.33170131334098735, - 0.33496623298684286, - null - ], - [ - 17, - 17, - 0.7637699187064003, - 0.7630892813327846, - null - ], - [ - 17, - 18, - 0.23623008129359976, - 0.1, - null - ], - [ - 18, - 18, - 0.8491883291845923, - 0.838730899927014, - null - ], - [ - 18, - 19, - 0.15081167081540764, - 0.16126910007298595, - null - ], - [ - 19, - 19, - 0.7650673608944687, - 0.7617013308320338, - null - ], - [ - 19, - 20, - 0.2349326391055314, - 0.1, - null - ], - [ - 20, - 20, - 0.6320886814421868, - 0.6362766698886175, - null - ], - [ - 20, - 21, - 0.3679113185578133, - 0.36372333011138247, - null - ], - [ - 21, - 21, - 0.5935127135914391, - 0.5969557414010658, - null - ], - [ - 21, - 22, - 0.4064872864085609, - 0.4030442585989343, - null - ], - [ - 22, - 22, - 0.564474114438885, - 0.5687771519363383, - null - ], - [ - 22, - 23, - 0.43552588556111516, - 0.4312228480636617, - null - ], - [ - 23, - 23, - 0.39833984547315493, - 0.4099471519796639, - null - ], - [ - 23, - 24, - 0.6016601545268451, - 0.590052848020336, - null - ], - [ - 24, - 24, - 0.5923894051040478, - 0.6040090788637088, - null - ], - [ - 24, - 0, - 0.03883084098291284, - 0.1, - null - ], - [ - 24, - 5, - 0.3687797539130394, - 0.1, - null - ], - [ - 25, - 0, - 0.038763172623228245, - 1.0, - null - ], - [ - 25, - 1, - 0.35590729415701633, - 1.0, - null - ], - [ - 25, - 2, - 0.33544071521963925, - 1.0, - null - ], - [ - 25, - 3, - 0.19669724255778726, - 1.0, - null - ], - [ - 25, - 4, - 0.07319157544232908, - 1.0, - null - ] - ], - "distribution ties": [] - } - }, - "name": "segmentation_model", - "stop_threshold": 1e-09, - "stride_model": { - "_gaitmap_obj": "SimpleHmm", - "params": { - "algo_train": "baum-welch", - "architecture": "left-right-strict", - "data_columns": ["raw__gyr_ml", "gradient__gradient__gyr_ml"], - "max_iterations": 10, - "model": { - "_obj_type": "HiddenMarkovModel", - "hmm": { - "class": "HiddenMarkovModel", - "name": "stride_model-trained", - "start": { - "class": "State", - "distribution": null, - "name": "None-start", - "weight": 1.0 - }, - "end": { - "class": "State", - "distribution": null, - "name": "None-end", - "weight": 1.0 - }, - "states": [ - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -2.724722078278822, - 1.9915188464133518 - ], - [ - [ - 0.2932194592373769, - 0.21134149736125363 - ], - [ - 0.21134149736125363, - 0.3844722708781578 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.6945547288226518, - 0.2560157714515537 - ], - [ - [ - 0.1258018118620837, - 0.03268783972099044 - ], - [ - 0.03268783972099044, - 0.06484802385053644 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -2.4919699535246735, - 1.3536090707748252 - ], - [ - [ - 0.20727245631858418, - 0.18784664909434642 - ], - [ - 0.18784664909434642, - 0.27038877692794705 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -2.5621310517208085, - 0.22980081390054788 - ], - [ - [ - 0.14264334757636107, - -0.0008131419084383741 - ], - [ - -0.0008131419084383741, - 0.2498846469036756 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.4154413684634914, - 1.5500380278531312 - ], - [ - [ - 0.3281468084248711, - -0.18643228479944526 - ], - [ - -0.18643228479944526, - 0.5149454717059048 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -3.345238257207037, - 2.5233670301321913 - ], - [ - [ - 1.2607062895679058, - 0.46505700492524954 - ], - [ - 0.46505700492524954, - 2.706722355023741 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.2355327614387686, - 0.057494594752470506, - 0.2626428997077363, - 0.24060010369417817, - 0.12754264748582914, - 0.07618699292101734 - ] - }, - "name": "s0", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.7930589020789591, - 2.776017698946987 - ], - [ - [ - 0.09996804486098272, - 0.06050851708242607 - ], - [ - 0.06050851708242607, - 0.17635779190130996 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.07410480792811462, - 2.8369130167639804 - ], - [ - [ - 0.26661496431849807, - 0.04533211383325763 - ], - [ - 0.04533211383325763, - 0.23499477853090558 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.5499648961782752, - 4.016524487247618 - ], - [ - [ - 1.8009599929123774, - 0.479280726833787 - ], - [ - 0.479280726833787, - 1.0132508140131788 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.159929491910014, - 2.314834344841815 - ], - [ - [ - 0.1950762112167603, - 0.050067757422238277 - ], - [ - 0.050067757422238277, - 0.04769252944268426 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.12128992309555985, - 0.9095184467635742 - ], - [ - [ - 0.06072026114906567, - 0.025823716783847178 - ], - [ - 0.025823716783847178, - 0.18864286460386825 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.4307456569548131, - 3.018466419910416 - ], - [ - [ - 0.24375621645852313, - 0.06190626212456217 - ], - [ - 0.06190626212456217, - 0.12503450568373897 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.25368257048318127, - 0.3411652775615879, - 0.07354865979398134, - 0.11471339990143539, - 0.09455027921178155, - 0.1223398130480325 - ] - }, - "name": "s1", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.2802011321565605, - 2.500739011826583 - ], - [ - [ - 0.0843297312497741, - 0.04133439057213231 - ], - [ - 0.04133439057213231, - 0.14871587853137966 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.9605234360877333, - 2.415775039498101 - ], - [ - [ - 0.38364850605834805, - 0.09221962856919243 - ], - [ - 0.09221962856919243, - 0.5185153864229178 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.7321736458164637, - 0.4154981602303466 - ], - [ - [ - 0.026014384995446, - 0.0026043157968326717 - ], - [ - 0.0026043157968326717, - 0.0043425368895156575 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.7751828510703308, - 0.6462409838355484 - ], - [ - [ - 0.05641946558947635, - 0.016402205868372734 - ], - [ - 0.016402205868372734, - 0.047810521379711125 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.2710648987648279, - 1.1365894604122162 - ], - [ - [ - 0.020193272960135265, - -0.0005018838421025301 - ], - [ - -0.0005018838421025301, - 0.04710664910564171 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.193912713048431, - 1.7591104210891768 - ], - [ - [ - 0.07004778999475839, - 0.006591811226348407 - ], - [ - 0.006591811226348407, - 0.2032920419957315 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.18917134266322164, - 0.059234457848795864, - 6.369546945313624e-05, - 0.09119353461762139, - 0.14382680656165214, - 0.5165101628392558 - ] - }, - "name": "s2", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 2.588995717055121, - 0.8789043376811468 - ], - [ - [ - 0.6626160146039229, - -0.021945851053280268 - ], - [ - -0.021945851053280268, - 0.12105293229274139 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.8034889546645247, - 0.12875200546532437 - ], - [ - [ - 0.02209208248042997, - -0.0007065543531791519 - ], - [ - -0.0007065543531791519, - 0.010705971196589312 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.449093432630524, - 0.6975644415346912 - ], - [ - [ - 0.04321221967087894, - 0.035042780346912 - ], - [ - 0.035042780346912, - 0.08435484950404089 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.7771752601252138, - 0.4884361937186219 - ], - [ - [ - 0.03345096665657911, - 0.013501793650040743 - ], - [ - 0.013501793650040743, - 0.034990364907834665 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.5554288451528646, - 0.2910244131006228 - ], - [ - [ - 0.007935994462359793, - -0.0012908658054826393 - ], - [ - -0.0012908658054826393, - 0.015869103125689548 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.9212711529186459, - 0.23692285397120277 - ], - [ - [ - 0.11701600721357804, - 0.017944771790248344 - ], - [ - 0.017944771790248344, - 0.01638735251011646 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.0511894878453503, - 0.05628653258032807, - 0.3379563212723616, - 0.3207220937761932, - 0.23102873292190057, - 0.002816831603866323 - ] - }, - "name": "s3", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.135160280244912, - -0.20582274730759018 - ], - [ - [ - 0.16263305162878075, - -0.0086558700063166 - ], - [ - -0.0086558700063166, - 0.013265794297089984 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.5888006563906878, - -0.08355686852250115 - ], - [ - [ - 0.009065847876272537, - -0.001325353468483505 - ], - [ - -0.001325353468483505, - 0.024323351356194067 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 3.1145666080383294, - -0.26622013470717487 - ], - [ - [ - 0.7413599936175174, - -0.14153652956695537 - ], - [ - -0.14153652956695537, - 0.13085166155366815 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.965936569593422, - 0.22769758495347936 - ], - [ - [ - 0.04821971474061846, - 0.02712810067158472 - ], - [ - 0.02712810067158472, - 0.03464927705368995 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.9567186220908748, - -0.059774539111321724 - ], - [ - [ - 0.05048279418396253, - 0.017208198318734342 - ], - [ - 0.017208198318734342, - 0.033513773245658664 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 2.303780455950171, - 0.011003844807491194 - ], - [ - [ - 0.20338331470545354, - 0.033060523133077106 - ], - [ - 0.033060523133077106, - 0.04682552911061615 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.19117236787545577, - 0.19979701906744884, - 0.020503508717646293, - 0.2627348187063074, - 0.25019792781386685, - 0.07559435781927484 - ] - }, - "name": "s4", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.8492503509098017, - -0.517674096978054 - ], - [ - [ - 0.1314520414716851, - 0.009453990931379717 - ], - [ - 0.009453990931379717, - 0.032138606445269044 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.4779806837478076, - -0.7673169330509768 - ], - [ - [ - 0.24266237123325962, - -0.06956522848236406 - ], - [ - -0.06956522848236406, - 0.06389572907811006 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.4868813925919653, - -0.7592635645806777 - ], - [ - [ - 0.043220713100580246, - 0.019850048196437552 - ], - [ - 0.019850048196437552, - 0.06280482423710004 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.5499837897404262, - -0.44238497714333336 - ], - [ - [ - 0.003209322035000341, - -0.001122106672117212 - ], - [ - -0.001122106672117212, - 0.0037959423665965344 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.8440176223426598, - -0.7542580382187342 - ], - [ - [ - 0.04268880169163222, - -0.0451262672569672 - ], - [ - -0.0451262672569672, - 0.07154674145328065 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 2.5812993921162994, - -1.1558478789372724 - ], - [ - [ - 0.4728108559414795, - -0.16837431399085548 - ], - [ - -0.16837431399085548, - 0.2619421250390748 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.32249917441683423, - 0.29044915095751495, - 0.2964251165623499, - 0.000739903416525329, - 0.04957554424664409, - 0.04031111040013154 - ] - }, - "name": "s5", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.6944985871963718, - -1.41481332143877 - ], - [ - [ - 0.11257065996238892, - 0.00422995061874635 - ], - [ - 0.00422995061874635, - 0.053041991998435546 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.3880379606185462, - -1.992882538304213 - ], - [ - [ - 0.3883793056517977, - -0.24840027012110866 - ], - [ - -0.24840027012110866, - 0.3040576431994222 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.894613245792346, - -1.6233424909890515 - ], - [ - [ - 0.11708375696743395, - -0.03300151179311639 - ], - [ - -0.03300151179311639, - 0.08401589894596406 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.3791160119835582, - -1.7785598280424566 - ], - [ - [ - 0.16580992319121038, - -0.032432764923117634 - ], - [ - -0.032432764923117634, - 0.1005612394231465 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.3995392648509291, - -0.8491146604912162 - ], - [ - [ - 0.07576252505265106, - -0.07108608124993676 - ], - [ - -0.07108608124993676, - 0.1548823458667788 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 1.167491513223562, - -1.3969922884295676 - ], - [ - [ - 0.11006581691860405, - 0.054481500102950456 - ], - [ - 0.054481500102950456, - 0.0507797309141409 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.1526978686526657, - 0.032832093191063075, - 0.15771216486137704, - 0.1575831622918626, - 0.26089659156134026, - 0.23827811944169133 - ] - }, - "name": "s6", - "weight": 1.0 }, { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.14526418438830446, - -1.5393628764547853 - ], - [ - [ - 0.06066619096130978, - -0.0345657843289396 - ], - [ - -0.0345657843289396, - 0.28220405348618727 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.16699602024620364, + 0.031817558075169526 + ], + [ + 0.031817558075169526, + 0.053522226407302094 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.0176476828946697, + -0.27200633152025705 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.3021341235263017, - -0.7311913259859005 - ], - [ - [ - 0.030641588742904128, - 0.007226311809544816 - ], - [ - 0.007226311809544816, - 0.042641029223435996 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.3594060164032232, + -0.2671581379581159 + ], + [ + -0.2671581379581159, + 0.30160115588446085 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.0644517466774976, + -2.1991321950535982 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.47314790179396854, - -2.1342542481330145 - ], - [ - [ - 0.20070706393994658, - -0.06789065902248924 - ], - [ - -0.06789065902248924, - 0.14746889140690772 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.012388023639425582, + 0.000795594061740385 + ], + [ + 0.000795594061740385, + 0.08412273590215606 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.2100961846947191, + -0.32998632569538794 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.09318530245112652, - -0.2260515953806998 - ], - [ - [ - 0.0008470381227825778, - 0.00039227448579345973 - ], - [ - 0.00039227448579345973, - 0.00540765875958852 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.10645157428423571, + 0.040557857879518176 + ], + [ + 0.040557857879518176, + 0.13532172610437926 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.0189876568501428, + -0.8359772815724409 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.5455792319996824, - -1.3950760479968436 - ], - [ - [ - 0.025557738363392642, - -0.024841081291224087 - ], - [ - -0.024841081291224087, - 0.07542718216602565 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.06617606076240037, + -0.0771740438505756 + ], + [ + -0.0771740438505756, + 0.16503252732782633 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.8078437980870343, + -1.662671061959941 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.07773576131758664, - -1.728652277158465 - ], - [ - [ - 0.1261871751915607, - -0.0407425710402396 - ], - [ - -0.0407425710402396, - 0.028358919658514 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.008259635779222625, + 0.0010286115715905952 + ], + [ + 0.0010286115715905952, + 0.0018811618575603263 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.21655273772777373, + -0.34104934607275766 + ] + } + } } ], - "weights": [ - 0.2635593664385791, - 0.11191576048522776, - 0.32668904933163473, - 0.010416452387057722, - 0.16390090053764342, - 0.1235184708198574 - ] - }, - "name": "s7", - "weight": 1.0 + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.1517450986896751, + 0.06307348153200129, + 0.17094507475178897, + 0.40869945186129886, + 0.20033861771458414, + 0.0051982754506516805 + ] + } + } }, { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.0578077392832066, - -0.27535704369905123 - ], - [ - [ - 0.1385388576634102, - 0.025437129539282632 - ], - [ - 0.025437129539282632, - 0.057222372152883484 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.03954606364565164, + -0.06515626632815194 + ], + [ + -0.06515626632815194, + 0.14995161063496934 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.5552432920716407, + 0.5730472173579441 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.0398243373112683, - -2.2104796127562936 - ], - [ - [ - 0.3376502976933182, - -0.23893072679110938 - ], - [ - -0.23893072679110938, - 0.2636489067480696 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.056055145811250394, + 0.03929252313334932 + ], + [ + 0.03929252313334932, + 0.14931411279350598 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.3791795270345042, + 0.24492076461481646 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.20694293168809, - -0.34727263493800814 - ], - [ - [ - 0.013274970591163854, - -0.000749596332664595 - ], - [ - -0.000749596332664595, - 0.08933002469354956 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0063017444230055505, + -0.00038404366653848937 + ], + [ + -0.00038404366653848937, + 0.005085173165065696 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.053767251322347866, + -0.18158537017572296 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.0276002136303142, - -0.8599718695622972 - ], - [ - [ - 0.10387644431599524, - 0.03479042980432082 - ], - [ - 0.03479042980432082, - 0.1374803290943473 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.055410195509785085, + 0.020143760531716383 + ], + [ + 0.020143760531716383, + 0.05446672103114752 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.8809947587855619, + 0.5959360914721322 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.7889536047997324, - -1.6837636830604956 - ], - [ - [ - 0.0693486491821651, - -0.07887713154855715 - ], - [ - -0.07887713154855715, - 0.1647734266069371 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.4686782624746718, + 0.10279949546631781 + ], + [ + 0.10279949546631781, + 0.8541004456870454 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -2.1436418823697503, + 0.3636090351284151 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.16054996901565144, - -0.3268177779960233 - ], - [ - [ - 0.013335831287349048, - 0.003730406086506273 - ], - [ - 0.003730406086506273, - 0.0023635147669782545 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 9.405813156506171e-05, + -0.00016152243633298097 + ], + [ + -0.00016152243633298097, + 0.00034643104476464296 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.006597300342228862, + -0.029415132468641525 + ] + } + } } ], - "weights": [ - 0.14096007183959808, - 0.06199511935885327, - 0.18140073351397204, - 0.40780974906434253, - 0.20439129071910989, - 0.00344303550412422 - ] - }, - "name": "s8", - "weight": 1.0 + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.09809202020858468, + 0.42614327239134175, + 0.005683477957041834, + 0.40907976224164017, + 0.059764009992946114, + 0.0012374572084454959 + ] + } + } }, { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.5727164350397596, - 0.6166587909319007 - ], - [ - [ - 0.037373792958641974, - -0.058454082734011134 - ], - [ - -0.058454082734011134, - 0.13502033499130328 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.3822356433089114, - 0.24042336510856135 - ], - [ - [ - 0.05553464551904911, - 0.03900398721187749 - ], - [ - 0.03900398721187749, - 0.15132009999417226 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.015514005612152042, + 0.00435242136870637 + ], + [ + 0.00435242136870637, + 0.007200698423556048 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.0760223137379458, + 0.00261115543341902 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.014382389802330763, - -0.19862001007236144 - ], - [ - [ - 0.0070427449331162606, - -0.0005876158762260867 - ], - [ - -0.0005876158762260867, - 0.005022376435244604 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.04898558854468965, + 0.018952657457887392 + ], + [ + 0.018952657457887392, + 0.023230936345009442 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.5561662482556352, + 1.0733535363617486 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.8791940813511246, - 0.5925553963089454 - ], - [ - [ - 0.05924227017796291, - 0.021056737352547674 - ], - [ - 0.021056737352547674, - 0.050103042085747666 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.021743963351466222, + -0.0007388536970455881 + ], + [ + -0.0007388536970455881, + 0.020776681756822728 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.33097620803218303, + 0.8542953635369246 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -2.1360336122346895, - 0.33645421921019586 - ], - [ - [ - 0.43977786147200093, - 0.059190727579042896 - ], - [ - 0.059190727579042896, - 0.7953945973221657 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0007979293993163288, + -0.0002658258522799146 + ], + [ + -0.0002658258522799146, + 0.018546129606030744 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.05446041131292501, + 0.683140927392867 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.006695852283756774, - -0.029249215613094914 - ], - [ - [ - 9.420023791560235e-05, - -0.0001623697125511453 - ], - [ - -0.0001623697125511453, - 0.00034866904731997245 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.005857985448356958, + 0.0015872208652658339 + ], + [ + 0.0015872208652658339, + 0.06978267814605611 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.13575596750127816, + 0.7667586063209816 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.1539560701453866, + -0.1152465249193895 + ], + [ + -0.1152465249193895, + 0.34023114454628495 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.36109168742622083, + 1.6640270594133126 + ] + } + } } ], - "weights": [ - 0.08605312624347378, - 0.4283436423269542, - 0.003760517025671518, - 0.42033374156237563, - 0.060207490735349284, - 0.0013014821061756823 - ] - }, - "name": "s9", - "weight": 1.0 + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.008937515507038937, + 0.06556513133752329, + 0.28196646146901216, + 0.307712340759529, + 0.3127501232423219, + 0.02306842768457472 + ] + } + } }, { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.10229168021919927, - 0.0006640836400204043 - ], - [ - [ - 0.006977820457102412, - 0.0033734421771272247 - ], - [ - 0.0033734421771272247, - 0.00828595676375109 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0006871042297588102, + 0.00013518528303884552 + ], + [ + 0.00013518528303884552, + 0.0038939607277044025 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.041833307664748624, + 0.1705769569246408 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.5833243527699679, - 1.0667242688694345 - ], - [ - [ - 0.05841627488904584, - 0.022283459731048772 - ], - [ - 0.022283459731048772, - 0.02418519231468338 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.00047550412043388906, + 6.803488627608004e-05 + ], + [ + 6.803488627608004e-05, + 0.00021834172197806893 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.026857807622764684, + 0.08873487937793743 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.33293282473485025, - 0.8577476812437226 - ], - [ - [ - 0.021117457977541967, - -0.0013522105135089048 - ], - [ - -0.0013522105135089048, - 0.020562690900044412 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0031436039581924386, + -0.0032240370080416528 + ], + [ + -0.0032240370080416528, + 0.024708397759146874 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.08339982052410588, + 0.242332017680003 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.053952839795998166, - 0.6832643218179306 - ], - [ - [ - 0.0007851313121907877, - -0.00029129287704494824 - ], - [ - -0.00029129287704494824, - 0.019411777433438474 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.045990379971836845, + -0.0016290743752791397 + ], + [ + -0.0016290743752791397, + 0.027330904352321026 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.24518560861368674, + 0.2606805845081343 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.13473919281704502, - 0.7859986341314606 - ], - [ - [ - 0.005761352237848329, - 0.0008089250395260794 - ], - [ - 0.0008089250395260794, - 0.06034918110414123 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0752085979801202, + 0.06448282495522933 + ], + [ + 0.06448282495522933, + 0.05579634529098662 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -2.2719619768144157, + 2.502586182579559 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.4713914864540872, - 1.7356142142068267 - ], - [ - [ - 0.135216576131183, - -0.07379395917991213 - ], - [ - -0.07379395917991213, - 0.28068468195755925 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0008488727785275026, + 0.0008470552877879244 + ], + [ + 0.0008470552877879244, + 0.013264068480053043 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.0457877259303524, + 0.3806205639337352 + ] + } + } } ], - "weights": [ - 0.0070628270142855856, - 0.07373189941735797, - 0.2770281109678939, - 0.3119845519183724, - 0.30925310840561776, - 0.02093950227647246 - ] - }, - "name": "sa", - "weight": 1.0 + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.38734486081276215, + 0.13693955969089786, + 0.005873780473782596, + 0.029588087565230316, + 0.0, + 0.4402537114573273 + ] + } + } }, { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.04155842236963722, - 0.1710588670266402 - ], - [ - [ - 0.0006312762518363987, - 0.00011565843048043362 - ], - [ - 0.00011565843048043362, - 0.004072731393708572 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0009390615935839225, + 0.0001653813861364287 + ], + [ + 0.0001653813861364287, + 0.0003971604780331884 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.013242201938851396, + 0.031845925507977775 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.02576901268008257, - 0.08866083541306206 - ], - [ - [ - 0.000447340046416533, - 6.752776101500221e-05 - ], - [ - 6.752776101500221e-05, - 0.000203790774695365 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.017310669781983718, + -0.0032851138741958824 + ], + [ + -0.0032851138741958824, + 0.028025374341380447 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.08607979584990627, + -0.05853224594881856 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.08101378295388252, - 0.250400484904686 - ], - [ - [ - 0.0035991736672574023, - -0.0037425097879729805 - ], - [ - -0.0037425097879729805, - 0.03133333380910439 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.05082507032632953, + -0.02489997080381228 + ], + [ + -0.02489997080381228, + 0.024879660571573092 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.49559332236428194, + -1.4003207695102506 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.1691501993551277, - 0.24930444600241328 - ], - [ - [ - 0.022808696763420802, - 0.00279700933776769 - ], - [ - 0.00279700933776769, - 0.021865673926420477 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.09272965372035101, + -0.11219290093828721 + ], + [ + -0.11219290093828721, + 0.15912577082362542 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.22880383487895914, + 2.233272904641477 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -2.2719619768144157, - 2.502586182579559 - ], - [ - [ - 0.0752085979801202, - 0.06448282495522933 - ], - [ - 0.06448282495522933, - 0.05579634529098662 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.017873757721209162, + -0.026395549313954637 + ], + [ + -0.026395549313954637, + 0.040930957947626384 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.9360490778396979, + -0.6804093789388136 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.045511437732322295, - 0.3841912177925317 - ], - [ - [ - 0.0008169534080270306, - 0.0007215806175151005 - ], - [ - 0.0007215806175151005, - 0.013207629451113825 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.001273042410046457, + -0.000624788885604057 + ], + [ + -0.000624788885604057, + 0.004761234282595781 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.04320742109721851, + 0.017722063947934085 + ] + } + } } ], - "weights": [ - 0.40009335823623604, - 0.12896729372953442, - 0.0045226707436735, - 0.021693900223194146, - 0.0, - 0.4447227770673619 - ] - }, - "name": "sb", - "weight": 1.0 + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.8896901334468729, + 0.029644742547909133, + 0.0, + 1.429641799472359e-130, + 0.0, + 0.08066512400521812 + ] + } + } }, { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.013697849284792386, - 0.03187180529165685 - ], - [ - [ - 0.0008931916737143883, - 0.00016960376600592337 - ], - [ - 0.00016960376600592337, - 0.0004082053891664813 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0006997403911677459, + 0.000197241011394837 + ], + [ + 0.000197241011394837, + 0.00017257673760017698 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.008827958881681865, + -0.0015608819531710305 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.01158183812933871, - 0.04505296285990928 - ], - [ - [ - 0.024841318706922032, - -0.001932002543910128 - ], - [ - -0.001932002543910128, - 0.01859820830329953 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0050698935923880195, + -0.0020252898314969624 + ], + [ + -0.0020252898314969624, + 0.0014778595924074371 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.031742160869992694, + 0.010427224672933595 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.49559332236428194, - -1.4003207695102506 - ], - [ - [ - 0.05082507032632953, - -0.02489997080381228 - ], - [ - -0.02489997080381228, - 0.024879660571573092 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.00035202792732571197, + -7.625771115500745e-05 + ], + [ + -7.625771115500745e-05, + 0.0003118798146958923 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.0024529527282765636, + -0.016897856998535694 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.22880383487895914, - 2.233272904641477 - ], - [ - [ - 0.09272965372035101, - -0.11219290093828721 - ], - [ - -0.11219290093828721, - 0.15912577082362542 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0004567326578116454, + 4.4726963279365465e-05 + ], + [ + 4.4726963279365465e-05, + 0.0002455696041855514 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.0059796486520338745, + -0.010538630549651045 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.9360490778396979, - -0.6804093789388136 - ], - [ - [ - 0.017873757721209162, - -0.026395549313954637 - ], - [ - -0.026395549313954637, - 0.040930957947626384 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.002993239256349607, + 0.003495396492033091 + ], + [ + 0.003495396492033091, + 0.0041630276471643505 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.032888802555955984, + -0.28205734639824087 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.0457922759975125, - 0.011078260567076446 - ], - [ - [ - 0.001207008939553208, - -0.00027912092254045744 - ], - [ - -0.00027912092254045744, - 0.005590698650562169 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0017866950442623649, + 0.0007428793313783671 + ], + [ + 0.0007428793313783671, + 0.0025389327809397784 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.024975547985649188, + -0.01776568349982063 + ] + } + } } ], - "weights": [ - 0.8852277007581102, - 0.027215049800020423, - 0.0, - 4.320108657751521e-112, - 0.0, - 0.08755724944186935 - ] - }, - "name": "sc", - "weight": 1.0 + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.19326211776265484, + 0.003536095588351562, + 0.033835128929433765, + 0.7455382977987395, + 0.0012524582722173107, + 0.022575901648603035 + ] + } + } }, { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.009813232745128535, - -0.0017777092971490265 - ], - [ - [ - 0.000689854800964919, - 0.00019507901315179388 - ], - [ - 0.00019507901315179388, - 0.0001773865573264895 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0007380047121639887, + -0.000294352812212126 + ], + [ + -0.000294352812212126, + 0.0007622718466989874 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.029258631317761023, + -0.06785963954483779 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.08512671652286258, - -0.012229143100892226 - ], - [ - [ - 0.005941469797300738, - -0.001750034432757849 - ], - [ - -0.001750034432757849, - 0.0012328460151280292 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0006949909364778672, + 0.00039460183494006165 + ], + [ + 0.00039460183494006165, + 0.004227729978722817 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.10681333199205689, + -0.16431574895247184 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.002504102671403666, - -0.01642553264376932 - ], - [ - [ - 0.00032245511931481126, - -4.11777989793518e-05 - ], - [ - -4.11777989793518e-05, - 0.0003101694129874433 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.003279943647079228, + -0.0005722779496733451 + ], + [ + -0.0005722779496733451, + 0.004423250902669305 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.0982313136869901, + -0.05999869880340171 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.006375396381445229, - -0.010273488179072645 - ], - [ - [ - 0.00045300255570937623, - 4.87148853239048e-05 - ], - [ - 4.87148853239048e-05, - 0.00023526666319245232 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.007805912682974319, + -0.002365491335194562 + ], + [ + -0.002365491335194562, + 0.0017640650904148314 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.23967396096114335, + -0.044176610184626236 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.008157917093319726, - -0.23656374165005967 - ], - [ - [ - 0.01053832021444366, - 0.012013635966460615 - ], - [ - 0.012013635966460615, - 0.013785915148229667 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0003413497573864167, + -3.679660983239417e-05 + ], + [ + -3.679660983239417e-05, + 0.0001025775666203872 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.00706630215268268, + -0.0465621944038333 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.038808115196346096, - -0.002498990088827769 - ], - [ - [ - 0.002179809625555247, - 0.0012024144658443377 - ], - [ - 0.0012024144658443377, - 0.0032824562211315076 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0002389447751020781, + 9.378125589935128e-06 + ], + [ + 9.378125589935128e-06, + 0.0017752863476281002 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.02643426799967081, + -0.11778981749989963 + ] + } + } } ], - "weights": [ - 0.1925338820121368, - 0.0027224897608586107, - 0.033129928869399757, - 0.7500619541261082, - 0.0016592215297413138, - 0.019892523701755378 - ] - }, - "name": "sd", - "weight": 1.0 + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.3205850888601075, + 0.004480647679044943, + 0.2391029919382351, + 0.0065125893108611025, + 0.22315056355660529, + 0.20616811865514614 + ] + } + } }, { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.029429523168237944, - -0.0684572732693875 - ], - [ - [ - 0.0007740879596465278, - -0.00029782600700208284 - ], - [ - -0.00029782600700208284, - 0.0007669516770160771 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 3.895041183238874e-05, + 1.5110535804900476e-05 + ], + [ + 1.5110535804900476e-05, + 9.938342079244728e-05 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.052185394426412235, + -0.1264636712258031 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.11721780966197501, - -0.15883909287941314 - ], - [ - [ - 0.001388095165755166, - -0.00019780539016636765 - ], - [ - -0.00019780539016636765, - 0.0034637652651714193 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.008249500478174077, + -0.001725734835458333 + ], + [ + -0.001725734835458333, + 0.012903869169936433 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.0232896748355558, + -0.3131096562787241 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.09845961764496235, - -0.06028730644219839 - ], - [ - [ - 0.003042600451156792, - -0.0008032835022392746 - ], - [ - -0.0008032835022392746, - 0.0042710395327714675 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0018566431450146867, + 0.0029010466417030865 + ], + [ + 0.0029010466417030865, + 0.012389070286510307 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.06383665666202674, + -0.4301302922465954 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.23373778140525506, - -0.0416941936740481 - ], - [ - [ - 0.014422159139307058, - -0.0037061819592565572 - ], - [ - -0.0037061819592565572, - 0.002029534773624044 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0025198006868689493, + -0.003425815281748251 + ], + [ + -0.003425815281748251, + 0.015009296271451917 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.12427511677783334, + -0.33159599453676286 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.006611429769027014, - -0.046518283636927976 - ], - [ - [ - 0.00033990546567971017, - -2.9416599926317968e-05 - ], - [ - -2.9416599926317968e-05, - 9.90896577570092e-05 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.3230774852749861, + -0.22932274626399304 + ], + [ + -0.22932274626399304, + 0.20091304830455892 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.924277366844015, + -2.293939099737936 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.025797000427098937, - -0.1181334462714847 - ], - [ - [ - 0.00023159247532124545, - 3.3897455102418097e-05 - ], - [ - 3.3897455102418097e-05, - 0.001816622622830031 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0009102758658479725, + 6.688618538384926e-05 + ], + [ + 6.688618538384926e-05, + 0.005359558467161061 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.049820538605244914, + -0.23405186488447458 + ] + } + } } ], - "weights": [ - 0.32372417453627345, - 0.005646903148746796, - 0.22659220177199924, - 0.004005489639378, - 0.22764487385241908, - 0.21238635705118347 - ] - }, - "name": "se", - "weight": 1.0 + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.007929469644550718, + 0.002888493528012088, + 0.1676231612565627, + 0.25015820268295313, + 0.0, + 0.5714006728879214 + ] + } + } }, { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.05074696440427314, - -0.1236221473704218 - ], - [ - [ - 5.559248382208153e-05, - 1.3165481758496438e-05 - ], - [ - 1.3165481758496438e-05, - 0.00013013865111573925 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0008492927991259382, + 0.00036953053955581856 + ], + [ + 0.00036953053955581856, + 0.0016894558832424625 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.06650795758039411, + -0.03828225768903544 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.015282738290680588, - -0.33222340726233823 - ], - [ - [ - 0.014650109332722263, - -0.005839198971705541 - ], - [ - -0.005839198971705541, - 0.01674887631479012 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.005424608779422052, + 0.00026721868416634055 + ], + [ + 0.00026721868416634055, + 0.03496650501790332 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.1931965000364244, + -0.5774908509624352 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.062259385113171285, - -0.42242095683555436 - ], - [ - [ - 0.0024974664659826087, - 0.0035114610971988167 - ], - [ - 0.0035114610971988167, - 0.013409199384634364 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 1e-08, + 0.0 + ], + [ + 0.0, + 1.0000000888192436e-08 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.010217170843726848, + -1.782576822454449 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.1303437048519121, - -0.3185650760006381 - ], - [ - [ - 0.003012986237382397, - -0.004353982357664468 - ], - [ - -0.004353982357664468, - 0.01757187795578027 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.015281768031916079, + 0.006727682809622518 + ], + [ + 0.006727682809622518, + 0.004590248401683354 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.2552963883542676, + -1.2568871579388492 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.19726018779377366, + -0.48735363438086454 + ], + [ + -0.48735363438086454, + 1.6647880160509023 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -4.106099574147729, + 0.9651243360773101 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0101607357320042, + 0.0137489790772033 + ], + [ + 0.0137489790772033, + 0.04203423666629918 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.298875977792634, + -0.9654092605486819 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.005781355509645208, + 0.5109441053804828, + 0.0005427067289165129, + 0.01482887831318407, + 0.0, + 0.46790295406777144 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.001962099749312198, + 0.0003631866223520721 + ], + [ + 0.0003631866223520721, + 0.0007249053631467968 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.09315740685905076, + -0.009839348242461434 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.031889397321262704, + -0.013916481666086401 + ], + [ + -0.013916481666086401, + 0.014365209337659914 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.9265097779492681, + -1.5309388755370257 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.924277366844015, - -2.293939099737936 - ], - [ - [ - 0.3230774852749861, - -0.22932274626399304 - ], - [ - -0.22932274626399304, - 0.20091304830455892 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.06417880305787836, + -0.01566893633616545 + ], + [ + -0.01566893633616545, + 0.08423830135229891 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.6101341393146054, + -1.547727383776736 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.05047094423011195, - -0.2375420219268807 - ], - [ - [ - 0.0009041721419701907, - 6.261307479984376e-05 - ], - [ - 6.261307479984376e-05, - 0.005582789827387766 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.010520970052894667, + 0.024198280392720654 + ], + [ + 0.024198280392720654, + 0.0868262321535988 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.39743511415271965, + -0.4890166043279221 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.005228228513695661, + -0.004290989066737287 + ], + [ + -0.004290989066737287, + 0.005359314588070637 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.346206593792657, + -1.3038892915133093 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.023339123594084955, + 0.01816728623706648 + ], + [ + 0.01816728623706648, + 0.06091308072369845 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.5621829772614868, + -1.4552088705802286 + ] + } + } } ], - "weights": [ - 0.004553769405334752, - 0.003156410907173705, - 0.1578167147070365, - 0.25686813296391336, - 1.404443791269134e-309, - 0.5776049720165417 - ] - }, - "name": "sf", - "weight": 1.0 + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.006299372250790258, + 0.1842960344078894, + 0.04297926171697383, + 0.10066573873288805, + 0.008198558936925534, + 0.6575610339545329 + ] + } + } }, { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.06053025822113253, - -0.05678961892903366 - ], - [ - [ - 0.00029404322296734485, - -8.665365779096349e-06 - ], - [ - -8.665365779096349e-06, - 0.0013625554757067788 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 1.281951268242188e-08, + 2.8411929444230552e-08 + ], + [ + 2.8411929444230552e-08, + 2.9630399138709924e-07 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.06820732869652332, + -0.030739694751622748 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.19323757351166143, - -0.584549691234176 - ], - [ - [ - 0.0058120873156812395, - 0.0013860199736877563 - ], - [ - 0.0013860199736877563, - 0.03128595722962331 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.014255315490516243, + 0.00023233428227283082 + ], + [ + 0.00023233428227283082, + 0.023381465280093224 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.8280509668491328, + -2.0233563871055273 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.010217170843726848, - -1.782576822454449 - ], - [ - [ - 1.0000000888165264e-08, - 0.0 - ], - [ - 0.0, - 1e-08 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.03447413681035377, + -0.03948845510512655 + ], + [ + -0.03948845510512655, + 0.06112960343519014 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.8297998414459817, + -0.8305243338114182 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.25025819540284583, - -1.2533973955530195 - ], - [ - [ - 0.016840286697173506, - 0.007918130666011755 - ], - [ - 0.007918130666011755, - 0.005308803483273602 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.08238214884867226, + 0.10357156836380109 + ], + [ + 0.10357156836380109, + 0.16509228860251804 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.6038224344853582, + -0.4298387970597698 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -4.106099574147729, - 0.9651243360773101 - ], - [ - [ - 0.19726018779377366, - -0.48735363438086454 - ], - [ - -0.48735363438086454, - 1.6647880160509023 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.07935983585931271, + 0.013070792313596298 + ], + [ + 0.013070792313596298, + 0.1101082506602764 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.4207697137501396, + -1.2171099736997577 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.297710902151732, - -0.9689255885736716 - ], - [ - [ - 0.010894890364569101, - 0.015312121859834384 - ], - [ - 0.015312121859834384, - 0.044024214522729616 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.06870284941851519, + -0.05812524946160287 + ], + [ + -0.05812524946160287, + 0.1537331894060799 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.2513886217659775, + -1.728297281151107 + ] + } + } } ], - "weights": [ - 0.004480017358422617, - 0.5088508652515609, - 0.0005559228672557062, - 0.012654033516611539, - 1.445096435728279e-274, - 0.47345916100614927 - ] - }, - "name": "sg", - "weight": 1.0 + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.0003502807143412272, + 0.09046606834602917, + 0.15047159892933065, + 0.09533880681688435, + 0.14749232460435996, + 0.5158809205890548 + ] + } + } }, { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.0863381546690409, - -0.03146685189463013 - ], - [ - [ - 0.0006318857874222218, - -0.00010151830564735462 - ], - [ - -0.00010151830564735462, - 0.0020421944082882086 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.7509957355602215, + -0.4483626609922767 + ], + [ + -0.4483626609922767, + 2.1009006610264254 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -3.0318558205186696, + -0.606131152222811 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.9272673905710538, - -1.5315719104601915 - ], - [ - [ - 0.03590459981513257, - -0.016273381685460858 - ], - [ - -0.016273381685460858, - 0.015461774590793242 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.002170498202160566, + -0.0017584741374254158 + ], + [ + -0.0017584741374254158, + 0.005525196937021685 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.1448790682720736, + 0.09941095585911952 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.6303582579643107, - -1.5376521259268146 - ], - [ - [ - 0.06701932143715264, - -0.015114654490901097 - ], - [ - -0.015114654490901097, - 0.07771061489207423 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.23420446228074726, + 0.08444919858879225 + ], + [ + 0.08444919858879225, + 0.07718096700671238 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.9757080528164253, + -0.12635826480896817 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.3907371153589305, - -0.49106880392998 - ], - [ - [ - 0.009990510077411181, - 0.018002543049359165 - ], - [ - 0.018002543049359165, - 0.06266105201154165 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.012770246266524739, + 0.03540284455523518 + ], + [ + 0.03540284455523518, + 0.10952888518397509 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.2346438738431826, + -2.744229681274007 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.3625459175143544, - -1.296038286309833 - ], - [ - [ - 0.006056331947810139, - -0.0055313132541431375 - ], - [ - -0.0055313132541431375, - 0.0072087784551515535 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.07857390614309127, + -0.09378174561727863 + ], + [ + -0.09378174561727863, + 0.18071050305601855 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.8111897167794737, + -1.8064922746353294 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.5638732726134105, - -1.4509740352047997 - ], - [ - [ - 0.02380235062663921, - 0.019054846018397898 - ], - [ - 0.019054846018397898, - 0.06351884725172426 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.16246567201820478, + -0.16476229714755336 + ], + [ + -0.16476229714755336, + 0.6751434856877658 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -2.4813301038305045, + -0.35623254814678684 + ] + } + } } ], - "weights": [ - 0.005665436993786792, - 0.1925067595175893, - 0.041923582722676545, - 0.09112939775315052, - 0.008138253452983246, - 0.6606365695598135 + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.14273815443538637, + 0.007096462652893818, + 0.06166599941429372, + 0.00651136379756934, + 0.10848278234144826, + 0.6735052373584085 + ] + } + } + } + ], + "graph": { + "_gaitmap_obj": "HmmGraphState", + "params": { + "end_probs": { + "_obj_type": "Array", + "array": [ + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ] }, - "name": "sh", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.06645215320920503, - -0.013052978029981177 - ], - [ - [ - 3.083547951094923e-06, - 3.107258476581441e-05 - ], - [ - 3.107258476581441e-05, - 0.00031311513209533336 - ] - ] - ], - "frozen": false + "start_probs": { + "_obj_type": "Array", + "array": [ + 0.03876317262322824, + 0.35590729415701633, + 0.33544071521963925, + 0.19669724255778723, + 0.07319157544232907, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + "transition_probs": { + "_obj_type": "Array", + "array": [ + [ + 0.7057312725881639, + 0.028770718132282904, + 0.0, + 0.0431724345479052, + 0.0, + 0.022325574731648024, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.7798557912879199, + 0.020144208712080242, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.7797804242877058, + 0.020219575712294335, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.7532012064666431, + 0.04679879353335703, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.06787704359472536, + 0.0, + 0.0, + 0.0, + 0.7321229564052747, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5888317954975275, + 0.4111682045024725, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6665756708574827, + 0.3334243291425172, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6800610372080454, + 0.3199389627919546, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6940311667096045, + 0.3059688332903955, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6960613747358361, + 0.303938625264164, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6800290288208687, + 0.31997097117913126, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6908660524108106, + 0.3091339475891894, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6824646638708582, + 0.31753533612914164, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6529268521083446, + 0.3470731478916555, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6742615053779222, + 0.3257384946220778, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6899877046925306, + 0.3100122953074694, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6682986866590126, + 0.33170131334098735, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.7637699187064003, + 0.23623008129359976, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.8491883291845923, + 0.1508116708154076, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.7650673608944687, + 0.23493263910553136, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6320886814421868, + 0.36791131855781334, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5935127135914391, + 0.4064872864085609, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.564474114438885, + 0.43552588556111516, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.39833984547315493, + 0.6016601545268451 + ], + [ + 0.03883084098291283, + 0.0, + 0.0, + 0.0, + 0.0, + 0.36877975391303947, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5923894051040478 + ] + ] + } + } + }, + "name": "segmentation_model", + "state_names": [ + "s0", + "s1", + "s2", + "s3", + "s4", + "s5", + "s6", + "s7", + "s8", + "s9", + "sa", + "sb", + "sc", + "sd", + "se", + "sf", + "sg", + "sh", + "si", + "sj", + "sk", + "sl", + "sm", + "sn", + "so" + ] + } + }, + "cross_module_transitions": [], + "submodels": [ + { + "_gaitmap_obj": "HmmSubModelState", + "params": { + "model": { + "_gaitmap_obj": "FlatHmmState", + "params": { + "emissions": [ + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 8.842840026328015e-05, + 1.1338481537820202e-05 + ], + [ + 1.1338481537820202e-05, + 4.556413286920051e-06 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.007233451562330584, + -0.007330355514672498 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.014827626255887452, + 0.0025736693349791566 + ], + [ + 0.0025736693349791566, + 0.018481617784122783 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.11964294638342145, + 0.0577670288224613 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 1.233868175423583, + 0.0324464068411531 + ], + [ + 0.0324464068411531, + 1.1793321438109976 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.12794885444102908, + 0.014293171998573542 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.02301733242902098, + 0.2031283248948388, + 0.7738543426761403 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.8252683950341506, - -2.036248006521894 - ], - [ - [ - 0.015164498594261468, - 0.002324514083182148 - ], - [ - 0.002324514083182148, - 0.023326039373269394 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 3.807730126825786e-05, + 1.908955838909436e-06 + ], + [ + 1.908955838909436e-06, + 7.707601133292083e-06 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.005039454110249612, + -0.00034980388317343736 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.00028206582190738304, + 3.6470742173564024e-06 + ], + [ + 3.6470742173564024e-06, + 0.00030368662052212485 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.01103243231767086, + -6.192723859128065e-05 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.07420621661549168, + -0.03078488592290488 + ], + [ + -0.03078488592290488, + 0.1262019192108308 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.1797177941190743, + 0.16207185321752562 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.5295564238555952, + 0.4512804986504573, + 0.019163077493947505 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.8549093579697702, - -0.8060963688170146 - ], - [ - [ - 0.03456432962932951, - -0.04043819576353697 - ], - [ - -0.04043819576353697, - 0.06524705446144048 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 2.6313155747753145e-05, + -1.783739620524242e-07 + ], + [ + -1.783739620524242e-07, + 3.7963524012398994e-06 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.015346763424992856, + -3.6790391628333324e-05 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.05158150486093591, + -0.021966624348381664 + ], + [ + -0.021966624348381664, + 0.060126526318448295 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.2920953137724989, + 0.144045132678054 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0005022951002119547, + 0.0005178665208248837 + ], + [ + 0.0005178665208248837, + 0.0008672959899629389 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.005206473414310127, + -0.012791990525392677 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.9168180843120244, + 0.05965042684934262, + 0.02353148883863304 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.6199325410621133, - -0.4557811379503244 - ], - [ - [ - 0.06583033898123342, - 0.07721014651089114 - ], - [ - 0.07721014651089114, - 0.12324220783011819 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 3.853306236840277e-05, + 1.1098530393653894e-05 + ], + [ + 1.1098530393653894e-05, + 6.708836856898209e-05 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.011789218020809872, + -0.01018332715941769 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0027109303675238354, + 0.0005895498614017715 + ], + [ + 0.0005895498614017715, + 0.003877823455517969 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.003322532886390006, + -0.016737870080289687 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.027449237739849735, + -0.002872053648095782 + ], + [ + -0.002872053648095782, + 0.03360538152452191 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.08525857899573236, + -0.0488377859551271 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.016431881811618677, + 0.5877309363803197, + 0.39583718180806166 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.4338195871801298, - -1.2184940984906643 - ], - [ + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.00012429296435198056, + 2.06260989280824e-05 + ], + [ + 2.06260989280824e-05, + 0.00013011384295424634 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.0220888200130197, + -0.0030625800868904285 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.007414853848424646, + 0.008249052358414617 + ], + [ + 0.008249052358414617, + 0.045260554553124444 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.09800188317736504, + -0.3465116271591456 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 2.8917395676099207, + 0.5976362161613022 + ], + [ + 0.5976362161613022, + 1.0225423930138333 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.936793248998849, + -2.4940462409287374 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.8677435037776553, + 0.1277410966649748, + 0.004515399557369943 + ] + } + } + } + ], + "graph": { + "_gaitmap_obj": "HmmGraphState", + "params": { + "end_probs": { + "_obj_type": "Array", + "array": [ + 0.02056934881733034, + 0.0018245426253681637, + 0.0022358551368813723, + 0.0009010089526879675, + 0.001955412892527215 + ] + }, + "start_probs": { + "_obj_type": "Array", + "array": [ + 0.5850631086889839, + 0.11947492542857634, + 0.14086174236230364, + 0.13224562340444435, + 0.022354600115691858 + ] + }, + "transition_probs": { + "_obj_type": "Array", + "array": [ + [ + 0.9074951827010481, + 0.07193546848162154, + 0.0, + 0.0, + 0.0 + ], [ - 0.08719585180621496, - 0.013158083491567505 + 0.0, + 0.94909416209142, + 0.049081295283211916, + 0.0, + 0.0 ], [ - 0.013158083491567505, - 0.13353128018759655 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.2598238476561028, - -1.719814250883815 - ], - [ + 0.0, + 0.0, + 0.9423970523876716, + 0.055367092475447056, + 0.0 + ], [ - 0.07157795441916256, - -0.05915265436446085 + 0.0, + 0.0, + 0.0, + 0.9417576875514561, + 0.05734130349585611 ], [ - -0.05915265436446085, - 0.15127052514462735 + 0.08519924751300234, + 0.0, + 0.0, + 0.0, + 0.9128453395944705 ] ] - ], - "frozen": false + } } - ], - "weights": [ - 0.0016274614177954664, - 0.08243786868136634, - 0.16254662694854732, - 0.0847292046610475, - 0.15602490437884078, - 0.5126339339124028 + }, + "name": "transition_model-trained", + "state_names": [ + "s0", + "s1", + "s2", + "s3", + "s4" ] - }, - "name": "si", - "weight": 1.0 + } }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -3.08331640615476, - -0.42002013405963734 - ], - [ - [ - 0.8897960267180814, - -0.4828282126421503 - ], - [ - -0.4828282126421503, - 1.8864819931490393 - ] - ] - ], - "frozen": false + "name": "transition", + "role": "transition" + } + }, + { + "_gaitmap_obj": "HmmSubModelState", + "params": { + "model": { + "_gaitmap_obj": "FlatHmmState", + "params": { + "emissions": [ + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.2932194592373769, + 0.21134149736125363 + ], + [ + 0.21134149736125363, + 0.3844722708781578 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -2.724722078278822, + 1.9915188464133518 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.1258018118620837, + 0.03268783972099044 + ], + [ + 0.03268783972099044, + 0.06484802385053644 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.6945547288226518, + 0.2560157714515537 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.20727245631858418, + 0.18784664909434642 + ], + [ + 0.18784664909434642, + 0.27038877692794705 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -2.4919699535246735, + 1.3536090707748252 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.14264334757636107, + -0.0008131419084383741 + ], + [ + -0.0008131419084383741, + 0.2498846469036756 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -2.5621310517208085, + 0.22980081390054788 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.3281468084248711, + -0.18643228479944526 + ], + [ + -0.18643228479944526, + 0.5149454717059048 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.4154413684634914, + 1.5500380278531312 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 1.2607062895679058, + 0.46505700492524954 + ], + [ + 0.46505700492524954, + 2.706722355023741 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -3.345238257207037, + 2.5233670301321913 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.2355327614387686, + 0.057494594752470506, + 0.2626428997077363, + 0.24060010369417817, + 0.12754264748582916, + 0.07618699292101734 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.1580326794726197, - 0.094447380511384 - ], - [ - [ - 0.002128341444427401, - -0.002809722639680756 - ], - [ - -0.002809722639680756, - 0.008556111791608945 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.09996804486098272, + 0.06050851708242607 + ], + [ + 0.06050851708242607, + 0.17635779190130996 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.7930589020789591, + 2.776017698946987 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.26661496431849807, + 0.04533211383325763 + ], + [ + 0.04533211383325763, + 0.23499477853090558 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.07410480792811462, + 2.8369130167639804 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 1.8009599929123774, + 0.479280726833787 + ], + [ + 0.479280726833787, + 1.0132508140131788 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.5499648961782752, + 4.016524487247618 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.1950762112167603, + 0.050067757422238277 + ], + [ + 0.050067757422238277, + 0.04769252944268426 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.159929491910014, + 2.314834344841815 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.06072026114906567, + 0.025823716783847178 + ], + [ + 0.025823716783847178, + 0.18864286460386825 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.12128992309555985, + 0.9095184467635742 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.24375621645852313, + 0.06190626212456217 + ], + [ + 0.06190626212456217, + 0.12503450568373897 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.4307456569548131, + 3.018466419910416 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.25368257048318127, + 0.3411652775615879, + 0.07354865979398133, + 0.11471339990143538, + 0.09455027921178154, + 0.1223398130480325 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.00733019645414, - -0.10832062428498664 - ], - [ - [ - 0.18457933668774, - 0.041973577608714625 - ], - [ - 0.041973577608714625, - 0.0618877158511528 + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0843297312497741, + 0.04133439057213231 + ], + [ + 0.04133439057213231, + 0.14871587853137966 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.2802011321565605, + 2.500739011826583 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.38364850605834805, + 0.09221962856919243 + ], + [ + 0.09221962856919243, + 0.5185153864229178 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.9605234360877333, + 2.415775039498101 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.026014384995446, + 0.0026043157968326717 + ], + [ + 0.0026043157968326717, + 0.0043425368895156575 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.7321736458164637, + 0.4154981602303466 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.05641946558947635, + 0.016402205868372734 + ], + [ + 0.016402205868372734, + 0.047810521379711125 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.7751828510703308, + 0.6462409838355484 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.020193272960135265, + -0.0005018838421025301 + ], + [ + -0.0005018838421025301, + 0.04710664910564171 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.2710648987648279, + 1.1365894604122162 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.07004778999475839, + 0.006591811226348407 + ], + [ + 0.006591811226348407, + 0.2032920419957315 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.193912713048431, + 1.7591104210891768 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.18917134266322166, + 0.059234457848795864, + 6.369546945313624e-05, + 0.09119353461762139, + 0.14382680656165217, + 0.5165101628392558 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.6626160146039229, + -0.021945851053280268 + ], + [ + -0.021945851053280268, + 0.12105293229274139 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 2.588995717055121, + 0.8789043376811468 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.02209208248042997, + -0.0007065543531791519 + ], + [ + -0.0007065543531791519, + 0.010705971196589312 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.8034889546645247, + 0.12875200546532437 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.04321221967087894, + 0.035042780346912 + ], + [ + 0.035042780346912, + 0.08435484950404089 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.449093432630524, + 0.6975644415346912 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.03345096665657911, + 0.013501793650040743 + ], + [ + 0.013501793650040743, + 0.034990364907834665 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.7771752601252138, + 0.4884361937186219 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.007935994462359793, + -0.0012908658054826393 + ], + [ + -0.0012908658054826393, + 0.015869103125689548 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.5554288451528646, + 0.2910244131006228 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.11701600721357804, + 0.017944771790248344 + ], + [ + 0.017944771790248344, + 0.01638735251011646 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.9212711529186459, + 0.23692285397120277 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.0511894878453503, + 0.056286532580328064, + 0.3379563212723615, + 0.3207220937761932, + 0.23102873292190054, + 0.002816831603866322 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.16263305162878075, + -0.0086558700063166 + ], + [ + -0.0086558700063166, + 0.013265794297089984 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.135160280244912, + -0.20582274730759018 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.009065847876272537, + -0.001325353468483505 + ], + [ + -0.001325353468483505, + 0.024323351356194067 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.5888006563906878, + -0.08355686852250115 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.7413599936175174, + -0.14153652956695537 + ], + [ + -0.14153652956695537, + 0.13085166155366815 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 3.1145666080383294, + -0.26622013470717487 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.04821971474061846, + 0.02712810067158472 + ], + [ + 0.02712810067158472, + 0.03464927705368995 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.965936569593422, + 0.22769758495347936 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.05048279418396253, + 0.017208198318734342 + ], + [ + 0.017208198318734342, + 0.033513773245658664 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.9567186220908748, + -0.059774539111321724 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.20338331470545354, + 0.033060523133077106 + ], + [ + 0.033060523133077106, + 0.04682552911061615 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 2.303780455950171, + 0.011003844807491194 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.1911723678754558, + 0.19979701906744887, + 0.02050350871764629, + 0.2627348187063074, + 0.25019792781386685, + 0.07559435781927483 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.1314520414716851, + 0.009453990931379717 + ], + [ + 0.009453990931379717, + 0.032138606445269044 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.8492503509098017, + -0.517674096978054 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.24266237123325962, + -0.06956522848236406 + ], + [ + -0.06956522848236406, + 0.06389572907811006 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.4779806837478076, + -0.7673169330509768 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.043220713100580246, + 0.019850048196437552 + ], + [ + 0.019850048196437552, + 0.06280482423710004 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.4868813925919653, + -0.7592635645806777 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.003209322035000341, + -0.001122106672117212 + ], + [ + -0.001122106672117212, + 0.0037959423665965344 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.5499837897404262, + -0.44238497714333336 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.04268880169163222, + -0.0451262672569672 + ], + [ + -0.0451262672569672, + 0.07154674145328065 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.8440176223426598, + -0.7542580382187342 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.4728108559414795, + -0.16837431399085548 + ], + [ + -0.16837431399085548, + 0.2619421250390748 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 2.5812993921162994, + -1.1558478789372724 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.32249917441683423, + 0.29044915095751495, + 0.2964251165623499, + 0.0007399034165253291, + 0.04957554424664409, + 0.04031111040013156 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.11257065996238892, + 0.00422995061874635 + ], + [ + 0.00422995061874635, + 0.053041991998435546 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.6944985871963718, + -1.41481332143877 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.3883793056517977, + -0.24840027012110866 + ], + [ + -0.24840027012110866, + 0.3040576431994222 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.3880379606185462, + -1.992882538304213 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.11708375696743395, + -0.03300151179311639 + ], + [ + -0.03300151179311639, + 0.08401589894596406 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.894613245792346, + -1.6233424909890515 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.16580992319121038, + -0.032432764923117634 + ], + [ + -0.032432764923117634, + 0.1005612394231465 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.3791160119835582, + -1.7785598280424566 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.07576252505265106, + -0.07108608124993676 + ], + [ + -0.07108608124993676, + 0.1548823458667788 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.3995392648509291, + -0.8491146604912162 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.11006581691860405, + 0.054481500102950456 + ], + [ + 0.054481500102950456, + 0.0507797309141409 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 1.167491513223562, + -1.3969922884295676 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.15269786865266569, + 0.032832093191063075, + 0.15771216486137704, + 0.1575831622918626, + 0.26089659156134026, + 0.2382781194416913 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.06066619096130978, + -0.0345657843289396 + ], + [ + -0.0345657843289396, + 0.28220405348618727 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.14526418438830446, + -1.5393628764547853 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.030641588742904128, + 0.007226311809544816 + ], + [ + 0.007226311809544816, + 0.042641029223435996 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.3021341235263017, + -0.7311913259859005 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.20070706393994658, + -0.06789065902248924 + ], + [ + -0.06789065902248924, + 0.14746889140690772 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.47314790179396854, + -2.1342542481330145 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0008470381227825778, + 0.00039227448579345973 + ], + [ + 0.00039227448579345973, + 0.00540765875958852 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.09318530245112652, + -0.2260515953806998 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.025557738363392642, + -0.024841081291224087 + ], + [ + -0.024841081291224087, + 0.07542718216602565 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.5455792319996824, + -1.3950760479968436 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.1261871751915607, + -0.0407425710402396 + ], + [ + -0.0407425710402396, + 0.028358919658514 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.07773576131758664, + -1.728652277158465 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.2635593664385791, + 0.11191576048522776, + 0.32668904933163473, + 0.010416452387057726, + 0.16390090053764345, + 0.12351847081985741 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.1385388576634102, + 0.025437129539282632 + ], + [ + 0.025437129539282632, + 0.057222372152883484 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.0578077392832066, + -0.27535704369905123 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.3376502976933182, + -0.23893072679110938 + ], + [ + -0.23893072679110938, + 0.2636489067480696 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.0398243373112683, + -2.2104796127562936 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.013274970591163854, + -0.000749596332664595 + ], + [ + -0.000749596332664595, + 0.08933002469354956 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.20694293168809, + -0.34727263493800814 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.10387644431599524, + 0.03479042980432082 + ], + [ + 0.03479042980432082, + 0.1374803290943473 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.0276002136303142, + -0.8599718695622972 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0693486491821651, + -0.07887713154855715 + ], + [ + -0.07887713154855715, + 0.1647734266069371 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.7889536047997324, + -1.6837636830604956 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.013335831287349048, + 0.003730406086506273 + ], + [ + 0.003730406086506273, + 0.0023635147669782545 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.16054996901565144, + -0.3268177779960233 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.14096007183959808, + 0.061995119358853254, + 0.181400733513972, + 0.4078097490643425, + 0.20439129071910986, + 0.0034430355041242186 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.037373792958641974, + -0.058454082734011134 + ], + [ + -0.058454082734011134, + 0.13502033499130328 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.5727164350397596, + 0.6166587909319007 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.05553464551904911, + 0.03900398721187749 + ], + [ + 0.03900398721187749, + 0.15132009999417226 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.3822356433089114, + 0.24042336510856135 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0070427449331162606, + -0.0005876158762260867 + ], + [ + -0.0005876158762260867, + 0.005022376435244604 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.014382389802330763, + -0.19862001007236144 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.05924227017796291, + 0.021056737352547674 + ], + [ + 0.021056737352547674, + 0.050103042085747666 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.8791940813511246, + 0.5925553963089454 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.43977786147200093, + 0.059190727579042896 + ], + [ + 0.059190727579042896, + 0.7953945973221657 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -2.1360336122346895, + 0.33645421921019586 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 9.420023791560235e-05, + -0.0001623697125511453 + ], + [ + -0.0001623697125511453, + 0.00034866904731997245 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.006695852283756774, + -0.029249215613094914 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.08605312624347378, + 0.42834364232695404, + 0.0037605170256715167, + 0.4203337415623755, + 0.06020749073534929, + 0.0013014821061756818 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.006977820457102412, + 0.0033734421771272247 + ], + [ + 0.0033734421771272247, + 0.00828595676375109 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.10229168021919927, + 0.0006640836400204043 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.05841627488904584, + 0.022283459731048772 + ], + [ + 0.022283459731048772, + 0.02418519231468338 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.5833243527699679, + 1.0667242688694345 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.021117457977541967, + -0.0013522105135089048 + ], + [ + -0.0013522105135089048, + 0.020562690900044412 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.33293282473485025, + 0.8577476812437226 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0007851313121907877, + -0.00029129287704494824 + ], + [ + -0.00029129287704494824, + 0.019411777433438474 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.053952839795998166, + 0.6832643218179306 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.005761352237848329, + 0.0008089250395260794 + ], + [ + 0.0008089250395260794, + 0.06034918110414123 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.13473919281704502, + 0.7859986341314606 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.135216576131183, + -0.07379395917991213 + ], + [ + -0.07379395917991213, + 0.28068468195755925 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.4713914864540872, + 1.7356142142068267 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.007062827014285585, + 0.07373189941735797, + 0.2770281109678939, + 0.3119845519183723, + 0.30925310840561776, + 0.020939502276472458 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0006312762518363987, + 0.00011565843048043362 + ], + [ + 0.00011565843048043362, + 0.004072731393708572 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.04155842236963722, + 0.1710588670266402 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.000447340046416533, + 6.752776101500221e-05 + ], + [ + 6.752776101500221e-05, + 0.000203790774695365 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.02576901268008257, + 0.08866083541306206 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0035991736672574023, + -0.0037425097879729805 + ], + [ + -0.0037425097879729805, + 0.03133333380910439 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.08101378295388252, + 0.250400484904686 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.022808696763420802, + 0.00279700933776769 + ], + [ + 0.00279700933776769, + 0.021865673926420477 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.1691501993551277, + 0.24930444600241328 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0752085979801202, + 0.06448282495522933 + ], + [ + 0.06448282495522933, + 0.05579634529098662 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -2.2719619768144157, + 2.502586182579559 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0008169534080270306, + 0.0007215806175151005 + ], + [ + 0.0007215806175151005, + 0.013207629451113825 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.045511437732322295, + 0.3841912177925317 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.40009335823623604, + 0.12896729372953442, + 0.004522670743673502, + 0.021693900223194146, + 0.0, + 0.4447227770673619 ] - ] - ], - "frozen": false + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.2393895721920731, - -2.757170713177026 - ], - [ - [ - 0.013146245901879666, - 0.03651767150071825 - ], - [ - 0.03651767150071825, - 0.11282120803266092 + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0008931916737143883, + 0.00016960376600592337 + ], + [ + 0.00016960376600592337, + 0.0004082053891664813 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.013697849284792386, + 0.03187180529165685 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.024841318706922032, + -0.001932002543910128 + ], + [ + -0.001932002543910128, + 0.01859820830329953 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.01158183812933871, + 0.04505296285990928 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.05082507032632953, + -0.02489997080381228 + ], + [ + -0.02489997080381228, + 0.024879660571573092 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.49559332236428194, + -1.4003207695102506 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.09272965372035101, + -0.11219290093828721 + ], + [ + -0.11219290093828721, + 0.15912577082362542 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.22880383487895914, + 2.233272904641477 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.017873757721209162, + -0.026395549313954637 + ], + [ + -0.026395549313954637, + 0.040930957947626384 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.9360490778396979, + -0.6804093789388136 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.001207008939553208, + -0.00027912092254045744 + ], + [ + -0.00027912092254045744, + 0.005590698650562169 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.0457922759975125, + 0.011078260567076446 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.8852277007581101, + 0.02721504980002042, + 0.0, + 4.320108657751578e-112, + 0.0, + 0.08755724944186934 ] - ] - ], - "frozen": false + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.000689854800964919, + 0.00019507901315179388 + ], + [ + 0.00019507901315179388, + 0.0001773865573264895 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.009813232745128535, + -0.0017777092971490265 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.005941469797300738, + -0.001750034432757849 + ], + [ + -0.001750034432757849, + 0.0012328460151280292 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.08512671652286258, + -0.012229143100892226 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.00032245511931481126, + -4.11777989793518e-05 + ], + [ + -4.11777989793518e-05, + 0.0003101694129874433 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.002504102671403666, + -0.01642553264376932 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.00045300255570937623, + 4.87148853239048e-05 + ], + [ + 4.87148853239048e-05, + 0.00023526666319245232 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.006375396381445229, + -0.010273488179072645 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.01053832021444366, + 0.012013635966460615 + ], + [ + 0.012013635966460615, + 0.013785915148229667 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.008157917093319726, + -0.23656374165005967 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.002179809625555247, + 0.0012024144658443377 + ], + [ + 0.0012024144658443377, + 0.0032824562211315076 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.038808115196346096, + -0.002498990088827769 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.19253388201213678, + 0.0027224897608586094, + 0.033129928869399757, + 0.7500619541261081, + 0.001659221529741313, + 0.019892523701755375 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0007740879596465278, + -0.00029782600700208284 + ], + [ + -0.00029782600700208284, + 0.0007669516770160771 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.029429523168237944, + -0.0684572732693875 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.001388095165755166, + -0.00019780539016636765 + ], + [ + -0.00019780539016636765, + 0.0034637652651714193 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.11721780966197501, + -0.15883909287941314 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.003042600451156792, + -0.0008032835022392746 + ], + [ + -0.0008032835022392746, + 0.0042710395327714675 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.09845961764496235, + -0.06028730644219839 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.014422159139307058, + -0.0037061819592565572 + ], + [ + -0.0037061819592565572, + 0.002029534773624044 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.23373778140525506, + -0.0416941936740481 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.00033990546567971017, + -2.9416599926317968e-05 + ], + [ + -2.9416599926317968e-05, + 9.90896577570092e-05 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.006611429769027014, + -0.046518283636927976 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.00023159247532124545, + 3.3897455102418097e-05 + ], + [ + 3.3897455102418097e-05, + 0.001816622622830031 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.025797000427098937, + -0.1181334462714847 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.32372417453627345, + 0.005646903148746798, + 0.22659220177199924, + 0.004005489639378, + 0.2276448738524191, + 0.2123863570511835 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 5.559248382208153e-05, + 1.3165481758496438e-05 + ], + [ + 1.3165481758496438e-05, + 0.00013013865111573925 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.05074696440427314, + -0.1236221473704218 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.014650109332722263, + -0.005839198971705541 + ], + [ + -0.005839198971705541, + 0.01674887631479012 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.015282738290680588, + -0.33222340726233823 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0024974664659826087, + 0.0035114610971988167 + ], + [ + 0.0035114610971988167, + 0.013409199384634364 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.062259385113171285, + -0.42242095683555436 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.003012986237382397, + -0.004353982357664468 + ], + [ + -0.004353982357664468, + 0.01757187795578027 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.1303437048519121, + -0.3185650760006381 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.3230774852749861, + -0.22932274626399304 + ], + [ + -0.22932274626399304, + 0.20091304830455892 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.924277366844015, + -2.293939099737936 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0009041721419701907, + 6.261307479984376e-05 + ], + [ + 6.261307479984376e-05, + 0.005582789827387766 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.05047094423011195, + -0.2375420219268807 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.004553769405334751, + 0.0031564109071737043, + 0.1578167147070365, + 0.25686813296391336, + 1.404443791269144e-309, + 0.5776049720165417 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.00029404322296734485, + -8.665365779096349e-06 + ], + [ + -8.665365779096349e-06, + 0.0013625554757067788 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.06053025822113253, + -0.05678961892903366 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0058120873156812395, + 0.0013860199736877563 + ], + [ + 0.0013860199736877563, + 0.03128595722962331 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.19323757351166143, + -0.584549691234176 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 1.0000000888165264e-08, + 0.0 + ], + [ + 0.0, + 1e-08 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + 0.010217170843726848, + -1.782576822454449 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.016840286697173506, + 0.007918130666011755 + ], + [ + 0.007918130666011755, + 0.005308803483273602 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.25025819540284583, + -1.2533973955530195 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.19726018779377366, + -0.48735363438086454 + ], + [ + -0.48735363438086454, + 1.6647880160509023 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -4.106099574147729, + 0.9651243360773101 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.010894890364569101, + 0.015312121859834384 + ], + [ + 0.015312121859834384, + 0.044024214522729616 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.297710902151732, + -0.9689255885736716 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.004480017358422617, + 0.508850865251561, + 0.0005559228672557062, + 0.012654033516611539, + 1.4450964357282184e-274, + 0.47345916100614927 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.0006318857874222218, + -0.00010151830564735462 + ], + [ + -0.00010151830564735462, + 0.0020421944082882086 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.0863381546690409, + -0.03146685189463013 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.03590459981513257, + -0.016273381685460858 + ], + [ + -0.016273381685460858, + 0.015461774590793242 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.9272673905710538, + -1.5315719104601915 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.06701932143715264, + -0.015114654490901097 + ], + [ + -0.015114654490901097, + 0.07771061489207423 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.6303582579643107, + -1.5376521259268146 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.009990510077411181, + 0.018002543049359165 + ], + [ + 0.018002543049359165, + 0.06266105201154165 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.3907371153589305, + -0.49106880392998 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.006056331947810139, + -0.0055313132541431375 + ], + [ + -0.0055313132541431375, + 0.0072087784551515535 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.3625459175143544, + -1.296038286309833 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.02380235062663921, + 0.019054846018397898 + ], + [ + 0.019054846018397898, + 0.06351884725172426 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.5638732726134105, + -1.4509740352047997 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.005665436993786792, + 0.19250675951758933, + 0.041923582722676545, + 0.09112939775315053, + 0.008138253452983246, + 0.6606365695598135 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.822114254106643, - -1.805006324351433 - ], - [ - [ - 0.07356176103493177, - -0.0837695083590111 - ], - [ - -0.0837695083590111, - 0.15545013202681926 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 3.083547951094923e-06, + 3.107258476581441e-05 + ], + [ + 3.107258476581441e-05, + 0.00031311513209533336 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.06645215320920503, + -0.013052978029981177 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.015164498594261468, + 0.002324514083182148 + ], + [ + 0.002324514083182148, + 0.023326039373269394 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.8252683950341506, + -2.036248006521894 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.03456432962932951, + -0.04043819576353697 + ], + [ + -0.04043819576353697, + 0.06524705446144048 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.8549093579697702, + -0.8060963688170146 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.06583033898123342, + 0.07721014651089114 + ], + [ + 0.07721014651089114, + 0.12324220783011819 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.6199325410621133, + -0.4557811379503244 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.08719585180621496, + 0.013158083491567505 + ], + [ + 0.013158083491567505, + 0.13353128018759655 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.4338195871801298, + -1.2184940984906643 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.07157795441916256, + -0.05915265436446085 + ], + [ + -0.05915265436446085, + 0.15127052514462735 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.2598238476561028, + -1.719814250883815 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.001627461417795466, + 0.08243786868136631, + 0.16254662694854732, + 0.08472920466104748, + 0.15602490437884076, + 0.5126339339124026 + ] + } + } }, { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -2.5373272787685486, - -0.27244401433033716 - ], - [ - [ - 0.17213498736946134, - -0.18995189113367558 - ], - [ - -0.18995189113367558, - 0.7295565815732279 - ] - ] - ], - "frozen": false + "_gaitmap_obj": "GaussianMixtureEmissionState", + "params": { + "components": [ + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.8897960267180814, + -0.4828282126421503 + ], + [ + -0.4828282126421503, + 1.8864819931490393 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -3.08331640615476, + -0.42002013405963734 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.002128341444427401, + -0.002809722639680756 + ], + [ + -0.002809722639680756, + 0.008556111791608945 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -0.1580326794726197, + 0.094447380511384 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.18457933668774, + 0.041973577608714625 + ], + [ + 0.041973577608714625, + 0.0618877158511528 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.00733019645414, + -0.10832062428498664 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.013146245901879666, + 0.03651767150071825 + ], + [ + 0.03651767150071825, + 0.11282120803266092 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.2393895721920731, + -2.757170713177026 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.07356176103493177, + -0.0837695083590111 + ], + [ + -0.0837695083590111, + 0.15545013202681926 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -1.822114254106643, + -1.805006324351433 + ] + } + } + }, + { + "_gaitmap_obj": "GaussianEmissionState", + "params": { + "covariance": { + "_obj_type": "Array", + "array": [ + [ + 0.17213498736946134, + -0.18995189113367558 + ], + [ + -0.18995189113367558, + 0.7295565815732279 + ] + ] + }, + "covariance_type": "full", + "frozen": false, + "mean": { + "_obj_type": "Array", + "array": [ + -2.5373272787685486, + -0.27244401433033716 + ] + } + } + } + ], + "frozen": false, + "weights": { + "_obj_type": "Array", + "array": [ + 0.14599848918033992, + 0.00484390441923573, + 0.0637979000892877, + 0.0066480502847708674, + 0.09733508574040968, + 0.6813765702859561 + ] + } + } } ], - "weights": [ - 0.14599848918033992, - 0.004843904419235729, - 0.0637979000892877, - 0.006648050284770866, - 0.09733508574040971, - 0.6813765702859561 - ] - }, - "name": "sj", - "weight": 1.0 - }, - { - "class": "State", - "distribution": null, - "name": "None-start", - "weight": 1.0 - }, - { - "class": "State", - "distribution": null, - "name": "None-end", - "weight": 1.0 - } - ], - "end_index": 21, - "start_index": 20, - "silent_index": 20, - "edges": [ - [ - 0, - 0, - 0.6954172648012702, - 1.0, - null - ], - [ - 0, - 1, - 0.30458273519872986, - 1.0, - null - ], - [ - 1, - 1, - 0.6844013426420876, - 1.0, - null - ], - [ - 1, - 2, - 0.31559865735791237, - 1.0, - null - ], - [ - 2, - 2, - 0.6843798839610127, - 1.0, - null - ], - [ - 2, - 3, - 0.3156201160389874, - 1.0, - null - ], - [ - 3, - 3, - 0.699144506945995, - 1.0, - null - ], - [ - 3, - 4, - 0.3008554930540052, - 1.0, - null - ], - [ - 4, - 4, - 0.7021252947139494, - 1.0, - null - ], - [ - 4, - 5, - 0.29787470528605076, - 1.0, - null - ], - [ - 5, - 5, - 0.68846903198653, - 1.0, - null - ], - [ - 5, - 6, - 0.31153096801347, - 1.0, - null - ], - [ - 6, - 6, - 0.6827534790694575, - 1.0, - null - ], - [ - 6, - 7, - 0.3172465209305424, - 1.0, - null - ], - [ - 7, - 7, - 0.6792167535169966, - 1.0, - null - ], - [ - 7, - 8, - 0.3207832464830032, - 1.0, - null - ], - [ - 8, - 8, - 0.6593498187398975, - 1.0, - null - ], - [ - 8, - 9, - 0.3406501812601026, - 1.0, - null - ], - [ - 9, - 9, - 0.6767793585516855, - 1.0, - null - ], - [ - 9, - 10, - 0.3232206414483146, - 1.0, - null - ], - [ - 10, - 10, - 0.694512552877693, - 1.0, - null - ], - [ - 10, - 11, - 0.305487447122307, - 1.0, - null - ], - [ - 11, - 11, - 0.6650337670131573, - 1.0, - null - ], - [ - 11, - 12, - 0.33496623298684286, - 1.0, - null - ], - [ - 12, - 12, - 0.7630892813327846, - 1.0, - null - ], - [ - 12, - 13, - 0.23691071866721528, - 1.0, - null - ], - [ - 13, - 13, - 0.838730899927014, - 1.0, - null - ], - [ - 13, - 14, - 0.16126910007298595, - 1.0, - null - ], - [ - 14, - 14, - 0.7617013308320338, - 1.0, - null - ], - [ - 14, - 15, - 0.23829866916796624, - 1.0, - null - ], - [ - 15, - 15, - 0.6362766698886175, - 1.0, - null - ], - [ - 15, - 16, - 0.3637233301113825, - 1.0, - null - ], - [ - 16, - 16, - 0.5969557414010658, - 1.0, - null - ], - [ - 16, - 17, - 0.4030442585989343, - 1.0, - null - ], - [ - 17, - 17, - 0.5687771519363383, - 1.0, - null - ], - [ - 17, - 18, - 0.4312228480636618, - 1.0, - null - ], - [ - 18, - 18, - 0.4099471519796639, - 1.0, - null - ], - [ - 18, - 19, - 0.590052848020336, - 1.0, - null - ], - [ - 19, - 19, - 0.6040090788637088, - 1.0, - null - ], - [ - 19, - 21, - 0.3959909211362912, - 1.0, - null - ], - [ - 20, - 0, - 1.0, - 1.0, - null - ] - ], - "distribution ties": [] - } - }, - "n_gmm_components": 6, - "n_states": 20, - "name": "stride_model", - "random_seed": null, - "stop_threshold": 1e-09, - "verbose": true - } - }, - "transition_model": { - "_gaitmap_obj": "SimpleHmm", - "params": { - "algo_train": "baum-welch", - "architecture": "left-right-loose", - "data_columns": ["raw__gyr_ml", "gradient__gradient__gyr_ml"], - "max_iterations": 10, - "model": { - "_obj_type": "HiddenMarkovModel", - "hmm": { - "class": "HiddenMarkovModel", - "name": "transition_model-trained", - "start": { - "class": "State", - "distribution": null, - "name": "None-start", - "weight": 1.0 - }, - "end": { - "class": "State", - "distribution": null, - "name": "None-end", - "weight": 1.0 - }, - "states": [ - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.007233451562330584, - -0.007330355514672498 - ], - [ - [ - 8.842840026328015e-05, - 1.1338481537820202e-05 + "graph": { + "_gaitmap_obj": "HmmGraphState", + "params": { + "end_probs": { + "_obj_type": "Array", + "array": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.39599092113629114 + ] + }, + "start_probs": { + "_obj_type": "Array", + "array": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + "transition_probs": { + "_obj_type": "Array", + "array": [ + [ + 0.6954172648012702, + 0.30458273519872986, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ], [ - 1.1338481537820202e-05, - 4.556413286920051e-06 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.11964294638342145, - 0.0577670288224613 - ], - [ - [ - 0.014827626255887452, - 0.0025736693349791566 + 0.0, + 0.6844013426420876, + 0.31559865735791237, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ], [ - 0.0025736693349791566, - 0.018481617784122783 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.12794885444102908, - 0.014293171998573542 - ], - [ - [ - 1.233868175423583, - 0.0324464068411531 + 0.0, + 0.0, + 0.6843798839610127, + 0.3156201160389874, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ], [ - 0.0324464068411531, - 1.1793321438109976 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.023017332429020973, - 0.2031283248948388, - 0.7738543426761402 - ] - }, - "name": "s0", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.005039454110249612, - -0.00034980388317343736 - ], - [ - [ - 3.807730126825786e-05, - 1.908955838909436e-06 + 0.0, + 0.0, + 0.0, + 0.699144506945995, + 0.30085549305400516, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ], [ - 1.908955838909436e-06, - 7.707601133292083e-06 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.01103243231767086, - -6.192723859128065e-05 - ], - [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.7021252947139494, + 0.2978747052860507, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], [ - 0.00028206582190738304, - 3.6470742173564024e-06 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.68846903198653, + 0.31153096801346997, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ], [ - 3.6470742173564024e-06, - 0.00030368662052212485 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.1797177941190743, - 0.16207185321752562 - ], - [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6827534790694575, + 0.3172465209305424, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], [ - 0.07420621661549168, - -0.03078488592290488 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6792167535169966, + 0.32078324648300316, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ], [ - -0.03078488592290488, - 0.1262019192108308 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.5295564238555952, - 0.4512804986504573, - 0.019163077493947505 - ] - }, - "name": "s1", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.015346763424992856, - -3.6790391628333324e-05 - ], - [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6593498187398975, + 0.34065018126010255, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], [ - 2.6313155747753145e-05, - -1.783739620524242e-07 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6767793585516855, + 0.3232206414483146, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ], [ - -1.783739620524242e-07, - 3.7963524012398994e-06 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.2920953137724989, - 0.144045132678054 - ], - [ - [ - 0.05158150486093591, - -0.021966624348381664 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.694512552877693, + 0.305487447122307, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ], [ - -0.021966624348381664, - 0.060126526318448295 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.005206473414310127, - -0.012791990525392677 - ], - [ - [ - 0.0005022951002119547, - 0.0005178665208248837 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6650337670131573, + 0.3349662329868428, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ], [ - 0.0005178665208248837, - 0.0008672959899629389 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.9168180843120244, - 0.0596504268493426, - 0.023531488838633038 - ] - }, - "name": "s2", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.011789218020809872, - -0.01018332715941769 - ], - [ - [ - 3.853306236840277e-05, - 1.1098530393653894e-05 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.7630892813327846, + 0.23691071866721525, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ], [ - 1.1098530393653894e-05, - 6.708836856898209e-05 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.003322532886390006, - -0.016737870080289687 - ], - [ - [ - 0.0027109303675238354, - 0.0005895498614017715 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.838730899927014, + 0.16126910007298595, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ], [ - 0.0005895498614017715, - 0.003877823455517969 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.08525857899573236, - -0.0488377859551271 - ], - [ - [ - 0.027449237739849735, - -0.002872053648095782 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.7617013308320338, + 0.23829866916796624, + 0.0, + 0.0, + 0.0, + 0.0 ], [ - -0.002872053648095782, - 0.03360538152452191 - ] - ] - ], - "frozen": false - } - ], - "weights": [ - 0.01643188181161868, - 0.5877309363803197, - 0.39583718180806166 - ] - }, - "name": "s3", - "weight": 1.0 - }, - { - "class": "State", - "distribution": { - "class": "GeneralMixtureModel", - "distributions": [ - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - 0.0220888200130197, - -0.0030625800868904285 - ], - [ - [ - 0.00012429296435198056, - 2.06260989280824e-05 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6362766698886175, + 0.3637233301113825, + 0.0, + 0.0, + 0.0 ], [ - 2.06260989280824e-05, - 0.00013011384295424634 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -0.09800188317736504, - -0.3465116271591456 - ], - [ - [ - 0.007414853848424646, - 0.008249052358414617 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5969557414010658, + 0.4030442585989343, + 0.0, + 0.0 ], [ - 0.008249052358414617, - 0.045260554553124444 - ] - ] - ], - "frozen": false - }, - { - "class": "Distribution", - "name": "MultivariateGaussianDistribution", - "parameters": [ - [ - -1.936793248998849, - -2.4940462409287374 - ], - [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5687771519363383, + 0.4312228480636618, + 0.0 + ], [ - 2.8917395676099207, - 0.5976362161613022 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4099471519796639, + 0.590052848020336 ], [ - 0.5976362161613022, - 1.0225423930138333 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6040090788637088 ] ] - ], - "frozen": false + } } - ], - "weights": [ - 0.8677435037776553, - 0.12774109666497482, - 0.004515399557369942 + }, + "name": "stride_model-trained", + "state_names": [ + "s0", + "s1", + "s2", + "s3", + "s4", + "s5", + "s6", + "s7", + "s8", + "s9", + "sa", + "sb", + "sc", + "sd", + "se", + "sf", + "sg", + "sh", + "si", + "sj" ] - }, - "name": "s4", - "weight": 1.0 - }, - { - "class": "State", - "distribution": null, - "name": "None-start", - "weight": 1.0 + } }, - { - "class": "State", - "distribution": null, - "name": "None-end", - "weight": 1.0 - } - ], - "end_index": 6, - "start_index": 5, - "silent_index": 5, - "edges": [ - [ - 0, - 0, - 0.9074951827010481, - 1.0, - null - ], - [ - 0, - 1, - 0.07193546848162155, - 1.0, - null - ], - [ - 0, - 6, - 0.020569348817330343, - 1.0, - null - ], - [ - 1, - 1, - 0.94909416209142, - 1.0, - null - ], - [ - 1, - 2, - 0.04908129528321192, - 1.0, - null - ], - [ - 1, - 6, - 0.0018245426253681643, - 1.0, - null - ], - [ - 2, - 2, - 0.9423970523876716, - 1.0, - null - ], - [ - 2, - 3, - 0.05536709247544706, - 1.0, - null - ], - [ - 2, - 6, - 0.002235855136881373, - 1.0, - null - ], - [ - 3, - 3, - 0.9417576875514561, - 1.0, - null - ], - [ - 3, - 4, - 0.057341303495856116, - 1.0, - null - ], - [ - 3, - 6, - 0.0009010089526879678, - 1.0, - null - ], - [ - 4, - 0, - 0.08519924751300235, - 1.0, - null - ], - [ - 4, - 4, - 0.9128453395944705, - 1.0, - null - ], - [ - 4, - 6, - 0.0019554128925272155, - 1.0, - null - ], - [ - 5, - 0, - 0.5850631086889839, - 1.0, - null - ], - [ - 5, - 1, - 0.11947492542857635, - 1.0, - null - ], - [ - 5, - 2, - 0.14086174236230364, - 1.0, - null - ], - [ - 5, - 3, - 0.13224562340444437, - 1.0, - null - ], - [ - 5, - 4, - 0.022354600115691858, - 1.0, - null - ] - ], - "distribution ties": [] + "name": "stride", + "role": "stride" + } } - }, - "n_gmm_components": 3, - "n_states": 5, - "name": "transition_model", - "random_seed": null, - "stop_threshold": 1e-09, - "verbose": true + ], + "trained_with": { + "_gaitmap_obj": "BackendInfo", + "params": { + "backend_id": "pomegranate-legacy-migrated", + "backend_version": "0.14.6", + "state_schema_version": 1 + } + } } - }, - "verbose": true + } } -} \ No newline at end of file +} From 48a14f25dab794e923928a1fcd7580ef228d8550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 10:29:24 +0100 Subject: [PATCH 13/28] Refactor HMM backends into dedicated packages --- gaitmap/stride_segmentation/hmm.py | 36 +- .../stride_segmentation/hmm/__init__.py | 33 +- .../stride_segmentation/hmm/_backend.py | 441 +------------- .../stride_segmentation/hmm/_backend_base.py | 92 +++ .../hmm/_backend_common.py | 76 +++ .../hmm/_backend_legacy.py | 200 ------- .../hmm/_segmentation_model.py | 122 +--- .../stride_segmentation/hmm/_simple_model.py | 452 -------------- .../stride_segmentation/hmm/_state.py | 147 +---- .../stride_segmentation/hmm/_utils.py | 556 +----------------- .../hmm/legacy/__init__.py | 9 + .../hmm/legacy/_backend.py | 437 ++++++++++++++ .../stride_segmentation/hmm/legacy/_state.py | 168 ++++++ .../stride_segmentation/hmm/legacy/_utils.py | 316 ++++++++++ .../hmm/modern/__init__.py | 5 + .../hmm/modern/_backend.py | 285 +++++++++ .../stride_segmentation/hmm/modern/_state.py | 174 ++++++ .../stride_segmentation/hmm/modern/_utils.py | 175 ++++++ .../stride_segmentation/hmm/scipy/__init__.py | 5 + .../stride_segmentation/hmm/scipy/_backend.py | 66 +++ .../stride_segmentation/hmm/scipy/_utils.py | 107 ++++ .../test_stride_segmentation/test_roth_hmm.py | 176 ++++-- 22 files changed, 2167 insertions(+), 1911 deletions(-) create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_common.py delete mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_legacy.py delete mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_simple_model.py create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_state.py create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/__init__.py create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_utils.py create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/__init__.py create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/_backend.py create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/_utils.py diff --git a/gaitmap/stride_segmentation/hmm.py b/gaitmap/stride_segmentation/hmm.py index d6a494d7..96001608 100644 --- a/gaitmap/stride_segmentation/hmm.py +++ b/gaitmap/stride_segmentation/hmm.py @@ -7,6 +7,9 @@ """ +from importlib import import_module +from typing import TYPE_CHECKING + from gaitmap.utils._gaitmap_mad import patch_gaitmap_mad_import _gaitmap_mad_modules = { @@ -24,13 +27,16 @@ "HmmGraphState", "HmmSubModelConfig", "HmmSubModelState", + "PomegranateLegacyHmmBackend", "PomegranateHmmBackend", + "PomegranateModernHmmBackend", "SimpleHmm", "RothHmmConfig", "RothSegmentationHmm", "ScipyHmmInferenceBackend", "PreTrainedRothSegmentationModel", "BaseSegmentationHmm", + "get_default_hmm_backend", } if not (__getattr__ := patch_gaitmap_mad_import(_gaitmap_mad_modules, __name__)): @@ -50,15 +56,36 @@ HmmStrideSegmentation, HmmSubModelConfig, HmmSubModelState, - PomegranateHmmBackend, PreTrainedRothSegmentationModel, RothHmmConfig, RothHmmFeatureTransformer, RothSegmentationHmm, - ScipyHmmInferenceBackend, - SimpleHmm, + get_default_hmm_backend, ) + if TYPE_CHECKING: + from gaitmap_mad.stride_segmentation.hmm.legacy import ( + PomegranateLegacyHmmBackend, + ) + from gaitmap_mad.stride_segmentation.hmm.legacy import ( + PomegranateLegacyHmmBackend as PomegranateHmmBackend, + ) + from gaitmap_mad.stride_segmentation.hmm.legacy import SimpleHmm + from gaitmap_mad.stride_segmentation.hmm.modern import PomegranateModernHmmBackend + from gaitmap_mad.stride_segmentation.hmm.scipy import ScipyHmmInferenceBackend + + def __getattr__(name: str): + if name in { + "PomegranateLegacyHmmBackend", + "PomegranateHmmBackend", + "PomegranateModernHmmBackend", + "ScipyHmmInferenceBackend", + }: + return getattr(import_module("gaitmap_mad.stride_segmentation.hmm"), name) + if name == "SimpleHmm": + return import_module("gaitmap_mad.stride_segmentation.hmm.legacy").SimpleHmm + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + __all__ = [ "BackendInfo", @@ -76,10 +103,13 @@ "HmmSubModelConfig", "HmmSubModelState", "PomegranateHmmBackend", + "PomegranateLegacyHmmBackend", + "PomegranateModernHmmBackend", "PreTrainedRothSegmentationModel", "RothHmmConfig", "RothHmmFeatureTransformer", "RothSegmentationHmm", "ScipyHmmInferenceBackend", "SimpleHmm", + "get_default_hmm_backend", ] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py index dcef26d8..23378149 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py @@ -2,8 +2,10 @@ import multiprocessing import warnings +from importlib import import_module +from typing import TYPE_CHECKING -from gaitmap_mad.stride_segmentation.hmm._backend import BaseHmmBackend, PomegranateHmmBackend, ScipyHmmInferenceBackend +from gaitmap_mad.stride_segmentation.hmm._backend import BaseHmmBackend, BaseTrainableHmm, get_default_hmm_backend from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig, RothHmmConfig from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import ( BaseHmmFeatureTransformer, @@ -14,7 +16,6 @@ PreTrainedRothSegmentationModel, ) from gaitmap_mad.stride_segmentation.hmm._segmentation_model import BaseSegmentationHmm, RothSegmentationHmm -from gaitmap_mad.stride_segmentation.hmm._simple_model import SimpleHmm from gaitmap_mad.stride_segmentation.hmm._state import ( BackendInfo, CrossModuleTransition, @@ -26,6 +27,17 @@ HmmSubModelState, ) +if TYPE_CHECKING: + from gaitmap_mad.stride_segmentation.hmm.legacy import ( + PomegranateLegacyHmmBackend, + ) + from gaitmap_mad.stride_segmentation.hmm.legacy import ( + PomegranateLegacyHmmBackend as PomegranateHmmBackend, + ) + from gaitmap_mad.stride_segmentation.hmm.legacy import SimpleHmm + from gaitmap_mad.stride_segmentation.hmm.modern import PomegranateModernHmmBackend + from gaitmap_mad.stride_segmentation.hmm.scipy import ScipyHmmInferenceBackend + if multiprocessing.parent_process() is None: warnings.warn( "The hmm support in gaitmap is still quite experimental and you might run into some rough edges. " @@ -35,9 +47,23 @@ UserWarning, ) + +def __getattr__(name: str): + if name in { + "PomegranateLegacyHmmBackend", + "PomegranateHmmBackend", + "PomegranateModernHmmBackend", + "ScipyHmmInferenceBackend", + }: + return getattr(import_module("gaitmap_mad.stride_segmentation.hmm._backend"), name) + if name == "SimpleHmm": + return import_module("gaitmap_mad.stride_segmentation.hmm.legacy").SimpleHmm + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + __all__ = [ "BackendInfo", "BaseHmmBackend", + "BaseTrainableHmm", "BaseHmmFeatureTransformer", "BaseSegmentationHmm", "CompositeHmmConfig", @@ -51,10 +77,13 @@ "HmmSubModelConfig", "HmmSubModelState", "PomegranateHmmBackend", + "PomegranateLegacyHmmBackend", + "PomegranateModernHmmBackend", "PreTrainedRothSegmentationModel", "RothHmmConfig", "RothHmmFeatureTransformer", "RothSegmentationHmm", "ScipyHmmInferenceBackend", "SimpleHmm", + "get_default_hmm_backend", ] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py index 24eb4eed..c7c8a00a 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py @@ -1,432 +1,23 @@ -"""Backend abstractions for HMM training and inference.""" +"""Lazy facade for HMM backends.""" from __future__ import annotations -import copy -from typing import Literal +from importlib import import_module -import numpy as np -import pandas as pd -import pomegranate as pg -from pomegranate.hmm import History -from scipy.special import logsumexp -from scipy.stats import multivariate_normal +from gaitmap_mad.stride_segmentation.hmm._backend_base import BaseHmmBackend, BaseTrainableHmm, get_default_hmm_backend -from gaitmap.base import _BaseSerializable -from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig -from gaitmap_mad.stride_segmentation.hmm._simple_model import SimpleHmm -from gaitmap_mad.stride_segmentation.hmm._state import ( - BackendInfo, - CrossModuleTransition, - GaussianEmissionState, - GaussianMixtureEmissionState, - HMMState, - HmmSubModelState, - hmm_state_to_pomegranate_model, - pomegranate_model_to_flat_hmm_state, - pomegranate_model_to_hmm_state, -) -from gaitmap_mad.stride_segmentation.hmm._utils import ( - _clone_model, - _DataToShortError, - add_transition, - check_history_for_training_failure, - create_transition_matrix_fully_connected, - extract_transitions_starts_stops_from_hidden_state_sequence, - fix_model_names, - get_model_distributions, - labels_to_strings, - predict, -) +__all__ = ["BaseHmmBackend", "BaseTrainableHmm", "get_default_hmm_backend"] -class BaseHmmBackend(_BaseSerializable): - """Base abstraction for backend-specific HMM primitives.""" - - backend_id: str - - def __init__(self, backend_id: str) -> None: - self.backend_id = backend_id - - def create_submodel(self, config: HmmSubModelConfig) -> SimpleHmm: - """Create a backend-specific trainable flat HMM wrapper.""" - raise NotImplementedError - - def predict( - self, - model: HMMState, - data: pd.DataFrame, - *, - expected_columns: tuple[str, ...], - algorithm: Literal["viterbi", "map"], - verbose: bool, - ) -> np.ndarray: - """Predict hidden states with a serialized model.""" - raise NotImplementedError - - def finalize_model( - self, - *, - trained_models: dict[str, SimpleHmm], - labels_train_sequence: list[np.ndarray], - data_sequence_feature_space: list[pd.DataFrame], - data_columns: tuple[str, ...], - model_config: CompositeHmmConfig, - module_offsets: dict[str, int], - initialization: Literal["labels", "fully-connected"], - algo_train: Literal["viterbi", "baum-welch"], - stop_threshold: float, - max_iterations: int, - verbose: bool, - n_jobs: int, - name: str, - ) -> tuple[HMMState, History]: - """Create, train, and serialize the final combined HMM.""" - raise NotImplementedError - - -def _create_simple_hmm_from_config(config: HmmSubModelConfig) -> SimpleHmm: - return SimpleHmm( - n_states=config.n_states, - n_gmm_components=config.n_gmm_components, - architecture=config.architecture, - algo_train=config.algo_train, - stop_threshold=config.stop_threshold, - max_iterations=config.max_iterations, - verbose=config.verbose, - n_jobs=config.n_jobs, - name=config.name, - ) - - -def _build_submodel_states( - model_config: CompositeHmmConfig, trained_models: dict[str, SimpleHmm] -) -> tuple[HmmSubModelState, ...]: - return tuple( - HmmSubModelState( - name=module.name, - role=module.role, - model=pomegranate_model_to_flat_hmm_state(trained_models[module.name].model), - ) - for module in model_config.modules - ) - - -def _extract_cross_module_transitions( - compiled_state: HMMState, - module_offsets: dict[str, int], -) -> tuple[CrossModuleTransition, ...]: - transitions = [] - transition_matrix = compiled_state.compiled.graph.transition_probs - module_sizes = {submodel.name: len(submodel.model.state_names) for submodel in compiled_state.submodels} - ordered_modules = tuple(submodel.name for submodel in compiled_state.submodels) - for from_module in ordered_modules: - from_offset = module_offsets[from_module] - from_size = module_sizes[from_module] - for to_module in ordered_modules: - if from_module == to_module: - continue - to_offset = module_offsets[to_module] - to_size = module_sizes[to_module] - for from_state in range(from_size): - for to_state in range(to_size): - probability = transition_matrix[from_offset + from_state, to_offset + to_state] - if probability <= 0: - continue - transitions.append( - CrossModuleTransition( - from_module=from_module, - from_state=from_state, - to_module=to_module, - to_state=to_state, - probability=float(probability), - ) - ) - return tuple(transitions) - - -class PomegranateHmmBackend(BaseHmmBackend): - """`pomegranate 0.14` backend for HMM training and inference.""" - - def __init__(self, backend_id: str = "pomegranate-legacy") -> None: - super().__init__(backend_id=backend_id) - - def create_submodel(self, config: HmmSubModelConfig) -> SimpleHmm: - """Create a pomegranate-backed trainable submodel.""" - return _create_simple_hmm_from_config(config) - - def predict( - self, - model: HMMState, - data: pd.DataFrame, - *, - expected_columns: tuple[str, ...], - algorithm: Literal["viterbi", "map"], - verbose: bool, - ) -> np.ndarray: - """Compile the serialized state and predict hidden states.""" - runtime_model = hmm_state_to_pomegranate_model(model, verbose=verbose) - return predict(runtime_model, data, expected_columns=expected_columns, algorithm=algorithm) - - def finalize_model( - self, - *, - trained_models: dict[str, SimpleHmm], - labels_train_sequence: list[np.ndarray], - data_sequence_feature_space: list[pd.DataFrame], - data_columns: tuple[str, ...], - model_config: CompositeHmmConfig, - module_offsets: dict[str, int], - initialization: Literal["labels", "fully-connected"], - algo_train: Literal["viterbi", "baum-welch"], - stop_threshold: float, - max_iterations: int, - verbose: bool, - n_jobs: int, - name: str, - ) -> tuple[HMMState, History]: - """Build the final pomegranate model, train it, and convert it to `HMMState`.""" - distributions = [] - for module in model_config.modules: - distributions.extend(get_model_distributions(trained_models[module.name].model)) - - model = self._create_combined_model( - trained_models=trained_models, - labels_train_sequence=labels_train_sequence, - distributions=distributions, - model_config=model_config, - module_offsets=module_offsets, - initialization=initialization, - verbose=verbose, - ) - - labels_train_sequence_str = labels_to_strings(labels_train_sequence) - data_train_sequence = [ - np.ascontiguousarray(feature_data[list(data_columns)].to_numpy().copy()) - for feature_data in data_sequence_feature_space - ] - - _, history = model.fit( - sequences=np.array(data_train_sequence, dtype=object), - labels=np.array(labels_train_sequence_str, dtype=object).copy(), - algorithm=algo_train, - stop_threshold=stop_threshold, - max_iterations=max_iterations, - return_history=True, - verbose=verbose, - n_jobs=n_jobs, - multiple_check_input=False, - ) - check_history_for_training_failure(history) - model.name = name - - submodel_states = _build_submodel_states(model_config, trained_models) - model_state = pomegranate_model_to_hmm_state( - model, - submodels=submodel_states, - backend_info=BackendInfo(backend_id=self.backend_id), - ) - model_state.cross_module_transitions = _extract_cross_module_transitions(model_state, module_offsets) - return model_state, history - - def _create_combined_model( - self, - *, - trained_models: dict[str, SimpleHmm], - labels_train_sequence: list[np.ndarray], - distributions: list[pg.Distribution], - model_config: CompositeHmmConfig, - module_offsets: dict[str, int], - initialization: Literal["labels", "fully-connected"], - verbose: bool, - ) -> pg.HiddenMarkovModel: - n_states = sum(module.n_states for module in model_config.modules) - if initialization == "fully-connected": - trans_mat, start_probs, _end_probs = create_transition_matrix_fully_connected(n_states) - model = pg.HiddenMarkovModel.from_matrix( - transition_probabilities=copy.deepcopy(trans_mat), - distributions=copy.deepcopy(distributions), - starts=start_probs, - ends=None, - state_names=None, - verbose=verbose, - ) - else: - trans_mat = np.zeros((n_states, n_states)) - for module in model_config.modules: - module_transition_matrix = trained_models[module.name].model.dense_transition_matrix()[:-2, :-2] - offset = module_offsets[module.name] - trans_mat[ - offset : offset + module.n_states, - offset : offset + module.n_states, - ] = module_transition_matrix - - transitions, starts, _ = extract_transitions_starts_stops_from_hidden_state_sequence(labels_train_sequence) - - start_probs = np.zeros(n_states) - start_probs[starts] = 1.0 - - model = pg.HiddenMarkovModel.from_matrix( - transition_probabilities=copy.deepcopy(trans_mat), - distributions=copy.deepcopy(distributions), - starts=start_probs, - ends=None, - state_names=None, - verbose=verbose, - ) - - existing_transitions = {(start.name, end.name) for start, end in model.graph.edges()} - for transition in sorted(transitions - existing_transitions): - add_transition(model, transition, 0.1) - - model = fix_model_names(model) - model.bake() - model.freeze_distributions() - return _clone_model(model, assert_correct=False) - - -def _prepare_predict_data(data: pd.DataFrame, expected_columns: tuple[str, ...], n_states: int) -> np.ndarray: - try: - data = data[list(expected_columns)] - except KeyError as e: - raise ValueError( - "The provided feature data is expected to have the following columns:\n\n" - f"{expected_columns}\n\n" - "But it only has the following columns:\n\n" - f"{data.columns}" - ) from e - - if len(data) < n_states: - raise _DataToShortError( - "The provided feature data is expected to have at least as many samples as the number of states " - f"of the model ({n_states}). " - f"But it only has {len(data)} samples." - ) - return np.ascontiguousarray(data.to_numpy()) - - -def _log_emission_probabilities(model: HMMState, observations: np.ndarray) -> np.ndarray: - log_emissions = np.empty((len(observations), len(model.compiled.emissions)), dtype=float) - for state_idx, emission in enumerate(model.compiled.emissions): - if isinstance(emission, GaussianEmissionState): - log_emissions[:, state_idx] = multivariate_normal.logpdf( - observations, - mean=emission.mean, - cov=emission.covariance, - allow_singular=True, - ) - continue - if isinstance(emission, GaussianMixtureEmissionState): - component_log_probs = np.column_stack([ - multivariate_normal.logpdf( - observations, - mean=component.mean, - cov=component.covariance, - allow_singular=True, - ) - for component in emission.components - ]) - with np.errstate(divide="ignore"): - log_weights = np.log(np.asarray(emission.weights, dtype=float)) - log_emissions[:, state_idx] = logsumexp(component_log_probs + log_weights, axis=1) - continue - raise TypeError(f"Unsupported serialized emission state `{type(emission).__name__}`.") - return log_emissions - - -def _viterbi_decode(model: HMMState, log_emissions: np.ndarray) -> np.ndarray: - with np.errstate(divide="ignore"): - transition_log_probs = np.log(np.asarray(model.compiled.graph.transition_probs, dtype=float)) - start_log_probs = np.log(np.asarray(model.compiled.graph.start_probs, dtype=float)) - - n_samples, n_states = log_emissions.shape - dp = np.full((n_samples, n_states), -np.inf, dtype=float) - pointers = np.zeros((n_samples, n_states), dtype=int) - - dp[0] = start_log_probs + log_emissions[0] - for sample_idx in range(1, n_samples): - scores = dp[sample_idx - 1][:, None] + transition_log_probs - pointers[sample_idx] = np.argmax(scores, axis=0) - dp[sample_idx] = scores[pointers[sample_idx], np.arange(n_states)] + log_emissions[sample_idx] - - # Match the behavior of `pomegranate 0.14`'s `model.predict(..., algorithm="viterbi")`, - # which returns a path that later gets trimmed to `path[1:-1]` in our compatibility wrapper. - last_state = int(np.argmax(dp[-1])) - path = np.zeros(n_samples, dtype=int) - path[-1] = last_state - for sample_idx in range(n_samples - 1, 0, -1): - path[sample_idx - 1] = pointers[sample_idx, path[sample_idx]] - return path[:-1] - - -def _map_decode(model: HMMState, log_emissions: np.ndarray) -> np.ndarray: - with np.errstate(divide="ignore"): - transition_log_probs = np.log(np.asarray(model.compiled.graph.transition_probs, dtype=float)) - start_log_probs = np.log(np.asarray(model.compiled.graph.start_probs, dtype=float)) - end_log_probs = np.log(np.asarray(model.compiled.graph.end_probs, dtype=float)) - - n_samples, n_states = log_emissions.shape - forward = np.full((n_samples, n_states), -np.inf, dtype=float) - backward = np.full((n_samples, n_states), -np.inf, dtype=float) - - forward[0] = start_log_probs + log_emissions[0] - for sample_idx in range(1, n_samples): - forward[sample_idx] = log_emissions[sample_idx] + logsumexp( - forward[sample_idx - 1][:, None] + transition_log_probs, - axis=0, - ) - - backward[-1] = end_log_probs - for sample_idx in range(n_samples - 2, -1, -1): - backward[sample_idx] = logsumexp( - transition_log_probs + log_emissions[sample_idx + 1][None, :] + backward[sample_idx + 1][None, :], - axis=1, - ) - - posterior = forward + backward - return np.argmax(posterior, axis=1) - - -class ScipyHmmInferenceBackend(BaseHmmBackend): - """SciPy-based inference-only backend operating directly on `HMMState`.""" - - def __init__(self, backend_id: str = "scipy-inference") -> None: - super().__init__(backend_id=backend_id) - - def create_submodel(self, config: HmmSubModelConfig) -> SimpleHmm: - raise NotImplementedError("ScipyHmmInferenceBackend is inference-only and can not create trainable submodels.") - - def finalize_model( - self, - *, - trained_models: dict[str, SimpleHmm], - labels_train_sequence: list[np.ndarray], - data_sequence_feature_space: list[pd.DataFrame], - data_columns: tuple[str, ...], - model_config: CompositeHmmConfig, - module_offsets: dict[str, int], - initialization: Literal["labels", "fully-connected"], - algo_train: Literal["viterbi", "baum-welch"], - stop_threshold: float, - max_iterations: int, - verbose: bool, - n_jobs: int, - name: str, - ) -> tuple[HMMState, History]: - raise NotImplementedError("ScipyHmmInferenceBackend is inference-only and can not finalize/train models.") - - def predict( - self, - model: HMMState, - data: pd.DataFrame, - *, - expected_columns: tuple[str, ...], - algorithm: Literal["viterbi", "map"], - verbose: bool, - ) -> np.ndarray: - del verbose - observations = _prepare_predict_data(data, expected_columns, len(model.compiled.state_names)) - log_emissions = _log_emission_probabilities(model, observations) - if algorithm == "viterbi": - return _viterbi_decode(model, log_emissions) - return _map_decode(model, log_emissions) +def __getattr__(name: str): + if name == "PomegranateLegacyHmmBackend": + return import_module("gaitmap_mad.stride_segmentation.hmm.legacy").PomegranateLegacyHmmBackend + if name == "PomegranateHmmBackend": + return import_module("gaitmap_mad.stride_segmentation.hmm.legacy").PomegranateLegacyHmmBackend + if name == "PomegranateModernHmmBackend": + return import_module("gaitmap_mad.stride_segmentation.hmm.modern").PomegranateModernHmmBackend + if name == "ScipyHmmInferenceBackend": + return import_module("gaitmap_mad.stride_segmentation.hmm.scipy").ScipyHmmInferenceBackend + if name == "SimpleHmm": + return import_module("gaitmap_mad.stride_segmentation.hmm.legacy").SimpleHmm + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py new file mode 100644 index 00000000..281d8d11 --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py @@ -0,0 +1,92 @@ +"""Base backend abstractions and backend selection.""" + +from __future__ import annotations + +from importlib import import_module +from typing import TYPE_CHECKING, Any, Literal, Sequence, TypeVar + +import numpy as np +import pandas as pd +from typing_extensions import Self + +from gaitmap.base import _BaseSerializable + +if TYPE_CHECKING: + from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig + from gaitmap_mad.stride_segmentation.hmm._state import HMMState + +TrainableModelT = TypeVar("TrainableModelT") +HmmInferenceResult = np.ndarray +HmmTrainingResult = tuple[TrainableModelT, Any] + + +class BaseTrainableHmm(_BaseSerializable): + """Backend-specific trainable flat-HMM primitive.""" + + def predict_hidden_state_sequence( + self, + feature_data: pd.DataFrame, + algorithm: Literal["viterbi", "map"] = "viterbi", + ) -> HmmInferenceResult: + raise NotImplementedError + + def self_optimize_with_info( + self, + data_sequence: Sequence[pd.DataFrame | np.ndarray], + labels_sequence: Sequence[np.ndarray], + ) -> HmmTrainingResult[Self]: + raise NotImplementedError + + +class BaseHmmBackend(_BaseSerializable): + """Base abstraction for backend-specific HMM primitives.""" + + backend_id: str + + def __init__(self, backend_id: str) -> None: + self.backend_id = backend_id + + def create_submodel(self, config: HmmSubModelConfig) -> BaseTrainableHmm: + raise NotImplementedError + + def predict( + self, + model: HMMState, + data: pd.DataFrame, + *, + expected_columns: tuple[str, ...], + algorithm: Literal["viterbi", "map"], + verbose: bool, + ) -> HmmInferenceResult: + raise NotImplementedError + + def finalize_model( + self, + *, + trained_models: dict[str, BaseTrainableHmm], + labels_train_sequence: list[np.ndarray], + data_sequence_feature_space: list[pd.DataFrame], + data_columns: tuple[str, ...], + model_config: CompositeHmmConfig, + module_offsets: dict[str, int], + initialization: Literal["labels", "fully-connected"], + algo_train: Literal["viterbi", "baum-welch"], + stop_threshold: float, + max_iterations: int, + verbose: bool, + n_jobs: int, + name: str, + ) -> HmmTrainingResult[HMMState]: + raise NotImplementedError + + +def get_default_hmm_backend() -> BaseHmmBackend: + """Return the default runtime backend for the installed environment.""" + try: + return import_module("gaitmap_mad.stride_segmentation.hmm.modern").PomegranateModernHmmBackend() + except ImportError: + pass + try: + return import_module("gaitmap_mad.stride_segmentation.hmm.legacy").PomegranateLegacyHmmBackend() + except ImportError: + return import_module("gaitmap_mad.stride_segmentation.hmm.scipy").ScipyHmmInferenceBackend() diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_common.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_common.py new file mode 100644 index 00000000..4db7939a --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_common.py @@ -0,0 +1,76 @@ +"""Shared helpers for multiple HMM backends.""" + +from __future__ import annotations + +import numpy as np +import pandas as pd + +from gaitmap_mad.stride_segmentation.hmm._state import CrossModuleTransition, HMMState +from gaitmap_mad.stride_segmentation.hmm._utils import _DataToShortError + + +def extract_cross_module_transitions( + compiled_state: HMMState, + module_offsets: dict[str, int], +) -> tuple[CrossModuleTransition, ...]: + transitions = [] + transition_matrix = compiled_state.compiled.graph.transition_probs + module_sizes = {submodel.name: len(submodel.model.state_names) for submodel in compiled_state.submodels} + ordered_modules = tuple(submodel.name for submodel in compiled_state.submodels) + for from_module in ordered_modules: + from_offset = module_offsets[from_module] + from_size = module_sizes[from_module] + for to_module in ordered_modules: + if from_module == to_module: + continue + to_offset = module_offsets[to_module] + to_size = module_sizes[to_module] + for from_state in range(from_size): + for to_state in range(to_size): + probability = transition_matrix[from_offset + from_state, to_offset + to_state] + if probability <= 0: + continue + transitions.append( + CrossModuleTransition( + from_module=from_module, + from_state=from_state, + to_module=to_module, + to_state=to_state, + probability=float(probability), + ) + ) + return tuple(transitions) + + +def prepare_predict_data(data: pd.DataFrame, expected_columns: tuple[str, ...], n_states: int) -> np.ndarray: + try: + data = data[list(expected_columns)] + except KeyError as e: + raise ValueError( + "The provided feature data is expected to have the following columns:\n\n" + f"{expected_columns}\n\n" + "But it only has the following columns:\n\n" + f"{data.columns}" + ) from e + + if len(data) < n_states: + raise _DataToShortError( + "The provided feature data is expected to have at least as many samples as the number of states " + f"of the model ({n_states}). " + f"But it only has {len(data)} samples." + ) + return np.ascontiguousarray(data.to_numpy()) + + +def normalize_transition_and_end_probs( + transition_probs: np.ndarray, end_probs: np.ndarray +) -> tuple[np.ndarray, np.ndarray]: + normalized_transitions = np.asarray(transition_probs, dtype=float).copy() + normalized_ends = np.asarray(end_probs, dtype=float).copy() + for row_idx in range(len(normalized_transitions)): + total = normalized_transitions[row_idx].sum() + normalized_ends[row_idx] + if total <= 0: + continue + normalized_transitions[row_idx] /= total + normalized_ends[row_idx] /= total + return normalized_transitions, normalized_ends diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_legacy.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_legacy.py deleted file mode 100644 index 6505e937..00000000 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_legacy.py +++ /dev/null @@ -1,200 +0,0 @@ -"""Legacy pomegranate backend.""" - -from __future__ import annotations - -import copy -from importlib import import_module -from typing import Any, Literal - -import numpy as np -import pandas as pd - -from gaitmap_mad.stride_segmentation.hmm._backend_base import BaseHmmBackend -from gaitmap_mad.stride_segmentation.hmm._backend_common import ( - extract_cross_module_transitions, - normalize_transition_and_end_probs, -) -from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig -from gaitmap_mad.stride_segmentation.hmm._pomegranate import create_state_names -from gaitmap_mad.stride_segmentation.hmm._state import ( - BackendInfo, - HMMState, - HmmSubModelState, - hmm_state_to_pomegranate_model, - pomegranate_model_to_flat_hmm_state, - pomegranate_model_to_hmm_state, -) -from gaitmap_mad.stride_segmentation.hmm._utils import ( - _clone_model, - check_history_for_training_failure, - create_transition_matrix_fully_connected, - estimate_sequence_boundary_probs, - extract_transitions_starts_stops_from_hidden_state_sequence, - fix_model_names, - get_model_distributions, - labels_to_strings, - predict, -) - - -def _create_simple_hmm_from_config(config: HmmSubModelConfig) -> Any: - simple_hmm = import_module("gaitmap_mad.stride_segmentation.hmm._simple_model").SimpleHmm - return simple_hmm( - n_states=config.n_states, - n_gmm_components=config.n_gmm_components, - architecture=config.architecture, - algo_train=config.algo_train, - stop_threshold=config.stop_threshold, - max_iterations=config.max_iterations, - verbose=config.verbose, - n_jobs=config.n_jobs, - name=config.name, - ) - - -class PomegranateLegacyHmmBackend(BaseHmmBackend): - """`pomegranate 0.x` backend for HMM training and inference.""" - - def __init__(self, backend_id: str = "pomegranate-legacy") -> None: - super().__init__(backend_id=backend_id) - - def create_submodel(self, config: HmmSubModelConfig) -> Any: - return _create_simple_hmm_from_config(config) - - def predict( - self, - model: HMMState, - data: pd.DataFrame, - *, - expected_columns: tuple[str, ...], - algorithm: Literal["viterbi", "map"], - verbose: bool, - ) -> np.ndarray: - runtime_model = hmm_state_to_pomegranate_model(model, verbose=verbose) - return predict(runtime_model, data, expected_columns=expected_columns, algorithm=algorithm) - - def finalize_model( - self, - *, - trained_models: dict[str, Any], - labels_train_sequence: list[np.ndarray], - data_sequence_feature_space: list[pd.DataFrame], - data_columns: tuple[str, ...], - model_config: CompositeHmmConfig, - module_offsets: dict[str, int], - initialization: Literal["labels", "fully-connected"], - algo_train: Literal["viterbi", "baum-welch"], - stop_threshold: float, - max_iterations: int, - verbose: bool, - n_jobs: int, - name: str, - ) -> tuple[HMMState, Any]: - - distributions = [] - for module in model_config.modules: - distributions.extend(get_model_distributions(trained_models[module.name].model)) - - model = self._create_combined_model( - trained_models=trained_models, - labels_train_sequence=labels_train_sequence, - distributions=distributions, - model_config=model_config, - module_offsets=module_offsets, - initialization=initialization, - verbose=verbose, - ) - - labels_train_sequence_str = labels_to_strings(labels_train_sequence) - data_train_sequence = [ - np.ascontiguousarray(feature_data[list(data_columns)].to_numpy().copy()) - for feature_data in data_sequence_feature_space - ] - - _, history = model.fit( - sequences=np.array(data_train_sequence, dtype=object), - labels=np.array(labels_train_sequence_str, dtype=object).copy(), - algorithm=algo_train, - stop_threshold=stop_threshold, - max_iterations=max_iterations, - return_history=True, - verbose=verbose, - n_jobs=n_jobs, - multiple_check_input=False, - ) - check_history_for_training_failure(history) - model.name = name - - submodel_states = tuple( - HmmSubModelState( - name=module.name, - role=module.role, - model=pomegranate_model_to_flat_hmm_state(trained_models[module.name].model), - ) - for module in model_config.modules - ) - model_state = pomegranate_model_to_hmm_state( - model, - submodels=submodel_states, - backend_info=BackendInfo(backend_id=self.backend_id), - ) - model_state.cross_module_transitions = extract_cross_module_transitions(model_state, module_offsets) - return model_state, history - - def _create_combined_model( - self, - *, - trained_models: dict[str, Any], - labels_train_sequence: list[np.ndarray], - distributions: list[Any], - model_config: CompositeHmmConfig, - module_offsets: dict[str, int], - initialization: Literal["labels", "fully-connected"], - verbose: bool, - ) -> Any: - pg = import_module("pomegranate") - - n_states = sum(module.n_states for module in model_config.modules) - if initialization == "fully-connected": - trans_mat, start_probs, end_probs = create_transition_matrix_fully_connected(n_states) - trans_mat, end_probs = normalize_transition_and_end_probs(trans_mat, end_probs) - model = pg.HiddenMarkovModel.from_matrix( - transition_probabilities=copy.deepcopy(trans_mat), - distributions=copy.deepcopy(distributions), - starts=start_probs, - ends=end_probs, - state_names=list(create_state_names(n_states)), - verbose=verbose, - ) - else: - trans_mat = np.zeros((n_states, n_states)) - for module in model_config.modules: - module_transition_matrix = trained_models[module.name].model.dense_transition_matrix()[:-2, :-2] - offset = module_offsets[module.name] - trans_mat[ - offset : offset + module.n_states, - offset : offset + module.n_states, - ] = module_transition_matrix - - transitions, _, _ = extract_transitions_starts_stops_from_hidden_state_sequence(labels_train_sequence) - start_probs, end_probs = estimate_sequence_boundary_probs(labels_train_sequence, n_states) - for from_state, to_state in transitions: - trans_mat[int(from_state[1:]), int(to_state[1:])] = max( - trans_mat[int(from_state[1:]), int(to_state[1:])], - 0.1, - ) - trans_mat, end_probs = normalize_transition_and_end_probs(trans_mat, end_probs) - - model = pg.HiddenMarkovModel.from_matrix( - transition_probabilities=copy.deepcopy(trans_mat), - distributions=copy.deepcopy(distributions), - starts=start_probs, - ends=end_probs, - state_names=list(create_state_names(n_states)), - verbose=verbose, - ) - - model = fix_model_names(model) - model.bake() - model.freeze_distributions() - return _clone_model(model, assert_correct=False) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py index 9754839b..e8052c84 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py @@ -1,14 +1,14 @@ """Segmentation _model base classes and helper.""" -import warnings +from __future__ import annotations + +# ruff: noqa: UP045 from collections.abc import Sequence from typing import Any, Literal, Optional import numpy as np import pandas as pd -import pomegranate as pg import tpcp -from pomegranate.hmm import History from tpcp import OptiPara, cf, make_optimize_safe from typing_extensions import Self @@ -17,19 +17,12 @@ SingleSensorData, SingleSensorRegionsOfInterestList, ) -from gaitmap_mad.stride_segmentation.hmm._backend import BaseHmmBackend, PomegranateHmmBackend +from gaitmap_mad.stride_segmentation.hmm._backend import BaseHmmBackend, BaseTrainableHmm, get_default_hmm_backend from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig, RothHmmConfig from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import RothHmmFeatureTransformer -from gaitmap_mad.stride_segmentation.hmm._simple_model import SimpleHmm -from gaitmap_mad.stride_segmentation.hmm._state import ( - BackendInfo, - HMMState, - HmmSubModelState, - pomegranate_model_to_flat_hmm_state, - pomegranate_model_to_hmm_state, -) +from gaitmap_mad.stride_segmentation.hmm._state import HMMState +from gaitmap_mad.stride_segmentation.hmm.legacy._utils import ShortenedHMMPrint from gaitmap_mad.stride_segmentation.hmm._utils import ( - ShortenedHMMPrint, _DataToShortError, convert_region_list_to_transition_list, get_train_data_sequences_regions, @@ -37,11 +30,13 @@ validate_trainable_region_list, ) +DEFAULT_HMM_BACKEND = get_default_hmm_backend() + def create_fully_labeled_hidden_state_sequences( data_train_sequence: Sequence[pd.DataFrame], region_list_sequence: Sequence[SingleSensorRegionsOfInterestList], - module_models: dict[str, SimpleHmm], + module_models: dict[str, Any], state_offsets: dict[str, int], transition_model_name: str, algo_predict: Literal["viterbi", "map"], @@ -140,7 +135,7 @@ def _get_training_sequences_for_module( def _predict_labeled_training_sequences( data_sequence_feature_space: list[pd.DataFrame], region_list_feature_space: list[SingleSensorRegionsOfInterestList], - trained_models: dict[str, SimpleHmm], + trained_models: dict[str, Any], module_offsets: dict[str, int], transition_model_name: str, algo_predict: Literal["viterbi", "map"], @@ -346,95 +341,6 @@ def _from_json_dict(cls, json_dict: dict) -> Self: n_jobs=params.pop("n_jobs", 1), name=params.pop("name", "segmentation_model"), ) - if "hmm_config" not in params and {"stride_model", "transition_model"} <= set(params): - # TODO: Remove this compatibility shim once legacy serialized Roth models have been migrated. - warnings.warn( - "Loading a legacy RothSegmentationHmm serialization with raw pomegranate models. " - "The model is migrated to the new HMMState representation during loading.", - UserWarning, - ) - stride_model = params.pop("stride_model") - transition_model = params.pop("transition_model") - stride_params = ( - stride_model.get_params(deep=False) if isinstance(stride_model, SimpleHmm) else stride_model["params"] - ) - transition_params = ( - transition_model.get_params(deep=False) - if isinstance(transition_model, SimpleHmm) - else transition_model["params"] - ) - params["hmm_config"] = RothHmmConfig( - model_config=CompositeHmmConfig( - modules=( - HmmSubModelConfig( - name="transition", - role="transition", - n_states=transition_params["n_states"], - n_gmm_components=transition_params["n_gmm_components"], - architecture=transition_params["architecture"], - algo_train=transition_params["algo_train"], - stop_threshold=transition_params["stop_threshold"], - max_iterations=transition_params["max_iterations"], - verbose=transition_params.get("verbose", True), - n_jobs=transition_params.get("n_jobs", 1), - ), - HmmSubModelConfig( - name="stride", - role="stride", - n_states=stride_params["n_states"], - n_gmm_components=stride_params["n_gmm_components"], - architecture=stride_params["architecture"], - algo_train=stride_params["algo_train"], - stop_threshold=stride_params["stop_threshold"], - max_iterations=stride_params["max_iterations"], - verbose=stride_params.get("verbose", True), - n_jobs=stride_params.get("n_jobs", 1), - ), - ) - ), - feature_transform=params.pop("feature_transform", RothHmmFeatureTransformer()), - algo_predict=params.pop("algo_predict", "viterbi"), - algo_train=params.pop("algo_train", "baum-welch"), - stop_threshold=params.pop("stop_threshold", 1e-9), - max_iterations=params.pop("max_iterations", 1), - initialization=params.pop("initialization", "labels"), - verbose=params.pop("verbose", True), - n_jobs=params.pop("n_jobs", 1), - name=params.pop("name", "segmentation_model"), - ) - legacy_submodels = [] - if getattr(transition_model, "model", None) is not None: - legacy_submodels.append( - HmmSubModelState( - name="transition", - role="transition", - model=pomegranate_model_to_flat_hmm_state(transition_model.model), - ) - ) - if getattr(stride_model, "model", None) is not None: - legacy_submodels.append( - HmmSubModelState( - name="stride", - role="stride", - model=pomegranate_model_to_flat_hmm_state(stride_model.model), - ) - ) - if params.get("model") is not None: - params["model"] = pomegranate_model_to_hmm_state( - params["model"], - submodels=tuple(legacy_submodels), - backend_info=BackendInfo(backend_id="pomegranate-legacy-migrated"), - ) - elif isinstance(params.get("model"), pg.HiddenMarkovModel): - warnings.warn( - "Loading a RothSegmentationHmm with a raw pomegranate model parameter. " - "The model is migrated to the new HMMState representation during loading.", - UserWarning, - ) - params["model"] = pomegranate_model_to_hmm_state( - params["model"], - backend_info=BackendInfo(backend_id="pomegranate-legacy-migrated"), - ) input_data = {k: params[k] for k in tpcp.get_param_names(cls) if k in params} return cls(**input_data) @@ -451,7 +357,7 @@ def __init__( self, hmm_config: RothHmmConfig = cf(RothHmmConfig()), model: Optional[HMMState] = None, - backend: BaseHmmBackend = cf(PomegranateHmmBackend()), + backend: BaseHmmBackend = cf(DEFAULT_HMM_BACKEND), ) -> None: self.hmm_config = hmm_config self.model = model @@ -649,7 +555,7 @@ def self_optimize_with_info( data_sequence: Sequence[SingleSensorData], region_list_sequence: Sequence[SingleSensorRegionsOfInterestList], sampling_rate_hz: float, - ) -> tuple[Self, dict[str, History]]: + ) -> tuple[Self, dict[str, Any]]: """Create and train the HMM model based on the given data and labels. This is identical to `self_optimize`, but returns additional information about the training process. @@ -696,8 +602,8 @@ def self_optimize_with_info( if region_list_feature_space is None: raise RuntimeError("The feature transform did not produce region lists for optimization.") - trained_models: dict[str, SimpleHmm] = {} - histories: dict[str, History] = {} + trained_models: dict[str, BaseTrainableHmm] = {} + histories: dict[str, Any] = {} for module_config in self.model_config.modules: module_name = module_config.name train_sequence, init_state_labels = _get_training_sequences_for_module( diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_simple_model.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_simple_model.py deleted file mode 100644 index ce3e7371..00000000 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_simple_model.py +++ /dev/null @@ -1,452 +0,0 @@ -"""Simple model base classes and helper.""" - -import copy -import warnings -from collections.abc import Sequence -from typing import Literal, Optional, Union - -import numpy as np -import pandas as pd -import pomegranate as pg -from pomegranate import HiddenMarkovModel as pgHMM -from pomegranate.hmm import History -from tpcp import OptiPara, make_optimize_safe -from typing_extensions import Self - -from gaitmap.base import _BaseSerializable -from gaitmap.utils.datatype_helper import SingleSensorData -from gaitmap_mad.stride_segmentation.hmm._utils import ( - ShortenedHMMPrint, - _clone_model, - _HackyClonableHMMFix, - check_history_for_training_failure, - create_transition_matrix_fully_connected, - create_transition_matrix_left_right, - fix_model_names, - gmms_from_samples, - predict, -) - - -def initialize_hmm( - data_train_sequence: Sequence[np.ndarray], - labels_initialization_sequence: Sequence[np.ndarray], - *, - n_states: int, - n_gmm_components: int, - architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"], - name: str = "untrained", - verbose: bool = False, -) -> pgHMM: - """Model Initialization. - - Parameters - ---------- - data_train_sequence - list of training sequences this might be e.g. a list of strides where each strides is represented by one - np.ndarray (which might contain multiple dimensions) - labels_initialization_sequence - list of labels which are used to initialize the emission distributions. Note: These labels are just for - initialization. Distributions will be optimized later in the training process of the model! - Length of each np.ndarray in "labels_initialization_sequence" needs to match the length of each - corresponding np.ndarray in "data_train_sequence" - n_states - number of hidden-states within the model - n_gmm_components - number of components for multivariate distributions - architecture - type of model architecture, for more details see below - name - The name of the final model object - verbose - Whether info should be printed to stdout - - - Notes - ----- - This function supports currently the following "architectures": - - - "left-right-strict": - This will result in a strictly left-right structure, with no self-transitions and - start- and end-state bound to the first and last state, respectively. - Example transition matrix for a 5-state model: - transition_matrix: 1 1 0 0 0 starts: 1 0 0 0 0 - 0 1 1 0 0 stops: 0 0 0 0 1 - 0 0 1 1 0 - 0 0 0 1 1 - 0 0 0 0 1 - - "left-right-loose": - This will result in a loose left-right structure, with allowed self-transitions and - start- and end-state not specified initially. - Example transition matrix for a 5-state model: - transition_matrix: 1 1 0 0 0 starts: 1 1 1 1 1 - 0 1 1 0 0 stops: 1 1 1 1 1 - 0 0 1 1 0 - 0 0 0 1 1 - 1 0 0 0 1 - - "fully-connected": - This will result in a fully connected structure where all existing edges are initialized with the same - probability. - Example transition matrix for a 5-state model: - transition_matrix: 1 1 1 1 1 starts: 1 1 1 1 1 - 1 1 1 1 1 stops: 1 1 1 1 1 - 1 1 1 1 1 - 1 1 1 1 1 - 1 1 1 1 1 - - """ - if architecture not in ["left-right-strict", "left-right-loose", "fully-connected"]: - raise ValueError( - 'Invalid architecture given. Must be either "left-right-strict", "left-right-loose" or "fully-connected"' - ) - - # Note: In the past we used a fixed random state when generating the gmms. - # Now we are using a different method of initialization, where this is not needed anymore. - distributions, _ = gmms_from_samples( - data_train_sequence, - labels_initialization_sequence, - n_gmm_components, - n_states, - verbose=verbose, - ) - - # if we force the model into a left-right architecture we know that stride borders should correspond to the point - # where the model "loops" (aka state-0 and state-n) so we also will enforce the model to start with "state-0" and - # always end with "state-n" - if architecture == "left-right-strict": - transition_matrix, start_probs, end_probs = create_transition_matrix_left_right(n_states, self_transition=False) - - # allow transition model to start and end in all states (as we do not have any specific information about - # "transitions", this could be actually anything in the data which is no stride) - elif architecture == "left-right-loose": - transition_matrix, _, _ = create_transition_matrix_left_right(n_states, self_transition=True) - - start_probs = np.ones(n_states).astype(float) - end_probs = np.ones(n_states).astype(float) - - # fully connected model with all transitions initialized equally. Allowing all possible transitions. - else: # architecture == "fully-connected" - transition_matrix, start_probs, end_probs = create_transition_matrix_fully_connected(n_states) - - model = pg.HiddenMarkovModel.from_matrix( - transition_probabilities=transition_matrix, - distributions=copy.deepcopy(distributions), - starts=start_probs, - ends=end_probs, - verbose=verbose, - ) - - # pomegranate seems to have a strange sorting bug where state names >= 10 (e.g. s10 get sorted in a bad order like - # s0, s1, s10, s2 usw..) - model = fix_model_names(model) - # make sure that transition-matrix is normalized - model.bake() - model.name = name - - return model - - -class SimpleHmm(_BaseSerializable, _HackyClonableHMMFix, ShortenedHMMPrint): - """Wrap all required information to train a new HMM. - - This is a thin wrapper around the pomegranate HiddenMarkovModel class and basically calls out the pomegranate for - all core functionality. - - .. note:: This class is not intended to be used directly, but should be used as stride/transition model in the - :class:`~gaitmap.stride_segmentation.hmm.SegmentationModel`. - `SimpleHmm` therefore does not provide the same interface as other gaitmap algorithms. - It does not have a dedicated action method, but only has a `predict_hidden_state_sequence` method that - directly returns the hidden state sequence and does not store it on the object. - The reason for that is, that we regularly need to call this method with different algorithms on the same - model. - Hence, it felt more natural to do it that way. - - Parameters - ---------- - n_states - The number of states in the model. - n_gmm_components - The number of components in the GMMs. - Each state will be represented by its own GMM with this number of components. - architecture - The architecture of the model. Can be either "left-right-strict", "left-right-loose" or "fully-connected". - See Notes for more information. - algo_train - The algorithm to use for training. - Can be either "viterbi" or "baum-welch". - stop_threshold - The threshold for the training algorithm to stop. - max_iterations - The maximum number of iterations for the training algorithm. - name - The name of the model. - verbose - Whether to print progress information during training. - n_jobs - The number of jobs to use for training. - If set to -1, all available cores will be used. - model - The actual pomegranate HMM model. - This can be set to `None` initially. - A model will then be created during the optimization step. - If you want to use a pre-trained model, you can set this parameter to the respective model. - However, we recommend to ideally export this entire class instead of just the model to make sure that things - like the feature transform are also exported/stored. - data_columns - The expected columns of the input data in feature space. - This will be automatically set based on the feature transform output during the optimization step. - This does not affect the output, but is used as a sanity check to ensure that valid input data is provided - and that the column order is correct. - - Notes - ----- - This model supports currently the following "architectures": - - - "left-right-strict": - This will result in a strictly left-right structure, with no self-transitions and - start- and end-state bound to the first and last state, respectively. - Example transition matrix for a 5-state model: - - .. code:: - - transition_matrix: 1 1 0 0 0 starts: 1 0 0 0 0 - 0 1 1 0 0 stops: 0 0 0 0 1 - 0 0 1 1 0 - 0 0 0 1 1 - 0 0 0 0 1 - - - "left-right-loose": - This will result in a loose left-right structure, with allowed self-transitions and - start- and end-state not specified initially. - Example transition matrix for a 5-state model: - - .. code:: - - transition_matrix: 1 1 0 0 0 starts: 1 1 1 1 1 - 0 1 1 0 0 stops: 1 1 1 1 1 - 0 0 1 1 0 - 0 0 0 1 1 - 1 0 0 0 1 - - - "fully-connected": - This will result in a fully connected structure where all existing edges are initialized with the same - probability. - Example transition matrix for a 5-state model: - - .. code:: - - transition_matrix: 1 1 1 1 1 starts: 1 1 1 1 1 - 1 1 1 1 1 stops: 1 1 1 1 1 - 1 1 1 1 1 - 1 1 1 1 1 - 1 1 1 1 1 - - See Also - -------- - TBD - - """ - - n_states: int - n_gmm_components: int - architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"] - algo_train: Literal["viterbi", "baum-welch"] - stop_threshold: float - max_iterations: int - verbose: bool - n_jobs: int - name: Optional[str] - model: OptiPara[Optional[pgHMM]] - data_columns: OptiPara[Optional[tuple[str, ...]]] - - def __init__( - self, - n_states: int, - n_gmm_components: int, - *, - architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"] = "left-right-strict", - algo_train: Literal["viterbi", "baum-welch", "labeled"] = "viterbi", - stop_threshold: float = 1e-9, - max_iterations: int = 1e8, - verbose: bool = True, - n_jobs: int = 1, - name: str = "my_model", - model: Optional[pgHMM] = None, - data_columns: Optional[tuple[str, ...]] = None, - ) -> None: - self.n_states = n_states - self.n_gmm_components = n_gmm_components - self.algo_train = algo_train - self.stop_threshold = stop_threshold - self.max_iterations = max_iterations - self.architecture = architecture - self.verbose = verbose - self.n_jobs = n_jobs - self.name = name - self.model = model - self.data_columns = data_columns - - def predict_hidden_state_sequence( - self, feature_data: SingleSensorData, algorithm: Literal["viterbi", "map"] = "viterbi" - ) -> np.ndarray: - """Perform prediction based on given data and given model. - - Parameters - ---------- - feature_data - The data to predict the hidden state sequence for. - Note, that the data must have at least the same columns as the data used for training. - The order of the columns does not matter. - algorithm - The algorithm to use for prediction. - Can be either "viterbi" or "map". - - Returns - ------- - np.ndarray - The predicted hidden state sequence. - - """ - # NOTE: We don't consider this method an "action method" by definition, as it requires the algorithm to be - # specified and does not return self. - # The reason for that is, that we regularly need to call this method with different algorithms on the same - # model. - # Hence, it felt more natural to do it that way. - # However, as this means this model should always be wrapped in a `RothSegmentationHmm` to be used with a - # standardized API. - return predict(self.model, feature_data, expected_columns=self.data_columns, algorithm=algorithm) - - @make_optimize_safe - def self_optimize( - self, - data_sequence: Sequence[SingleSensorData], - labels_sequence: Sequence[Union[np.ndarray, pd.Series, pd.DataFrame]], - ) -> Self: - """Create and train the HMM model based on the given data and labels. - - Parameters - ---------- - data_sequence - Sequence of gaitmap sensordata objects. - labels_sequence - Sequence of gaitmap stride lists. - The number of stride lists must match the number of sensordata objects (i.e. they must belong together). - Each label sequence should only contain integers in the range [0, n_states - 1]. - The usage of the labels depends on the train algorithm. - In case of `viterbi` and `baum-welch`, the labels are only used to identify the initial data clusters. - - Returns - ------- - self - The trained model instance. - - """ - return self.self_optimize_with_info(data_sequence, labels_sequence)[0] - - def self_optimize_with_info( - self, - data_sequence: Sequence[SingleSensorData], - labels_sequence: Sequence[Union[np.ndarray, pd.Series, pd.DataFrame]], - ) -> tuple[Self, History]: - """Create and train the HMM model based on the given data and labels. - - This is identical to `self_optimize`, but returns additional information about the training process. - - Parameters - ---------- - data_sequence - Sequence of gaitmap sensordata objects. - labels_sequence - Sequence of gaitmap stride lists. - The number of stride lists must match the number of sensordata objects (i.e. they must belong together). - Each label sequence should only contain integers in the range [0, n_states - 1]. - The usage of the labels depends on the train algorithm. - In case of `viterbi` and `baum-welch`, the labels are only used to identify the intial data clusters. - - Returns - ------- - self - The trained model instance. - history - The history callback containing the training history. - - """ - if len(data_sequence) != len(labels_sequence): - raise ValueError( - "The given training sequence and initial training labels do not match in their number of individual " - f"sequences! len(data_train_sequence_list) = {len(data_sequence)} != {len(labels_sequence)} = len(" - "initial_hidden_states_sequence_list)" - ) - - for i, (data, labels) in enumerate(zip(data_sequence, labels_sequence)): - if len(data) < self.n_states: - raise ValueError( - "Invalid training sequence! At least one training sequence has less samples than the specified " - "value of states! " - f"For sequence {i}: n_states = {self.n_states} > {len(data)} = len(data)" - ) - # We allow None labels for some sequences, as this is also supported by pomegranate. - if labels is not None: - if len(data) != len(labels): - raise ValueError( - "Invalid training sequence! At least one training sequence has a different number of samples " - "than the corresponding label sequence! " - f"For sequence {i}: len(data) = {len(data)} != {len(labels)} = len(labels)" - ) - if not np.all(np.logical_and(labels >= 0, labels < self.n_states)): - raise ValueError( - "Invalid label sequence! At least one training sequence contains invalid state labels! " - f"For sequence {i}: labels not in [0, {self.n_states})" - ) - - self.data_columns = tuple(data_sequence[0].columns) - # you have to make always sure that the input data is in a correct format when using pomegranate, if not this - # can lead to extremely strange behaviour! Unfortunately pomegranate will not tell if data has a bad format! - # We also ensure that in all provided dataframes the same columns and column order exists - data_sequence_train = [ - np.ascontiguousarray(dataset[list(self.data_columns)].to_numpy().copy().squeeze()) - for dataset in data_sequence - ] - labels_sequence_train = [] - for labels in labels_sequence: - if labels is None: - labels_sequence_train.append(None) - continue - labels = labels.to_numpy().squeeze() if isinstance(labels, (pd.Series, pd.DataFrame)) else labels.squeeze() - labels_sequence_train.append(np.ascontiguousarray(labels.copy())) - - if self.model is not None: - warnings.warn("Model already exists. Overwriting existing model.") - - # initialize model by naive equidistant labels - model_untrained = initialize_hmm( - data_sequence_train, - labels_sequence_train, - n_states=self.n_states, - n_gmm_components=self.n_gmm_components, - architecture=self.architecture, - name=self.name + "-untrained", - ) - - # make copy from untrained model, as pomegranate will just update parameters in the given model and not - # returning a copy - model_trained = _clone_model(model_untrained, assert_correct=False) - - history: History - _, history = model_trained.fit( - sequences=np.array(data_sequence_train, dtype=object), - labels=np.array(labels_sequence_train, dtype=object), - algorithm=self.algo_train, - stop_threshold=self.stop_threshold, - max_iterations=self.max_iterations, - return_history=True, - verbose=self.verbose, - n_jobs=self.n_jobs, - multiple_check_input=False, - ) - check_history_for_training_failure(history) - model_trained.name = self.name + "_trained" - - self.model = model_trained - - return self, history diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_state.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_state.py index 4ba2a1ab..d6237302 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_state.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_state.py @@ -1,24 +1,13 @@ -"""Serializable HMM state objects and pomegranate conversion helpers.""" +"""Canonical, backend-neutral HMM state objects.""" from __future__ import annotations -from importlib.metadata import PackageNotFoundError, version from typing import Union import numpy as np -import pomegranate as pg -from pomegranate import HiddenMarkovModel as pgHMM from typing_extensions import Literal from gaitmap.base import _BaseSerializable -from gaitmap_mad.stride_segmentation.hmm._utils import add_transition - - -def _get_pomegranate_version() -> str | None: - try: - return version("pomegranate") - except PackageNotFoundError: - return None class BackendInfo(_BaseSerializable): @@ -181,137 +170,3 @@ def __getstate__(self) -> str: def __setstate__(self, state: str) -> None: restored = type(self).from_json(state) self.__dict__.update(restored.__dict__) - - -def _normalize_mixture_weights(log_weights: np.ndarray) -> np.ndarray: - shifted = np.exp(log_weights - np.max(log_weights)) - return shifted / np.sum(shifted) - - -def _distribution_to_state(distribution: pg.Distribution) -> EmissionState: - if isinstance(distribution, pg.GeneralMixtureModel): - return GaussianMixtureEmissionState( - weights=_normalize_mixture_weights(np.asarray(distribution.weights, dtype=float)), - components=tuple(_distribution_to_state(component) for component in distribution.distributions), # type: ignore[arg-type] - frozen=bool(getattr(distribution, "frozen", False)), - ) - if isinstance(distribution, pg.MultivariateGaussianDistribution): - mean, covariance = distribution.parameters - return GaussianEmissionState( - mean=np.asarray(mean, dtype=float), - covariance=np.asarray(covariance, dtype=float), - frozen=bool(getattr(distribution, "frozen", False)), - ) - raise TypeError( - f"Unsupported pomegranate emission distribution `{type(distribution).__name__}`. " - "Only multivariate Gaussian and Gaussian mixture emissions are supported in the serialized HMM state." - ) - - -def _state_to_distribution(state: EmissionState) -> pg.Distribution: - if isinstance(state, GaussianEmissionState): - distribution = pg.MultivariateGaussianDistribution(state.mean.tolist(), state.covariance.tolist()) - distribution.frozen = state.frozen - return distribution - if isinstance(state, GaussianMixtureEmissionState): - weights = np.asarray(state.weights, dtype=float) - weights = np.clip(weights, np.finfo(float).tiny, None) - weights = weights / np.sum(weights) - distribution = pg.GeneralMixtureModel( - [_state_to_distribution(component) for component in state.components], - weights=weights.tolist(), - ) - distribution.frozen = state.frozen - return distribution - raise TypeError(f"Unsupported serialized emission state `{type(state).__name__}`.") - - -def pomegranate_model_to_flat_hmm_state(model: pgHMM) -> FlatHmmState: - """Convert a pomegranate HMM into a serializable flat state. - - The canonical state only stores emitting states. `pomegranate`'s silent - start/end nodes are folded into explicit `start_probs`/`end_probs`. - """ - dense_transition_matrix = model.dense_transition_matrix() - graph_state = HmmGraphState( - transition_probs=np.asarray(dense_transition_matrix[:-2, :-2], dtype=float), - start_probs=np.asarray(dense_transition_matrix[-2, :-2], dtype=float), - end_probs=np.asarray(dense_transition_matrix[:-2, -1], dtype=float), - ) - hidden_states = [state for state in model.states if state.distribution is not None] - return FlatHmmState( - graph=graph_state, - emissions=tuple(_distribution_to_state(state.distribution) for state in hidden_states), - state_names=tuple(state.name for state in hidden_states), - name=model.name, - ) - - -def flat_hmm_state_to_pomegranate_model(state: FlatHmmState, *, verbose: bool = False) -> pgHMM: - """Compile a serializable flat state into a pomegranate HMM.""" - model = pg.HiddenMarkovModel.from_matrix( - transition_probabilities=np.asarray(state.graph.transition_probs, dtype=float), - distributions=[_state_to_distribution(distribution) for distribution in state.emissions], - starts=np.asarray(state.graph.start_probs, dtype=float), - ends=np.asarray(state.graph.end_probs, dtype=float), - state_names=list(state.state_names), - verbose=verbose, - ) - model.bake() - if state.name is not None: - model.name = state.name - return model - - -def pomegranate_model_to_hmm_state( - compiled_model: pgHMM, - *, - submodels: tuple[HmmSubModelState, ...] = (), - cross_module_transitions: tuple[CrossModuleTransition, ...] = (), - backend_info: BackendInfo | None = None, -) -> HMMState: - """Convert a compiled pomegranate HMM and optional hierarchy into a serializable HMM state.""" - if backend_info is None: - backend_info = BackendInfo(backend_id="pomegranate-legacy", backend_version=_get_pomegranate_version()) - elif backend_info.backend_version is None and backend_info.backend_id.startswith("pomegranate"): - backend_info = BackendInfo( - backend_id=backend_info.backend_id, - backend_version=_get_pomegranate_version(), - state_schema_version=backend_info.state_schema_version, - ) - return HMMState( - trained_with=backend_info, - compiled=pomegranate_model_to_flat_hmm_state(compiled_model), - submodels=submodels, - cross_module_transitions=cross_module_transitions, - ) - - -def hmm_state_to_pomegranate_model(state: HMMState, *, verbose: bool = False) -> pgHMM: - """Compile a serializable HMM state into a pomegranate HMM.""" - model = flat_hmm_state_to_pomegranate_model(state.compiled, verbose=verbose) - existing_transitions = {(start.name, end.name) for start, end in model.graph.edges()} - for transition in state.cross_module_transitions: - from_state = state.compiled.state_names[_find_state_index(state, transition.from_module, transition.from_state)] - to_state = state.compiled.state_names[_find_state_index(state, transition.to_module, transition.to_state)] - if (from_state, to_state) in existing_transitions: - continue - add_transition(model, (from_state, to_state), transition.probability) - existing_transitions.add((from_state, to_state)) - model.bake() - return model - - -def _find_state_index(state: HMMState, module_name: str, state_idx: int) -> int: - offset = 0 - for submodel in state.submodels: - n_states = len(submodel.model.state_names) - if submodel.name == module_name: - if state_idx >= n_states: - raise ValueError( - f"Cross-module transition refers to state {state_idx} of module `{module_name}`, " - f"but the module only has {n_states} states." - ) - return offset + state_idx - offset += n_states - raise ValueError(f"No submodel named `{module_name}` exists in the serialized HMM state.") diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py index ab0fa8c3..ab017cae 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py @@ -1,15 +1,11 @@ -"""Utils and helper functions for HMM classes.""" +"""Backend-neutral helper utilities for HMM preprocessing and label handling.""" + +from __future__ import annotations -import json import warnings -from typing import Any, Literal, Optional import numpy as np import pandas as pd -import pomegranate as pg -from pomegranate.hmm import History -from tpcp import BaseTpcpObject, CloneFactory -from tpcp._hash import custom_hash from gaitmap.utils.datatype_helper import ( SingleSensorData, @@ -20,418 +16,62 @@ from gaitmap.utils.exceptions import ValidationError -def _add_transition(model, a, b, probability, pseudocount, group) -> None: - """Hacky way to add a transition when cloning a model in the "wrong" way.""" - pseudocount = pseudocount or probability - model.graph.add_edge(a, b, probability=probability, pseudocount=pseudocount, group=group) - - -def _clone_model(orig_model: pg.HiddenMarkovModel, assert_correct: bool = True) -> pg.HiddenMarkovModel: - """Clone a HMM without changing its values using a hacky way. - - XXX: This method can clone a HMM by copying over all values individually. - It skips the init of the models, as using them lead to rounding issues, that result in models that are not - strictly identical. - This should not be a problem mathematically (as the rounding errors are small), but if you expect to get an - exact copy, this method can help you. - - This should only be used internally! If you have a tpcp/gaitmap algorithm that contains a HMM as input param, - you should use `_clone_model` in the implementation, when you need a copy. - To make your algorithm itself properly clonable (including your HMM model), the algorithm needs to inherit from - `_HackyClonableHMMFix`. - This will overwrite how your algorithm handles `tpcp.clone`. - Note, it will not fix how deepcopy (`copy.deepcopy`) is handled. - - .. warning:: This will not work all the time! In particular when transitions are added to the model after the - initial creation, it seems like it is impossible to clone the model correctly, because the order of - the edges can not be restored. - A possible workaround in this case is to clone the model twice. - The first clone will reorder the edges in a predictable way and the order will stay consistent - afterwards. - - Parameters - ---------- - orig_model - The HMM model to clone - assert_correct - If True, the cloned model will be compared to the original model and an AssertionError will be raised if they - not identical. - In general, this should be True, however, when you want to clone a model with certain edges that are not - fully covered, you can set this to False at your own risk. - - Returns - ------- - model - A deepcopy of the HMM model. - - """ - d = json.loads(orig_model.to_json()) - - # Make a new generic HMM - model = pg.HiddenMarkovModel(str(d["name"])) - - with np.errstate(divide="ignore"): - states = [pg.State.from_dict(j) for j in d["states"]] - - for cloned_state, state in zip(states, orig_model.states): - assert cloned_state.name == state.name - if state.distribution is not None: - cloned_state.distribution.frozen = state.distribution.frozen - if isinstance(state.distribution, pg.GeneralMixtureModel): - # Fix the distribution weights - # Note the `[:]`! This is important, because pg keeps a pointer to the original weights vector internally. - # If we would reassign weights (and not just its content), we would update weights, but pg would still - # use the old values internally, as it uses the pointer to access it. - cloned_state.distribution.weights[:] = np.copy(state.distribution.weights) - - for i, j in d["distribution ties"]: - # Tie appropriate states together - states[i].tie(states[j]) - - # Add all the states to the model - model.add_states(states) - - # Indicate appropriate start and end states - model.start = states[d["start_index"]] - model.end = states[d["end_index"]] - - new_state_order = [state.name for state in states] - - # Add all the edges to the model - for start, end, data in list(orig_model.graph.edges(data=True)): - _add_transition( - model, - states[new_state_order.index(start.name)], - states[new_state_order.index(end.name)], - data["probability"], - data["pseudocount"], - data["group"], - ) - - # Bake the model - model.bake(verbose=False) - - if assert_correct: - assert custom_hash(model) == custom_hash(orig_model), ( - "Cloning the provided HMM model failed! Please open an issue on github with an example." - ) - - return model - - -def _is_serialized_hmm_state(value: Any) -> bool: - return ( - hasattr(value, "compiled") - and hasattr(value, "trained_with") - and callable(getattr(value, "to_json", None)) - and callable(getattr(type(value), "from_json", None)) - ) - - -class _HackyClonableHMMFix(BaseTpcpObject): - """A hacky implementation to ensure that HMM parameters are actually cloned, when cloning the algorithm. - - This implements and alternative cloning method for all parameters that are of type `pg.HiddenMarkovModel` that is - used when `tpcp.clone` is used. - When you implement an algorithm with a `pg.HiddenMarkovModel` as parameter, you should inherit from this class. - In addition, you should use `_clone_model` internally, when you need a identical copy/clone of your hmm. - - For more information see the `_clone_model` function. - - """ - - @classmethod - def __clone_param__(cls, param_name: str, value: Any) -> Any: - """Overwrite cloning for HMM models. - - XXX: This is hacky shit and it is stupid that I have to do it in the first place, but there is no build in - way in pomegrante to properly deepcopy a HMM. - For several reasons, a deepcopied HMM will only be approximately identical to the original (rounding issues). - Our cloning implementation, does some bad stuff (and should always be covered by tests), but seems to - properly clone the objects (so that the hashs are identical). - """ - if isinstance(value, pg.HiddenMarkovModel): - return _clone_model(value) - if _is_serialized_hmm_state(value): - return type(value).from_json(value.to_json()) - return super().__clone_param__(param_name, value) - - -class ShortenedHMMPrint(BaseTpcpObject): - """Mixin class to better format pg.HMM models when printing them.""" - - def __repr_parameter__(self, name: str, value: Any) -> str: - """Representation with specific care for HMM models.""" - if name == "model": - if _is_serialized_hmm_state(value): - n_states = len(value.compiled.state_names) if getattr(value, "compiled", None) is not None else "?" - backend = getattr(getattr(value, "trained_with", None), "backend_id", "?") - return f"{name}=HMMState[backend={backend}, states={n_states}](...)" - if isinstance(value, pg.HiddenMarkovModel): - return f"{name}=HiddenMarkovModel[name={value.name}](...)" - if isinstance(value, CloneFactory) and isinstance(value.default_value, pg.HiddenMarkovModel): - return f"{name}=cf(HiddenMarkovModel[name={value.get_value().name}](...))" - return super().__repr_parameter__(name, value) - - def create_transition_matrix_fully_connected(n_states: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """Create nxn transition matrix with only 1 entries.""" + """Create a fully-connected transition matrix with uniform probabilities.""" transition_matrix = np.ones((n_states, n_states)) / n_states start_probs = np.ones(n_states) end_probs = np.ones(n_states) - return transition_matrix, start_probs, end_probs def create_transition_matrix_left_right( n_states: int, self_transition: bool = True ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """Create nxn transition for left to right model.""" + """Create a left-right transition matrix.""" transition_matrix = np.zeros((n_states, n_states)) transition_matrix[range(n_states - 1), range(1, n_states)] = 1 transition_matrix[range(n_states), range(n_states)] = 1 if self_transition: transition_matrix[-1][0] = 1 - # force start with first state start_probs = np.zeros(n_states) start_probs[0] = 1 - # and force end with last state end_probs = np.zeros(n_states) end_probs[-1] = 1 return transition_matrix, start_probs, end_probs -def print_transition_matrix(model: pg.HiddenMarkovModel, precision: int = 3) -> None: - """Print model transition matrix in user-friendly format.""" - np.set_printoptions(suppress=True) - np.set_printoptions(precision) - if isinstance(model, pg.HiddenMarkovModel): - print(model.dense_transition_matrix()[0:-2, 0:-2]) - if isinstance(model, np.ndarray): - print(model) +def create_state_names(n_states: int) -> tuple[str, ...]: + """Create stable state names matching the legacy naming scheme.""" + state_names = [] + for state_number in range(n_states): + if state_number < 10: + state_names.append(f"s{state_number}") + continue + state_names.append(f"s{chr(87 + state_number)}") + return tuple(state_names) def cluster_data_by_labels(data_list: list[np.ndarray], label_list: list[np.ndarray]): - """Cluster data by labels.""" + """Cluster data arrays by integer labels.""" assert isinstance(label_list, list), "label_list must be list!" assert isinstance(data_list, list), "data_list must be list!" label_list = [np.asarray(label).tolist() for label in label_list] data_list = [np.asarray(data).tolist() for data in data_list] - # remove datasets where the labellist is None x_ = [x for x, label in zip(data_list, label_list) if label is not None] - x_ = np.concatenate(x_) # concatenate all datasets with label_list to a single array - - labels_ = np.concatenate( - [label for label in label_list if label is not None] - ) # concatenate all not None labellists to a single array - label_set = np.unique(labels_) # get set of unique label_list - - clustered_data = [x_[labels_ == label] for label in label_set] - - return clustered_data - - -def gmms_from_samples( - data, - labels, - n_components: int, - n_expected_states: int, - verbose: bool = False, - n_init: int = 5, - n_jobs: int = 1, -): - """Create Gaussian Mixture Models from samples. - - This function clusters the data by the given labels and fits either univariate or multivariate - Normal Distributions for each cluster. If n_components is > 1 then a Mixture Model of n-univariate or n-multivariate - Gaussian Distributions will be fitted. - """ - if not np.array(data, dtype=object).data.c_contiguous: - raise ValueError("Memory Layout of given input data is not contiguous! Consider using numpy.ascontiguousarray.") - - clustered_data = cluster_data_by_labels(data, labels) - - if len(clustered_data) < n_expected_states: - raise ValueError( - f"The training labels did only provide samples for {len(clustered_data)} states, but " - f"{n_expected_states} states were expected. " - "Ensure that the training data contains samples for all states." - ) - - if data[0].ndim == 1: - # we need to reshape the 1D-data for pomegranate! - clustered_data = [np.reshape(data, (len(data), 1)) for data in clustered_data] - - # We use a Multivariate Normal Distribution even if we only have 1D data. - # For some reason they are handled better in pomegranate. - pg_dist_type = pg.MultivariateGaussianDistribution - - if n_components > 1: - for cluster in clustered_data: - if len(cluster) < n_components: - raise ValueError( - f"The training labels did only provide a small number of samples ({len(cluster)}) for one of the " - "states. " - f"To initialize {n_components} components in a mixture model, we need at least {n_components} " - f"samples! " - "Ensure that the training data contains enough samples." - ) - # calculate Mixture Model for each state, clustered by labels - distributions = [] - for cluster in clustered_data: - dist = pg.GeneralMixtureModel.from_samples( - pg_dist_type, - n_components=n_components, - X=cluster, - verbose=verbose, - n_jobs=n_jobs, - n_init=n_init, - init="first-k", # With this initialisation, we don't have any randomnes! -> No need for any random - # seed. - ) - for d in dist.distributions: - if np.any([np.isnan(p).any() for p in d.parameters]).any(): - raise ValueError( - "NaN in parameters during distribution fitting! " - "This usually happens when there is not enough data for a large number of distributions and " - "states. " - "To avoid this issue, reduce the number of distributions per state or the number of states. " - "Or ideally, provide more data." - ) - distributions.append(dist) - else: - # if n components is just 1 we do not need a mixture model and just build either multivariate Normal - # Distribution - distributions = [pg_dist_type.from_samples(dataset) for dataset in clustered_data] - - return distributions, clustered_data - - -def fix_model_names(model): - """Fix pomegranate model names. - - Replace state name from s10 to sN with characters as pomegranate seems to have a "sorting" bug. Where states - get sorted like s0, s1, s10, s2, .... so we will map state names >10 to letters. E.g. "s10" -> "sa", "s11" -> "sb" - """ - for state in model.states: - if state.name[0] == "s": - try: - state_number = int(state.name[1:]) - except ValueError: - continue - # replace state numbers >= 10 by characters form the ascii-table :) - if state_number >= 10: - state.name = "s" + chr(87 + state_number) - return model - - -def _iter_nested_distributions(distribution): - if isinstance(distribution, pg.GeneralMixtureModel): - yield distribution - for d in distribution.distributions: - yield from _iter_nested_distributions(d) - else: - yield distribution - - -def model_params_are_finite(model: pg.HiddenMarkovModel) -> bool: - """Check if model parameters are finite.""" - for state in model.states: - for dist in _iter_nested_distributions(state.distribution): - if hasattr(dist, "parameters"): - for param in dist.parameters: - if not np.all(np.isfinite(param)): - return False - if hasattr(dist, "weights") and not np.all(np.isfinite(dist.weights)): - return False - return True - - -def check_history_for_training_failure(history: History) -> None: - """Check if training history contains any NaNs.""" - if not np.all(np.isfinite(history.improvements)) or np.any(np.array(history.improvements) < 0): - warnings.warn( - "During training the improvement per epoch became NaN/infinite or negative! " - "Run `self_optimize_with_info` and inspect the history element for more information. " - "With a high likelihood, the final model is not usable and will result in errors during prediction. " - "This usually happens when there is not enough data for a large number of distributions and " - "states. " - "To avoid this issue, reduce the number of distributions per state or the number of states. " - "Or ideally, provide more data." - ) - - -def get_state_by_name(model: pg.HiddenMarkovModel, state_name: str) -> str: - """Get state object from model by name.""" - for state in model.states: - if state.name == state_name: - return state - raise ValueError(f"State {state_name} not found within given _model!") - - -def add_transition(model: pg.HiddenMarkovModel, transition: tuple[str, str], transition_probability: float) -> None: - """Add a transition to an existing model by state-names. - - add_transition(model, transition = ("s0","s1"), transition_probability = 0.5) - to add an edge from state s0 to state s1 with a transition probability of 0.5. - """ - model.add_transition( - get_state_by_name(model, transition[0]), - get_state_by_name(model, transition[1]), - transition_probability, - ) - - -def get_model_distributions(model: pg.HiddenMarkovModel) -> list[pg.Distribution]: - """Return all not None distributions as list from given model.""" - distributions = [] - for state in model.states: - if state.distribution is not None: - distributions.append(state.distribution) - return distributions - - -def labels_to_strings(labelsequence: list[Optional[np.ndarray]]) -> list[Optional[list[str]]]: - """Convert label sequence of ints to strings. - - Pomegranated messes up sorting of states: it will sort like this: s0, s1, s10, s2.... which can lead to unexpected - behaviour. - """ - assert isinstance(labelsequence, list), "labelsequence must be list!" - - labelsequence_str = [] - for sequence in labelsequence: - if sequence is None: - labelsequence_str.append(sequence) - continue - labelsequence_str.append([f"s{i:02}" for i in sequence]) - return labelsequence_str + x_ = np.concatenate(x_) + labels_ = np.concatenate([label for label in label_list if label is not None]) + label_set = np.unique(labels_) + return [x_[labels_ == label] for label in label_set] def extract_transitions_starts_stops_from_hidden_state_sequence( hidden_state_sequence: list[np.ndarray], ) -> tuple[set[tuple[str, str]], np.ndarray, np.ndarray]: - """Extract transitions from hidden state sequence. - - This function will return a list of transitions as well as start and stop labels that can be found within the - input sequences. - - input = [[1,1,1,1,1,3,3,3,3,2,2,2,2,4,4,4,4,5,5], - [0,0,1,1,1,3,3,3,3,2,2,2,6]] - output_transitions = [[s1,s3], - [s3,s2], - [s2,s4], - [s4,s5], - [s0,s1], - [s2,s6]] - - output_starts = [1,0] - output_stops = [5,6] - """ + """Extract observed transitions and start/end states from labeled sequences.""" assert isinstance(hidden_state_sequence, list), "Hidden state sequence must be list!" transitions = [] @@ -475,50 +115,21 @@ def estimate_sequence_boundary_probs( def create_equidistant_label_sequence(n_labels: int, n_states: int) -> np.ndarray: - """Create equidistant label sequence. - - create label sequence of length n_states with n_labels unique labels. - This can be used to e.g. initialize labels for a single stride or sequence that is expected to be left-right strict. - - In case n_labels is not cleanly dividable by n_states, some of the states will be repeated to ensure that the - sequence is of length n_labels. - Specifically, we will repeat states at the start and the end of the sequence. - - If the number of labels is smaller than the number of states, an error is raised. - - Parameters - ---------- - n_labels : int - Number of labels to create. - n_states : int - Number of unique states in the output sequence. - - Example - ------- - >>> create_equidistant_label_sequence(n_labels=10, n_states=5) - array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4]) - >>> create_equidistant_label_sequence(n_labels=10, n_states=4) - array([0, 0, 0, 1, 1, 2, 2, 3, 3, 3]) - >>> create_equidistant_label_sequence(n_labels=10, n_states=3) - array([0, 0, 0, 1, 1, 1, 2, 2, 2, 2]) - - """ + """Create a length-`n_labels` sequence with `n_states` approximately equidistant labels.""" if n_labels < n_states: raise ValueError("n_labels must be larger than n_states!") - # calculate the samples per state (must be integer!) - save_repeats = int(n_labels // n_states) + safe_repeats = int(n_labels // n_states) remainder = n_labels % n_states - # create label sequence max_handled_state = remainder // 2 - label_sequence = np.repeat(np.arange(remainder // 2), save_repeats + 1) + label_sequence = np.repeat(np.arange(remainder // 2), safe_repeats + 1) label_sequence = np.append( - label_sequence, np.repeat(np.arange(n_states - remainder) + max_handled_state, save_repeats) + label_sequence, np.repeat(np.arange(n_states - remainder) + max_handled_state, safe_repeats) ) max_handled_state = n_states - remainder + max_handled_state label_sequence = np.append( - label_sequence, np.repeat(np.arange(n_states - max_handled_state) + max_handled_state, save_repeats + 1) + label_sequence, np.repeat(np.arange(n_states - max_handled_state) + max_handled_state, safe_repeats + 1) ) return label_sequence @@ -527,34 +138,20 @@ def create_equidistant_label_sequence(n_labels: int, n_states: int) -> np.ndarra def convert_stride_list_to_transition_list( stride_list: SingleSensorStrideList, last_end: int ) -> SingleSensorRegionsOfInterestList: - """Extract the regions between strides as transitions from a stride list. - - Parameters - ---------- - stride_list - Stride list to extract transitions from. - last_end - End of the final transition (usually len(data)). - - """ - # Transitions are everything between two strides + """Extract the regions between strides as transitions from a stride list.""" transition_starts = [0, *(stride_list["end"] + 1)] transition_ends = [*stride_list["start"], last_end] return pd.DataFrame( - [(s, e) for s, e in zip(transition_starts, transition_ends) if e - s > 0], columns=["start", "end"] + [(start, end) for start, end in zip(transition_starts, transition_ends) if end - start > 0], + columns=["start", "end"], ) def validate_trainable_region_list( region_list: SingleSensorRegionsOfInterestList, explicit_region_types: tuple[str, ...] ) -> SingleSensorRegionsOfInterestList: - """Validate and normalize a typed region list for HMM training. - - The passed region list must be a valid ROI list with an additional `type` column. - The `type` values must map to the explicit region modules of the HMM config. - Regions must not overlap. - """ + """Validate and normalize a typed region list for HMM training.""" try: is_single_sensor_regions_of_interest_list(region_list, region_type="any", raise_exception=True) normalized_region_list = region_list.reset_index() @@ -603,22 +200,14 @@ def get_train_data_sequences_transitions( region_list_sequence: list[SingleSensorRegionsOfInterestList], n_states: int, ) -> tuple[list[np.ndarray], list[np.ndarray]]: - """Extract Transition Training set. - - - data_train_sequence: list of datasets in feature space - - stride_list_sequence: list of gaitmap stride-lists - - n_states: number of labels. - """ + """Extract transition training sequences and naive initial labels.""" trans_data_train_sequence = [] trans_labels_train_sequence = [] - n_too_short_transitions = 0 for data, region_list in zip(data_train_sequence, region_list_sequence): - # for each transition, get data and create some naive labels for initialization transition_regions = convert_region_list_to_transition_list(region_list, data.shape[0]) for start, end in transition_regions[["start", "end"]].to_numpy(): - # append extracted sequences and corresponding label set to results list try: labels = create_equidistant_label_sequence(end - start, n_states).astype("int64") except ValueError: @@ -645,13 +234,9 @@ def get_train_data_sequences_regions( region_type: str, n_states: int, ) -> tuple[list[np.ndarray], list[np.ndarray]]: - """Extract training sequences for one explicit region type. - - The region list is expected to have `start`, `end`, and `type` columns. - """ + """Extract training sequences for one explicit region type.""" region_data_train_sequence = [] region_labels_train_sequence = [] - n_too_short_regions = 0 for data, region_list in zip(data_train_sequence, region_list_sequence): @@ -678,81 +263,4 @@ def get_train_data_sequences_regions( class _DataToShortError(ValueError): - pass - - -def predict( - model: Optional[pg.HiddenMarkovModel], - data: pd.DataFrame, - *, - expected_columns: tuple[str, ...], - algorithm: Literal["viterbi", "map"], -) -> np.ndarray: - """Predict the hidden state sequence for the given data. - - Parameters - ---------- - model - The hidden markov model to use for prediction. - data - The data to predict the hidden state sequence for. - expected_columns - The expected columns of the data. - This is used to check if the data has the correct format and re-order the columns if necessary. - algorithm - The algorithm to use for prediction. - - Returns - ------- - hidden_state_sequence - A numpy array containing the predicted hidden state sequence. - - """ - if model is None: - raise ValueError( - "You need to train the HMM before calling `predict_hidden_state_sequence`. " - "Use `self_optimize` or `self_optimize_with_info` for that." - ) - - try: - data = data[list(expected_columns)] - except KeyError as e: - raise ValueError( - "The provided feature data is expected to have the following columns:\n\n" - f"{expected_columns}\n\n" - "But it only has the following columns:\n\n" - f"{data.columns}" - ) from e - - if len(data) < len(model.states) - 2: - raise _DataToShortError( - "The provided feature data is expected to have at least as many samples as the number of states " - f"of the model ({len(model.states) - 2}). " - f"But it only has {len(data)} samples." - ) - - data = np.ascontiguousarray(data.to_numpy()) - try: - labels_predicted = np.asarray(model.predict(data.copy(), algorithm=algorithm)) - except Exception as e: - if not model_params_are_finite(model): - raise ValueError( - "Prediction failed! (See error above.). " - "However, the provided pomegranate model has non-finite/NaN parameters. " - "This might be the source of the observed error and indicates problems during training. " - "Check the training history and the model parameters to confirm invalid training behaviour. " - "Unfortunately, there is no way to automatically fix these issues. " - "Simply speaking, your training data could not be represented well by the selected model architecture. " - "Check for obvious errors in your pre-processing or try to use a different model architecture. " - ) from e - raise ValueError( - "Prediction failed! (See error above.). " - "Unfortunately, we are not sure what happened. " - "The error was caused by pomegrante internals." - ) from e - - # pomegranate always adds an additional label for the start- and end-state, which can be ignored here! - # Note: This only seems to happen for the viterbi algorithm, not for the map algorithm. - if algorithm == "viterbi": - labels_predicted = labels_predicted[1:-1] - return np.asarray(labels_predicted) + """Raised when feature-space input is shorter than the model topology requires.""" diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py new file mode 100644 index 00000000..751b02e1 --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py @@ -0,0 +1,9 @@ +"""Legacy pomegranate backend package.""" + +from gaitmap_mad.stride_segmentation.hmm.legacy._backend import ( + PomegranateLegacyHmmBackend, + SimpleHmm, + initialize_hmm, +) + +__all__ = ["PomegranateLegacyHmmBackend", "SimpleHmm", "initialize_hmm"] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py new file mode 100644 index 00000000..6aca707c --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py @@ -0,0 +1,437 @@ +"""Legacy pomegranate backend.""" + +from __future__ import annotations + +import copy +import warnings +from importlib.metadata import PackageNotFoundError, version +from typing import Any, Literal, Optional + +import numpy as np +import pandas as pd +try: + import pomegranate as pg +except ImportError: # pragma: no cover - exercised in environments without pomegranate + pg = None +try: + from pomegranate.hmm import History +except (ImportError, AttributeError): + History = Any +from tpcp import OptiPara, make_optimize_safe +from typing_extensions import Self + +from gaitmap_mad.stride_segmentation.hmm._backend_base import BaseHmmBackend, BaseTrainableHmm +from gaitmap_mad.stride_segmentation.hmm._backend_common import ( + extract_cross_module_transitions, + normalize_transition_and_end_probs, +) +from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig +from gaitmap_mad.stride_segmentation.hmm._state import BackendInfo, HMMState, HmmSubModelState +from gaitmap_mad.stride_segmentation.hmm._utils import ( + create_state_names, + create_transition_matrix_fully_connected, + create_transition_matrix_left_right, + estimate_sequence_boundary_probs, + extract_transitions_starts_stops_from_hidden_state_sequence, +) +from gaitmap_mad.stride_segmentation.hmm.legacy._state import ( + hmm_state_to_pomegranate_model, + pomegranate_model_to_flat_hmm_state, + pomegranate_model_to_hmm_state, +) +from gaitmap_mad.stride_segmentation.hmm.legacy._utils import ( + ShortenedHMMPrint, + _clone_model, + _HackyClonableHMMFix, + check_history_for_training_failure, + fix_model_names, + get_model_distributions, + gmms_from_samples, + labels_to_strings, + predict, +) + + +def _get_pomegranate_version() -> str | None: + try: + return version("pomegranate") + except PackageNotFoundError: + return None + + +def _require_legacy_pomegranate(): + legacy_hmm = getattr(pg, "HiddenMarkovModel", None) + if pg is None or legacy_hmm is None: + raise ImportError("The legacy HMM backend requires pomegranate 0.x with `HiddenMarkovModel` support.") + return pg + + +def initialize_hmm( + data_train_sequence: list[np.ndarray], + labels_initialization_sequence: list[np.ndarray], + *, + n_states: int, + n_gmm_components: int, + architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"], + name: str = "untrained", + verbose: bool = False, +) -> Any: + """Initialize a legacy pomegranate HMM from labeled clusters.""" + if architecture not in ["left-right-strict", "left-right-loose", "fully-connected"]: + raise ValueError( + 'Invalid architecture given. Must be either "left-right-strict", "left-right-loose" or "fully-connected"' + ) + + distributions, _ = gmms_from_samples( + data_train_sequence, + labels_initialization_sequence, + n_gmm_components, + n_states, + verbose=verbose, + ) + + if architecture == "left-right-strict": + transition_matrix, start_probs, end_probs = create_transition_matrix_left_right(n_states, self_transition=False) + elif architecture == "left-right-loose": + transition_matrix, _, _ = create_transition_matrix_left_right(n_states, self_transition=True) + start_probs = np.ones(n_states, dtype=float) + end_probs = np.ones(n_states, dtype=float) + else: + transition_matrix, start_probs, end_probs = create_transition_matrix_fully_connected(n_states) + + pg = _require_legacy_pomegranate() + model = pg.HiddenMarkovModel.from_matrix( + transition_probabilities=transition_matrix, + distributions=copy.deepcopy(distributions), + starts=start_probs, + ends=end_probs, + verbose=verbose, + ) + model = fix_model_names(model) + model.bake() + model.name = name + return model + + +class SimpleHmm(BaseTrainableHmm, _HackyClonableHMMFix, ShortenedHMMPrint): + """Wrap all required information to train a legacy flat HMM. + + This is a thin wrapper around the legacy `pomegranate.HiddenMarkovModel` class and delegates training and + inference to that runtime. + + Parameters + ---------- + n_states + The number of hidden states in the model. + n_gmm_components + The number of Gaussian-mixture components per state. + architecture + The HMM topology. Supported values are `"left-right-strict"`, `"left-right-loose"`, and + `"fully-connected"`. + algo_train + Training algorithm used by legacy `pomegranate`. + stop_threshold + Training convergence threshold. + max_iterations + Maximum number of training iterations. + verbose + Whether training progress should be printed. + n_jobs + Number of parallel jobs used by legacy `pomegranate`. + name + Name assigned to the runtime model. + model + Optional pre-existing runtime model. + data_columns + Expected feature-space column order used for prediction. + """ + + n_states: int + n_gmm_components: int + architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"] + algo_train: Literal["viterbi", "baum-welch"] + stop_threshold: float + max_iterations: int + verbose: bool + n_jobs: int + name: Optional[str] + model: OptiPara[Optional[Any]] + data_columns: OptiPara[Optional[tuple[str, ...]]] + + def __init__( + self, + n_states: int, + n_gmm_components: int, + *, + architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"] = "left-right-strict", + algo_train: Literal["viterbi", "baum-welch", "labeled"] = "viterbi", + stop_threshold: float = 1e-9, + max_iterations: int = 1e8, + verbose: bool = True, + n_jobs: int = 1, + name: str = "my_model", + model: Optional[Any] = None, + data_columns: Optional[tuple[str, ...]] = None, + ) -> None: + self.n_states = n_states + self.n_gmm_components = n_gmm_components + self.algo_train = algo_train + self.stop_threshold = stop_threshold + self.max_iterations = max_iterations + self.architecture = architecture + self.verbose = verbose + self.n_jobs = n_jobs + self.name = name + self.model = model + self.data_columns = data_columns + + def predict_hidden_state_sequence( + self, feature_data: pd.DataFrame, algorithm: Literal["viterbi", "map"] = "viterbi" + ) -> np.ndarray: + return predict(self.model, feature_data, expected_columns=self.data_columns, algorithm=algorithm) + + @make_optimize_safe + def self_optimize( + self, + data_sequence: list[pd.DataFrame], + labels_sequence: list[np.ndarray | pd.Series | pd.DataFrame], + ) -> Self: + return self.self_optimize_with_info(data_sequence, labels_sequence)[0] + + def self_optimize_with_info( + self, + data_sequence: list[pd.DataFrame], + labels_sequence: list[np.ndarray | pd.Series | pd.DataFrame], + ) -> tuple[Self, History]: + if len(data_sequence) != len(labels_sequence): + raise ValueError( + "The given training sequence and initial training labels do not match in their number of individual " + f"sequences! len(data_train_sequence_list) = {len(data_sequence)} != {len(labels_sequence)} = len(" + "initial_hidden_states_sequence_list)" + ) + + for i, (data, labels) in enumerate(zip(data_sequence, labels_sequence)): + if len(data) < self.n_states: + raise ValueError( + "Invalid training sequence! At least one training sequence has less samples than the specified " + "value of states! " + f"For sequence {i}: n_states = {self.n_states} > {len(data)} = len(data)" + ) + if labels is not None: + if len(data) != len(labels): + raise ValueError( + "Invalid training sequence! At least one training sequence has a different number of samples " + "than the corresponding label sequence! " + f"For sequence {i}: len(data) = {len(data)} != {len(labels)} = len(labels)" + ) + if not np.all(np.logical_and(labels >= 0, labels < self.n_states)): + raise ValueError( + "Invalid label sequence! At least one training sequence contains invalid state labels! " + f"For sequence {i}: labels not in [0, {self.n_states})" + ) + + self.data_columns = tuple(data_sequence[0].columns) + data_sequence_train = [ + np.ascontiguousarray(dataset[list(self.data_columns)].to_numpy().copy().squeeze()) + for dataset in data_sequence + ] + labels_sequence_train = [] + for labels in labels_sequence: + if labels is None: + labels_sequence_train.append(None) + continue + labels = labels.to_numpy().squeeze() if isinstance(labels, (pd.Series, pd.DataFrame)) else labels.squeeze() + labels_sequence_train.append(np.ascontiguousarray(labels.copy())) + + if self.model is not None: + warnings.warn("Model already exists. Overwriting existing model.") + + model_untrained = initialize_hmm( + data_sequence_train, + labels_sequence_train, + n_states=self.n_states, + n_gmm_components=self.n_gmm_components, + architecture=self.architecture, + name=self.name + "-untrained", + ) + model_trained = _clone_model(model_untrained, assert_correct=False) + + _, history = model_trained.fit( + sequences=np.array(data_sequence_train, dtype=object), + labels=np.array(labels_sequence_train, dtype=object), + algorithm=self.algo_train, + stop_threshold=self.stop_threshold, + max_iterations=self.max_iterations, + return_history=True, + verbose=self.verbose, + n_jobs=self.n_jobs, + multiple_check_input=False, + ) + check_history_for_training_failure(history) + model_trained.name = self.name + "_trained" + self.model = model_trained + return self, history + + +def _create_simple_hmm_from_config(config: HmmSubModelConfig) -> SimpleHmm: + return SimpleHmm( + n_states=config.n_states, + n_gmm_components=config.n_gmm_components, + architecture=config.architecture, + algo_train=config.algo_train, + stop_threshold=config.stop_threshold, + max_iterations=config.max_iterations, + verbose=config.verbose, + n_jobs=config.n_jobs, + name=config.name, + ) + + +class PomegranateLegacyHmmBackend(BaseHmmBackend): + """`pomegranate 0.x` backend for HMM training and inference.""" + + def __init__(self, backend_id: str = "pomegranate-legacy") -> None: + if getattr(pg, "HiddenMarkovModel", None) is None: + raise ImportError( + "Failed to initialize `PomegranateLegacyHmmBackend`. " + "This backend requires `pomegranate 0.x` with `HiddenMarkovModel` support. " + f"Installed version: {_get_pomegranate_version() or 'not installed'}." + ) + super().__init__(backend_id=backend_id) + + def create_submodel(self, config: HmmSubModelConfig) -> BaseTrainableHmm: + return _create_simple_hmm_from_config(config) + + def predict( + self, + model: HMMState, + data: pd.DataFrame, + *, + expected_columns: tuple[str, ...], + algorithm: Literal["viterbi", "map"], + verbose: bool, + ) -> np.ndarray: + runtime_model = hmm_state_to_pomegranate_model(model, verbose=verbose) + return predict(runtime_model, data, expected_columns=expected_columns, algorithm=algorithm) + + def finalize_model( + self, + *, + trained_models: dict[str, BaseTrainableHmm], + labels_train_sequence: list[np.ndarray], + data_sequence_feature_space: list[pd.DataFrame], + data_columns: tuple[str, ...], + model_config: CompositeHmmConfig, + module_offsets: dict[str, int], + initialization: Literal["labels", "fully-connected"], + algo_train: Literal["viterbi", "baum-welch"], + stop_threshold: float, + max_iterations: int, + verbose: bool, + n_jobs: int, + name: str, + ) -> tuple[HMMState, Any]: + distributions = [] + for module in model_config.modules: + distributions.extend(get_model_distributions(trained_models[module.name].model)) + + model = self._create_combined_model( + trained_models=trained_models, + labels_train_sequence=labels_train_sequence, + distributions=distributions, + model_config=model_config, + module_offsets=module_offsets, + initialization=initialization, + verbose=verbose, + ) + + labels_train_sequence_str = labels_to_strings(labels_train_sequence) + data_train_sequence = [ + np.ascontiguousarray(feature_data[list(data_columns)].to_numpy().copy()) + for feature_data in data_sequence_feature_space + ] + + _, history = model.fit( + sequences=np.array(data_train_sequence, dtype=object), + labels=np.array(labels_train_sequence_str, dtype=object).copy(), + algorithm=algo_train, + stop_threshold=stop_threshold, + max_iterations=max_iterations, + return_history=True, + verbose=verbose, + n_jobs=n_jobs, + multiple_check_input=False, + ) + check_history_for_training_failure(history) + model.name = name + + submodel_states = tuple( + HmmSubModelState( + name=module.name, + role=module.role, + model=pomegranate_model_to_flat_hmm_state(trained_models[module.name].model), + ) + for module in model_config.modules + ) + model_state = pomegranate_model_to_hmm_state( + model, + submodels=submodel_states, + backend_info=BackendInfo(backend_id=self.backend_id), + ) + model_state.cross_module_transitions = extract_cross_module_transitions(model_state, module_offsets) + return model_state, history + + def _create_combined_model( + self, + *, + trained_models: dict[str, BaseTrainableHmm], + labels_train_sequence: list[np.ndarray], + distributions: list[Any], + model_config: CompositeHmmConfig, + module_offsets: dict[str, int], + initialization: Literal["labels", "fully-connected"], + verbose: bool, + ) -> Any: + pg = _require_legacy_pomegranate() + + n_states = sum(module.n_states for module in model_config.modules) + if initialization == "fully-connected": + trans_mat, start_probs, end_probs = create_transition_matrix_fully_connected(n_states) + trans_mat, end_probs = normalize_transition_and_end_probs(trans_mat, end_probs) + model = pg.HiddenMarkovModel.from_matrix( + transition_probabilities=copy.deepcopy(trans_mat), + distributions=copy.deepcopy(distributions), + starts=start_probs, + ends=end_probs, + state_names=list(create_state_names(n_states)), + verbose=verbose, + ) + else: + trans_mat = np.zeros((n_states, n_states)) + for module in model_config.modules: + module_transition_matrix = trained_models[module.name].model.dense_transition_matrix()[:-2, :-2] + offset = module_offsets[module.name] + trans_mat[offset : offset + module.n_states, offset : offset + module.n_states] = module_transition_matrix + + transitions, _, _ = extract_transitions_starts_stops_from_hidden_state_sequence(labels_train_sequence) + start_probs, end_probs = estimate_sequence_boundary_probs(labels_train_sequence, n_states) + for from_state, to_state in transitions: + trans_mat[int(from_state[1:]), int(to_state[1:])] = max( + trans_mat[int(from_state[1:]), int(to_state[1:])], + 0.1, + ) + trans_mat, end_probs = normalize_transition_and_end_probs(trans_mat, end_probs) + + model = pg.HiddenMarkovModel.from_matrix( + transition_probabilities=copy.deepcopy(trans_mat), + distributions=copy.deepcopy(distributions), + starts=start_probs, + ends=end_probs, + state_names=list(create_state_names(n_states)), + verbose=verbose, + ) + + model = fix_model_names(model) + model.bake() + model.freeze_distributions() + return _clone_model(model, assert_correct=False) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_state.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_state.py new file mode 100644 index 00000000..ca373f1b --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_state.py @@ -0,0 +1,168 @@ +"""Legacy pomegranate adapters for serializable HMM states.""" + +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, version +from typing import Any + +import numpy as np +try: + import pomegranate as pg +except ImportError: # pragma: no cover - exercised in environments without pomegranate + pg = None + +from gaitmap_mad.stride_segmentation.hmm._state import ( + BackendInfo, + CrossModuleTransition, + EmissionState, + FlatHmmState, + GaussianEmissionState, + GaussianMixtureEmissionState, + HMMState, + HmmGraphState, + HmmSubModelState, +) +from gaitmap_mad.stride_segmentation.hmm.legacy._utils import add_transition + + +def _get_pomegranate_version() -> str | None: + try: + return version("pomegranate") + except PackageNotFoundError: + return None + + +def _require_legacy_pomegranate(): + legacy_hmm = getattr(pg, "HiddenMarkovModel", None) + if pg is None or legacy_hmm is None: + raise ImportError("The legacy HMM backend requires pomegranate 0.x with `HiddenMarkovModel` support.") + return pg + + +def _normalize_mixture_weights(log_weights: np.ndarray) -> np.ndarray: + shifted = np.exp(log_weights - np.max(log_weights)) + return shifted / np.sum(shifted) + + +def _legacy_distribution_to_state(distribution: Any) -> EmissionState: + pg = _require_legacy_pomegranate() + if isinstance(distribution, pg.GeneralMixtureModel): + return GaussianMixtureEmissionState( + weights=_normalize_mixture_weights(np.asarray(distribution.weights, dtype=float)), + components=tuple(_legacy_distribution_to_state(component) for component in distribution.distributions), + frozen=bool(getattr(distribution, "frozen", False)), + ) + if isinstance(distribution, pg.MultivariateGaussianDistribution): + mean, covariance = distribution.parameters + return GaussianEmissionState( + mean=np.asarray(mean, dtype=float), + covariance=np.asarray(covariance, dtype=float), + frozen=bool(getattr(distribution, "frozen", False)), + ) + raise TypeError( + f"Unsupported pomegranate emission distribution `{type(distribution).__name__}`. " + "Only multivariate Gaussian and Gaussian mixture emissions are supported in the serialized HMM state." + ) + + +def _state_to_legacy_distribution(state: EmissionState) -> Any: + pg = _require_legacy_pomegranate() + if isinstance(state, GaussianEmissionState): + distribution = pg.MultivariateGaussianDistribution(state.mean.tolist(), state.covariance.tolist()) + distribution.frozen = state.frozen + return distribution + if isinstance(state, GaussianMixtureEmissionState): + weights = np.asarray(state.weights, dtype=float) + weights = np.clip(weights, np.finfo(float).tiny, None) + weights = weights / np.sum(weights) + distribution = pg.GeneralMixtureModel( + [_state_to_legacy_distribution(component) for component in state.components], + weights=weights.tolist(), + ) + distribution.frozen = state.frozen + return distribution + raise TypeError(f"Unsupported serialized emission state `{type(state).__name__}`.") + + +def pomegranate_model_to_flat_hmm_state(model: Any) -> FlatHmmState: + dense_transition_matrix = model.dense_transition_matrix() + graph_state = HmmGraphState( + transition_probs=np.asarray(dense_transition_matrix[:-2, :-2], dtype=float), + start_probs=np.asarray(dense_transition_matrix[-2, :-2], dtype=float), + end_probs=np.asarray(dense_transition_matrix[:-2, -1], dtype=float), + ) + hidden_states = [state for state in model.states if state.distribution is not None] + return FlatHmmState( + graph=graph_state, + emissions=tuple(_legacy_distribution_to_state(state.distribution) for state in hidden_states), + state_names=tuple(state.name for state in hidden_states), + name=model.name, + ) + + +def flat_hmm_state_to_pomegranate_model(state: FlatHmmState, *, verbose: bool = False) -> Any: + pg = _require_legacy_pomegranate() + model = pg.HiddenMarkovModel.from_matrix( + transition_probabilities=np.asarray(state.graph.transition_probs, dtype=float), + distributions=[_state_to_legacy_distribution(distribution) for distribution in state.emissions], + starts=np.asarray(state.graph.start_probs, dtype=float), + ends=np.asarray(state.graph.end_probs, dtype=float), + state_names=list(state.state_names), + verbose=verbose, + ) + model.bake() + if state.name is not None: + model.name = state.name + return model + + +def pomegranate_model_to_hmm_state( + compiled_model: Any, + *, + submodels: tuple[HmmSubModelState, ...] = (), + cross_module_transitions: tuple[CrossModuleTransition, ...] = (), + backend_info: BackendInfo | None = None, +) -> HMMState: + if backend_info is None: + backend_info = BackendInfo(backend_id="pomegranate-legacy", backend_version=_get_pomegranate_version()) + elif backend_info.backend_version is None and backend_info.backend_id.startswith("pomegranate"): + backend_info = BackendInfo( + backend_id=backend_info.backend_id, + backend_version=_get_pomegranate_version(), + state_schema_version=backend_info.state_schema_version, + ) + return HMMState( + trained_with=backend_info, + compiled=pomegranate_model_to_flat_hmm_state(compiled_model), + submodels=submodels, + cross_module_transitions=cross_module_transitions, + ) + + +def hmm_state_to_pomegranate_model(state: HMMState, *, verbose: bool = False) -> Any: + model = flat_hmm_state_to_pomegranate_model(state.compiled, verbose=verbose) + existing_transitions = {(start.name, end.name) for start, end in model.graph.edges()} + for transition in state.cross_module_transitions: + from_state = state.compiled.state_names[_find_state_index(state, transition.from_module, transition.from_state)] + to_state = state.compiled.state_names[_find_state_index(state, transition.to_module, transition.to_state)] + if (from_state, to_state) in existing_transitions: + continue + add_transition(model, (from_state, to_state), transition.probability) + existing_transitions.add((from_state, to_state)) + model.bake() + return model + + +def _find_state_index(state: HMMState, module_name: str, state_idx: int) -> int: + offset = 0 + for submodel in state.submodels: + n_states = len(submodel.model.state_names) + if submodel.name == module_name: + if state_idx >= n_states: + raise ValueError( + f"Cross-module transition refers to state {state_idx} of module `{module_name}`, " + f"but the module only has {n_states} states." + ) + return offset + state_idx + offset += n_states + raise ValueError(f"No submodel named `{module_name}` exists in the serialized HMM state.") diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py new file mode 100644 index 00000000..9b5fb386 --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py @@ -0,0 +1,316 @@ +"""Legacy pomegranate-specific helper utilities.""" + +from __future__ import annotations + +import json +import warnings +from typing import Any, Literal + +import numpy as np +import pandas as pd + +try: + import pomegranate as pg +except ImportError: # pragma: no cover - exercised in environments without pomegranate + pg = None +try: + from pomegranate.hmm import History +except (ImportError, AttributeError): + History = Any +from tpcp import BaseTpcpObject, CloneFactory +from tpcp._hash import custom_hash + +from gaitmap_mad.stride_segmentation.hmm._utils import _DataToShortError, cluster_data_by_labels + + +def _add_transition(model, a, b, probability, pseudocount, group) -> None: + pseudocount = pseudocount or probability + model.graph.add_edge(a, b, probability=probability, pseudocount=pseudocount, group=group) + + +def _clone_model(orig_model: pg.HiddenMarkovModel, assert_correct: bool = True) -> pg.HiddenMarkovModel: + """Clone a legacy pomegranate HMM without changing its values.""" + d = json.loads(orig_model.to_json()) + model = pg.HiddenMarkovModel(str(d["name"])) + + with np.errstate(divide="ignore"): + states = [pg.State.from_dict(j) for j in d["states"]] + + for cloned_state, state in zip(states, orig_model.states): + assert cloned_state.name == state.name + if state.distribution is not None: + cloned_state.distribution.frozen = state.distribution.frozen + if isinstance(state.distribution, pg.GeneralMixtureModel): + cloned_state.distribution.weights[:] = np.copy(state.distribution.weights) + + for i, j in d["distribution ties"]: + states[i].tie(states[j]) + + model.add_states(states) + model.start = states[d["start_index"]] + model.end = states[d["end_index"]] + + new_state_order = [state.name for state in states] + for start, end, data in list(orig_model.graph.edges(data=True)): + _add_transition( + model, + states[new_state_order.index(start.name)], + states[new_state_order.index(end.name)], + data["probability"], + data["pseudocount"], + data["group"], + ) + + model.bake(verbose=False) + + if assert_correct: + assert custom_hash(model) == custom_hash(orig_model), ( + "Cloning the provided HMM model failed! Please open an issue on github with an example." + ) + + return model + + +def _is_serialized_hmm_state(value: Any) -> bool: + return ( + hasattr(value, "compiled") + and hasattr(value, "trained_with") + and callable(getattr(value, "to_json", None)) + and callable(getattr(type(value), "from_json", None)) + ) + + +class _HackyClonableHMMFix(BaseTpcpObject): + """Mixin that teaches `tpcp.clone` how to clone legacy pomegranate HMMs.""" + + @classmethod + def __clone_param__(cls, param_name: str, value: Any) -> Any: + if isinstance(value, pg.HiddenMarkovModel): + return _clone_model(value) + if _is_serialized_hmm_state(value): + return type(value).from_json(value.to_json()) + return super().__clone_param__(param_name, value) + + +class ShortenedHMMPrint(BaseTpcpObject): + """Mixin class to better format HMM models when printing them.""" + + def __repr_parameter__(self, name: str, value: Any) -> str: + if name == "model": + if _is_serialized_hmm_state(value): + n_states = len(value.compiled.state_names) if getattr(value, "compiled", None) is not None else "?" + backend = getattr(getattr(value, "trained_with", None), "backend_id", "?") + return f"{name}=HMMState[backend={backend}, states={n_states}](...)" + if isinstance(value, pg.HiddenMarkovModel): + return f"{name}=HiddenMarkovModel[name={value.name}](...)" + if isinstance(value, CloneFactory) and isinstance(value.default_value, pg.HiddenMarkovModel): + return f"{name}=cf(HiddenMarkovModel[name={value.get_value().name}](...))" + return super().__repr_parameter__(name, value) + + +def gmms_from_samples( + data, + labels, + n_components: int, + n_expected_states: int, + verbose: bool = False, + n_init: int = 5, + n_jobs: int = 1, +): + """Create Gaussian mixture distributions from clustered samples.""" + if not np.array(data, dtype=object).data.c_contiguous: + raise ValueError("Memory Layout of given input data is not contiguous! Consider using numpy.ascontiguousarray.") + + clustered_data = cluster_data_by_labels(data, labels) + + if len(clustered_data) < n_expected_states: + raise ValueError( + f"The training labels did only provide samples for {len(clustered_data)} states, but " + f"{n_expected_states} states were expected. " + "Ensure that the training data contains samples for all states." + ) + + if data[0].ndim == 1: + clustered_data = [np.reshape(data, (len(data), 1)) for data in clustered_data] + + pg_dist_type = pg.MultivariateGaussianDistribution + + if n_components > 1: + for cluster in clustered_data: + if len(cluster) < n_components: + raise ValueError( + f"The training labels did only provide a small number of samples ({len(cluster)}) for one of the " + "states. " + f"To initialize {n_components} components in a mixture model, we need at least {n_components} " + f"samples! " + "Ensure that the training data contains enough samples." + ) + distributions = [] + for cluster in clustered_data: + dist = pg.GeneralMixtureModel.from_samples( + pg_dist_type, + n_components=n_components, + X=cluster, + verbose=verbose, + n_jobs=n_jobs, + n_init=n_init, + init="first-k", + ) + for distribution in dist.distributions: + if np.any([np.isnan(p).any() for p in distribution.parameters]).any(): + raise ValueError( + "NaN in parameters during distribution fitting! " + "This usually happens when there is not enough data for a large number of distributions and " + "states. " + "To avoid this issue, reduce the number of distributions per state or the number of states. " + "Or ideally, provide more data." + ) + distributions.append(dist) + else: + distributions = [pg_dist_type.from_samples(dataset) for dataset in clustered_data] + + return distributions, clustered_data + + +def fix_model_names(model): + """Fix legacy pomegranate state-name ordering for state indices >= 10.""" + for state in model.states: + if state.name[0] == "s": + try: + state_number = int(state.name[1:]) + except ValueError: + continue + if state_number >= 10: + state.name = "s" + chr(87 + state_number) + return model + + +def _iter_nested_distributions(distribution): + if isinstance(distribution, pg.GeneralMixtureModel): + yield distribution + for nested_distribution in distribution.distributions: + yield from _iter_nested_distributions(nested_distribution) + else: + yield distribution + + +def model_params_are_finite(model: pg.HiddenMarkovModel) -> bool: + """Check if all legacy pomegranate distribution parameters are finite.""" + for state in model.states: + for distribution in _iter_nested_distributions(state.distribution): + if hasattr(distribution, "parameters"): + for param in distribution.parameters: + if not np.all(np.isfinite(param)): + return False + if hasattr(distribution, "weights") and not np.all(np.isfinite(distribution.weights)): + return False + return True + + +def check_history_for_training_failure(history: History) -> None: + """Warn if the legacy pomegranate training history indicates failure.""" + if not np.all(np.isfinite(history.improvements)) or np.any(np.array(history.improvements) < 0): + warnings.warn( + "During training the improvement per epoch became NaN/infinite or negative! " + "Run `self_optimize_with_info` and inspect the history element for more information. " + "With a high likelihood, the final model is not usable and will result in errors during prediction. " + "This usually happens when there is not enough data for a large number of distributions and " + "states. " + "To avoid this issue, reduce the number of distributions per state or the number of states. " + "Or ideally, provide more data." + ) + + +def get_state_by_name(model: pg.HiddenMarkovModel, state_name: str): + """Get a state object by name.""" + for state in model.states: + if state.name == state_name: + return state + raise ValueError(f"State {state_name} not found within given model.") + + +def add_transition(model: pg.HiddenMarkovModel, transition: tuple[str, str], transition_probability: float) -> None: + """Add a transition to an existing model by state names.""" + model.add_transition( + get_state_by_name(model, transition[0]), + get_state_by_name(model, transition[1]), + transition_probability, + ) + + +def get_model_distributions(model: pg.HiddenMarkovModel) -> list[pg.Distribution]: + """Return all emitting distributions from a legacy pomegranate model.""" + distributions = [] + for state in model.states: + if state.distribution is not None: + distributions.append(state.distribution) + return distributions + + +def labels_to_strings(labelsequence: list[np.ndarray | None]) -> list[list[str] | None]: + """Convert integer label sequences to legacy state names.""" + assert isinstance(labelsequence, list), "labelsequence must be list!" + + labelsequence_str = [] + for sequence in labelsequence: + if sequence is None: + labelsequence_str.append(sequence) + continue + labelsequence_str.append([f"s{i:02}" for i in sequence]) + return labelsequence_str + + +def predict( + model: Any | None, + data: pd.DataFrame, + *, + expected_columns: tuple[str, ...], + algorithm: Literal["viterbi", "map"], +) -> np.ndarray: + """Predict a hidden-state sequence with a legacy pomegranate runtime model.""" + if model is None: + raise ValueError( + "You need to train the HMM before calling `predict_hidden_state_sequence`. " + "Use `self_optimize` or `self_optimize_with_info` for that." + ) + + try: + data = data[list(expected_columns)] + except KeyError as e: + raise ValueError( + "The provided feature data is expected to have the following columns:\n\n" + f"{expected_columns}\n\n" + "But it only has the following columns:\n\n" + f"{data.columns}" + ) from e + + if len(data) < len(model.states) - 2: + raise _DataToShortError( + "The provided feature data is expected to have at least as many samples as the number of states " + f"of the model ({len(model.states) - 2}). " + f"But it only has {len(data)} samples." + ) + + data = np.ascontiguousarray(data.to_numpy()) + try: + labels_predicted = np.asarray(model.predict(data.copy(), algorithm=algorithm)) + except Exception as e: + if not model_params_are_finite(model): + raise ValueError( + "Prediction failed! (See error above.). " + "However, the provided pomegranate model has non-finite/NaN parameters. " + "This might be the source of the observed error and indicates problems during training. " + "Check the training history and the model parameters to confirm invalid training behaviour. " + "Unfortunately, there is no way to automatically fix these issues. " + "Simply speaking, your training data could not be represented well by the selected model architecture. " + "Check for obvious errors in your pre-processing or try to use a different model architecture. " + ) from e + raise ValueError( + "Prediction failed! (See error above.). " + "Unfortunately, we are not sure what happened. " + "The error was caused by pomegrante internals." + ) from e + + if algorithm == "viterbi": + labels_predicted = labels_predicted[1:-1] + return np.asarray(labels_predicted) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/__init__.py new file mode 100644 index 00000000..94495256 --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/__init__.py @@ -0,0 +1,5 @@ +"""Modern pomegranate backend package.""" + +from gaitmap_mad.stride_segmentation.hmm.modern._backend import PomegranateModernHmmBackend + +__all__ = ["PomegranateModernHmmBackend"] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py new file mode 100644 index 00000000..56202531 --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py @@ -0,0 +1,285 @@ +"""Modern pomegranate backend.""" + +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, version +from typing import Any, Literal + +import numpy as np +import pandas as pd +try: + from pomegranate.hmm import DenseHMM +except (ImportError, AttributeError): # pragma: no cover - exercised in environments without modern pomegranate + DenseHMM = None +try: + import torch +except ImportError: # pragma: no cover - exercised in environments without torch + torch = None + +from gaitmap_mad.stride_segmentation.hmm._backend_base import BaseHmmBackend, BaseTrainableHmm +from gaitmap_mad.stride_segmentation.hmm._backend_common import ( + extract_cross_module_transitions, + normalize_transition_and_end_probs, + prepare_predict_data, +) +from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig +from gaitmap_mad.stride_segmentation.hmm._state import BackendInfo, FlatHmmState, HmmGraphState, HMMState, HmmSubModelState +from gaitmap_mad.stride_segmentation.hmm._utils import ( + create_state_names, + create_transition_matrix_fully_connected, + estimate_sequence_boundary_probs, + extract_transitions_starts_stops_from_hidden_state_sequence, +) +from gaitmap_mad.stride_segmentation.hmm.modern._state import ( + flat_hmm_state_to_pomegranate_modern_model, + hmm_state_to_pomegranate_modern_model, + pomegranate_modern_model_to_flat_hmm_state, + pomegranate_modern_model_to_hmm_state, +) +from gaitmap_mad.stride_segmentation.hmm.modern._utils import ( + PomegranateModernHistory, + freeze_emission, + labels_to_priors, + to_modern_input, + to_training_arrays, + trainable_state_from_clusters, +) + + +def _get_pomegranate_version() -> str | None: + try: + return version("pomegranate") + except PackageNotFoundError: + return None + + +def _predict_with_native_runtime_model( + runtime_model: Any, + observations: np.ndarray, + *, + algorithm: Literal["viterbi", "map"], +) -> np.ndarray: + runtime_input = torch.tensor(observations, dtype=torch.float64).unsqueeze(0) + with torch.no_grad(): + if algorithm == "viterbi": + return runtime_model.viterbi(runtime_input).detach().cpu().numpy()[0] + return runtime_model.predict(runtime_input).detach().cpu().numpy()[0] + + +class PomegranateModernTrainableHmm(BaseTrainableHmm): + """Internal trainable HMM wrapper used by the modern pomegranate backend.""" + + def __init__(self, config: HmmSubModelConfig, backend_id: str) -> None: + self.config = config + self.backend_id = backend_id + self.data_columns: tuple[str, ...] | None = None + self.model: FlatHmmState | None = None + + def predict_hidden_state_sequence( + self, feature_data: pd.DataFrame, algorithm: Literal["viterbi", "map"] = "viterbi" + ) -> np.ndarray: + if self.model is None or self.data_columns is None: + raise ValueError("No trained model available. Call `self_optimize` first.") + observations = prepare_predict_data(feature_data, self.data_columns, len(self.model.state_names)) + runtime_model = flat_hmm_state_to_pomegranate_modern_model( + self.model, + verbose=self.config.verbose, + max_iterations=self.config.max_iterations, + stop_threshold=self.config.stop_threshold, + ).to(torch.float64) + return _predict_with_native_runtime_model(runtime_model, observations, algorithm=algorithm) + + def self_optimize_with_info( + self, + data_sequence: list[pd.DataFrame], + labels_sequence: list[np.ndarray], + ) -> tuple[PomegranateModernTrainableHmm, PomegranateModernHistory]: + if self.config.algo_train != "baum-welch": + raise NotImplementedError( + "The modern pomegranate backend currently only supports `baum-welch` training." + ) + if len(data_sequence) != len(labels_sequence): + raise ValueError( + "The given training sequence and initial training labels do not match in their number of individual " + f"sequences! len(data_sequence) = {len(data_sequence)} != {len(labels_sequence)} = len(labels_sequence)" + ) + + self.data_columns = tuple(data_sequence[0].columns) + arrays = to_training_arrays(data_sequence, data_columns=self.data_columns) + flat_state = trainable_state_from_clusters( + arrays, + labels_sequence, + n_states=self.config.n_states, + n_gmm_components=self.config.n_gmm_components, + architecture=self.config.architecture, + name=self.config.name, + normalize_transition_and_end_probs=normalize_transition_and_end_probs, + ) + runtime_model = flat_hmm_state_to_pomegranate_modern_model( + flat_state, + verbose=self.config.verbose, + max_iterations=self.config.max_iterations, + stop_threshold=self.config.stop_threshold, + ) + runtime_model.fit(to_modern_input(arrays)) + self.model = pomegranate_modern_model_to_flat_hmm_state( + runtime_model, + state_names=flat_state.state_names, + name=f"{self.config.name}_trained", + ) + return self, PomegranateModernHistory() + + +class PomegranateModernHmmBackend(BaseHmmBackend): + """`pomegranate 1.x` backend using DenseHMM for training.""" + + inference_implementation: Literal["canonical", "native"] + + def __init__( + self, + backend_id: str = "pomegranate-modern", + *, + inference_implementation: Literal["canonical", "native"] = "native", + ) -> None: + if DenseHMM is None: + raise ImportError( + "Failed to initialize `PomegranateModernHmmBackend`. " + "This backend requires `pomegranate 1.x` with `DenseHMM` support. " + f"Installed version: {_get_pomegranate_version() or 'not installed'}." + ) + if torch is None: + raise ImportError( + "Failed to initialize `PomegranateModernHmmBackend`. " + "This backend requires `torch` because native `pomegranate 1.x` inference and training run on PyTorch." + ) + super().__init__(backend_id=backend_id) + self.inference_implementation = inference_implementation + + def create_submodel(self, config: HmmSubModelConfig) -> BaseTrainableHmm: + return PomegranateModernTrainableHmm(config, backend_id=self.backend_id) + + def predict( + self, + model: HMMState, + data: pd.DataFrame, + *, + expected_columns: tuple[str, ...], + algorithm: Literal["viterbi", "map"], + verbose: bool, + ) -> np.ndarray: + observations = prepare_predict_data(data, expected_columns, len(model.compiled.state_names)) + return self._predict_native(model, observations, algorithm=algorithm, verbose=verbose) + + def _predict_native( + self, + model: HMMState, + observations: np.ndarray, + *, + algorithm: Literal["viterbi", "map"], + verbose: bool, + ) -> np.ndarray: + runtime_model = hmm_state_to_pomegranate_modern_model(model, verbose=verbose).to(torch.float64) + return _predict_with_native_runtime_model(runtime_model, observations, algorithm=algorithm) + + def finalize_model( + self, + *, + trained_models: dict[str, BaseTrainableHmm], + labels_train_sequence: list[np.ndarray], + data_sequence_feature_space: list[pd.DataFrame], + data_columns: tuple[str, ...], + model_config: CompositeHmmConfig, + module_offsets: dict[str, int], + initialization: Literal["labels", "fully-connected"], + algo_train: Literal["viterbi", "baum-welch"], + stop_threshold: float, + max_iterations: int, + verbose: bool, + n_jobs: int, + name: str, + ) -> tuple[HMMState, Any]: + del n_jobs + if algo_train != "baum-welch": + raise NotImplementedError("The modern pomegranate backend currently only supports `baum-welch` training.") + + submodel_states = tuple( + HmmSubModelState(name=module.name, role=module.role, model=trained_models[module.name].model) + for module in model_config.modules + ) + combined_state = self._create_combined_state( + trained_models=trained_models, + labels_train_sequence=labels_train_sequence, + model_config=model_config, + module_offsets=module_offsets, + initialization=initialization, + name=name, + ) + runtime_model = flat_hmm_state_to_pomegranate_modern_model( + combined_state, + verbose=verbose, + max_iterations=max_iterations, + stop_threshold=stop_threshold, + ) + priors = labels_to_priors(labels_train_sequence, len(combined_state.state_names)) + training_data = to_modern_input(to_training_arrays(data_sequence_feature_space, data_columns=data_columns)) + runtime_model.fit(training_data, priors=priors) + + model_state = pomegranate_modern_model_to_hmm_state( + runtime_model, + submodels=submodel_states, + backend_info=BackendInfo(backend_id=self.backend_id, backend_version=_get_pomegranate_version()), + state_names=combined_state.state_names, + name=name, + ) + model_state.cross_module_transitions = extract_cross_module_transitions(model_state, module_offsets) + return model_state, PomegranateModernHistory() + + def _create_combined_state( + self, + *, + trained_models: dict[str, BaseTrainableHmm], + labels_train_sequence: list[np.ndarray], + model_config: CompositeHmmConfig, + module_offsets: dict[str, int], + initialization: Literal["labels", "fully-connected"], + name: str, + ) -> FlatHmmState: + n_states = sum(module.n_states for module in model_config.modules) + if initialization == "fully-connected": + transition_probs, start_probs, end_probs = create_transition_matrix_fully_connected(n_states) + return FlatHmmState( + graph=HmmGraphState(transition_probs=transition_probs, start_probs=start_probs, end_probs=end_probs), + emissions=tuple( + freeze_emission(emission) + for module in model_config.modules + for emission in trained_models[module.name].model.emissions + ), + state_names=create_state_names(n_states), + name=name, + ) + + transition_probs = np.zeros((n_states, n_states), dtype=float) + emissions = [] + for module in model_config.modules: + module_state = trained_models[module.name].model + offset = module_offsets[module.name] + size = module.n_states + transition_probs[offset : offset + size, offset : offset + size] = module_state.graph.transition_probs + emissions.extend(freeze_emission(emission) for emission in module_state.emissions) + + transitions, _, _ = extract_transitions_starts_stops_from_hidden_state_sequence(labels_train_sequence) + start_probs, end_probs = estimate_sequence_boundary_probs(labels_train_sequence, n_states) + for from_state, to_state in transitions: + transition_probs[int(from_state[1:]), int(to_state[1:])] = max( + transition_probs[int(from_state[1:]), int(to_state[1:])], + 0.1, + ) + + transition_probs, end_probs = normalize_transition_and_end_probs(transition_probs, end_probs) + + return FlatHmmState( + graph=HmmGraphState(transition_probs=transition_probs, start_probs=start_probs, end_probs=end_probs), + emissions=tuple(emissions), + state_names=create_state_names(n_states), + name=name, + ) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py new file mode 100644 index 00000000..ae5e8781 --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py @@ -0,0 +1,174 @@ +"""Modern pomegranate adapters for serializable HMM states.""" + +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, version +from typing import Any + +import numpy as np +try: + from pomegranate.distributions import Normal +except (ImportError, AttributeError): # pragma: no cover - exercised in environments without modern pomegranate + Normal = None +try: + from pomegranate.gmm import GeneralMixtureModel +except (ImportError, AttributeError): # pragma: no cover - exercised in environments without modern pomegranate + GeneralMixtureModel = None +try: + from pomegranate.hmm import DenseHMM +except (ImportError, AttributeError): # pragma: no cover - exercised in environments without modern pomegranate + DenseHMM = None + +from gaitmap_mad.stride_segmentation.hmm._state import ( + BackendInfo, + EmissionState, + FlatHmmState, + GaussianEmissionState, + GaussianMixtureEmissionState, + HMMState, + HmmGraphState, + HmmSubModelState, +) +from gaitmap_mad.stride_segmentation.hmm._utils import create_state_names + + +def _get_pomegranate_version() -> str | None: + try: + return version("pomegranate") + except PackageNotFoundError: + return None + + +def _require_modern_pomegranate() -> tuple[Any, Any, Any]: + if DenseHMM is None or Normal is None or GeneralMixtureModel is None: + raise ImportError("The modern HMM backend requires pomegranate 1.x with `DenseHMM` support.") + return DenseHMM, Normal, GeneralMixtureModel + + +def _parameter_to_numpy(parameter: Any) -> np.ndarray: + if hasattr(parameter, "detach"): + return parameter.detach().cpu().numpy() + return np.asarray(parameter, dtype=float) + + +def _modern_distribution_to_state(distribution: Any) -> EmissionState: + _, normal, general_mixture_model = _require_modern_pomegranate() + if isinstance(distribution, general_mixture_model): + return GaussianMixtureEmissionState( + weights=np.asarray(_parameter_to_numpy(distribution.priors), dtype=float), + components=tuple(_modern_distribution_to_state(component) for component in distribution.distributions), + frozen=bool(_parameter_to_numpy(distribution.frozen)), + ) + if isinstance(distribution, normal): + return GaussianEmissionState( + mean=np.asarray(_parameter_to_numpy(distribution.means), dtype=float), + covariance=np.asarray(_parameter_to_numpy(distribution.covs), dtype=float), + frozen=bool(_parameter_to_numpy(distribution.frozen)), + ) + raise TypeError( + f"Unsupported modern pomegranate emission distribution `{type(distribution).__name__}`. " + "Only Normal and GeneralMixtureModel emissions are supported in the serialized HMM state." + ) + + +def _state_to_modern_distribution(state: EmissionState) -> Any: + _, normal, general_mixture_model = _require_modern_pomegranate() + if isinstance(state, GaussianEmissionState): + return normal( + means=np.asarray(state.mean, dtype=float), + covs=np.asarray(state.covariance, dtype=float), + covariance_type=state.covariance_type, + frozen=state.frozen, + ) + if isinstance(state, GaussianMixtureEmissionState): + weights = np.asarray(state.weights, dtype=float) + weights = np.clip(weights, np.finfo(float).tiny, None) + weights = weights / np.sum(weights) + return general_mixture_model( + [_state_to_modern_distribution(component) for component in state.components], + priors=weights, + frozen=state.frozen, + ) + raise TypeError(f"Unsupported serialized emission state `{type(state).__name__}`.") + + +def pomegranate_modern_model_to_flat_hmm_state( + model: Any, + *, + state_names: tuple[str, ...] | None = None, + name: str | None = None, +) -> FlatHmmState: + if state_names is None: + state_names = create_state_names(len(model.distributions)) + return FlatHmmState( + graph=HmmGraphState( + transition_probs=np.exp(np.asarray(_parameter_to_numpy(model.edges), dtype=float)), + start_probs=np.exp(np.asarray(_parameter_to_numpy(model.starts), dtype=float)), + end_probs=np.exp(np.asarray(_parameter_to_numpy(model.ends), dtype=float)), + ), + emissions=tuple(_modern_distribution_to_state(distribution) for distribution in model.distributions), + state_names=state_names, + name=name, + ) + + +def flat_hmm_state_to_pomegranate_modern_model( + state: FlatHmmState, + *, + verbose: bool = False, + max_iterations: int = 1000, + stop_threshold: float = 0.1, +) -> Any: + dense_hmm, _, _ = _require_modern_pomegranate() + model = dense_hmm( + distributions=[_state_to_modern_distribution(distribution) for distribution in state.emissions], + edges=np.asarray(state.graph.transition_probs, dtype=float), + starts=np.asarray(state.graph.start_probs, dtype=float), + ends=np.asarray(state.graph.end_probs, dtype=float), + max_iter=max_iterations, + tol=stop_threshold, + verbose=verbose, + ) + if state.name is not None: + model.name = state.name + return model + + +def pomegranate_modern_model_to_hmm_state( + compiled_model: Any, + *, + submodels: tuple[HmmSubModelState, ...] = (), + cross_module_transitions: tuple[Any, ...] = (), + backend_info: BackendInfo | None = None, + state_names: tuple[str, ...] | None = None, + name: str | None = None, +) -> HMMState: + if backend_info is None: + backend_info = BackendInfo(backend_id="pomegranate-modern", backend_version=_get_pomegranate_version()) + elif backend_info.backend_version is None and backend_info.backend_id.startswith("pomegranate"): + backend_info = BackendInfo( + backend_id=backend_info.backend_id, + backend_version=_get_pomegranate_version(), + state_schema_version=backend_info.state_schema_version, + ) + return HMMState( + trained_with=backend_info, + compiled=pomegranate_modern_model_to_flat_hmm_state(compiled_model, state_names=state_names, name=name), + submodels=submodels, + cross_module_transitions=cross_module_transitions, + ) + + +def hmm_state_to_pomegranate_modern_model( + state: HMMState, + *, + verbose: bool = False, + max_iterations: int = 1000, + stop_threshold: float = 0.1, +) -> Any: + return flat_hmm_state_to_pomegranate_modern_model( + state.compiled, + verbose=verbose, + max_iterations=max_iterations, + stop_threshold=stop_threshold, + ) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_utils.py new file mode 100644 index 00000000..ac07cbba --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_utils.py @@ -0,0 +1,175 @@ +"""Modern-backend-specific training helpers.""" + +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, version +from typing import Literal + +import numpy as np +import pandas as pd +from sklearn.mixture import GaussianMixture + +from gaitmap.base import _BaseSerializable +from gaitmap_mad.stride_segmentation.hmm._state import ( + BackendInfo, + FlatHmmState, + GaussianEmissionState, + GaussianMixtureEmissionState, + HMMState, + HmmGraphState, +) +from gaitmap_mad.stride_segmentation.hmm._utils import ( + cluster_data_by_labels, + create_state_names, + create_transition_matrix_fully_connected, + create_transition_matrix_left_right, +) + + +def _get_pomegranate_version() -> str | None: + try: + return version("pomegranate") + except PackageNotFoundError: + return None + + +class PomegranateModernHistory(_BaseSerializable): + """Minimal training trace for the modern pomegranate backend.""" + + improvements: tuple[float, ...] + + def __init__(self, improvements: tuple[float, ...] = ()) -> None: + self.improvements = improvements + + +def fit_gaussian_emission( + cluster: np.ndarray, n_components: int +) -> GaussianEmissionState | GaussianMixtureEmissionState: + """Fit one Gaussian or Gaussian-mixture emission to a clustered dataset.""" + if len(cluster) < n_components: + raise ValueError( + f"The training labels did only provide {len(cluster)} samples for one state, " + f"but {n_components} GMM components were requested." + ) + if cluster.ndim == 1: + cluster = cluster.reshape(-1, 1) + mixture = GaussianMixture( + n_components=n_components, + covariance_type="full", + random_state=0, + n_init=5, + reg_covar=np.finfo(float).eps, + ) + mixture.fit(cluster) + if n_components == 1: + return GaussianEmissionState(mean=mixture.means_[0], covariance=mixture.covariances_[0]) + return GaussianMixtureEmissionState( + weights=mixture.weights_, + components=tuple( + GaussianEmissionState(mean=mean, covariance=covariance) + for mean, covariance in zip(mixture.means_, mixture.covariances_) + ), + ) + + +def to_training_arrays( + data_sequence: list[pd.DataFrame] | list[np.ndarray], + *, + data_columns: tuple[str, ...] | None = None, +) -> list[np.ndarray]: + """Convert feature-space training data to contiguous numpy arrays.""" + arrays = [] + for data in data_sequence: + if isinstance(data, pd.DataFrame): + columns = data.columns if data_columns is None else list(data_columns) + arrays.append(np.ascontiguousarray(data[columns].to_numpy().copy())) + continue + arrays.append(np.ascontiguousarray(data.copy())) + return arrays + + +def to_modern_input(data_sequence: list[np.ndarray]) -> list[np.ndarray]: + """Normalize training arrays to the dtype expected by modern pomegranate.""" + return [sequence.astype(float, copy=False) for sequence in data_sequence] + + +def labels_to_priors(labels_sequence: list[np.ndarray], n_states: int) -> list[np.ndarray]: + """Convert hard labels into one-hot prior matrices for modern pomegranate.""" + priors = [] + for labels in labels_sequence: + one_hot = np.zeros((len(labels), n_states), dtype=float) + one_hot[np.arange(len(labels)), labels.astype(int)] = 1.0 + priors.append(one_hot) + return priors + + +def freeze_emission( + emission: GaussianEmissionState | GaussianMixtureEmissionState, +) -> GaussianEmissionState | GaussianMixtureEmissionState: + """Clone an emission and mark it as frozen for combined-model training.""" + if isinstance(emission, GaussianEmissionState): + return GaussianEmissionState( + mean=emission.mean.copy(), + covariance=emission.covariance.copy(), + covariance_type=emission.covariance_type, + frozen=True, + ) + return GaussianMixtureEmissionState( + weights=emission.weights.copy(), + components=tuple(freeze_emission(component) for component in emission.components), + frozen=True, + ) + + +def create_initial_graph_state( + n_states: int, + architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"], + normalize_transition_and_end_probs, +) -> HmmGraphState: + """Create an initial graph state for modern backend training.""" + if architecture == "left-right-strict": + transition_matrix, start_probs, end_probs = create_transition_matrix_left_right(n_states, self_transition=False) + elif architecture == "left-right-loose": + transition_matrix, _, _ = create_transition_matrix_left_right(n_states, self_transition=True) + start_probs = np.ones(n_states).astype(float) + end_probs = np.ones(n_states).astype(float) + else: + transition_matrix, start_probs, end_probs = create_transition_matrix_fully_connected(n_states) + transition_matrix, end_probs = normalize_transition_and_end_probs(transition_matrix, end_probs) + start_probs = np.asarray(start_probs, dtype=float) + start_probs /= start_probs.sum() + return HmmGraphState(transition_probs=transition_matrix, start_probs=start_probs, end_probs=end_probs) + + +def trainable_state_from_clusters( + data_sequence: list[np.ndarray], + labels_sequence: list[np.ndarray], + *, + n_states: int, + n_gmm_components: int, + architecture: Literal["left-right-strict", "left-right-loose", "fully-connected"], + name: str, + normalize_transition_and_end_probs, +) -> FlatHmmState: + """Create an initial trainable flat state from clustered labeled data.""" + clustered_data = cluster_data_by_labels(data_sequence, labels_sequence) + if len(clustered_data) < n_states: + raise ValueError( + f"The training labels did only provide samples for {len(clustered_data)} states, but {n_states} states " + "were expected." + ) + emissions = tuple(fit_gaussian_emission(cluster, n_gmm_components) for cluster in clustered_data[:n_states]) + return FlatHmmState( + graph=create_initial_graph_state(n_states, architecture, normalize_transition_and_end_probs), + emissions=emissions, + state_names=create_state_names(n_states), + name=name, + ) + + +def build_tmp_hmm_state(model: FlatHmmState, backend_id: str) -> HMMState: + """Wrap a flat state in a temporary `HMMState` container.""" + return HMMState( + trained_with=BackendInfo(backend_id=backend_id, backend_version=_get_pomegranate_version()), + compiled=model, + ) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/__init__.py new file mode 100644 index 00000000..8d7c34d5 --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/__init__.py @@ -0,0 +1,5 @@ +"""SciPy backend package.""" + +from gaitmap_mad.stride_segmentation.hmm.scipy._backend import ScipyHmmInferenceBackend + +__all__ = ["ScipyHmmInferenceBackend"] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/_backend.py new file mode 100644 index 00000000..5ab24580 --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/_backend.py @@ -0,0 +1,66 @@ +"""SciPy inference backend.""" + +from __future__ import annotations + +from typing import Any, Literal + +import numpy as np +import pandas as pd + +from gaitmap_mad.stride_segmentation.hmm._backend_base import BaseHmmBackend, BaseTrainableHmm +from gaitmap_mad.stride_segmentation.hmm._backend_common import prepare_predict_data +from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig +from gaitmap_mad.stride_segmentation.hmm._state import HMMState +from gaitmap_mad.stride_segmentation.hmm.scipy._utils import ( + log_emission_probabilities, + map_decode, + viterbi_decode, +) + + +class ScipyHmmInferenceBackend(BaseHmmBackend): + """SciPy-based inference-only backend operating directly on `HMMState`.""" + + def __init__(self, backend_id: str = "scipy-inference") -> None: + super().__init__(backend_id=backend_id) + + def create_submodel(self, config: HmmSubModelConfig) -> BaseTrainableHmm: + raise NotImplementedError("ScipyHmmInferenceBackend is inference-only and can not create trainable submodels.") + + def finalize_model( + self, + *, + trained_models: dict[str, BaseTrainableHmm], + labels_train_sequence: list[np.ndarray], + data_sequence_feature_space: list[pd.DataFrame], + data_columns: tuple[str, ...], + model_config: CompositeHmmConfig, + module_offsets: dict[str, int], + initialization: Literal["labels", "fully-connected"], + algo_train: Literal["viterbi", "baum-welch"], + stop_threshold: float, + max_iterations: int, + verbose: bool, + n_jobs: int, + name: str, + ) -> tuple[HMMState, Any]: + raise NotImplementedError("ScipyHmmInferenceBackend is inference-only and can not finalize/train models.") + + def predict( + self, + model: HMMState, + data: pd.DataFrame, + *, + expected_columns: tuple[str, ...], + algorithm: Literal["viterbi", "map"], + verbose: bool, + ) -> np.ndarray: + del verbose + observations = prepare_predict_data(data, expected_columns, len(model.compiled.state_names)) + log_emissions = log_emission_probabilities(model, observations) + if algorithm == "viterbi": + return viterbi_decode(model, log_emissions) + return map_decode(model, log_emissions) + + +__all__ = ["ScipyHmmInferenceBackend"] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/_utils.py new file mode 100644 index 00000000..5e97fca4 --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/_utils.py @@ -0,0 +1,107 @@ +"""SciPy-specific HMM inference helpers.""" + +from __future__ import annotations + +import numpy as np +from scipy.special import logsumexp +from scipy.stats import multivariate_normal + +from gaitmap_mad.stride_segmentation.hmm._state import ( + GaussianEmissionState, + GaussianMixtureEmissionState, + HMMState, +) + + +def log_emission_probabilities(model: HMMState, observations: np.ndarray) -> np.ndarray: + """Evaluate log-emission probabilities for each state and observation.""" + log_emissions = np.empty((len(observations), len(model.compiled.emissions)), dtype=float) + for state_idx, emission in enumerate(model.compiled.emissions): + if isinstance(emission, GaussianEmissionState): + log_emissions[:, state_idx] = multivariate_normal.logpdf( + observations, + mean=emission.mean, + cov=emission.covariance, + allow_singular=True, + ) + continue + if isinstance(emission, GaussianMixtureEmissionState): + component_log_probs = np.column_stack([ + multivariate_normal.logpdf( + observations, + mean=component.mean, + cov=component.covariance, + allow_singular=True, + ) + for component in emission.components + ]) + with np.errstate(divide="ignore"): + log_weights = np.log(np.asarray(emission.weights, dtype=float)) + log_emissions[:, state_idx] = logsumexp(component_log_probs + log_weights, axis=1) + continue + raise TypeError(f"Unsupported serialized emission state `{type(emission).__name__}`.") + return log_emissions + + +def viterbi_decode(model: HMMState, log_emissions: np.ndarray) -> np.ndarray: + """Run Viterbi decoding on the canonical serialized HMM state.""" + end_probs = np.asarray(model.compiled.graph.end_probs, dtype=float) + use_terminal_state = np.any(end_probs > 0) + with np.errstate(divide="ignore"): + transition_log_probs = np.log(np.asarray(model.compiled.graph.transition_probs, dtype=float)) + start_log_probs = np.log(np.asarray(model.compiled.graph.start_probs, dtype=float)) + end_log_probs = np.log(end_probs) + + n_samples, n_states = log_emissions.shape + dp = np.full((n_samples, n_states), -np.inf, dtype=float) + pointers = np.zeros((n_samples, n_states), dtype=int) + + dp[0] = start_log_probs + log_emissions[0] + for sample_idx in range(1, n_samples): + scores = dp[sample_idx - 1][:, None] + transition_log_probs + pointers[sample_idx] = np.argmax(scores, axis=0) + dp[sample_idx] = scores[pointers[sample_idx], np.arange(n_states)] + log_emissions[sample_idx] + + path = np.zeros(n_samples, dtype=int) + if use_terminal_state: + path[-1] = int(np.argmax(dp[-1] + end_log_probs)) + else: + path[-1] = int(np.argmax(dp[-1])) + for sample_idx in range(n_samples - 1, 0, -1): + path[sample_idx - 1] = pointers[sample_idx, path[sample_idx]] + if use_terminal_state: + return path + return path[:-1] + + +def map_decode(model: HMMState, log_emissions: np.ndarray) -> np.ndarray: + """Run forward-backward MAP decoding on the canonical serialized HMM state.""" + end_probs = np.asarray(model.compiled.graph.end_probs, dtype=float) + with np.errstate(divide="ignore"): + transition_log_probs = np.log(np.asarray(model.compiled.graph.transition_probs, dtype=float)) + start_log_probs = np.log(np.asarray(model.compiled.graph.start_probs, dtype=float)) + end_log_probs = np.log(end_probs) + + n_samples, n_states = log_emissions.shape + forward = np.full((n_samples, n_states), -np.inf, dtype=float) + backward = np.full((n_samples, n_states), -np.inf, dtype=float) + + forward[0] = start_log_probs + log_emissions[0] + for sample_idx in range(1, n_samples): + forward[sample_idx] = log_emissions[sample_idx] + logsumexp( + forward[sample_idx - 1][:, None] + transition_log_probs, + axis=0, + ) + + if np.any(end_probs > 0): + backward[-1] = end_log_probs + else: + backward[-1] = 0.0 + for sample_idx in range(n_samples - 2, -1, -1): + backward[sample_idx] = logsumexp( + transition_log_probs + log_emissions[sample_idx + 1][None, :] + backward[sample_idx + 1][None, :], + axis=1, + ) + + posterior = forward + backward + return np.argmax(posterior, axis=1) diff --git a/tests/test_stride_segmentation/test_roth_hmm.py b/tests/test_stride_segmentation/test_roth_hmm.py index d1268b02..1e98f68e 100644 --- a/tests/test_stride_segmentation/test_roth_hmm.py +++ b/tests/test_stride_segmentation/test_roth_hmm.py @@ -1,11 +1,15 @@ import json -from pathlib import Path +from types import SimpleNamespace from unittest.mock import patch import numpy as np import pandas as pd import pytest from numpy.testing import assert_almost_equal, assert_array_equal +try: + from pomegranate.hmm import DenseHMM +except (ImportError, AttributeError): + DenseHMM = None pytest.importorskip("pomegranate") @@ -13,7 +17,6 @@ from pomegranate.hmm import History from tpcp._hash import custom_hash -from gaitmap.base import _custom_deserialize from gaitmap.data_transform import SlidingWindowMean from gaitmap.utils.consts import BF_COLS from gaitmap.utils.coordinate_conversion import convert_left_foot_to_fbf, convert_to_fbf @@ -29,6 +32,7 @@ HmmStrideSegmentation, HmmSubModelConfig, PomegranateHmmBackend, + PomegranateModernHmmBackend, PreTrainedRothSegmentationModel, RothHmmConfig, RothHmmFeatureTransformer, @@ -36,16 +40,29 @@ ScipyHmmInferenceBackend, SimpleHmm, ) -from gaitmap_mad.stride_segmentation.hmm import _backend as backend_module -from gaitmap_mad.stride_segmentation.hmm._simple_model import initialize_hmm -from gaitmap_mad.stride_segmentation.hmm._state import hmm_state_to_pomegranate_model, pomegranate_model_to_hmm_state -from gaitmap_mad.stride_segmentation.hmm._utils import predict +from gaitmap_mad.stride_segmentation.hmm.legacy import _backend as backend_module +from gaitmap_mad.stride_segmentation.hmm.legacy._backend import initialize_hmm +from gaitmap_mad.stride_segmentation.hmm.legacy._state import ( + hmm_state_to_pomegranate_model, + pomegranate_model_to_hmm_state, +) +from gaitmap_mad.stride_segmentation.hmm.legacy._utils import predict +from gaitmap_mad.stride_segmentation.hmm._utils import estimate_sequence_boundary_probs from tests.mixins.test_algorithm_mixin import TestAlgorithmMixin # Fix random seed for reproducibility np.random.seed(1) +def _runtime_inference_backend_params(): + params = [pytest.param(ScipyHmmInferenceBackend(), id="scipy")] + if DenseHMM is not None: + params.append(pytest.param(PomegranateModernHmmBackend(), id="pomegranate-modern")) + else: + params.append(pytest.param(None, id="pomegranate-modern", marks=pytest.mark.skip(reason="requires pomegranate 1.x"))) + return params + + def _create_roth_model_config(*, stride_n_states=20, stride_n_gmm_components=6, transition_n_gmm_components=3): return CompositeHmmConfig( modules=( @@ -90,15 +107,6 @@ def _stride_list_to_region_list(stride_list: pd.DataFrame, region_type: str = "s return region_list.set_index("roi_id") -def _load_raw_pretrained_pomegranate_model(): - raw = json.loads( - Path( - "packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_pre_trained_models/fallriskpd_at_lab_model.json" - ).read_text() - ) - return _custom_deserialize(raw["params"]["model"]) - - class TestMetaFunctionalityRothSegmentationHmm(TestAlgorithmMixin): __test__ = True @@ -399,6 +407,84 @@ def test_invalid_architecture_raises_error(self) -> None: class TestRothSegmentationHmm: + def test_boundary_prob_estimation_uses_empirical_counts(self) -> None: + start_probs, end_probs = estimate_sequence_boundary_probs( + [np.array([0, 1, 2]), np.array([1, 2, 2]), np.array([1, 0, 2])], + 3, + ) + + assert_array_equal(start_probs, np.array([1 / 3, 2 / 3, 0.0])) + assert_array_equal(end_probs, np.array([0.0, 0.0, 1.0])) + + def test_legacy_combined_model_uses_global_end_probabilities(self) -> None: + backend = backend_module.PomegranateLegacyHmmBackend() + model_config = CompositeHmmConfig( + modules=( + HmmSubModelConfig( + name="transition", + role="transition", + n_states=2, + n_gmm_components=1, + architecture="left-right-loose", + ), + HmmSubModelConfig( + name="stride", + role="stride", + n_states=2, + n_gmm_components=1, + architecture="left-right-strict", + ), + ) + ) + module_offsets = {"transition": 0, "stride": 2} + trained_models = { + "transition": SimpleNamespace( + model=initialize_hmm( + [np.random.rand(12, 1)], + [np.tile(np.arange(2), 6)], + n_states=2, + n_gmm_components=1, + architecture="left-right-loose", + verbose=False, + ) + ), + "stride": SimpleNamespace( + model=initialize_hmm( + [np.random.rand(12, 1)], + [np.tile(np.arange(2), 6)], + n_states=2, + n_gmm_components=1, + architecture="left-right-strict", + verbose=False, + ) + ), + } + + combined = backend._create_combined_model( + trained_models=trained_models, + labels_train_sequence=[ + np.array([0, 1, 2, 3]), + np.array([0, 1, 2, 3]), + np.array([0, 1, 2, 2]), + ], + distributions=[ + distribution + for model in trained_models.values() + for distribution in backend_module.get_model_distributions(model.model) + ], + model_config=model_config, + module_offsets=module_offsets, + initialization="labels", + verbose=False, + ) + + end_probs = combined.dense_transition_matrix()[:-2, -1] + + assert end_probs[2] > 0 + assert end_probs[3] > 0 + assert end_probs[0] == 0 + assert end_probs[1] == 0 + def test_predict_without_model_raises_error(self) -> None: with pytest.raises(ValueError) as e: RothSegmentationHmm().predict(pd.DataFrame(np.random.rand(100, 3)), sampling_rate_hz=100) @@ -585,8 +671,7 @@ def test_training_updates_final_model(self) -> None: assert hash_model != custom_hash(instance.model) def test_pretrained_model_is_migrated_to_hmm_state(self) -> None: - with pytest.warns(UserWarning, match="migrated to the new HMMState representation"): - model = PreTrainedRothSegmentationModel() + model = PreTrainedRothSegmentationModel() assert isinstance(model.model, HMMState) assert model.model.trained_with.backend_id == "pomegranate-legacy-migrated" @@ -604,36 +689,21 @@ def test_pretrained_model_migration_removes_silent_backend_states(self) -> None: assert len(compiled.emissions) == len(compiled.state_names) assert all(name not in {"start", "end"} for name in compiled.state_names) - def test_pretrained_model_roundtrip_matches_legacy_hidden_states(self, healthy_example_imu_data) -> None: - raw_model = _load_raw_pretrained_pomegranate_model() - model = PreTrainedRothSegmentationModel() - runtime_model = hmm_state_to_pomegranate_model(model.model) - data = convert_left_foot_to_fbf(healthy_example_imu_data["left_sensor"]) - feature_data, _ = model._transform([data], None, sampling_rate_hz=100) - feature_data = feature_data[0] - - raw_sequence = predict(raw_model, feature_data, expected_columns=model.data_columns, algorithm=model.algo_predict) - roundtrip_sequence = predict( - runtime_model, - feature_data, - expected_columns=model.data_columns, - algorithm=model.algo_predict, - ) - - assert_array_equal(raw_sequence, roundtrip_sequence) - - def test_pretrained_model_scipy_backend_matches_pomegranate_hidden_states(self, healthy_example_imu_data) -> None: + @pytest.mark.parametrize("inference_backend", _runtime_inference_backend_params()) + def test_pretrained_inference_backend_matches_pomegranate_hidden_states( + self, healthy_example_imu_data, inference_backend + ) -> None: model = PreTrainedRothSegmentationModel() - scipy_model = model.clone().set_params(backend=ScipyHmmInferenceBackend()) + comparison_model = model.clone().set_params(backend=inference_backend) data = convert_left_foot_to_fbf(healthy_example_imu_data["left_sensor"]) pomegranate_result = model.predict(data, sampling_rate_hz=100) - scipy_result = scipy_model.predict(data, sampling_rate_hz=100) + comparison_result = comparison_model.predict(data, sampling_rate_hz=100) - assert_array_equal(pomegranate_result.hidden_state_sequence_, scipy_result.hidden_state_sequence_) + assert_array_equal(pomegranate_result.hidden_state_sequence_, comparison_result.hidden_state_sequence_) assert_array_equal( pomegranate_result.hidden_state_sequence_feature_space_, - scipy_result.hidden_state_sequence_feature_space_, + comparison_result.hidden_state_sequence_feature_space_, ) def test_trained_model_roundtrip_matches_original_hidden_states(self) -> None: @@ -677,7 +747,8 @@ def _capture_and_convert(model, *args, **kwargs): assert instance.model.trained_with.backend_id == "pomegranate-legacy" assert instance.model.trained_with.backend_version is not None - def test_trained_model_scipy_backend_matches_pomegranate_hidden_states(self) -> None: + @pytest.mark.parametrize("inference_backend", _runtime_inference_backend_params()) + def test_trained_inference_backend_matches_pomegranate_hidden_states(self, inference_backend) -> None: data_sequence = [pd.DataFrame(np.random.rand(120, 6), columns=BF_COLS)] region_list_sequence = [ _stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]})) @@ -689,14 +760,14 @@ def test_trained_model_scipy_backend_matches_pomegranate_hidden_states(self) -> ) instance.self_optimize(data_sequence, region_list_sequence, sampling_rate_hz=100) - scipy_instance = instance.clone().set_params(backend=ScipyHmmInferenceBackend()) + comparison_instance = instance.clone().set_params(backend=inference_backend) pomegranate_result = instance.predict(data_sequence[0], sampling_rate_hz=100) - scipy_result = scipy_instance.predict(data_sequence[0], sampling_rate_hz=100) + comparison_result = comparison_instance.predict(data_sequence[0], sampling_rate_hz=100) - assert_array_equal(pomegranate_result.hidden_state_sequence_, scipy_result.hidden_state_sequence_) + assert_array_equal(pomegranate_result.hidden_state_sequence_, comparison_result.hidden_state_sequence_) assert_array_equal( pomegranate_result.hidden_state_sequence_feature_space_, - scipy_result.hidden_state_sequence_feature_space_, + comparison_result.hidden_state_sequence_feature_space_, ) @@ -716,18 +787,21 @@ def test_segment_with_single_dataset(self, healthy_example_imu_data) -> None: assert isinstance(result.hidden_state_sequence_, np.ndarray) assert result.hidden_state_sequence_ is result.result_model_.hidden_state_sequence_ - def test_pretrained_scipy_backend_matches_pomegranate_segmentation(self, healthy_example_imu_data) -> None: + @pytest.mark.parametrize("inference_backend", _runtime_inference_backend_params()) + def test_pretrained_inference_backend_matches_pomegranate_segmentation( + self, healthy_example_imu_data, inference_backend + ) -> None: data = convert_left_foot_to_fbf(healthy_example_imu_data["left_sensor"]) pomegranate_result = HmmStrideSegmentation(model=PreTrainedRothSegmentationModel()).segment(data, 204.8) - scipy_result = HmmStrideSegmentation( - model=PreTrainedRothSegmentationModel().set_params(backend=ScipyHmmInferenceBackend()) + comparison_result = HmmStrideSegmentation( + model=PreTrainedRothSegmentationModel().set_params(backend=inference_backend) ).segment(data, 204.8) - assert_array_equal(pomegranate_result.hidden_state_sequence_, scipy_result.hidden_state_sequence_) - assert_array_equal(pomegranate_result.matches_start_end_, scipy_result.matches_start_end_) + assert_array_equal(pomegranate_result.hidden_state_sequence_, comparison_result.hidden_state_sequence_) + assert_array_equal(pomegranate_result.matches_start_end_, comparison_result.matches_start_end_) assert_array_equal( pomegranate_result.matches_start_end_original_, - scipy_result.matches_start_end_original_, + comparison_result.matches_start_end_original_, ) def test_segment_with_multi_dataset(self, healthy_example_imu_data) -> None: From b7b75b8dadd75a2f6627cf1f01a0790000217578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 10:36:18 +0100 Subject: [PATCH 14/28] Limit HMM backend imports to backend packages --- .../segmentation_hmm_training.py | 3 +- gaitmap/stride_segmentation/hmm.py | 36 ---- .../stride_segmentation/hmm/__init__.py | 32 --- .../stride_segmentation/hmm/_backend.py | 20 +- .../hmm/_segmentation_model.py | 2 +- .../hmm/legacy/__init__.py | 3 +- .../hmm/legacy/_backend.py | 10 +- .../test_hmm_backend_modern_runtime.py | 97 ++++++++++ .../test_hmm_backend_selection.py | 82 ++++++++ .../test_stride_segmentation/test_roth_hmm.py | 182 ++++++++---------- 10 files changed, 266 insertions(+), 201 deletions(-) create mode 100644 tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py create mode 100644 tests/test_stride_segmentation/test_hmm_backend_selection.py diff --git a/examples/stride_segmentation/segmentation_hmm_training.py b/examples/stride_segmentation/segmentation_hmm_training.py index cf9eb286..f2851bba 100644 --- a/examples/stride_segmentation/segmentation_hmm_training.py +++ b/examples/stride_segmentation/segmentation_hmm_training.py @@ -121,7 +121,7 @@ # invoke the training process. # Again, all configurable parameters are exposed for demonstration purpose. # These parameters should again work for most usecases. -from gaitmap.stride_segmentation.hmm import PomegranateHmmBackend, RothSegmentationHmm +from gaitmap.stride_segmentation.hmm import RothSegmentationHmm segmentation_model = RothSegmentationHmm( hmm_config=RothHmmConfig( @@ -134,7 +134,6 @@ initialization="labels", name="segmentation_model", ), - backend=PomegranateHmmBackend(), ) # %% diff --git a/gaitmap/stride_segmentation/hmm.py b/gaitmap/stride_segmentation/hmm.py index 96001608..2b1ffde3 100644 --- a/gaitmap/stride_segmentation/hmm.py +++ b/gaitmap/stride_segmentation/hmm.py @@ -7,9 +7,6 @@ """ -from importlib import import_module -from typing import TYPE_CHECKING - from gaitmap.utils._gaitmap_mad import patch_gaitmap_mad_import _gaitmap_mad_modules = { @@ -27,13 +24,8 @@ "HmmGraphState", "HmmSubModelConfig", "HmmSubModelState", - "PomegranateLegacyHmmBackend", - "PomegranateHmmBackend", - "PomegranateModernHmmBackend", - "SimpleHmm", "RothHmmConfig", "RothSegmentationHmm", - "ScipyHmmInferenceBackend", "PreTrainedRothSegmentationModel", "BaseSegmentationHmm", "get_default_hmm_backend", @@ -63,29 +55,6 @@ get_default_hmm_backend, ) - if TYPE_CHECKING: - from gaitmap_mad.stride_segmentation.hmm.legacy import ( - PomegranateLegacyHmmBackend, - ) - from gaitmap_mad.stride_segmentation.hmm.legacy import ( - PomegranateLegacyHmmBackend as PomegranateHmmBackend, - ) - from gaitmap_mad.stride_segmentation.hmm.legacy import SimpleHmm - from gaitmap_mad.stride_segmentation.hmm.modern import PomegranateModernHmmBackend - from gaitmap_mad.stride_segmentation.hmm.scipy import ScipyHmmInferenceBackend - - def __getattr__(name: str): - if name in { - "PomegranateLegacyHmmBackend", - "PomegranateHmmBackend", - "PomegranateModernHmmBackend", - "ScipyHmmInferenceBackend", - }: - return getattr(import_module("gaitmap_mad.stride_segmentation.hmm"), name) - if name == "SimpleHmm": - return import_module("gaitmap_mad.stride_segmentation.hmm.legacy").SimpleHmm - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - __all__ = [ "BackendInfo", @@ -102,14 +71,9 @@ def __getattr__(name: str): "HmmStrideSegmentation", "HmmSubModelConfig", "HmmSubModelState", - "PomegranateHmmBackend", - "PomegranateLegacyHmmBackend", - "PomegranateModernHmmBackend", "PreTrainedRothSegmentationModel", "RothHmmConfig", "RothHmmFeatureTransformer", "RothSegmentationHmm", - "ScipyHmmInferenceBackend", - "SimpleHmm", "get_default_hmm_backend", ] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py index 23378149..029bdc58 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py @@ -2,8 +2,6 @@ import multiprocessing import warnings -from importlib import import_module -from typing import TYPE_CHECKING from gaitmap_mad.stride_segmentation.hmm._backend import BaseHmmBackend, BaseTrainableHmm, get_default_hmm_backend from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig, RothHmmConfig @@ -27,17 +25,6 @@ HmmSubModelState, ) -if TYPE_CHECKING: - from gaitmap_mad.stride_segmentation.hmm.legacy import ( - PomegranateLegacyHmmBackend, - ) - from gaitmap_mad.stride_segmentation.hmm.legacy import ( - PomegranateLegacyHmmBackend as PomegranateHmmBackend, - ) - from gaitmap_mad.stride_segmentation.hmm.legacy import SimpleHmm - from gaitmap_mad.stride_segmentation.hmm.modern import PomegranateModernHmmBackend - from gaitmap_mad.stride_segmentation.hmm.scipy import ScipyHmmInferenceBackend - if multiprocessing.parent_process() is None: warnings.warn( "The hmm support in gaitmap is still quite experimental and you might run into some rough edges. " @@ -46,20 +33,6 @@ "Monitor the changelog before upgrading to newer versions when using HMMs.", UserWarning, ) - - -def __getattr__(name: str): - if name in { - "PomegranateLegacyHmmBackend", - "PomegranateHmmBackend", - "PomegranateModernHmmBackend", - "ScipyHmmInferenceBackend", - }: - return getattr(import_module("gaitmap_mad.stride_segmentation.hmm._backend"), name) - if name == "SimpleHmm": - return import_module("gaitmap_mad.stride_segmentation.hmm.legacy").SimpleHmm - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - __all__ = [ "BackendInfo", "BaseHmmBackend", @@ -76,14 +49,9 @@ def __getattr__(name: str): "HmmStrideSegmentation", "HmmSubModelConfig", "HmmSubModelState", - "PomegranateHmmBackend", - "PomegranateLegacyHmmBackend", - "PomegranateModernHmmBackend", "PreTrainedRothSegmentationModel", "RothHmmConfig", "RothHmmFeatureTransformer", "RothSegmentationHmm", - "ScipyHmmInferenceBackend", - "SimpleHmm", "get_default_hmm_backend", ] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py index c7c8a00a..93740b3a 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend.py @@ -1,23 +1,5 @@ -"""Lazy facade for HMM backends.""" - -from __future__ import annotations - -from importlib import import_module +"""Base backend abstractions and backend selection.""" from gaitmap_mad.stride_segmentation.hmm._backend_base import BaseHmmBackend, BaseTrainableHmm, get_default_hmm_backend __all__ = ["BaseHmmBackend", "BaseTrainableHmm", "get_default_hmm_backend"] - - -def __getattr__(name: str): - if name == "PomegranateLegacyHmmBackend": - return import_module("gaitmap_mad.stride_segmentation.hmm.legacy").PomegranateLegacyHmmBackend - if name == "PomegranateHmmBackend": - return import_module("gaitmap_mad.stride_segmentation.hmm.legacy").PomegranateLegacyHmmBackend - if name == "PomegranateModernHmmBackend": - return import_module("gaitmap_mad.stride_segmentation.hmm.modern").PomegranateModernHmmBackend - if name == "ScipyHmmInferenceBackend": - return import_module("gaitmap_mad.stride_segmentation.hmm.scipy").ScipyHmmInferenceBackend - if name == "SimpleHmm": - return import_module("gaitmap_mad.stride_segmentation.hmm.legacy").SimpleHmm - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py index e8052c84..2d0c0028 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py @@ -308,7 +308,7 @@ class RothSegmentationHmm(BaseSegmentationHmm, ShortenedHMMPrint): Notes ----- The public trained-model parameter is stored as a serializable `HMMState`. - The default backend in this refactor step is `PomegranateHmmBackend`. + The default backend depends on the installed optional HMM runtimes. References ---------- diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py index 751b02e1..29312559 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py @@ -2,8 +2,7 @@ from gaitmap_mad.stride_segmentation.hmm.legacy._backend import ( PomegranateLegacyHmmBackend, - SimpleHmm, initialize_hmm, ) -__all__ = ["PomegranateLegacyHmmBackend", "SimpleHmm", "initialize_hmm"] +__all__ = ["PomegranateLegacyHmmBackend", "initialize_hmm"] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py index 6aca707c..02be98ab 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py @@ -113,8 +113,8 @@ def initialize_hmm( return model -class SimpleHmm(BaseTrainableHmm, _HackyClonableHMMFix, ShortenedHMMPrint): - """Wrap all required information to train a legacy flat HMM. +class _LegacyTrainableHmm(BaseTrainableHmm, _HackyClonableHMMFix, ShortenedHMMPrint): + """Internal wrapper used to train a legacy flat HMM. This is a thin wrapper around the legacy `pomegranate.HiddenMarkovModel` class and delegates training and inference to that runtime. @@ -273,8 +273,8 @@ def self_optimize_with_info( return self, history -def _create_simple_hmm_from_config(config: HmmSubModelConfig) -> SimpleHmm: - return SimpleHmm( +def _create_trainable_hmm_from_config(config: HmmSubModelConfig) -> _LegacyTrainableHmm: + return _LegacyTrainableHmm( n_states=config.n_states, n_gmm_components=config.n_gmm_components, architecture=config.architecture, @@ -300,7 +300,7 @@ def __init__(self, backend_id: str = "pomegranate-legacy") -> None: super().__init__(backend_id=backend_id) def create_submodel(self, config: HmmSubModelConfig) -> BaseTrainableHmm: - return _create_simple_hmm_from_config(config) + return _create_trainable_hmm_from_config(config) def predict( self, diff --git a/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py b/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py new file mode 100644 index 00000000..8c80f07e --- /dev/null +++ b/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py @@ -0,0 +1,97 @@ +"""Tests for the optional native pomegranate 1.x inference path.""" + +import numpy as np +import pytest +try: + from pomegranate.hmm import DenseHMM +except (ImportError, AttributeError): + DenseHMM = None +from gaitmap_mad.stride_segmentation.hmm import PreTrainedRothSegmentationModel +from gaitmap_mad.stride_segmentation.hmm._backend_common import prepare_predict_data +from gaitmap_mad.stride_segmentation.hmm.modern import PomegranateModernHmmBackend +from gaitmap_mad.stride_segmentation.hmm.scipy._utils import log_emission_probabilities + +from gaitmap.example_data import get_healthy_example_imu_data +from gaitmap.utils.coordinate_conversion import convert_left_foot_to_fbf + +pytestmark = pytest.mark.skipif(DenseHMM is None, reason="requires pomegranate 1.x") + + +def _viterbi_decode_with_end_probs(model, log_emissions: np.ndarray) -> np.ndarray: + with np.errstate(divide="ignore"): + transition_log_probs = np.log(np.asarray(model.compiled.graph.transition_probs, dtype=float)) + start_log_probs = np.log(np.asarray(model.compiled.graph.start_probs, dtype=float)) + end_log_probs = np.log(np.asarray(model.compiled.graph.end_probs, dtype=float)) + + n_samples, n_states = log_emissions.shape + dp = np.full((n_samples, n_states), -np.inf, dtype=float) + pointers = np.zeros((n_samples, n_states), dtype=int) + + dp[0] = start_log_probs + log_emissions[0] + for sample_idx in range(1, n_samples): + scores = dp[sample_idx - 1][:, None] + transition_log_probs + pointers[sample_idx] = np.argmax(scores, axis=0) + dp[sample_idx] = scores[pointers[sample_idx], np.arange(n_states)] + log_emissions[sample_idx] + + path = np.zeros(n_samples, dtype=int) + path[-1] = int(np.argmax(dp[-1] + end_log_probs)) + for sample_idx in range(n_samples - 1, 0, -1): + path[sample_idx - 1] = pointers[sample_idx, path[sample_idx]] + return path + + +def _get_pretrained_feature_data(): + model = PreTrainedRothSegmentationModel() + data = convert_left_foot_to_fbf(get_healthy_example_imu_data()["left_sensor"]) + feature_data, _ = model._transform([data], None, sampling_rate_hz=100) + return model, feature_data[0] + + +def test_modern_native_map_matches_canonical_backend() -> None: + """The native MAP path should match the canonical decoder exactly.""" + model, feature_data = _get_pretrained_feature_data() + + canonical = PomegranateModernHmmBackend().predict( + model.model, + feature_data, + expected_columns=model.data_columns, + algorithm="map", + verbose=model.verbose, + ) + native = PomegranateModernHmmBackend(inference_implementation="native").predict( + model.model, + feature_data, + expected_columns=model.data_columns, + algorithm="map", + verbose=model.verbose, + ) + + np.testing.assert_array_equal(canonical, native) + + +def test_modern_native_viterbi_matches_full_length_end_probability_decode() -> None: + """The native Viterbi path should use the full-length end-probability decode.""" + model, feature_data = _get_pretrained_feature_data() + + canonical = PomegranateModernHmmBackend().predict( + model.model, + feature_data, + expected_columns=model.data_columns, + algorithm="viterbi", + verbose=model.verbose, + ) + native = PomegranateModernHmmBackend(inference_implementation="native").predict( + model.model, + feature_data, + expected_columns=model.data_columns, + algorithm="viterbi", + verbose=model.verbose, + ) + + observations = prepare_predict_data(feature_data, model.data_columns, len(model.model.compiled.state_names)) + log_emissions = log_emission_probabilities(model.model, observations) + expected_native = _viterbi_decode_with_end_probs(model.model, log_emissions) + + assert len(native) == len(canonical) + 1 + np.testing.assert_array_equal(native, expected_native) + assert not np.array_equal(canonical, native[:-1]) diff --git a/tests/test_stride_segmentation/test_hmm_backend_selection.py b/tests/test_stride_segmentation/test_hmm_backend_selection.py new file mode 100644 index 00000000..a6d5aabe --- /dev/null +++ b/tests/test_stride_segmentation/test_hmm_backend_selection.py @@ -0,0 +1,82 @@ +import importlib +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from gaitmap_mad.stride_segmentation.hmm import _backend_base as backend_base +from gaitmap_mad.stride_segmentation.hmm import _segmentation_model as segmentation_model_module +from gaitmap_mad.stride_segmentation.hmm.legacy import PomegranateLegacyHmmBackend +from gaitmap_mad.stride_segmentation.hmm.modern import PomegranateModernHmmBackend +from gaitmap_mad.stride_segmentation.hmm.scipy import ScipyHmmInferenceBackend + + +@pytest.fixture(autouse=True) +def _restore_segmentation_model(): + yield + importlib.reload(segmentation_model_module) + + +class _FakeLegacyBackend(PomegranateLegacyHmmBackend): + def __init__(self) -> None: + backend_base.BaseHmmBackend.__init__(self, backend_id="pomegranate-legacy") + + +class _FakeModernBackend(PomegranateModernHmmBackend): + def __init__(self) -> None: + backend_base.BaseHmmBackend.__init__(self, backend_id="pomegranate-modern") + self.inference_implementation = "native" + + +class _FakeScipyBackend(ScipyHmmInferenceBackend): + def __init__(self) -> None: + backend_base.BaseHmmBackend.__init__(self, backend_id="scipy-inference") + + +def _reload_segmentation_model(monkeypatch, *, modern_available: bool, legacy_available: bool): + def _fake_import_module(module_name: str): + if module_name == "gaitmap_mad.stride_segmentation.hmm.modern": + if not modern_available: + raise ImportError("modern backend unavailable") + return SimpleNamespace(PomegranateModernHmmBackend=_FakeModernBackend) + if module_name == "gaitmap_mad.stride_segmentation.hmm.legacy": + if not legacy_available: + raise ImportError("legacy backend unavailable") + return SimpleNamespace(PomegranateLegacyHmmBackend=_FakeLegacyBackend) + if module_name == "gaitmap_mad.stride_segmentation.hmm.scipy": + return SimpleNamespace(ScipyHmmInferenceBackend=_FakeScipyBackend) + return importlib.import_module(module_name) + + monkeypatch.setattr(backend_base, "import_module", _fake_import_module) + return importlib.reload(segmentation_model_module) + + +def test_default_backend_without_pomegranate(monkeypatch) -> None: + segmentation_model = _reload_segmentation_model(monkeypatch, modern_available=False, legacy_available=False) + + assert isinstance(segmentation_model.DEFAULT_HMM_BACKEND, ScipyHmmInferenceBackend) + assert isinstance(segmentation_model.RothSegmentationHmm().backend, ScipyHmmInferenceBackend) + + +def test_default_backend_with_legacy_pomegranate(monkeypatch) -> None: + segmentation_model = _reload_segmentation_model(monkeypatch, modern_available=False, legacy_available=True) + + assert isinstance(segmentation_model.DEFAULT_HMM_BACKEND, PomegranateLegacyHmmBackend) + assert isinstance(segmentation_model.RothSegmentationHmm().backend, PomegranateLegacyHmmBackend) + + +def test_default_backend_with_modern_pomegranate(monkeypatch) -> None: + segmentation_model = _reload_segmentation_model(monkeypatch, modern_available=True, legacy_available=True) + + assert isinstance(segmentation_model.DEFAULT_HMM_BACKEND, PomegranateModernHmmBackend) + assert isinstance(segmentation_model.RothSegmentationHmm().backend, PomegranateModernHmmBackend) + + +def test_packaged_pretrained_model_uses_migrated_state_format() -> None: + model_json = Path( + "packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_pre_trained_models/fallriskpd_at_lab_model.json" + ).read_text(encoding="utf8") + + assert "SimpleHmm" not in model_json + assert '"stride_model"' not in model_json + assert '"transition_model"' not in model_json diff --git a/tests/test_stride_segmentation/test_roth_hmm.py b/tests/test_stride_segmentation/test_roth_hmm.py index 1e98f68e..9ad960dd 100644 --- a/tests/test_stride_segmentation/test_roth_hmm.py +++ b/tests/test_stride_segmentation/test_roth_hmm.py @@ -13,7 +13,6 @@ pytest.importorskip("pomegranate") -from pomegranate import GeneralMixtureModel from pomegranate.hmm import History from tpcp._hash import custom_hash @@ -31,22 +30,21 @@ HMMState, HmmStrideSegmentation, HmmSubModelConfig, - PomegranateHmmBackend, - PomegranateModernHmmBackend, PreTrainedRothSegmentationModel, RothHmmConfig, RothHmmFeatureTransformer, RothSegmentationHmm, - ScipyHmmInferenceBackend, - SimpleHmm, ) from gaitmap_mad.stride_segmentation.hmm.legacy import _backend as backend_module +from gaitmap_mad.stride_segmentation.hmm.legacy import PomegranateLegacyHmmBackend from gaitmap_mad.stride_segmentation.hmm.legacy._backend import initialize_hmm from gaitmap_mad.stride_segmentation.hmm.legacy._state import ( hmm_state_to_pomegranate_model, pomegranate_model_to_hmm_state, ) from gaitmap_mad.stride_segmentation.hmm.legacy._utils import predict +from gaitmap_mad.stride_segmentation.hmm.modern import PomegranateModernHmmBackend +from gaitmap_mad.stride_segmentation.hmm.scipy import ScipyHmmInferenceBackend from gaitmap_mad.stride_segmentation.hmm._utils import estimate_sequence_boundary_probs from tests.mixins.test_algorithm_mixin import TestAlgorithmMixin @@ -63,6 +61,15 @@ def _runtime_inference_backend_params(): return params +def _trainable_backend_params(): + params = [pytest.param(PomegranateLegacyHmmBackend(), id="pomegranate-legacy")] + if DenseHMM is not None: + params.append(pytest.param(PomegranateModernHmmBackend(), id="pomegranate-modern")) + else: + params.append(pytest.param(None, id="pomegranate-modern", marks=pytest.mark.skip(reason="requires pomegranate 1.x"))) + return params + + def _create_roth_model_config(*, stride_n_states=20, stride_n_gmm_components=6, transition_n_gmm_components=3): return CompositeHmmConfig( modules=( @@ -107,6 +114,35 @@ def _stride_list_to_region_list(stride_list: pd.DataFrame, region_type: str = "s return region_list.set_index("roi_id") +def _new_trainable_hmm( + backend, + *, + n_states: int = 5, + n_gmm_components: int = 3, + architecture: str = "left-right-strict", + algo_train: str = "baum-welch", + stop_threshold: float = 1e-9, + max_iterations: int = int(1e8), + verbose: bool = True, + n_jobs: int = 1, + name: str = "my_model", +): + return backend.create_submodel( + HmmSubModelConfig( + name=name, + role="stride", + n_states=n_states, + n_gmm_components=n_gmm_components, + architecture=architecture, + algo_train=algo_train, + stop_threshold=stop_threshold, + max_iterations=max_iterations, + verbose=verbose, + n_jobs=n_jobs, + ) + ) + + class TestMetaFunctionalityRothSegmentationHmm(TestAlgorithmMixin): __test__ = True @@ -149,19 +185,6 @@ def after_action_instance( return transform -class TestMetaFunctionalitySimpleHMM(TestAlgorithmMixin): - __test__ = True - - algorithm_class = SimpleHmm - - @pytest.fixture() - def valid_instance(self, after_action_instance): - return SimpleHmm(n_states=5, n_gmm_components=3) - - def test_empty_init(self) -> None: - pytest.skip() - - class TestRothHmmFeatureTransform: @pytest.mark.parametrize("target_sampling_rate", [50, 25, 16.3]) def test_inverse_transform_state_sequence(self, target_sampling_rate) -> None: @@ -245,40 +268,16 @@ def test_resample_roi(self) -> None: assert transform.sampling_rate_hz == 100 -class TestSimpleModel: - def test_error_on_different_number_data_and_labels(self) -> None: +class TestTrainableHmmBackends: + @pytest.mark.parametrize("backend", _trainable_backend_params()) + def test_error_on_different_number_data_and_labels(self, backend) -> None: with pytest.raises(ValueError) as e: - SimpleHmm(n_states=5, n_gmm_components=3).self_optimize( + _new_trainable_hmm(backend).self_optimize( [np.random.rand(100, 3)], [np.random.rand(100), np.random.rand(100)] ) assert "The given training sequence and initial training labels" in str(e.value) - def test_error_if_datasequence_shorter_nstates(self) -> None: - with pytest.raises(ValueError) as e: - SimpleHmm(n_states=5, n_gmm_components=3).self_optimize( - [np.random.rand(100, 3), np.random.rand(3, 3)], [np.random.rand(100), np.random.rand(3)] - ) - - assert "Invalid training sequence!" in str(e.value) - - def test_error_on_different_length_data_and_labels(self) -> None: - with pytest.raises(ValueError) as e: - SimpleHmm(n_states=5, n_gmm_components=3).self_optimize( - [pd.DataFrame(np.random.rand(100, 3))], [pd.Series(np.random.rand(99))] - ) - - assert "a different number of samples" in str(e.value) - - def test_invalid_label_sequence(self) -> None: - n_states = 5 - with pytest.raises(ValueError) as e: - SimpleHmm(n_states=n_states, n_gmm_components=3).self_optimize( - [pd.DataFrame(np.random.rand(100, 3))], [pd.Series(np.full(100, n_states + 1))] - ) - - assert "Invalid label sequence" in str(e.value) - @pytest.mark.parametrize( "data", [ @@ -287,45 +286,34 @@ def test_invalid_label_sequence(self) -> None: ], ) @pytest.mark.parametrize("n_gmm_components", [1, 3]) - # We test one value with n_states > 10, as this should trigger a sorting bug in pomegranate that we are handling - # explicitly @pytest.mark.parametrize("n_states", [5, 12]) - def test_optimize_with_single_sequence(self, data, n_gmm_components, n_states) -> None: - model = SimpleHmm(n_states=n_states, n_gmm_components=n_gmm_components, max_iterations=1) + @pytest.mark.parametrize("backend", _trainable_backend_params()) + def test_optimize_with_single_sequence(self, data, n_gmm_components, n_states, backend) -> None: + model = _new_trainable_hmm( + backend, + n_states=n_states, + n_gmm_components=n_gmm_components, + max_iterations=1, + ) model.self_optimize([data], [pd.Series(np.tile(np.arange(n_states), int(np.ceil(100 / n_states)))[:100])]) assert list(model.data_columns) == data.columns.tolist() - # -2 because of the start and end state - assert len(model.model.states) - 2 == n_states == model.n_states - # Test that each state has 3 gmm components - for state in model.model.states: - if state.name not in ["None-start", "None-end"]: - if n_gmm_components == 1: - dists = [state.distribution] - else: - assert isinstance(state.distribution, GeneralMixtureModel) - assert len(state.distribution.distributions) == model.n_gmm_components == n_gmm_components - dists = state.distribution.distributions - assert {d.name for d in dists} == {"MultivariateGaussianDistribution"} - - def test_model_exists_warning(self) -> None: - model = SimpleHmm(n_states=5, n_gmm_components=3) - model.self_optimize([pd.DataFrame(np.random.rand(100, 3))], [pd.Series(np.random.choice(5, 100))]) - with pytest.warns(UserWarning) as e: - model.self_optimize([pd.DataFrame(np.random.rand(100, 3))], [pd.Series(np.random.choice(5, 100))]) - - assert "Model already exists" in str(e[0].message) + prediction = model.predict_hidden_state_sequence(data) + assert len(prediction) == len(data) + assert set(prediction).issubset(set(range(n_states))) - def test_predict_rasies_error_without_optimize(self) -> None: + @pytest.mark.parametrize("backend", _trainable_backend_params()) + def test_predict_raises_error_without_optimize(self, backend) -> None: with pytest.raises(ValueError) as e: - SimpleHmm(n_states=5, n_gmm_components=3).predict_hidden_state_sequence( + _new_trainable_hmm(backend).predict_hidden_state_sequence( pd.DataFrame(np.random.rand(100, 3)) ) - assert "You need to train the HMM before calling `predict_hidden_state_sequence`" in str(e.value) + assert "Call `self_optimize` first" in str(e.value) or "You need to train the HMM" in str(e.value) - def test_predict_raises_error_on_invalid_columns(self) -> None: - model = SimpleHmm(n_states=5, n_gmm_components=3) + @pytest.mark.parametrize("backend", _trainable_backend_params()) + def test_predict_raises_error_on_invalid_columns(self, backend) -> None: + model = _new_trainable_hmm(backend) col_names = ["feature1", "feature2", "feature3"] invalid_col_names = ["feature1", "feature2", "feature4"] model.self_optimize( @@ -338,13 +326,24 @@ def test_predict_raises_error_on_invalid_columns(self) -> None: assert str(tuple(col_names)) in str(e.value) @pytest.mark.parametrize("algorithm", ["viterbi", "map"]) - def test_predict(self, algorithm) -> None: - model = SimpleHmm(n_states=5, n_gmm_components=3) + @pytest.mark.parametrize("backend", _trainable_backend_params()) + def test_predict(self, algorithm, backend) -> None: + model = _new_trainable_hmm(backend) model.self_optimize([pd.DataFrame(np.random.rand(100, 3))], [pd.Series(np.random.choice(5, 100))]) pred = model.predict_hidden_state_sequence(pd.DataFrame(np.random.rand(100, 3)), algorithm=algorithm) assert len(pred) == 100 assert set(pred) == set(range(5)) + @pytest.mark.parametrize("backend", _trainable_backend_params()) + def test_self_optimize_with_info_returns_history(self, backend) -> None: + data, labels = [pd.DataFrame(np.random.rand(100, 3))], [pd.Series(np.random.choice(5, 100))] + instance = _new_trainable_hmm(backend) + trained_instance, history = instance.self_optimize_with_info(data, labels) + assert instance is trained_instance + assert history is not None + + +class TestLegacyBackendHelpers: @pytest.mark.parametrize("architecture", ["left-right-strict", "left-right-loose", "fully-connected"]) def test_different_architectures(self, architecture) -> None: # We test initialization directly, otherwise training will modify the transition matrizes @@ -380,31 +379,6 @@ def test_different_architectures(self, architecture) -> None: expected[:5, 6] = 1 / 2 assert_almost_equal(transition_matrix, expected) - def test_self_optimize_calls_self_optimize_with_info(self) -> None: - data, labels = [pd.DataFrame(np.random.rand(100, 3))], [pd.Series(np.random.choice(5, 100))] - - with patch.object(SimpleHmm, "self_optimize_with_info") as mock: - instance = SimpleHmm(n_states=5, n_gmm_components=3) - mock.return_value = (instance, None) - instance.self_optimize(data, labels) - - mock.assert_called_once_with(data, labels) - - def test_self_optimize_with_info_returns_history(self) -> None: - data, labels = [pd.DataFrame(np.random.rand(100, 3))], [pd.Series(np.random.choice(5, 100))] - instance = SimpleHmm(n_states=5, n_gmm_components=3) - trained_instance, history = instance.self_optimize_with_info(data, labels) - assert instance is trained_instance - assert isinstance(history, History) - - def test_invalid_architecture_raises_error(self) -> None: - with pytest.raises(ValueError) as e: - SimpleHmm(n_states=5, n_gmm_components=3, architecture="invalid").self_optimize( - [pd.DataFrame(np.random.rand(100, 3))], [pd.Series(np.random.choice(5, 100))] - ) - - assert "Invalid architecture" in str(e.value) - class TestRothSegmentationHmm: def test_boundary_prob_estimation_uses_empirical_counts(self) -> None: @@ -528,7 +502,7 @@ def test_serialization_excludes_backend(self) -> None: restored = RothSegmentationHmm.from_json(instance.to_json()) assert set(payload["params"]) == {"hmm_config", "model"} - assert isinstance(restored.backend, PomegranateHmmBackend) + assert isinstance(restored.backend, PomegranateLegacyHmmBackend) def test_short_strides_raise_warning(self) -> None: data, labels = ( @@ -677,7 +651,7 @@ def test_pretrained_model_is_migrated_to_hmm_state(self) -> None: assert model.model.trained_with.backend_id == "pomegranate-legacy-migrated" assert model.model.trained_with.backend_version is not None assert len(model.model.submodels) == 2 - assert isinstance(model.backend, PomegranateHmmBackend) + assert isinstance(model.backend, PomegranateLegacyHmmBackend) def test_pretrained_model_migration_removes_silent_backend_states(self) -> None: model = PreTrainedRothSegmentationModel() From cdde465634119b11ea836bd65536d4638a464282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 10:36:22 +0100 Subject: [PATCH 15/28] Widen optional pomegranate dependency range --- pyproject.toml | 4 ++-- uv.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dc5c5e3c..46718b6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,12 +33,12 @@ dependencies = [ [project.optional-dependencies] hmm = [ - "pomegranate>=0.14.2,<=0.14.6; python_version < '3.10'", + "pomegranate>=0.14.2,<2; python_version < '3.10'", "numpy<2; python_version < '3.10'", ] stats = ["pingouin>=0.5.3"] all = [ - "pomegranate>=0.14.2,<=0.14.6; python_version < '3.10'", + "pomegranate>=0.14.2,<2; python_version < '3.10'", "numpy<2; python_version < '3.10'", "pingouin>=0.5.3", ] diff --git a/uv.lock b/uv.lock index b1273bbf..6a8bce2d 100644 --- a/uv.lock +++ b/uv.lock @@ -1109,8 +1109,8 @@ requires-dist = [ { name = "pandas", specifier = ">=2,<2.4" }, { name = "pingouin", marker = "extra == 'all'", specifier = ">=0.5.3" }, { name = "pingouin", marker = "extra == 'stats'", specifier = ">=0.5.3" }, - { name = "pomegranate", marker = "python_full_version < '3.10' and extra == 'all'", specifier = ">=0.14.2,<=0.14.6" }, - { name = "pomegranate", marker = "python_full_version < '3.10' and extra == 'hmm'", specifier = ">=0.14.2,<=0.14.6" }, + { name = "pomegranate", marker = "python_full_version < '3.10' and extra == 'all'", specifier = ">=0.14.2,<2" }, + { name = "pomegranate", marker = "python_full_version < '3.10' and extra == 'hmm'", specifier = ">=0.14.2,<2" }, { name = "pooch", specifier = ">=1.7.0" }, { name = "scikit-learn", specifier = ">=1.0.1" }, { name = "scipy", specifier = ">=1.6.1" }, From d865b626899720781e6118d8b3e339caf5f15256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 10:41:37 +0100 Subject: [PATCH 16/28] Apply repo formatting and lint cleanup --- docs/conf.py | 3 +- .../cross_validation.py | 1 - examples/datasets_and_pipelines/gridsearch.py | 1 - .../datasets_and_pipelines/gridsearch_cv.py | 1 - .../trajectory_reconstruction_region.py | 2 +- .../zupt_dependency.py | 1 - .../stride_segmentation/hmm/__init__.py | 2 +- .../stride_segmentation/hmm/_backend_base.py | 3 +- .../hmm/_segmentation_model.py | 2 +- .../stride_segmentation/hmm/_utils.py | 4 +- .../hmm/legacy/_backend.py | 17 +++--- .../stride_segmentation/hmm/legacy/_state.py | 3 +- .../hmm/modern/_backend.py | 13 +++-- .../stride_segmentation/hmm/modern/_state.py | 3 +- .../stride_segmentation/hmm/modern/_utils.py | 2 +- .../stride_segmentation/hmm/scipy/_utils.py | 20 ++++--- tests/mixins/test_algorithm_mixin.py | 4 +- tests/mixins/test_caching_mixin.py | 2 +- tests/test_base.py | 2 +- tests/test_data_transforms/test_base.py | 2 +- .../test_feature_transformer.py | 4 +- tests/test_data_transforms/test_filter.py | 2 +- tests/test_data_transforms/test_scalers.py | 2 +- .../test_event_detection_filtered_rampp.py | 2 +- .../test_event_detection_herzer.py | 2 +- .../test_event_detection_rampp.py | 12 ++-- .../test_ullrich_gait_sequence_detection.py | 4 +- .../test_spatial_parameters.py | 24 ++++---- .../test_temporal_parameter.py | 8 +-- .../test_forward_direction_alignment.py | 2 +- .../test_preprocessing/test_pca_alignment.py | 2 +- .../test_barth_dtw.py | 2 +- .../test_stride_segmentation/test_base_dtw.py | 4 +- .../test_constrained_barth_dtw.py | 2 +- .../test_hmm_backend_modern_runtime.py | 1 + .../test_hmm_backend_selection.py | 1 - .../test_roi_stride_segmentation.py | 2 +- .../test_stride_segmentation/test_roth_hmm.py | 56 +++++++++---------- .../test_orientation_methods/test_madgwick.py | 2 +- .../test_simple_gyro_integration.py | 2 +- .../test_forward_backwards_integration.py | 2 +- ...piece_wise_linear_dedrifted_integration.py | 2 +- .../test_region_level_trajectory.py | 2 +- .../test_stride_level_trajectory.py | 2 +- .../test_rts_kalman.py | 4 +- tests/test_utils/test_rotations.py | 2 +- .../test_combo_zupt_detector.py | 2 +- .../test_moving_window_zupt_detector.py | 6 +- .../test_stride_event_zupt_detector.py | 2 +- 49 files changed, 126 insertions(+), 122 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 7d494810..a124f5d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -219,8 +219,7 @@ def get_nested_attr(obj, attr): new_obj = getattr(obj, attrs[0]) if len(attrs) == 1: return new_obj - else: - return get_nested_attr(new_obj, attrs[1]) + return get_nested_attr(new_obj, attrs[1]) linkcode_resolve = make_linkcode_resolve( diff --git a/examples/datasets_and_pipelines/cross_validation.py b/examples/datasets_and_pipelines/cross_validation.py index f27cd43f..fcee1df5 100644 --- a/examples/datasets_and_pipelines/cross_validation.py +++ b/examples/datasets_and_pipelines/cross_validation.py @@ -30,7 +30,6 @@ import pandas as pd from tpcp import CloneFactory, Dataset, OptimizableParameter, OptimizablePipeline, Parameter - from gaitmap.data_transform import TrainableAbsMaxScaler from gaitmap.example_data import get_healthy_example_imu_data, get_healthy_example_stride_borders from gaitmap.stride_segmentation import ( diff --git a/examples/datasets_and_pipelines/gridsearch.py b/examples/datasets_and_pipelines/gridsearch.py index 3c90b00a..c933926d 100644 --- a/examples/datasets_and_pipelines/gridsearch.py +++ b/examples/datasets_and_pipelines/gridsearch.py @@ -12,7 +12,6 @@ import pandas as pd - # %% # To perform a GridSearch (or any other form of parameter optimization in Gaitmap), we first need to have a # **Dataset**, a **Pipeline** and a **score** function. diff --git a/examples/datasets_and_pipelines/gridsearch_cv.py b/examples/datasets_and_pipelines/gridsearch_cv.py index dde00fa3..1d910a51 100644 --- a/examples/datasets_and_pipelines/gridsearch_cv.py +++ b/examples/datasets_and_pipelines/gridsearch_cv.py @@ -25,7 +25,6 @@ import numpy as np import pandas as pd - from gaitmap.data_transform import TrainableAbsMaxScaler from gaitmap.utils.array_handling import iterate_region_data diff --git a/examples/trajectory_reconstruction/trajectory_reconstruction_region.py b/examples/trajectory_reconstruction/trajectory_reconstruction_region.py index 51f2349a..b5e5391b 100644 --- a/examples/trajectory_reconstruction/trajectory_reconstruction_region.py +++ b/examples/trajectory_reconstruction/trajectory_reconstruction_region.py @@ -28,7 +28,7 @@ imu_data = get_healthy_example_imu_data() dummy_regions_list = pd.DataFrame([[0, len(imu_data["left_sensor"])]], columns=["start", "end"]).rename_axis("gs_id") -dummy_regions_list = {k: dummy_regions_list for k in get_multi_sensor_names(imu_data)} +dummy_regions_list = dict.fromkeys(get_multi_sensor_names(imu_data), dummy_regions_list) dummy_regions_list["left_sensor"] diff --git a/examples/trajectory_reconstruction/zupt_dependency.py b/examples/trajectory_reconstruction/zupt_dependency.py index 54a8f7a8..3e32ee7e 100644 --- a/examples/trajectory_reconstruction/zupt_dependency.py +++ b/examples/trajectory_reconstruction/zupt_dependency.py @@ -21,7 +21,6 @@ import pandas as pd - # %% # The Data # -------- diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py index 029bdc58..b37d4c95 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/__init__.py @@ -36,9 +36,9 @@ __all__ = [ "BackendInfo", "BaseHmmBackend", - "BaseTrainableHmm", "BaseHmmFeatureTransformer", "BaseSegmentationHmm", + "BaseTrainableHmm", "CompositeHmmConfig", "CrossModuleTransition", "FlatHmmState", diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py index 281d8d11..1c679168 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py @@ -2,8 +2,9 @@ from __future__ import annotations +from collections.abc import Sequence from importlib import import_module -from typing import TYPE_CHECKING, Any, Literal, Sequence, TypeVar +from typing import TYPE_CHECKING, Any, Literal, TypeVar import numpy as np import pandas as pd diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py index 2d0c0028..d72e00be 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py @@ -21,7 +21,6 @@ from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig, RothHmmConfig from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import RothHmmFeatureTransformer from gaitmap_mad.stride_segmentation.hmm._state import HMMState -from gaitmap_mad.stride_segmentation.hmm.legacy._utils import ShortenedHMMPrint from gaitmap_mad.stride_segmentation.hmm._utils import ( _DataToShortError, convert_region_list_to_transition_list, @@ -29,6 +28,7 @@ get_train_data_sequences_transitions, validate_trainable_region_list, ) +from gaitmap_mad.stride_segmentation.hmm.legacy._utils import ShortenedHMMPrint DEFAULT_HMM_BACKEND = get_default_hmm_backend() diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py index ab017cae..18901c45 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_utils.py @@ -107,9 +107,7 @@ def estimate_sequence_boundary_probs( start_total = start_counts.sum() end_total = end_counts.sum() if start_total <= 0 or end_total <= 0: - raise ValueError( - "At least one non-empty hidden-state sequence is required to estimate boundary probabilities." - ) + raise ValueError("At least one non-empty hidden-state sequence is required to estimate boundary probabilities.") return start_counts / start_total, end_counts / end_total diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py index 02be98ab..895082b5 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py @@ -5,10 +5,11 @@ import copy import warnings from importlib.metadata import PackageNotFoundError, version -from typing import Any, Literal, Optional +from typing import Any, Literal import numpy as np import pandas as pd + try: import pomegranate as pg except ImportError: # pragma: no cover - exercised in environments without pomegranate @@ -154,9 +155,9 @@ class _LegacyTrainableHmm(BaseTrainableHmm, _HackyClonableHMMFix, ShortenedHMMPr max_iterations: int verbose: bool n_jobs: int - name: Optional[str] - model: OptiPara[Optional[Any]] - data_columns: OptiPara[Optional[tuple[str, ...]]] + name: str | None + model: OptiPara[Any | None] + data_columns: OptiPara[tuple[str, ...] | None] def __init__( self, @@ -170,8 +171,8 @@ def __init__( verbose: bool = True, n_jobs: int = 1, name: str = "my_model", - model: Optional[Any] = None, - data_columns: Optional[tuple[str, ...]] = None, + model: Any | None = None, + data_columns: tuple[str, ...] | None = None, ) -> None: self.n_states = n_states self.n_gmm_components = n_gmm_components @@ -411,7 +412,9 @@ def _create_combined_model( for module in model_config.modules: module_transition_matrix = trained_models[module.name].model.dense_transition_matrix()[:-2, :-2] offset = module_offsets[module.name] - trans_mat[offset : offset + module.n_states, offset : offset + module.n_states] = module_transition_matrix + trans_mat[offset : offset + module.n_states, offset : offset + module.n_states] = ( + module_transition_matrix + ) transitions, _, _ = extract_transitions_starts_stops_from_hidden_state_sequence(labels_train_sequence) start_probs, end_probs = estimate_sequence_boundary_probs(labels_train_sequence, n_states) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_state.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_state.py index ca373f1b..376bfdac 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_state.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_state.py @@ -6,6 +6,7 @@ from typing import Any import numpy as np + try: import pomegranate as pg except ImportError: # pragma: no cover - exercised in environments without pomegranate @@ -18,8 +19,8 @@ FlatHmmState, GaussianEmissionState, GaussianMixtureEmissionState, - HMMState, HmmGraphState, + HMMState, HmmSubModelState, ) from gaitmap_mad.stride_segmentation.hmm.legacy._utils import add_transition diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py index 56202531..727e2831 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py @@ -7,6 +7,7 @@ import numpy as np import pandas as pd + try: from pomegranate.hmm import DenseHMM except (ImportError, AttributeError): # pragma: no cover - exercised in environments without modern pomegranate @@ -23,7 +24,13 @@ prepare_predict_data, ) from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig -from gaitmap_mad.stride_segmentation.hmm._state import BackendInfo, FlatHmmState, HmmGraphState, HMMState, HmmSubModelState +from gaitmap_mad.stride_segmentation.hmm._state import ( + BackendInfo, + FlatHmmState, + HmmGraphState, + HMMState, + HmmSubModelState, +) from gaitmap_mad.stride_segmentation.hmm._utils import ( create_state_names, create_transition_matrix_fully_connected, @@ -95,9 +102,7 @@ def self_optimize_with_info( labels_sequence: list[np.ndarray], ) -> tuple[PomegranateModernTrainableHmm, PomegranateModernHistory]: if self.config.algo_train != "baum-welch": - raise NotImplementedError( - "The modern pomegranate backend currently only supports `baum-welch` training." - ) + raise NotImplementedError("The modern pomegranate backend currently only supports `baum-welch` training.") if len(data_sequence) != len(labels_sequence): raise ValueError( "The given training sequence and initial training labels do not match in their number of individual " diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py index ae5e8781..365c9396 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py @@ -6,6 +6,7 @@ from typing import Any import numpy as np + try: from pomegranate.distributions import Normal except (ImportError, AttributeError): # pragma: no cover - exercised in environments without modern pomegranate @@ -25,8 +26,8 @@ FlatHmmState, GaussianEmissionState, GaussianMixtureEmissionState, - HMMState, HmmGraphState, + HMMState, HmmSubModelState, ) from gaitmap_mad.stride_segmentation.hmm._utils import create_state_names diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_utils.py index ac07cbba..ad4aa04c 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_utils.py @@ -15,8 +15,8 @@ FlatHmmState, GaussianEmissionState, GaussianMixtureEmissionState, - HMMState, HmmGraphState, + HMMState, ) from gaitmap_mad.stride_segmentation.hmm._utils import ( cluster_data_by_labels, diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/_utils.py index 5e97fca4..27b1e7b1 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/scipy/_utils.py @@ -26,15 +26,17 @@ def log_emission_probabilities(model: HMMState, observations: np.ndarray) -> np. ) continue if isinstance(emission, GaussianMixtureEmissionState): - component_log_probs = np.column_stack([ - multivariate_normal.logpdf( - observations, - mean=component.mean, - cov=component.covariance, - allow_singular=True, - ) - for component in emission.components - ]) + component_log_probs = np.column_stack( + [ + multivariate_normal.logpdf( + observations, + mean=component.mean, + cov=component.covariance, + allow_singular=True, + ) + for component in emission.components + ] + ) with np.errstate(divide="ignore"): log_weights = np.log(np.asarray(emission.weights, dtype=float)) log_emissions[:, state_idx] = logsumexp(component_log_probs + log_weights, axis=1) diff --git a/tests/mixins/test_algorithm_mixin.py b/tests/mixins/test_algorithm_mixin.py index 5ea7e721..74ec3ec5 100644 --- a/tests/mixins/test_algorithm_mixin.py +++ b/tests/mixins/test_algorithm_mixin.py @@ -15,11 +15,11 @@ class TestAlgorithmMixin: algorithm_class = None __test__ = False - @pytest.fixture() + @pytest.fixture def valid_instance(self, after_action_instance): return after_action_instance - @pytest.fixture() + @pytest.fixture def after_action_instance(self) -> BaseType: pass diff --git a/tests/mixins/test_caching_mixin.py b/tests/mixins/test_caching_mixin.py index e4a32e42..3087a6c9 100644 --- a/tests/mixins/test_caching_mixin.py +++ b/tests/mixins/test_caching_mixin.py @@ -16,7 +16,7 @@ class TestCachingMixin: algorithm_class = None __test__ = False - @pytest.fixture() + @pytest.fixture def after_action_instance(self) -> BaseType: pass diff --git a/tests/test_base.py b/tests/test_base.py index 8d5498d0..bfac9665 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -64,7 +64,7 @@ def example_test_class_initialised(request) -> tuple[BaseAlgorithm, dict[str, An return test_instance, request.param -@pytest.fixture() +@pytest.fixture def example_test_class_after_action(example_test_class_initialised) -> tuple[BaseAlgorithm, dict[str, Any]]: test_instance, params = example_test_class_initialised action_params = { diff --git a/tests/test_data_transforms/test_base.py b/tests/test_data_transforms/test_base.py index 86c6fe88..a4c60b9f 100644 --- a/tests/test_data_transforms/test_base.py +++ b/tests/test_data_transforms/test_base.py @@ -38,7 +38,7 @@ def set_algo_class(self, request) -> None: def test_empty_init(self) -> None: pytest.skip() - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_borders) -> BaseTransformer: data_left = healthy_example_imu_data["left_sensor"].iloc[:10] data_left.columns = BF_COLS diff --git a/tests/test_data_transforms/test_feature_transformer.py b/tests/test_data_transforms/test_feature_transformer.py index 11fd3ac3..9d517838 100644 --- a/tests/test_data_transforms/test_feature_transformer.py +++ b/tests/test_data_transforms/test_feature_transformer.py @@ -30,7 +30,7 @@ class TestMetaFunctionalityResample(TestAlgorithmMixin): algorithm_class = Resample - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_borders) -> Resample: data_left = healthy_example_imu_data["left_sensor"].iloc[:1000] data_left.columns = BF_COLS @@ -50,7 +50,7 @@ class TestMetaFunctionalityRollingTransforms(TestAlgorithmMixin): def set_algo_class(self, request) -> None: self.algorithm_class, self.algo_params, _ = request.param - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_borders) -> BaseTransformer: data_left = healthy_example_imu_data["left_sensor"].iloc[:1000] data_left.columns = BF_COLS diff --git a/tests/test_data_transforms/test_filter.py b/tests/test_data_transforms/test_filter.py index 66b60b40..8d741e5c 100644 --- a/tests/test_data_transforms/test_filter.py +++ b/tests/test_data_transforms/test_filter.py @@ -17,7 +17,7 @@ class TestButterworthMetaFunctionality(TestAlgorithmMixin): def test_empty_init(self) -> None: pytest.skip() - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_borders) -> BaseFilter: data_left = healthy_example_imu_data["left_sensor"].iloc[:100] data_left.columns = BF_COLS diff --git a/tests/test_data_transforms/test_scalers.py b/tests/test_data_transforms/test_scalers.py index 91fb178e..4c5711b1 100644 --- a/tests/test_data_transforms/test_scalers.py +++ b/tests/test_data_transforms/test_scalers.py @@ -35,7 +35,7 @@ class TestMetaFunctionality(TestAlgorithmMixin): def set_algo_class(self, request) -> None: self.algorithm_class = request.param - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_borders) -> BaseType: data_left = healthy_example_imu_data["left_sensor"].iloc[:10] data_left.columns = BF_COLS diff --git a/tests/test_event_detection/test_event_detection_filtered_rampp.py b/tests/test_event_detection/test_event_detection_filtered_rampp.py index 3ec3d14a..2b501ded 100644 --- a/tests/test_event_detection/test_event_detection_filtered_rampp.py +++ b/tests/test_event_detection/test_event_detection_filtered_rampp.py @@ -14,7 +14,7 @@ class MetaTestConfig: algorithm_class = FilteredRamppEventDetection - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_borders) -> BaseType: data_left = healthy_example_imu_data["left_sensor"] data_left.columns = BF_COLS diff --git a/tests/test_event_detection/test_event_detection_herzer.py b/tests/test_event_detection/test_event_detection_herzer.py index 3d2f25e7..47663a88 100644 --- a/tests/test_event_detection/test_event_detection_herzer.py +++ b/tests/test_event_detection/test_event_detection_herzer.py @@ -19,7 +19,7 @@ class MetaTestConfig: algorithm_class = HerzerEventDetection - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_borders) -> BaseType: data_left = healthy_example_imu_data["left_sensor"] data_left.columns = BF_COLS diff --git a/tests/test_event_detection/test_event_detection_rampp.py b/tests/test_event_detection/test_event_detection_rampp.py index aec0eee0..cc569af0 100644 --- a/tests/test_event_detection/test_event_detection_rampp.py +++ b/tests/test_event_detection/test_event_detection_rampp.py @@ -4,6 +4,11 @@ import numpy as np import pandas as pd import pytest +from gaitmap_mad.event_detection._rampp_event_detection import ( + _detect_ic_for_ic_stride, + _detect_tc_for_ic_stride, + _detect_tc_for_segmented_stride, +) from numpy.testing import assert_array_equal from pandas._testing import assert_frame_equal @@ -13,11 +18,6 @@ from gaitmap.utils import coordinate_conversion, datatype_helper from gaitmap.utils.consts import BF_COLS from gaitmap.utils.exceptions import ValidationError -from gaitmap_mad.event_detection._rampp_event_detection import ( - _detect_ic_for_ic_stride, - _detect_tc_for_ic_stride, - _detect_tc_for_segmented_stride, -) from tests.mixins.test_algorithm_mixin import TestAlgorithmMixin from tests.mixins.test_caching_mixin import TestCachingMixin @@ -33,7 +33,7 @@ class MetaTestConfig: algorithm_class = RamppEventDetection - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_borders) -> BaseType: data_left = healthy_example_imu_data["left_sensor"] data_left.columns = BF_COLS diff --git a/tests/test_gait_detection/test_ullrich_gait_sequence_detection.py b/tests/test_gait_detection/test_ullrich_gait_sequence_detection.py index 57f837e8..3faab8bf 100644 --- a/tests/test_gait_detection/test_ullrich_gait_sequence_detection.py +++ b/tests/test_gait_detection/test_ullrich_gait_sequence_detection.py @@ -1,20 +1,20 @@ import numpy as np import pandas as pd import pytest +from gaitmap_mad.gait_detection._ullrich_gait_sequence_detection import _gait_sequence_concat from pandas._testing import assert_frame_equal from gaitmap.base import BaseType from gaitmap.gait_detection import UllrichGaitSequenceDetection from gaitmap.utils import coordinate_conversion from gaitmap.utils.consts import BF_COLS -from gaitmap_mad.gait_detection._ullrich_gait_sequence_detection import _gait_sequence_concat from tests.mixins.test_algorithm_mixin import TestAlgorithmMixin class MetaTestConfig: algorithm_class = UllrichGaitSequenceDetection - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data) -> BaseType: data = coordinate_conversion.convert_to_fbf( healthy_example_imu_data, left=["left_sensor"], right=["right_sensor"] diff --git a/tests/test_parameters/test_spatial_parameters.py b/tests/test_parameters/test_spatial_parameters.py index fb1f20ad..ad238c9f 100644 --- a/tests/test_parameters/test_spatial_parameters.py +++ b/tests/test_parameters/test_spatial_parameters.py @@ -16,7 +16,7 @@ from tests.mixins.test_algorithm_mixin import TestAlgorithmMixin -@pytest.fixture() +@pytest.fixture def single_sensor_stride_list(): stride_events_list = pd.DataFrame(columns=["s_id", "ic", "tc", "pre_ic", "gsd_id", "min_vel", "start", "end"]) stride_events_list["s_id"] = [0, 1, 2] @@ -28,14 +28,14 @@ def single_sensor_stride_list(): return stride_events_list -@pytest.fixture() +@pytest.fixture def single_sensor_stride_time(): out = pd.Series([2, 2, 2], index=[0, 1, 2]) out.index.name = "s_id" return out -@pytest.fixture() +@pytest.fixture def single_sensor_position_list(): position_list = pd.DataFrame(columns=["s_id", "sample", "pos_x", "pos_y", "pos_z"]) position_list["s_id"] = [0, 0, 0, 1, 1, 1, 2, 2, 2] @@ -46,31 +46,31 @@ def single_sensor_position_list(): return position_list -@pytest.fixture() +@pytest.fixture def single_sensor_position_list_with_index(single_sensor_position_list): return single_sensor_position_list.set_index(["s_id", "sample"]) -@pytest.fixture() +@pytest.fixture def single_sensor_stride_length(): out = pd.Series([2, np.sqrt(8), 0], index=[0, 1, 2]) out.index.name = "s_id" return out -@pytest.fixture() +@pytest.fixture def single_sensor_arc_length(): out = pd.Series([2, 2 * np.sqrt(2), 2], index=[0, 1, 2]) out.index.name = "s_id" return out -@pytest.fixture() +@pytest.fixture def single_sensor_gait_speed(single_sensor_stride_length, single_sensor_stride_time): return single_sensor_stride_length / single_sensor_stride_time -@pytest.fixture() +@pytest.fixture def single_sensor_orientation_list(): orientation_list = pd.DataFrame(columns=["s_id", "sample", "q_x", "q_y", "q_z", "q_w"]) orientation_list["s_id"] = [0, 0, 0, 1, 1, 1, 2, 2, 2] @@ -82,19 +82,19 @@ def single_sensor_orientation_list(): return orientation_list -@pytest.fixture() +@pytest.fixture def single_sensor_orientation_list_with_index(single_sensor_orientation_list): return single_sensor_orientation_list.set_index(["s_id", "sample"]) -@pytest.fixture() +@pytest.fixture def single_sensor_turning_angle(): out = pd.Series([0.0, 90, -90], index=[0, 1, 2]) out.index.name = "s_id" return out -@pytest.fixture() +@pytest.fixture def single_sensor_sole_angle_course(): index = [0, 0, 0, 1, 1, 1, 2, 2, 2] sample = [0, 1, 2, 0, 1, 2, 0, 1, 2] @@ -107,7 +107,7 @@ class TestMetaFunctionality(TestAlgorithmMixin): algorithm_class = SpatialParameterCalculation __test__ = True - @pytest.fixture() + @pytest.fixture def after_action_instance( self, single_sensor_stride_list, single_sensor_position_list, single_sensor_orientation_list ) -> BaseType: diff --git a/tests/test_parameters/test_temporal_parameter.py b/tests/test_parameters/test_temporal_parameter.py index a4d2dd05..d20bb6bd 100644 --- a/tests/test_parameters/test_temporal_parameter.py +++ b/tests/test_parameters/test_temporal_parameter.py @@ -27,7 +27,7 @@ def _min_vel_stride_list(): return stride_events_list, temporal_parameters -@pytest.fixture() +@pytest.fixture def min_vel_stride_list(): return _min_vel_stride_list() @@ -55,7 +55,7 @@ class TestMetaFunctionality(TestAlgorithmMixin): algorithm_class = TemporalParameterCalculation __test__ = True - @pytest.fixture() + @pytest.fixture def after_action_instance(self, min_vel_stride_list) -> BaseType: stride_events_list, _ = min_vel_stride_list t = TemporalParameterCalculation() @@ -67,11 +67,11 @@ def after_action_instance(self, min_vel_stride_list) -> BaseType: class TestTemporalParameterCalculation: """Test temporal parameters calculation.""" - @pytest.fixture() + @pytest.fixture def stride_list(self, stride_list_type): if stride_list_type == "min_vel": return _min_vel_stride_list() - elif stride_list_type == "ic": + if stride_list_type == "ic": return ic_stride_list() return None diff --git a/tests/test_preprocessing/test_forward_direction_alignment.py b/tests/test_preprocessing/test_forward_direction_alignment.py index 540526cc..39d638a9 100644 --- a/tests/test_preprocessing/test_forward_direction_alignment.py +++ b/tests/test_preprocessing/test_forward_direction_alignment.py @@ -14,7 +14,7 @@ class MetaTestConfig: algorithm_class = ForwardDirectionSignAlignment - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data) -> BaseType: fdsa = ForwardDirectionSignAlignment() fdsa.align(healthy_example_imu_data["left_sensor"].iloc[:1000], sampling_rate_hz=204.8) diff --git a/tests/test_preprocessing/test_pca_alignment.py b/tests/test_preprocessing/test_pca_alignment.py index 6b0665a7..f9706867 100644 --- a/tests/test_preprocessing/test_pca_alignment.py +++ b/tests/test_preprocessing/test_pca_alignment.py @@ -15,7 +15,7 @@ class TestMetaFunctionality(TestAlgorithmMixin): algorithm_class = PcaAlignment - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data) -> PcaAlignment: pcaa = PcaAlignment() pcaa.align(healthy_example_imu_data["left_sensor"].iloc[:10]) diff --git a/tests/test_stride_segmentation/test_barth_dtw.py b/tests/test_stride_segmentation/test_barth_dtw.py index 84b367cc..624f5e6e 100644 --- a/tests/test_stride_segmentation/test_barth_dtw.py +++ b/tests/test_stride_segmentation/test_barth_dtw.py @@ -21,7 +21,7 @@ class MetaTestConfig: algorithm_class = BarthDtw - @pytest.fixture() + @pytest.fixture def after_action_instance(self) -> BaseType: template = DtwTemplate(data=np.array([0, 1.0, 0]), sampling_rate_hz=100.0) dtw = self.algorithm_class(template=template, max_cost=0.5, min_match_length_s=None) diff --git a/tests/test_stride_segmentation/test_base_dtw.py b/tests/test_stride_segmentation/test_base_dtw.py index 3f9e6c62..0b162918 100644 --- a/tests/test_stride_segmentation/test_base_dtw.py +++ b/tests/test_stride_segmentation/test_base_dtw.py @@ -15,12 +15,12 @@ import numpy as np import pandas as pd import pytest +from gaitmap_mad.stride_segmentation.dtw._base_dtw import subsequence_cost_matrix_with_constrains from gaitmap.base import BaseType from gaitmap.stride_segmentation import BarthOriginalTemplate, BaseDtw, DtwTemplate from gaitmap.utils.datatype_helper import get_multi_sensor_names from gaitmap.utils.exceptions import ValidationError -from gaitmap_mad.stride_segmentation.dtw._base_dtw import subsequence_cost_matrix_with_constrains from tests.mixins.test_algorithm_mixin import TestAlgorithmMixin from tests.mixins.test_caching_mixin import TestCachingMixin @@ -28,7 +28,7 @@ class MetaTestConfig: algorithm_class = BaseDtw - @pytest.fixture() + @pytest.fixture def after_action_instance(self) -> BaseType: template = DtwTemplate(data=np.array([0, 1.0, 0]), sampling_rate_hz=100.0) dtw = self.algorithm_class(template=template, max_cost=0.5, min_match_length_s=None) diff --git a/tests/test_stride_segmentation/test_constrained_barth_dtw.py b/tests/test_stride_segmentation/test_constrained_barth_dtw.py index 07ba44d6..e5e7c167 100644 --- a/tests/test_stride_segmentation/test_constrained_barth_dtw.py +++ b/tests/test_stride_segmentation/test_constrained_barth_dtw.py @@ -16,7 +16,7 @@ class MetaTestConfig: algorithm_class = ConstrainedBarthDtw - @pytest.fixture() + @pytest.fixture def after_action_instance(self) -> BaseType: template = DtwTemplate(data=np.array([0, 1.0, 0]), sampling_rate_hz=100.0) dtw = self.algorithm_class(template=template, max_cost=0.5, min_match_length_s=None) diff --git a/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py b/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py index 8c80f07e..213242e6 100644 --- a/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py +++ b/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py @@ -2,6 +2,7 @@ import numpy as np import pytest + try: from pomegranate.hmm import DenseHMM except (ImportError, AttributeError): diff --git a/tests/test_stride_segmentation/test_hmm_backend_selection.py b/tests/test_stride_segmentation/test_hmm_backend_selection.py index a6d5aabe..35d86cc1 100644 --- a/tests/test_stride_segmentation/test_hmm_backend_selection.py +++ b/tests/test_stride_segmentation/test_hmm_backend_selection.py @@ -3,7 +3,6 @@ from types import SimpleNamespace import pytest - from gaitmap_mad.stride_segmentation.hmm import _backend_base as backend_base from gaitmap_mad.stride_segmentation.hmm import _segmentation_model as segmentation_model_module from gaitmap_mad.stride_segmentation.hmm.legacy import PomegranateLegacyHmmBackend diff --git a/tests/test_stride_segmentation/test_roi_stride_segmentation.py b/tests/test_stride_segmentation/test_roi_stride_segmentation.py index 170ce51b..0f7c345e 100644 --- a/tests/test_stride_segmentation/test_roi_stride_segmentation.py +++ b/tests/test_stride_segmentation/test_roi_stride_segmentation.py @@ -23,7 +23,7 @@ class TestMetaFunctionality(TestAlgorithmMixin): algorithm_class = RoiStrideSegmentation __test__ = True - @pytest.fixture() + @pytest.fixture def after_action_instance(self) -> RoiStrideSegmentation: # We use a simple dtw to create the instance template = DtwTemplate(data=pd.DataFrame([0, 1.0, 0]), sampling_rate_hz=100.0) diff --git a/tests/test_stride_segmentation/test_roth_hmm.py b/tests/test_stride_segmentation/test_roth_hmm.py index 9ad960dd..a9632ae6 100644 --- a/tests/test_stride_segmentation/test_roth_hmm.py +++ b/tests/test_stride_segmentation/test_roth_hmm.py @@ -6,6 +6,7 @@ import pandas as pd import pytest from numpy.testing import assert_almost_equal, assert_array_equal + try: from pomegranate.hmm import DenseHMM except (ImportError, AttributeError): @@ -13,18 +14,6 @@ pytest.importorskip("pomegranate") -from pomegranate.hmm import History -from tpcp._hash import custom_hash - -from gaitmap.data_transform import SlidingWindowMean -from gaitmap.utils.consts import BF_COLS -from gaitmap.utils.coordinate_conversion import convert_left_foot_to_fbf, convert_to_fbf -from gaitmap.utils.datatype_helper import ( - get_multi_sensor_names, - is_multi_sensor_stride_list, - is_single_sensor_stride_list, -) -from gaitmap.utils.exceptions import ValidationError from gaitmap_mad.stride_segmentation.hmm import ( CompositeHmmConfig, HMMState, @@ -35,17 +24,28 @@ RothHmmFeatureTransformer, RothSegmentationHmm, ) -from gaitmap_mad.stride_segmentation.hmm.legacy import _backend as backend_module +from gaitmap_mad.stride_segmentation.hmm._utils import estimate_sequence_boundary_probs from gaitmap_mad.stride_segmentation.hmm.legacy import PomegranateLegacyHmmBackend +from gaitmap_mad.stride_segmentation.hmm.legacy import _backend as backend_module from gaitmap_mad.stride_segmentation.hmm.legacy._backend import initialize_hmm from gaitmap_mad.stride_segmentation.hmm.legacy._state import ( hmm_state_to_pomegranate_model, - pomegranate_model_to_hmm_state, ) from gaitmap_mad.stride_segmentation.hmm.legacy._utils import predict from gaitmap_mad.stride_segmentation.hmm.modern import PomegranateModernHmmBackend from gaitmap_mad.stride_segmentation.hmm.scipy import ScipyHmmInferenceBackend -from gaitmap_mad.stride_segmentation.hmm._utils import estimate_sequence_boundary_probs +from pomegranate.hmm import History +from tpcp._hash import custom_hash + +from gaitmap.data_transform import SlidingWindowMean +from gaitmap.utils.consts import BF_COLS +from gaitmap.utils.coordinate_conversion import convert_left_foot_to_fbf, convert_to_fbf +from gaitmap.utils.datatype_helper import ( + get_multi_sensor_names, + is_multi_sensor_stride_list, + is_single_sensor_stride_list, +) +from gaitmap.utils.exceptions import ValidationError from tests.mixins.test_algorithm_mixin import TestAlgorithmMixin # Fix random seed for reproducibility @@ -57,7 +57,9 @@ def _runtime_inference_backend_params(): if DenseHMM is not None: params.append(pytest.param(PomegranateModernHmmBackend(), id="pomegranate-modern")) else: - params.append(pytest.param(None, id="pomegranate-modern", marks=pytest.mark.skip(reason="requires pomegranate 1.x"))) + params.append( + pytest.param(None, id="pomegranate-modern", marks=pytest.mark.skip(reason="requires pomegranate 1.x")) + ) return params @@ -66,7 +68,9 @@ def _trainable_backend_params(): if DenseHMM is not None: params.append(pytest.param(PomegranateModernHmmBackend(), id="pomegranate-modern")) else: - params.append(pytest.param(None, id="pomegranate-modern", marks=pytest.mark.skip(reason="requires pomegranate 1.x"))) + params.append( + pytest.param(None, id="pomegranate-modern", marks=pytest.mark.skip(reason="requires pomegranate 1.x")) + ) return params @@ -148,7 +152,7 @@ class TestMetaFunctionalityRothSegmentationHmm(TestAlgorithmMixin): algorithm_class = RothSegmentationHmm - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data) -> RothSegmentationHmm: hmm = PreTrainedRothSegmentationModel() hmm.predict(convert_left_foot_to_fbf(healthy_example_imu_data["left_sensor"]), sampling_rate_hz=100) @@ -160,7 +164,7 @@ class TestMetaFunctionalityHmmStrideSegmentation(TestAlgorithmMixin): algorithm_class = HmmStrideSegmentation - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data) -> HmmStrideSegmentation: hmm = HmmStrideSegmentation() hmm.segment(convert_left_foot_to_fbf(healthy_example_imu_data["left_sensor"]), sampling_rate_hz=100) @@ -172,7 +176,7 @@ class TestMetaFunctionalityRothHMMFeatureTransformer(TestAlgorithmMixin): algorithm_class = RothHmmFeatureTransformer - @pytest.fixture() + @pytest.fixture def after_action_instance( self, healthy_example_imu_data, healthy_example_stride_borders ) -> RothHmmFeatureTransformer: @@ -305,9 +309,7 @@ def test_optimize_with_single_sequence(self, data, n_gmm_components, n_states, b @pytest.mark.parametrize("backend", _trainable_backend_params()) def test_predict_raises_error_without_optimize(self, backend) -> None: with pytest.raises(ValueError) as e: - _new_trainable_hmm(backend).predict_hidden_state_sequence( - pd.DataFrame(np.random.rand(100, 3)) - ) + _new_trainable_hmm(backend).predict_hidden_state_sequence(pd.DataFrame(np.random.rand(100, 3))) assert "Call `self_optimize` first" in str(e.value) or "You need to train the HMM" in str(e.value) @@ -682,9 +684,7 @@ def test_pretrained_inference_backend_matches_pomegranate_hidden_states( def test_trained_model_roundtrip_matches_original_hidden_states(self) -> None: data_sequence = [pd.DataFrame(np.random.rand(120, 6), columns=BF_COLS)] - region_list_sequence = [ - _stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]})) - ] + region_list_sequence = [_stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]}))] instance = RothSegmentationHmm( hmm_config=_create_roth_hmm_config(stride_n_states=3, stride_n_gmm_components=3) ).set_params( @@ -724,9 +724,7 @@ def _capture_and_convert(model, *args, **kwargs): @pytest.mark.parametrize("inference_backend", _runtime_inference_backend_params()) def test_trained_inference_backend_matches_pomegranate_hidden_states(self, inference_backend) -> None: data_sequence = [pd.DataFrame(np.random.rand(120, 6), columns=BF_COLS)] - region_list_sequence = [ - _stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]})) - ] + region_list_sequence = [_stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]}))] instance = RothSegmentationHmm( hmm_config=_create_roth_hmm_config(stride_n_states=3, stride_n_gmm_components=3) ).set_params( diff --git a/tests/test_trajectory_reconstruction/test_orientation_methods/test_madgwick.py b/tests/test_trajectory_reconstruction/test_orientation_methods/test_madgwick.py index e6d49f7c..c63663a8 100644 --- a/tests/test_trajectory_reconstruction/test_orientation_methods/test_madgwick.py +++ b/tests/test_trajectory_reconstruction/test_orientation_methods/test_madgwick.py @@ -17,7 +17,7 @@ class MetaTestConfig: algorithm_class = MadgwickAHRS - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_events) -> BaseType: position = MadgwickAHRS() position.estimate(healthy_example_imu_data["left_sensor"].iloc[:10], sampling_rate_hz=1) diff --git a/tests/test_trajectory_reconstruction/test_orientation_methods/test_simple_gyro_integration.py b/tests/test_trajectory_reconstruction/test_orientation_methods/test_simple_gyro_integration.py index 10715efa..030d5d6e 100644 --- a/tests/test_trajectory_reconstruction/test_orientation_methods/test_simple_gyro_integration.py +++ b/tests/test_trajectory_reconstruction/test_orientation_methods/test_simple_gyro_integration.py @@ -12,7 +12,7 @@ class MetaTestConfig: algorithm_class = SimpleGyroIntegration - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_events) -> BaseType: position = SimpleGyroIntegration() position.estimate(healthy_example_imu_data["left_sensor"].iloc[:10], sampling_rate_hz=1) diff --git a/tests/test_trajectory_reconstruction/test_postition_methods/test_forward_backwards_integration.py b/tests/test_trajectory_reconstruction/test_postition_methods/test_forward_backwards_integration.py index b17956d5..e84911c5 100644 --- a/tests/test_trajectory_reconstruction/test_postition_methods/test_forward_backwards_integration.py +++ b/tests/test_trajectory_reconstruction/test_postition_methods/test_forward_backwards_integration.py @@ -11,7 +11,7 @@ class MetaTestConfig: algorithm_class = ForwardBackwardIntegration - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_events) -> BaseType: position = ForwardBackwardIntegration() position.estimate(healthy_example_imu_data["left_sensor"].iloc[:10], sampling_rate_hz=1) diff --git a/tests/test_trajectory_reconstruction/test_postition_methods/test_piece_wise_linear_dedrifted_integration.py b/tests/test_trajectory_reconstruction/test_postition_methods/test_piece_wise_linear_dedrifted_integration.py index 46eb1dc6..48a5d7fc 100644 --- a/tests/test_trajectory_reconstruction/test_postition_methods/test_piece_wise_linear_dedrifted_integration.py +++ b/tests/test_trajectory_reconstruction/test_postition_methods/test_piece_wise_linear_dedrifted_integration.py @@ -16,7 +16,7 @@ class MetaTestConfig: algorithm_class = PieceWiseLinearDedriftedIntegration - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_events) -> BaseType: position = PieceWiseLinearDedriftedIntegration() # Get enough samples from the signal to ensure a ZUPT diff --git a/tests/test_trajectory_reconstruction/test_region_level_trajectory.py b/tests/test_trajectory_reconstruction/test_region_level_trajectory.py index ccaa42f8..23008d0a 100644 --- a/tests/test_trajectory_reconstruction/test_region_level_trajectory.py +++ b/tests/test_trajectory_reconstruction/test_region_level_trajectory.py @@ -25,7 +25,7 @@ class TestMetaFunctionality(TestAlgorithmMixin): algorithm_class = RegionLevelTrajectory __test__ = True - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_events) -> BaseType: trajectory = RegionLevelTrajectory() trajectory.estimate( diff --git a/tests/test_trajectory_reconstruction/test_stride_level_trajectory.py b/tests/test_trajectory_reconstruction/test_stride_level_trajectory.py index 4ce5c7e5..a234c434 100644 --- a/tests/test_trajectory_reconstruction/test_stride_level_trajectory.py +++ b/tests/test_trajectory_reconstruction/test_stride_level_trajectory.py @@ -16,7 +16,7 @@ class TestMetaFunctionality(TestAlgorithmMixin): algorithm_class = StrideLevelTrajectory __test__ = True - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_events) -> BaseType: trajectory = StrideLevelTrajectory() trajectory.estimate( diff --git a/tests/test_trajectory_reconstruction/test_trajectory_methods/test_rts_kalman.py b/tests/test_trajectory_reconstruction/test_trajectory_methods/test_rts_kalman.py index 04efea06..9ae93fb8 100644 --- a/tests/test_trajectory_reconstruction/test_trajectory_methods/test_rts_kalman.py +++ b/tests/test_trajectory_reconstruction/test_trajectory_methods/test_rts_kalman.py @@ -19,7 +19,7 @@ class TestMetaFunctionalityRtsKalman(TestAlgorithmMixin): __test__ = True algorithm_class = RtsKalman - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_events) -> BaseType: kalman_filter = RtsKalman() kalman_filter.estimate(healthy_example_imu_data["left_sensor"].iloc[:15], sampling_rate_hz=100) @@ -30,7 +30,7 @@ class TestMetaFunctionalityMadgwickRtsKalman(TestAlgorithmMixin): __test__ = True algorithm_class = MadgwickRtsKalman - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data, healthy_example_stride_events) -> BaseType: kalman_filter = MadgwickRtsKalman() kalman_filter.estimate(healthy_example_imu_data["left_sensor"].iloc[:15], sampling_rate_hz=100) diff --git a/tests/test_utils/test_rotations.py b/tests/test_utils/test_rotations.py index f517880d..fc228161 100644 --- a/tests/test_utils/test_rotations.py +++ b/tests/test_utils/test_rotations.py @@ -25,7 +25,7 @@ ) -@pytest.fixture() +@pytest.fixture def cyclic_rotation(): """Rotation that turns x to y, y to z, and z to x.""" return rotation_from_angle(np.array([0, 0, 1.0]), np.pi / 2) * rotation_from_angle(np.array([1, 0, 0.0]), np.pi / 2) diff --git a/tests/test_zupt_detection/test_combo_zupt_detector.py b/tests/test_zupt_detection/test_combo_zupt_detector.py index 6d7c2424..fcee0d31 100644 --- a/tests/test_zupt_detection/test_combo_zupt_detector.py +++ b/tests/test_zupt_detection/test_combo_zupt_detector.py @@ -15,7 +15,7 @@ class TestMetaFunctionalityComboZuptDetector(TestAlgorithmMixin): __test__ = True algorithm_class = ComboZuptDetector - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data): data_left = healthy_example_imu_data["left_sensor"].iloc[:500] return ComboZuptDetector([("a", NormZuptDetector()), ("b", NormZuptDetector())]).detect( diff --git a/tests/test_zupt_detection/test_moving_window_zupt_detector.py b/tests/test_zupt_detection/test_moving_window_zupt_detector.py index 3b4f5e48..d9319ab1 100644 --- a/tests/test_zupt_detection/test_moving_window_zupt_detector.py +++ b/tests/test_zupt_detection/test_moving_window_zupt_detector.py @@ -17,7 +17,7 @@ class TestMetaFunctionalityNormZuptDetector(TestAlgorithmMixin): __test__ = True algorithm_class = NormZuptDetector - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data): data_left = healthy_example_imu_data["left_sensor"].iloc[:500] return NormZuptDetector().detect(data_left, sampling_rate_hz=204.8) @@ -27,7 +27,7 @@ class TestMetaFunctionalityShoeZuptDetector(TestAlgorithmMixin): __test__ = True algorithm_class = ShoeZuptDetector - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data): data_left = healthy_example_imu_data["left_sensor"].iloc[:500] return ShoeZuptDetector().detect(data_left, sampling_rate_hz=204.8) @@ -37,7 +37,7 @@ class TestMetaFunctionalityAredZuptDetector(TestAlgorithmMixin): __test__ = True algorithm_class = AredZuptDetector - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data): data_left = healthy_example_imu_data["left_sensor"].iloc[:500] return AredZuptDetector().detect(data_left, sampling_rate_hz=204.8) diff --git a/tests/test_zupt_detection/test_stride_event_zupt_detector.py b/tests/test_zupt_detection/test_stride_event_zupt_detector.py index 31d7c360..68a6177a 100644 --- a/tests/test_zupt_detection/test_stride_event_zupt_detector.py +++ b/tests/test_zupt_detection/test_stride_event_zupt_detector.py @@ -12,7 +12,7 @@ class TestMetaFunctionalityStrideEventZuptDetector(TestAlgorithmMixin): __test__ = True algorithm_class = StrideEventZuptDetector - @pytest.fixture() + @pytest.fixture def after_action_instance(self, healthy_example_imu_data): data_left = healthy_example_imu_data["left_sensor"].iloc[:10] return StrideEventZuptDetector().detect( From 95991b69e5265dd28e3fa43e3504b2a310921143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 10:45:54 +0100 Subject: [PATCH 17/28] Reload HMM package with backend selection tests --- .../test_hmm_backend_selection.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/test_stride_segmentation/test_hmm_backend_selection.py b/tests/test_stride_segmentation/test_hmm_backend_selection.py index 35d86cc1..5f4fc8b3 100644 --- a/tests/test_stride_segmentation/test_hmm_backend_selection.py +++ b/tests/test_stride_segmentation/test_hmm_backend_selection.py @@ -1,8 +1,10 @@ import importlib +import sys from pathlib import Path from types import SimpleNamespace import pytest +import gaitmap_mad.stride_segmentation.hmm as hmm_module from gaitmap_mad.stride_segmentation.hmm import _backend_base as backend_base from gaitmap_mad.stride_segmentation.hmm import _segmentation_model as segmentation_model_module from gaitmap_mad.stride_segmentation.hmm.legacy import PomegranateLegacyHmmBackend @@ -14,6 +16,10 @@ def _restore_segmentation_model(): yield importlib.reload(segmentation_model_module) + importlib.reload(hmm_module) + gaitmap_hmm_module = sys.modules.get("gaitmap.stride_segmentation.hmm") + if gaitmap_hmm_module is not None: + importlib.reload(gaitmap_hmm_module) class _FakeLegacyBackend(PomegranateLegacyHmmBackend): @@ -47,7 +53,9 @@ def _fake_import_module(module_name: str): return importlib.import_module(module_name) monkeypatch.setattr(backend_base, "import_module", _fake_import_module) - return importlib.reload(segmentation_model_module) + segmentation_model = importlib.reload(segmentation_model_module) + importlib.reload(hmm_module) + return segmentation_model def test_default_backend_without_pomegranate(monkeypatch) -> None: From fdaaa0a6c27b75c26efad22d0f9d8de090c74239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 10:58:51 +0100 Subject: [PATCH 18/28] Keep legacy HMM annotations compatible with Python 3.9 --- .../stride_segmentation/hmm/legacy/_backend.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py index 895082b5..e0661836 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py @@ -5,7 +5,7 @@ import copy import warnings from importlib.metadata import PackageNotFoundError, version -from typing import Any, Literal +from typing import Any, Literal, Optional import numpy as np import pandas as pd @@ -155,9 +155,9 @@ class _LegacyTrainableHmm(BaseTrainableHmm, _HackyClonableHMMFix, ShortenedHMMPr max_iterations: int verbose: bool n_jobs: int - name: str | None - model: OptiPara[Any | None] - data_columns: OptiPara[tuple[str, ...] | None] + name: Optional[str] # noqa: UP045 + model: OptiPara[Optional[Any]] # noqa: UP045 + data_columns: OptiPara[Optional[tuple[str, ...]]] # noqa: UP045 def __init__( self, @@ -171,8 +171,8 @@ def __init__( verbose: bool = True, n_jobs: int = 1, name: str = "my_model", - model: Any | None = None, - data_columns: tuple[str, ...] | None = None, + model: Optional[Any] = None, # noqa: UP045 + data_columns: Optional[tuple[str, ...]] = None, # noqa: UP045 ) -> None: self.n_states = n_states self.n_gmm_components = n_gmm_components From 83fc01bb922b765cbaef0e97d35cf770a0d0a11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 11:59:41 +0100 Subject: [PATCH 19/28] Align HMM backend imports and inference regression tests --- .../stride_segmentation/hmm/_repr_utils.py | 41 ++ .../hmm/_segmentation_model.py | 2 +- .../hmm/legacy/__init__.py | 8 + .../stride_segmentation/hmm/legacy/_utils.py | 31 +- .../hmm/modern/__init__.py | 17 +- pyproject.toml | 6 +- tests/_hmm_test_helpers.py | 34 ++ tests/test_base.py | 7 +- tests/test_examples/test_all_examples.py | 37 +- .../test_hmm_backend_modern_runtime.py | 8 +- .../test_hmm_backend_selection.py | 44 +-- .../test_stride_segmentation/test_roth_hmm.py | 152 ++++++-- uv.lock | 361 +++++++++++++++++- 13 files changed, 617 insertions(+), 131 deletions(-) create mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_repr_utils.py create mode 100644 tests/_hmm_test_helpers.py diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_repr_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_repr_utils.py new file mode 100644 index 00000000..6af63626 --- /dev/null +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_repr_utils.py @@ -0,0 +1,41 @@ +"""Backend-neutral helpers for HMM repr/clone support.""" + +from __future__ import annotations + +from typing import Any + +try: + import pomegranate as pg +except ImportError: # pragma: no cover - exercised in environments without pomegranate + pg = None + +from tpcp import BaseTpcpObject, CloneFactory + + +def is_serialized_hmm_state(value: Any) -> bool: + return ( + hasattr(value, "compiled") + and hasattr(value, "trained_with") + and callable(getattr(value, "to_json", None)) + and callable(getattr(type(value), "from_json", None)) + ) + + +class ShortenedHMMPrint(BaseTpcpObject): + """Mixin class to better format HMM models when printing them.""" + + def __repr_parameter__(self, name: str, value: Any) -> str: + if name == "model": + if is_serialized_hmm_state(value): + n_states = len(value.compiled.state_names) if getattr(value, "compiled", None) is not None else "?" + backend = getattr(getattr(value, "trained_with", None), "backend_id", "?") + return f"{name}=HMMState[backend={backend}, states={n_states}](...)" + if pg is not None and isinstance(value, pg.HiddenMarkovModel): + return f"{name}=HiddenMarkovModel[name={value.name}](...)" + if ( + pg is not None + and isinstance(value, CloneFactory) + and isinstance(value.default_value, pg.HiddenMarkovModel) + ): + return f"{name}=cf(HiddenMarkovModel[name={value.get_value().name}](...))" + return super().__repr_parameter__(name, value) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py index d72e00be..5dc09a89 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py @@ -20,6 +20,7 @@ from gaitmap_mad.stride_segmentation.hmm._backend import BaseHmmBackend, BaseTrainableHmm, get_default_hmm_backend from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig, RothHmmConfig from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import RothHmmFeatureTransformer +from gaitmap_mad.stride_segmentation.hmm._repr_utils import ShortenedHMMPrint from gaitmap_mad.stride_segmentation.hmm._state import HMMState from gaitmap_mad.stride_segmentation.hmm._utils import ( _DataToShortError, @@ -28,7 +29,6 @@ get_train_data_sequences_transitions, validate_trainable_region_list, ) -from gaitmap_mad.stride_segmentation.hmm.legacy._utils import ShortenedHMMPrint DEFAULT_HMM_BACKEND = get_default_hmm_backend() diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py index 29312559..1df26701 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py @@ -2,7 +2,15 @@ from gaitmap_mad.stride_segmentation.hmm.legacy._backend import ( PomegranateLegacyHmmBackend, + _get_pomegranate_version, initialize_hmm, + pg, ) +if getattr(pg, "HiddenMarkovModel", None) is None: + raise ImportError( + "The legacy HMM backend requires `pomegranate 0.x` with `HiddenMarkovModel` support. " + f"Installed version: {_get_pomegranate_version() or 'not installed'}." + ) + __all__ = ["PomegranateLegacyHmmBackend", "initialize_hmm"] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py index 9b5fb386..010232a9 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py @@ -17,9 +17,11 @@ from pomegranate.hmm import History except (ImportError, AttributeError): History = Any -from tpcp import BaseTpcpObject, CloneFactory +from tpcp import BaseTpcpObject from tpcp._hash import custom_hash +from gaitmap_mad.stride_segmentation.hmm._repr_utils import ShortenedHMMPrint as ShortenedHMMPrint # noqa: F401 +from gaitmap_mad.stride_segmentation.hmm._repr_utils import is_serialized_hmm_state from gaitmap_mad.stride_segmentation.hmm._utils import _DataToShortError, cluster_data_by_labels @@ -71,15 +73,6 @@ def _clone_model(orig_model: pg.HiddenMarkovModel, assert_correct: bool = True) return model -def _is_serialized_hmm_state(value: Any) -> bool: - return ( - hasattr(value, "compiled") - and hasattr(value, "trained_with") - and callable(getattr(value, "to_json", None)) - and callable(getattr(type(value), "from_json", None)) - ) - - class _HackyClonableHMMFix(BaseTpcpObject): """Mixin that teaches `tpcp.clone` how to clone legacy pomegranate HMMs.""" @@ -87,27 +80,11 @@ class _HackyClonableHMMFix(BaseTpcpObject): def __clone_param__(cls, param_name: str, value: Any) -> Any: if isinstance(value, pg.HiddenMarkovModel): return _clone_model(value) - if _is_serialized_hmm_state(value): + if is_serialized_hmm_state(value): return type(value).from_json(value.to_json()) return super().__clone_param__(param_name, value) -class ShortenedHMMPrint(BaseTpcpObject): - """Mixin class to better format HMM models when printing them.""" - - def __repr_parameter__(self, name: str, value: Any) -> str: - if name == "model": - if _is_serialized_hmm_state(value): - n_states = len(value.compiled.state_names) if getattr(value, "compiled", None) is not None else "?" - backend = getattr(getattr(value, "trained_with", None), "backend_id", "?") - return f"{name}=HMMState[backend={backend}, states={n_states}](...)" - if isinstance(value, pg.HiddenMarkovModel): - return f"{name}=HiddenMarkovModel[name={value.name}](...)" - if isinstance(value, CloneFactory) and isinstance(value.default_value, pg.HiddenMarkovModel): - return f"{name}=cf(HiddenMarkovModel[name={value.get_value().name}](...))" - return super().__repr_parameter__(name, value) - - def gmms_from_samples( data, labels, diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/__init__.py index 94495256..055b1b45 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/__init__.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/__init__.py @@ -1,5 +1,20 @@ """Modern pomegranate backend package.""" -from gaitmap_mad.stride_segmentation.hmm.modern._backend import PomegranateModernHmmBackend +from gaitmap_mad.stride_segmentation.hmm.modern._backend import ( + DenseHMM, + PomegranateModernHmmBackend, + _get_pomegranate_version, + torch, +) + +if DenseHMM is None: + raise ImportError( + "The modern HMM backend requires `pomegranate 1.x` with `DenseHMM` support. " + f"Installed version: {_get_pomegranate_version() or 'not installed'}." + ) +if torch is None: + raise ImportError( + "The modern HMM backend requires `torch` because `pomegranate 1.x` training and inference run on PyTorch." + ) __all__ = ["PomegranateModernHmmBackend"] diff --git a/pyproject.toml b/pyproject.toml index 46718b6a..cf2cc3b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,12 +33,14 @@ dependencies = [ [project.optional-dependencies] hmm = [ - "pomegranate>=0.14.2,<2; python_version < '3.10'", + "pomegranate>=0.14.2,<1; python_version < '3.10'", + "pomegranate>=1.1; python_version >= '3.10'", "numpy<2; python_version < '3.10'", ] stats = ["pingouin>=0.5.3"] all = [ - "pomegranate>=0.14.2,<2; python_version < '3.10'", + "pomegranate>=0.14.2,<1; python_version < '3.10'", + "pomegranate>=1.1; python_version >= '3.10'", "numpy<2; python_version < '3.10'", "pingouin>=0.5.3", ] diff --git a/tests/_hmm_test_helpers.py b/tests/_hmm_test_helpers.py new file mode 100644 index 00000000..cdd2ba48 --- /dev/null +++ b/tests/_hmm_test_helpers.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from pathlib import Path + +import pandas as pd +import pytest + + +def import_legacy_hmm_backend(): + return pytest.importorskip("gaitmap_mad.stride_segmentation.hmm.legacy") + + +def import_modern_hmm_backend(): + return pytest.importorskip("gaitmap_mad.stride_segmentation.hmm.modern") + + +def require_trainable_hmm_backend(): + try: + return import_modern_hmm_backend() + except pytest.skip.Exception: + return import_legacy_hmm_backend() + + +def get_pretrained_inference_snapshot_path(sensor: str) -> Path: + return ( + Path(__file__).resolve().parent + / "test_examples" + / "snapshot" + / f"test_roth_hmm_stride_segmentation_{sensor}.json" + ) + + +def load_pretrained_inference_stride_list_snapshot(sensor: str) -> pd.DataFrame: + return pd.read_json(get_pretrained_inference_snapshot_path(sensor), orient="table") diff --git a/tests/test_base.py b/tests/test_base.py index bfac9665..ee9419ee 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -5,7 +5,6 @@ import pytest from tpcp import clone, get_action_method, get_action_methods_names, get_action_params, get_results, is_action_applied -from tpcp._hash import custom_hash from gaitmap.base import BaseAlgorithm from tests.conftest import _get_params_without_nested_class @@ -201,8 +200,10 @@ def test_nested_clone() -> None: def test_clone_pomegranate() -> None: - pytest.importorskip("pomegranate") from gaitmap.stride_segmentation.hmm import PreTrainedRothSegmentationModel hmm_model = PreTrainedRothSegmentationModel() - assert custom_hash(clone(hmm_model)) == custom_hash(hmm_model) + cloned = clone(hmm_model) + + assert cloned is not hmm_model + assert cloned.to_json() == hmm_model.to_json() diff --git a/tests/test_examples/test_all_examples.py b/tests/test_examples/test_all_examples.py index 92960534..539614b4 100644 --- a/tests/test_examples/test_all_examples.py +++ b/tests/test_examples/test_all_examples.py @@ -5,6 +5,10 @@ from pandas.testing import assert_frame_equal from gaitmap.utils.consts import SF_ACC +from tests._hmm_test_helpers import ( + import_legacy_hmm_backend, + load_pretrained_inference_stride_list_snapshot, +) from tests.conftest import compare_algo_objects # This is needed to avoid plots to open @@ -263,27 +267,30 @@ def test_multi_process() -> None: """ -def test_roth_hmm_stride_segmentation(snapshot) -> None: - import pytest - - pytest.importorskip("pomegranate") +def test_roth_hmm_stride_segmentation() -> None: from examples.stride_segmentation.roth_hmm_stride_segmentation import hmm_seg - snapshot.assert_match(hmm_seg.stride_list_["left_sensor"], "left_sensor") - snapshot.assert_match(hmm_seg.stride_list_["right_sensor"], "right_sensor") - + assert_frame_equal(hmm_seg.stride_list_["left_sensor"], load_pretrained_inference_stride_list_snapshot("left_sensor")) + assert_frame_equal( + hmm_seg.stride_list_["right_sensor"], load_pretrained_inference_stride_list_snapshot("right_sensor") + ) -def test_segmentation_hmm_training(snapshot) -> None: - import pytest - pytest.importorskip("pomegranate") +def test_segmentation_hmm_training() -> None: + import_legacy_hmm_backend() from examples.stride_segmentation.segmentation_hmm_training import hmm - # XXX: For some sad reason the training does not seem to be deterministic accross different machines. - # Therefore, we will just check that the model will still find the same strides for now. - # snapshot.assert_match(segmentation_model.model.to_json()) - snapshot.assert_match(hmm.stride_list_["left_sensor"], "left_sensor") - snapshot.assert_match(hmm.stride_list_["right_sensor"], "right_sensor") + # Training is not deterministic enough for an exact snapshot across machines/backends. + # We keep it anchored to the same pretrained inference reference and allow small boundary drift. + for sensor in ["left_sensor", "right_sensor"]: + expected = load_pretrained_inference_stride_list_snapshot(sensor) + actual = hmm.stride_list_[sensor] + expected_duration = expected["end"] - expected["start"] + actual_duration = actual["end"] - actual["start"] + + assert abs(len(actual) - len(expected)) <= 1 + assert abs(actual_duration.median() - expected_duration.median()) <= 20 + assert abs(actual["end"].iloc[-1] - expected["end"].iloc[-1]) <= 50 def test_zupt_dependency() -> None: diff --git a/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py b/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py index 213242e6..284c71df 100644 --- a/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py +++ b/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py @@ -3,10 +3,7 @@ import numpy as np import pytest -try: - from pomegranate.hmm import DenseHMM -except (ImportError, AttributeError): - DenseHMM = None +pytest.importorskip("gaitmap_mad.stride_segmentation.hmm.modern") from gaitmap_mad.stride_segmentation.hmm import PreTrainedRothSegmentationModel from gaitmap_mad.stride_segmentation.hmm._backend_common import prepare_predict_data from gaitmap_mad.stride_segmentation.hmm.modern import PomegranateModernHmmBackend @@ -15,9 +12,6 @@ from gaitmap.example_data import get_healthy_example_imu_data from gaitmap.utils.coordinate_conversion import convert_left_foot_to_fbf -pytestmark = pytest.mark.skipif(DenseHMM is None, reason="requires pomegranate 1.x") - - def _viterbi_decode_with_end_probs(model, log_emissions: np.ndarray) -> np.ndarray: with np.errstate(divide="ignore"): transition_log_probs = np.log(np.asarray(model.compiled.graph.transition_probs, dtype=float)) diff --git a/tests/test_stride_segmentation/test_hmm_backend_selection.py b/tests/test_stride_segmentation/test_hmm_backend_selection.py index 5f4fc8b3..f2cc31af 100644 --- a/tests/test_stride_segmentation/test_hmm_backend_selection.py +++ b/tests/test_stride_segmentation/test_hmm_backend_selection.py @@ -1,44 +1,27 @@ import importlib -import sys from pathlib import Path from types import SimpleNamespace -import pytest -import gaitmap_mad.stride_segmentation.hmm as hmm_module from gaitmap_mad.stride_segmentation.hmm import _backend_base as backend_base -from gaitmap_mad.stride_segmentation.hmm import _segmentation_model as segmentation_model_module -from gaitmap_mad.stride_segmentation.hmm.legacy import PomegranateLegacyHmmBackend -from gaitmap_mad.stride_segmentation.hmm.modern import PomegranateModernHmmBackend -from gaitmap_mad.stride_segmentation.hmm.scipy import ScipyHmmInferenceBackend -@pytest.fixture(autouse=True) -def _restore_segmentation_model(): - yield - importlib.reload(segmentation_model_module) - importlib.reload(hmm_module) - gaitmap_hmm_module = sys.modules.get("gaitmap.stride_segmentation.hmm") - if gaitmap_hmm_module is not None: - importlib.reload(gaitmap_hmm_module) - - -class _FakeLegacyBackend(PomegranateLegacyHmmBackend): +class _FakeLegacyBackend(backend_base.BaseHmmBackend): def __init__(self) -> None: backend_base.BaseHmmBackend.__init__(self, backend_id="pomegranate-legacy") -class _FakeModernBackend(PomegranateModernHmmBackend): +class _FakeModernBackend(backend_base.BaseHmmBackend): def __init__(self) -> None: backend_base.BaseHmmBackend.__init__(self, backend_id="pomegranate-modern") self.inference_implementation = "native" -class _FakeScipyBackend(ScipyHmmInferenceBackend): +class _FakeScipyBackend(backend_base.BaseHmmBackend): def __init__(self) -> None: backend_base.BaseHmmBackend.__init__(self, backend_id="scipy-inference") -def _reload_segmentation_model(monkeypatch, *, modern_available: bool, legacy_available: bool): +def _get_default_backend(monkeypatch, *, modern_available: bool, legacy_available: bool): def _fake_import_module(module_name: str): if module_name == "gaitmap_mad.stride_segmentation.hmm.modern": if not modern_available: @@ -53,30 +36,25 @@ def _fake_import_module(module_name: str): return importlib.import_module(module_name) monkeypatch.setattr(backend_base, "import_module", _fake_import_module) - segmentation_model = importlib.reload(segmentation_model_module) - importlib.reload(hmm_module) - return segmentation_model + return backend_base.get_default_hmm_backend() def test_default_backend_without_pomegranate(monkeypatch) -> None: - segmentation_model = _reload_segmentation_model(monkeypatch, modern_available=False, legacy_available=False) + backend = _get_default_backend(monkeypatch, modern_available=False, legacy_available=False) - assert isinstance(segmentation_model.DEFAULT_HMM_BACKEND, ScipyHmmInferenceBackend) - assert isinstance(segmentation_model.RothSegmentationHmm().backend, ScipyHmmInferenceBackend) + assert isinstance(backend, _FakeScipyBackend) def test_default_backend_with_legacy_pomegranate(monkeypatch) -> None: - segmentation_model = _reload_segmentation_model(monkeypatch, modern_available=False, legacy_available=True) + backend = _get_default_backend(monkeypatch, modern_available=False, legacy_available=True) - assert isinstance(segmentation_model.DEFAULT_HMM_BACKEND, PomegranateLegacyHmmBackend) - assert isinstance(segmentation_model.RothSegmentationHmm().backend, PomegranateLegacyHmmBackend) + assert isinstance(backend, _FakeLegacyBackend) def test_default_backend_with_modern_pomegranate(monkeypatch) -> None: - segmentation_model = _reload_segmentation_model(monkeypatch, modern_available=True, legacy_available=True) + backend = _get_default_backend(monkeypatch, modern_available=True, legacy_available=True) - assert isinstance(segmentation_model.DEFAULT_HMM_BACKEND, PomegranateModernHmmBackend) - assert isinstance(segmentation_model.RothSegmentationHmm().backend, PomegranateModernHmmBackend) + assert isinstance(backend, _FakeModernBackend) def test_packaged_pretrained_model_uses_migrated_state_format() -> None: diff --git a/tests/test_stride_segmentation/test_roth_hmm.py b/tests/test_stride_segmentation/test_roth_hmm.py index a9632ae6..71a00e07 100644 --- a/tests/test_stride_segmentation/test_roth_hmm.py +++ b/tests/test_stride_segmentation/test_roth_hmm.py @@ -1,4 +1,5 @@ import json +from importlib import import_module from types import SimpleNamespace from unittest.mock import patch @@ -6,13 +7,7 @@ import pandas as pd import pytest from numpy.testing import assert_almost_equal, assert_array_equal - -try: - from pomegranate.hmm import DenseHMM -except (ImportError, AttributeError): - DenseHMM = None - -pytest.importorskip("pomegranate") +from pandas.testing import assert_frame_equal from gaitmap_mad.stride_segmentation.hmm import ( CompositeHmmConfig, @@ -23,18 +18,10 @@ RothHmmConfig, RothHmmFeatureTransformer, RothSegmentationHmm, + get_default_hmm_backend, ) from gaitmap_mad.stride_segmentation.hmm._utils import estimate_sequence_boundary_probs -from gaitmap_mad.stride_segmentation.hmm.legacy import PomegranateLegacyHmmBackend -from gaitmap_mad.stride_segmentation.hmm.legacy import _backend as backend_module -from gaitmap_mad.stride_segmentation.hmm.legacy._backend import initialize_hmm -from gaitmap_mad.stride_segmentation.hmm.legacy._state import ( - hmm_state_to_pomegranate_model, -) -from gaitmap_mad.stride_segmentation.hmm.legacy._utils import predict -from gaitmap_mad.stride_segmentation.hmm.modern import PomegranateModernHmmBackend from gaitmap_mad.stride_segmentation.hmm.scipy import ScipyHmmInferenceBackend -from pomegranate.hmm import History from tpcp._hash import custom_hash from gaitmap.data_transform import SlidingWindowMean @@ -46,30 +33,65 @@ is_single_sensor_stride_list, ) from gaitmap.utils.exceptions import ValidationError +from tests._hmm_test_helpers import ( + import_legacy_hmm_backend, + import_modern_hmm_backend, + load_pretrained_inference_stride_list_snapshot, +) from tests.mixins.test_algorithm_mixin import TestAlgorithmMixin # Fix random seed for reproducibility np.random.seed(1) +try: + PomegranateLegacyHmmBackend = import_module( + "gaitmap_mad.stride_segmentation.hmm.legacy" + ).PomegranateLegacyHmmBackend +except ImportError: + PomegranateLegacyHmmBackend = None + +try: + PomegranateModernHmmBackend = import_module( + "gaitmap_mad.stride_segmentation.hmm.modern" + ).PomegranateModernHmmBackend +except ImportError: + PomegranateModernHmmBackend = None + + +def _require_legacy_backend(): + import_legacy_hmm_backend() + backend_module = import_module("gaitmap_mad.stride_segmentation.hmm.legacy._backend") + state_module = import_module("gaitmap_mad.stride_segmentation.hmm.legacy._state") + utils_module = import_module("gaitmap_mad.stride_segmentation.hmm.legacy._utils") + return SimpleNamespace( + backend_module=backend_module, + backend_class=backend_module.PomegranateLegacyHmmBackend, + initialize_hmm=backend_module.initialize_hmm, + hmm_state_to_pomegranate_model=state_module.hmm_state_to_pomegranate_model, + predict=utils_module.predict, + ) + + +def _default_backend_type(): + return type(get_default_hmm_backend()) + def _runtime_inference_backend_params(): params = [pytest.param(ScipyHmmInferenceBackend(), id="scipy")] - if DenseHMM is not None: + if PomegranateModernHmmBackend is not None: params.append(pytest.param(PomegranateModernHmmBackend(), id="pomegranate-modern")) - else: - params.append( - pytest.param(None, id="pomegranate-modern", marks=pytest.mark.skip(reason="requires pomegranate 1.x")) - ) return params def _trainable_backend_params(): - params = [pytest.param(PomegranateLegacyHmmBackend(), id="pomegranate-legacy")] - if DenseHMM is not None: + params = [] + if PomegranateLegacyHmmBackend is not None: + params.append(pytest.param(PomegranateLegacyHmmBackend(), id="pomegranate-legacy")) + if PomegranateModernHmmBackend is not None: params.append(pytest.param(PomegranateModernHmmBackend(), id="pomegranate-modern")) - else: + if not params: params.append( - pytest.param(None, id="pomegranate-modern", marks=pytest.mark.skip(reason="requires pomegranate 1.x")) + pytest.param(None, id="no-trainable-backend", marks=pytest.mark.skip(reason="requires a trainable HMM backend")) ) return params @@ -348,8 +370,9 @@ def test_self_optimize_with_info_returns_history(self, backend) -> None: class TestLegacyBackendHelpers: @pytest.mark.parametrize("architecture", ["left-right-strict", "left-right-loose", "fully-connected"]) def test_different_architectures(self, architecture) -> None: + legacy = _require_legacy_backend() # We test initialization directly, otherwise training will modify the transition matrizes - model = initialize_hmm( + model = legacy.initialize_hmm( [np.random.rand(100, 3)], [np.random.choice(5, 100)], n_states=5, @@ -393,7 +416,8 @@ def test_boundary_prob_estimation_uses_empirical_counts(self) -> None: assert_array_equal(end_probs, np.array([0.0, 0.0, 1.0])) def test_legacy_combined_model_uses_global_end_probabilities(self) -> None: - backend = backend_module.PomegranateLegacyHmmBackend() + legacy = _require_legacy_backend() + backend = legacy.backend_class() model_config = CompositeHmmConfig( modules=( HmmSubModelConfig( @@ -415,7 +439,7 @@ def test_legacy_combined_model_uses_global_end_probabilities(self) -> None: module_offsets = {"transition": 0, "stride": 2} trained_models = { "transition": SimpleNamespace( - model=initialize_hmm( + model=legacy.initialize_hmm( [np.random.rand(12, 1)], [np.tile(np.arange(2), 6)], n_states=2, @@ -425,7 +449,7 @@ def test_legacy_combined_model_uses_global_end_probabilities(self) -> None: ) ), "stride": SimpleNamespace( - model=initialize_hmm( + model=legacy.initialize_hmm( [np.random.rand(12, 1)], [np.tile(np.arange(2), 6)], n_states=2, @@ -446,7 +470,7 @@ def test_legacy_combined_model_uses_global_end_probabilities(self) -> None: distributions=[ distribution for model in trained_models.values() - for distribution in backend_module.get_model_distributions(model.model) + for distribution in legacy.backend_module.get_model_distributions(model.model) ], model_config=model_config, module_offsets=module_offsets, @@ -494,7 +518,7 @@ def test_self_optimize_with_info_returns_history(self) -> None: assert instance is trained_instance assert isinstance(instance.model, HMMState) for v in history.values(): - assert isinstance(v, History) + assert v is not None assert set(history.keys()) == {"stride", "transition", "self"} def test_serialization_excludes_backend(self) -> None: @@ -504,7 +528,7 @@ def test_serialization_excludes_backend(self) -> None: restored = RothSegmentationHmm.from_json(instance.to_json()) assert set(payload["params"]) == {"hmm_config", "model"} - assert isinstance(restored.backend, PomegranateLegacyHmmBackend) + assert isinstance(restored.backend, _default_backend_type()) def test_short_strides_raise_warning(self) -> None: data, labels = ( @@ -653,7 +677,7 @@ def test_pretrained_model_is_migrated_to_hmm_state(self) -> None: assert model.model.trained_with.backend_id == "pomegranate-legacy-migrated" assert model.model.trained_with.backend_version is not None assert len(model.model.submodels) == 2 - assert isinstance(model.backend, PomegranateLegacyHmmBackend) + assert isinstance(model.backend, _default_backend_type()) def test_pretrained_model_migration_removes_silent_backend_states(self) -> None: model = PreTrainedRothSegmentationModel() @@ -665,6 +689,57 @@ def test_pretrained_model_migration_removes_silent_backend_states(self) -> None: assert len(compiled.emissions) == len(compiled.state_names) assert all(name not in {"start", "end"} for name in compiled.state_names) + @pytest.mark.parametrize("algorithm", ["map", "viterbi"]) + @pytest.mark.parametrize("sensor", ["left_sensor", "right_sensor"]) + def test_pretrained_scipy_backend_matches_legacy_backend_for_all_decoders( + self, healthy_example_imu_data, sensor, algorithm + ) -> None: + legacy = _require_legacy_backend() + model = PreTrainedRothSegmentationModel() + + if sensor == "left_sensor": + data = convert_left_foot_to_fbf(healthy_example_imu_data[sensor]) + else: + data = convert_to_fbf(healthy_example_imu_data, left_like="left_", right_like="right_")[sensor] + + feature_data, _ = model._transform([data], None, sampling_rate_hz=100) + feature_data = feature_data[0] + + legacy_result = legacy.backend_class().predict( + model.model, + feature_data, + expected_columns=model.data_columns, + algorithm=algorithm, + verbose=model.verbose, + ) + scipy_result = ScipyHmmInferenceBackend().predict( + model.model, + feature_data, + expected_columns=model.data_columns, + algorithm=algorithm, + verbose=model.verbose, + ) + + assert_array_equal(legacy_result, scipy_result) + + def test_pretrained_inference_regression_uses_shared_snapshot(self, healthy_example_imu_data) -> None: + data = convert_to_fbf(healthy_example_imu_data, left_like="left_", right_like="right_") + backends = [ScipyHmmInferenceBackend()] + if PomegranateLegacyHmmBackend is not None: + backends.append(PomegranateLegacyHmmBackend()) + if PomegranateModernHmmBackend is not None: + backends.append(PomegranateModernHmmBackend()) + + for backend in backends: + result = HmmStrideSegmentation( + model=PreTrainedRothSegmentationModel().set_params(backend=backend), + snap_to_min_win_ms=300, + snap_to_min_axis="gyr_ml", + ).segment(data, 204.8) + for sensor in ["left_sensor", "right_sensor"]: + expected = load_pretrained_inference_stride_list_snapshot(sensor) + assert_frame_equal(result.stride_list_[sensor], expected) + @pytest.mark.parametrize("inference_backend", _runtime_inference_backend_params()) def test_pretrained_inference_backend_matches_pomegranate_hidden_states( self, healthy_example_imu_data, inference_backend @@ -683,6 +758,7 @@ def test_pretrained_inference_backend_matches_pomegranate_hidden_states( ) def test_trained_model_roundtrip_matches_original_hidden_states(self) -> None: + legacy = _require_legacy_backend() data_sequence = [pd.DataFrame(np.random.rand(120, 6), columns=BF_COLS)] region_list_sequence = [_stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]}))] instance = RothSegmentationHmm( @@ -691,26 +767,26 @@ def test_trained_model_roundtrip_matches_original_hidden_states(self) -> None: hmm_config__feature_transform__sampling_rate_feature_space_hz=100, ) captured_model = {} - original_converter = backend_module.pomegranate_model_to_hmm_state + original_converter = legacy.backend_module.pomegranate_model_to_hmm_state def _capture_and_convert(model, *args, **kwargs): captured_model["raw_model"] = model return original_converter(model, *args, **kwargs) - with patch.object(backend_module, "pomegranate_model_to_hmm_state", side_effect=_capture_and_convert): + with patch.object(legacy.backend_module, "pomegranate_model_to_hmm_state", side_effect=_capture_and_convert): instance.self_optimize(data_sequence, region_list_sequence, sampling_rate_hz=100) - runtime_model = hmm_state_to_pomegranate_model(instance.model) + runtime_model = legacy.hmm_state_to_pomegranate_model(instance.model) feature_data, _ = instance._transform(data_sequence, None, sampling_rate_hz=100) feature_data = feature_data[0] - raw_sequence = predict( + raw_sequence = legacy.predict( captured_model["raw_model"], feature_data, expected_columns=instance.data_columns, algorithm=instance.algo_predict, ) - roundtrip_sequence = predict( + roundtrip_sequence = legacy.predict( runtime_model, feature_data, expected_columns=instance.data_columns, diff --git a/uv.lock b/uv.lock index 6a8bce2d..cc05a9bf 100644 --- a/uv.lock +++ b/uv.lock @@ -56,6 +56,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, ] +[[package]] +name = "apricot-select" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nose", marker = "python_full_version >= '3.10'" }, + { name = "numba", version = "0.64.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", marker = "python_full_version >= '3.10'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tqdm", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/96/7d9aec6ab622449aad7556101e90c2ef67b72b7cf33ded6cc9326a41e169/apricot-select-0.6.1.tar.gz", hash = "sha256:3bf872d43ee96af141c9e4c40e4359aa6ca5e3022ae43b2a8aa46b24947b8bd8", size = 28198, upload-time = "2021-02-18T06:55:02.195Z" } + [[package]] name = "asttokens" version = "3.0.1" @@ -814,6 +828,32 @@ toml = [ { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, ] +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "(python_full_version >= '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/d8/b546104b8da3f562c1ff8ab36d130c8fe1dd6a045ced80b4f6ad74f7d4e1/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d3c842c2a4303b2a580fe955018e31aea30278be19795ae05226235268032e5", size = 12148218, upload-time = "2025-10-21T14:51:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, + { url = "https://files.pythonhosted.org/packages/53/1d/f7f2bcffe788aebd4325a34d8a976b219a0751c06707aa89c9e70355ceae/cuda_bindings-12.9.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9866ceec83e39337d1a1d64837864c964ad902992478caa288a0bc1be95f21aa", size = 12152579, upload-time = "2025-10-21T14:52:16.731Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/de/8ca2b613042550dcf9ef50c596c8b1f602afda92cf9032ac28a73f6ee410/cuda_pathfinder-1.4.2-py3-none-any.whl", hash = "sha256:eb354abc20278f8609dc5b666a24648655bef5613c6dfe78a238a6fd95566754", size = 44779, upload-time = "2026-03-10T21:57:30.974Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -895,6 +935,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "filelock" +version = "3.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8b/4c32ecde6bea6486a2a5d05340e695174351ff6b06cf651a74c005f9df00/filelock-3.25.1.tar.gz", hash = "sha256:b9a2e977f794ef94d77cdf7d27129ac648a61f585bff3ca24630c1629f701aa9", size = 40319, upload-time = "2026-03-09T19:38:47.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/b8/2f664b56a3b4b32d28d3d106c71783073f712ba43ff6d34b9ea0ce36dc7b/filelock-3.25.1-py3-none-any.whl", hash = "sha256:18972df45473c4aa2c7921b609ee9ca4925910cc3a0fb226c96b92fc224ef7bf", size = 26720, upload-time = "2026-03-09T19:38:45.718Z" }, +] + [[package]] name = "fonttools" version = "4.60.2" @@ -1037,6 +1086,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, ] +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + [[package]] name = "gaitmap" version = "2.6.0" @@ -1063,11 +1121,13 @@ all = [ { name = "numpy", marker = "python_full_version < '3.10'" }, { name = "pingouin", version = "0.5.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pingouin", version = "0.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pomegranate", marker = "python_full_version < '3.10'" }, + { name = "pomegranate", version = "0.14.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pomegranate", version = "1.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] hmm = [ { name = "numpy", marker = "python_full_version < '3.10'" }, - { name = "pomegranate", marker = "python_full_version < '3.10'" }, + { name = "pomegranate", version = "0.14.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pomegranate", version = "1.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] stats = [ { name = "pingouin", version = "0.5.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -1109,8 +1169,10 @@ requires-dist = [ { name = "pandas", specifier = ">=2,<2.4" }, { name = "pingouin", marker = "extra == 'all'", specifier = ">=0.5.3" }, { name = "pingouin", marker = "extra == 'stats'", specifier = ">=0.5.3" }, - { name = "pomegranate", marker = "python_full_version < '3.10' and extra == 'all'", specifier = ">=0.14.2,<2" }, - { name = "pomegranate", marker = "python_full_version < '3.10' and extra == 'hmm'", specifier = ">=0.14.2,<2" }, + { name = "pomegranate", marker = "python_full_version >= '3.10' and extra == 'all'", specifier = ">=1.1" }, + { name = "pomegranate", marker = "python_full_version >= '3.10' and extra == 'hmm'", specifier = ">=1.1" }, + { name = "pomegranate", marker = "python_full_version < '3.10' and extra == 'all'", specifier = ">=0.14.2,<1" }, + { name = "pomegranate", marker = "python_full_version < '3.10' and extra == 'hmm'", specifier = ">=0.14.2,<1" }, { name = "pooch", specifier = ">=1.7.0" }, { name = "scikit-learn", specifier = ">=1.0.1" }, { name = "scipy", specifier = ">=1.6.1" }, @@ -2135,6 +2197,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/26/aaca612a0634ceede20682e692a6c55e35a94c21ba36b807cc40fe910ae1/memory_profiler-0.61.0-py3-none-any.whl", hash = "sha256:400348e61031e3942ad4d4109d18753b2fb08c2f6fb8290671c5513a34182d84", size = 31803, upload-time = "2022-11-15T17:57:27.031Z" }, ] +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + [[package]] name = "myst-parser" version = "1.0.0" @@ -2170,6 +2241,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/f0/8fbc882ca80cf077f1b246c0e3c3465f7f415439bdea6b899f6b19f61f70/networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2", size = 1647772, upload-time = "2023-10-28T08:41:36.945Z" }, ] +[[package]] +name = "nose" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", hash = "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98", size = 280488, upload-time = "2015-06-02T09:12:32.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/d8/dd071918c040f50fa1cf80da16423af51ff8ce4a0f2399b7bf8de45ac3d9/nose-1.3.7-py3-none-any.whl", hash = "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", size = 154731, upload-time = "2015-06-02T09:12:40.57Z" }, +] + [[package]] name = "numba" version = "0.60.0" @@ -2343,6 +2423,140 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl", hash = "sha256:3149da9874af890bcc2a82ef7aae5484e5aa81cb2778f08e3c307ba6d963721b", size = 69255, upload-time = "2025-12-02T16:39:11.561Z" }, ] +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(python_full_version >= '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(python_full_version >= '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cusparse-cu12", marker = "(python_full_version >= '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -2888,6 +3102,10 @@ wheels = [ name = "pomegranate" version = "0.14.6" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "(python_full_version < '3.10' and platform_machine != 'arm64') or (python_full_version < '3.10' and sys_platform != 'darwin')", +] dependencies = [ { name = "joblib", marker = "python_full_version < '3.10'" }, { name = "networkx", marker = "python_full_version < '3.10'" }, @@ -2905,6 +3123,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/67/5727ef43ebcc035280696d0a8af98101f5c9128dba37961105bfe246b59e/pomegranate-0.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:d14ed36ac12d782763b97ec3bd8cb1934ccad919ecc1e8008686d19591367ffd", size = 6745855, upload-time = "2021-11-01T20:14:01.511Z" }, ] +[[package]] +name = "pomegranate" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "(python_full_version >= '3.14' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", + "(python_full_version == '3.11.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", + "(python_full_version == '3.10.*' and platform_machine != 'arm64') or (python_full_version == '3.10.*' and sys_platform != 'darwin')", +] +dependencies = [ + { name = "apricot-select", marker = "python_full_version >= '3.10'" }, + { name = "networkx", marker = "python_full_version >= '3.10'" }, + { name = "numpy", marker = "python_full_version >= '3.10'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "torch", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/c7/c5a967b1edecf1b56a4b1c39e3d7fd3fe980d9a86502f0d54aace0ec40eb/pomegranate-1.1.2.tar.gz", hash = "sha256:bffe01521e8783ef84cb60862ea60161c2130835868d8a37b7d79477b328ad8f", size = 80699, upload-time = "2025-02-07T18:05:58.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/03/c8bc4317dc946e3501c1336f01bd12c31efd94a9e9afc9622b53741360ae/pomegranate-1.1.2-py3-none-any.whl", hash = "sha256:9112351f39d2219c8f82903fed4221be05e4344a7c5e8a4a8f713a11231ec486", size = 98449, upload-time = "2025-02-07T18:05:56.081Z" }, +] + [[package]] name = "pooch" version = "1.9.0" @@ -3690,6 +3943,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, ] +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -3878,6 +4140,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/9d/a3d33f4bda4e6a5f9b1118e81f93d5bc1620ad8d685df15d79b291ad9b7f/statsmodels-0.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:3bef39f8587754f2d644b2e831e102fa08ace9a5a1af4b583b122e6fd3e083ab", size = 9590613, upload-time = "2025-12-05T23:15:24.013Z" }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + [[package]] name = "tabulate" version = "0.9.0" @@ -3988,6 +4262,71 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] +[[package]] +name = "torch" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "filelock", marker = "python_full_version >= '3.10'" }, + { name = "fsspec", marker = "python_full_version >= '3.10'" }, + { name = "jinja2", marker = "python_full_version >= '3.10'" }, + { name = "networkx", marker = "python_full_version >= '3.10'" }, + { name = "nvidia-cublas-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy", marker = "python_full_version >= '3.10'" }, + { name = "triton", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/30/bfebdd8ec77db9a79775121789992d6b3b75ee5494971294d7b4b7c999bc/torch-2.10.0-2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2b980edd8d7c0a68c4e951ee1856334a43193f98730d97408fbd148c1a933313", size = 79411457, upload-time = "2026-02-10T21:44:59.189Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/c61f36cfd446170ec27b3a4984f072fd06dab6b5d7ce27e11adb35d6c838/torch-2.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5276fa790a666ee8becaffff8acb711922252521b28fbce5db7db5cf9cb2026d", size = 145992962, upload-time = "2026-01-21T16:24:14.04Z" }, + { url = "https://files.pythonhosted.org/packages/b5/60/6662535354191e2d1555296045b63e4279e5a9dbad49acf55a5d38655a39/torch-2.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:aaf663927bcd490ae971469a624c322202a2a1e68936eb952535ca4cd3b90444", size = 915599237, upload-time = "2026-01-21T16:23:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/40/b8/66bbe96f0d79be2b5c697b2e0b187ed792a15c6c4b8904613454651db848/torch-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:a4be6a2a190b32ff5c8002a0977a25ea60e64f7ba46b1be37093c141d9c49aeb", size = 113720931, upload-time = "2026-01-21T16:24:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/d820f90e69cda6c8169b32a0c6a3ab7b17bf7990b8f2c680077c24a3c14c/torch-2.10.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:35e407430795c8d3edb07a1d711c41cc1f9eaddc8b2f1cc0a165a6767a8fb73d", size = 79411450, upload-time = "2026-01-21T16:25:30.692Z" }, + { url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3d/c87b33c5f260a2a8ad68da7147e105f05868c281c63d65ed85aa4da98c66/torch-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd", size = 113723116, upload-time = "2026-01-21T16:25:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, + { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, + { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" }, + { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, + { url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" }, + { url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" }, + { url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, +] + [[package]] name = "tornado" version = "6.5.4" @@ -4047,6 +4386,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" From 775ebb7cf9daea3904c51409c4932371534048a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 13:23:33 +0100 Subject: [PATCH 20/28] Stabilize HMM training example snapshots --- .../segmentation_hmm_training.py | 8 +- .../hmm/modern/__init__.py | 17 +- .../hmm/modern/_backend.py | 22 +- .../stride_segmentation/hmm/modern/_state.py | 108 +- .../stride_segmentation/hmm/modern/_utils.py | 14 +- .../test_segmentation_hmm_training_0.txt | 3684 ----------------- ...egmentation_hmm_training_right_sensor.json | 6 +- tests/test_examples/test_all_examples.py | 17 +- .../test_hmm_backend_modern_runtime.py | 38 +- 9 files changed, 151 insertions(+), 3763 deletions(-) delete mode 100644 tests/test_examples/snapshot/test_segmentation_hmm_training_0.txt diff --git a/examples/stride_segmentation/segmentation_hmm_training.py b/examples/stride_segmentation/segmentation_hmm_training.py index f2851bba..c4370f56 100644 --- a/examples/stride_segmentation/segmentation_hmm_training.py +++ b/examples/stride_segmentation/segmentation_hmm_training.py @@ -197,18 +197,20 @@ # We will also plot the results to see how well the model performs. from gaitmap.stride_segmentation.hmm import HmmStrideSegmentation -hmm = HmmStrideSegmentation(segmentation_model).segment(bf_data, sampling_rate_hz=sampling_rate_hz) +# Note: We are using a high snap_to_min_win_ms here to get consistent output agaisnt all HMM backends. +# They have slight inconsitencies in some edge cases and we want to make sure this example provides the same results for consistent snapshots. +hmm = HmmStrideSegmentation(segmentation_model, snap_to_min_win_ms=300).segment(bf_data, sampling_rate_hz=sampling_rate_hz) hmm.stride_list_ # %% # Plotting the Results # -------------------- -sensor = "left_sensor" +sensor = "right_sensor" fig, axs = plt.subplots(nrows=2, sharex=True, figsize=(10, 5)) axs[0].set_title("gaitmap Body Frame Dataset") axs[0].plot(bf_data.reset_index(drop=True)[sensor]["gyr_ml"]) -for start, end in hmm.stride_list_["left_sensor"].to_numpy(): +for start, end in hmm.stride_list_[sensor].to_numpy(): axs[0].axvline(start, c="r") axs[0].axvline(end, c="r") axs[0].axvspan(start, end, alpha=0.2) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/__init__.py index 055b1b45..94495256 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/__init__.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/__init__.py @@ -1,20 +1,5 @@ """Modern pomegranate backend package.""" -from gaitmap_mad.stride_segmentation.hmm.modern._backend import ( - DenseHMM, - PomegranateModernHmmBackend, - _get_pomegranate_version, - torch, -) - -if DenseHMM is None: - raise ImportError( - "The modern HMM backend requires `pomegranate 1.x` with `DenseHMM` support. " - f"Installed version: {_get_pomegranate_version() or 'not installed'}." - ) -if torch is None: - raise ImportError( - "The modern HMM backend requires `torch` because `pomegranate 1.x` training and inference run on PyTorch." - ) +from gaitmap_mad.stride_segmentation.hmm.modern._backend import PomegranateModernHmmBackend __all__ = ["PomegranateModernHmmBackend"] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py index 727e2831..de651184 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py @@ -7,15 +7,8 @@ import numpy as np import pandas as pd - -try: - from pomegranate.hmm import DenseHMM -except (ImportError, AttributeError): # pragma: no cover - exercised in environments without modern pomegranate - DenseHMM = None -try: - import torch -except ImportError: # pragma: no cover - exercised in environments without torch - torch = None +import torch +from pomegranate.hmm import DenseHMM from gaitmap_mad.stride_segmentation.hmm._backend_base import BaseHmmBackend, BaseTrainableHmm from gaitmap_mad.stride_segmentation.hmm._backend_common import ( @@ -146,17 +139,6 @@ def __init__( *, inference_implementation: Literal["canonical", "native"] = "native", ) -> None: - if DenseHMM is None: - raise ImportError( - "Failed to initialize `PomegranateModernHmmBackend`. " - "This backend requires `pomegranate 1.x` with `DenseHMM` support. " - f"Installed version: {_get_pomegranate_version() or 'not installed'}." - ) - if torch is None: - raise ImportError( - "Failed to initialize `PomegranateModernHmmBackend`. " - "This backend requires `torch` because native `pomegranate 1.x` inference and training run on PyTorch." - ) super().__init__(backend_id=backend_id) self.inference_implementation = inference_implementation diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py index 365c9396..1985ec99 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py @@ -3,22 +3,15 @@ from __future__ import annotations from importlib.metadata import PackageNotFoundError, version +from types import MethodType from typing import Any import numpy as np - -try: - from pomegranate.distributions import Normal -except (ImportError, AttributeError): # pragma: no cover - exercised in environments without modern pomegranate - Normal = None -try: - from pomegranate.gmm import GeneralMixtureModel -except (ImportError, AttributeError): # pragma: no cover - exercised in environments without modern pomegranate - GeneralMixtureModel = None -try: - from pomegranate.hmm import DenseHMM -except (ImportError, AttributeError): # pragma: no cover - exercised in environments without modern pomegranate - DenseHMM = None +import torch +from pomegranate._utils import _update_parameter +from pomegranate.distributions import Normal +from pomegranate.gmm import GeneralMixtureModel +from pomegranate.hmm import DenseHMM from gaitmap_mad.stride_segmentation.hmm._state import ( BackendInfo, @@ -41,8 +34,6 @@ def _get_pomegranate_version() -> str | None: def _require_modern_pomegranate() -> tuple[Any, Any, Any]: - if DenseHMM is None or Normal is None or GeneralMixtureModel is None: - raise ImportError("The modern HMM backend requires pomegranate 1.x with `DenseHMM` support.") return DenseHMM, Normal, GeneralMixtureModel @@ -52,6 +43,87 @@ def _parameter_to_numpy(parameter: Any) -> np.ndarray: return np.asarray(parameter, dtype=float) +# Compatibility shim for pomegranate < the upstream fixes from PR #1143: +# https://github.com/jmschrei/pomegranate/pull/1143 +_MODERN_MIN_COVARIANCE = 1e-6 + + +def _stabilize_covariance(covariance: Any, covariance_type: str, min_cov: Any) -> Any: + min_cov = torch.as_tensor(min_cov, dtype=covariance.dtype, device=covariance.device) + if covariance_type == "full": + if not torch.isfinite(covariance).all(): + return min_cov * torch.eye(covariance.shape[-1], dtype=covariance.dtype, device=covariance.device) + stabilized = 0.5 * (covariance + covariance.transpose(-1, -2)) + min_eigenvalue = torch.linalg.eigvalsh(stabilized).min() + if not torch.isfinite(min_eigenvalue) or min_eigenvalue < min_cov: + stabilized = stabilized + (min_cov - min_eigenvalue + min_cov) * torch.eye( + stabilized.shape[-1], dtype=stabilized.dtype, device=stabilized.device + ) + return stabilized + stabilized = torch.nan_to_num(covariance, nan=min_cov.item(), posinf=min_cov.item(), neginf=min_cov.item()) + return torch.maximum(stabilized, min_cov) + + +def _patch_distribution_numerics(distribution: Any) -> None: + if getattr(distribution, "_gaitmap_min_cov_patch", False): + return + if hasattr(distribution, "distributions"): + for child in distribution.distributions: + _patch_distribution_numerics(child) + if not hasattr(distribution, "covs") or not hasattr(distribution, "from_summaries"): + return + + original_reset_cache = distribution._reset_cache + + def _reset_cache_with_stable_covariance(self) -> Any: + if getattr(self, "_initialized", False): + min_cov = _MODERN_MIN_COVARIANCE if self.min_cov is None else self.min_cov + with torch.no_grad(): + self.covs.copy_(_stabilize_covariance(self.covs, self.covariance_type, min_cov)) + return original_reset_cache() + + def _from_summaries_with_min_cov(self) -> Any: + # Mirror the upstream PR #1143 fix that applies `min_cov` during the + # Normal M-step. Current releases store `min_cov` but do not use it. + if self.frozen is True: + return None + + means = self._xw_sum / self._w_sum + min_cov = ( + None + if self.min_cov is None + else torch.as_tensor(self.min_cov, dtype=self.covs.dtype, device=self.covs.device) + ) + + if self.covariance_type == "full": + v = self._xw_sum.unsqueeze(0) * self._xw_sum.unsqueeze(1) + covs = self._xxw_sum / self._w_sum - v / self._w_sum**2.0 + covs = 0.5 * (covs + covs.transpose(-1, -2)) + if min_cov is not None: + covs = covs + min_cov * torch.eye(covs.shape[-1], dtype=covs.dtype, device=covs.device) + elif self.covariance_type in ["diag", "sphere"]: + covs = self._xxw_sum / self._w_sum - self._xw_sum**2.0 / self._w_sum**2.0 + if self.covariance_type == "sphere": + covs = covs.mean(dim=-1) + if min_cov is not None: + covs = torch.maximum(covs, min_cov) + else: # pragma: no cover - mirrors pomegranate's supported covariance types + raise ValueError(f"Unsupported covariance type `{self.covariance_type}`.") + + if not torch.isfinite(means).all() or not torch.isfinite(covs).all(): + self._reset_cache() + return None + + _update_parameter(self.means, means, self.inertia) + _update_parameter(self.covs, covs, self.inertia) + self._reset_cache() + return None + + distribution._reset_cache = MethodType(_reset_cache_with_stable_covariance, distribution) + distribution.from_summaries = MethodType(_from_summaries_with_min_cov, distribution) + distribution._gaitmap_min_cov_patch = True + + def _modern_distribution_to_state(distribution: Any) -> EmissionState: _, normal, general_mixture_model = _require_modern_pomegranate() if isinstance(distribution, general_mixture_model): @@ -79,6 +151,7 @@ def _state_to_modern_distribution(state: EmissionState) -> Any: means=np.asarray(state.mean, dtype=float), covs=np.asarray(state.covariance, dtype=float), covariance_type=state.covariance_type, + min_cov=_MODERN_MIN_COVARIANCE, frozen=state.frozen, ) if isinstance(state, GaussianMixtureEmissionState): @@ -132,6 +205,11 @@ def flat_hmm_state_to_pomegranate_modern_model( ) if state.name is not None: model.name = state.name + # PR #1143 also fixes dtype propagation in the runtime model. Until that is + # released, cast the constructed DenseHMM explicitly to float64 here. + model = model.to(torch.float64) + for distribution in model.distributions: + _patch_distribution_numerics(distribution) return model diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_utils.py index ad4aa04c..37cc5570 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_utils.py @@ -82,22 +82,22 @@ def to_training_arrays( for data in data_sequence: if isinstance(data, pd.DataFrame): columns = data.columns if data_columns is None else list(data_columns) - arrays.append(np.ascontiguousarray(data[columns].to_numpy().copy())) + arrays.append(np.ascontiguousarray(data[columns].to_numpy(dtype=np.float64, copy=True))) continue - arrays.append(np.ascontiguousarray(data.copy())) + arrays.append(np.ascontiguousarray(np.asarray(data, dtype=np.float64).copy())) return arrays def to_modern_input(data_sequence: list[np.ndarray]) -> list[np.ndarray]: """Normalize training arrays to the dtype expected by modern pomegranate.""" - return [sequence.astype(float, copy=False) for sequence in data_sequence] + return [sequence.astype(np.float64, copy=False) for sequence in data_sequence] def labels_to_priors(labels_sequence: list[np.ndarray], n_states: int) -> list[np.ndarray]: """Convert hard labels into one-hot prior matrices for modern pomegranate.""" priors = [] for labels in labels_sequence: - one_hot = np.zeros((len(labels), n_states), dtype=float) + one_hot = np.zeros((len(labels), n_states), dtype=np.float64) one_hot[np.arange(len(labels)), labels.astype(int)] = 1.0 priors.append(one_hot) return priors @@ -131,12 +131,12 @@ def create_initial_graph_state( transition_matrix, start_probs, end_probs = create_transition_matrix_left_right(n_states, self_transition=False) elif architecture == "left-right-loose": transition_matrix, _, _ = create_transition_matrix_left_right(n_states, self_transition=True) - start_probs = np.ones(n_states).astype(float) - end_probs = np.ones(n_states).astype(float) + start_probs = np.ones(n_states, dtype=np.float64) + end_probs = np.ones(n_states, dtype=np.float64) else: transition_matrix, start_probs, end_probs = create_transition_matrix_fully_connected(n_states) transition_matrix, end_probs = normalize_transition_and_end_probs(transition_matrix, end_probs) - start_probs = np.asarray(start_probs, dtype=float) + start_probs = np.asarray(start_probs, dtype=np.float64) start_probs /= start_probs.sum() return HmmGraphState(transition_probs=transition_matrix, start_probs=start_probs, end_probs=end_probs) diff --git a/tests/test_examples/snapshot/test_segmentation_hmm_training_0.txt b/tests/test_examples/snapshot/test_segmentation_hmm_training_0.txt deleted file mode 100644 index 124392da..00000000 --- a/tests/test_examples/snapshot/test_segmentation_hmm_training_0.txt +++ /dev/null @@ -1,3684 +0,0 @@ -{ - "class" : "HiddenMarkovModel", - "name" : "segmentation_model", - "start" : { - "class" : "State", - "distribution" : null, - "name" : "None-start", - "weight" : 1.0 - }, - "end" : { - "class" : "State", - "distribution" : null, - "name" : "None-end", - "weight" : 1.0 - }, - "states" : [ - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.8040436322356477, - 0.3591814709329526 - ], - [ - [ - 0.40646214331551517, - -0.24394655420810424 - ], - [ - -0.24394655420810424, - 0.7766879580450637 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.1720527560920257, - 0.06634172983979568 - ], - [ - [ - 0.05000209221403754, - -0.02778554338311081 - ], - [ - -0.02778554338311081, - 0.14960528980030702 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.01865725568788036, - 0.0016783219211641389 - ], - [ - [ - 2.4539965958570294e-06, - -1.2416427065431656e-06 - ], - [ - -1.2416427065431656e-06, - 6.714715629709089e-06 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.2970467172024148, - 0.6529437855146528, - 0.05000949728293232 - ] - }, - "name" : "s0", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.01461369568764822, - 0.03572297280264007 - ], - [ - [ - 0.00021963066207018273, - -2.586417845651764e-06 - ], - [ - -2.586417845651764e-06, - 0.0037706075820089302 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.34762176412929835, - 0.3153457598585078 - ], - [ - [ - 0.03125539581429149, - -0.004657647959915439 - ], - [ - -0.004657647959915439, - 0.006189589056233126 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.018540497221667743, - 0.00044558058005839764 - ], - [ - [ - 3.1515027780463164e-05, - -1.1404453092914148e-05 - ], - [ - -1.1404453092914148e-05, - 1.5032669166245287e-05 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.3879494520721146, - 0.0815011698751561, - 0.5305493780527293 - ] - }, - "name" : "s1", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.3593634366214855, - -0.03944909348585582 - ], - [ - [ - 0.0515932534639944, - -0.013070004239700192 - ], - [ - -0.013070004239700192, - 0.04909176641573044 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.011332989966055964, - -0.004932083681808868 - ], - [ - [ - 2.4433889774414404e-05, - 8.96221179962006e-06 - ], - [ - 8.96221179962006e-06, - 1.230936795797127e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.0028261556302423742, - -0.05679042932293115 - ], - [ - [ - 0.01061016141251362, - 0.003082361916922552 - ], - [ - 0.003082361916922552, - 0.025622176321037013 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.25253074103831163, - 0.176329229388306, - 0.5711400295733824 - ] - }, - "name" : "s2", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.5064439222558423, - -0.5569633887538881 - ], - [ - [ - 0.0007628171408683448, - -0.0003102561130424747 - ], - [ - -0.0003102561130424747, - 0.0001522938213987896 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.056532212003593256, - -0.665849256032684 - ], - [ - [ - 0.046688801503358156, - -0.008406277949864796 - ], - [ - -0.008406277949864796, - 0.027017704244622968 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.01829905346734807, - 4.791752597394427e-05 - ], - [ - [ - 1.3366910692423952e-06, - -1.4056940816359032e-07 - ], - [ - -1.4056940816359032e-07, - 1.8440893310603597e-07 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.007418935741327803, - 0.06172438914112581, - 0.9308566751175463 - ] - }, - "name" : "s3", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.2164689848314925, - -0.14120403712294458 - ], - [ - [ - 0.4481259332858317, - -0.35699465501376854 - ], - [ - -0.35699465501376854, - 1.0098709910992594 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.052527680411069276, - 0.11386702877721697 - ], - [ - [ - 0.0010064012674326369, - 0.00014460632706092324 - ], - [ - 0.00014460632706092324, - 2.0788190851552062e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.018655368249768836, - 0.000342673854818496 - ], - [ - [ - 3.2559195596507305e-06, - 8.02633833205566e-07 - ], - [ - 8.02633833205566e-07, - 1.5495141440876618e-06 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.6043298129485459, - 9.373672007408175e-13, - 0.3956701870505167 - ] - }, - "name" : "s4", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.2021921922033452, - 1.402559999173214 - ], - [ - [ - 0.2198377733147317, - 0.1836617795578009 - ], - [ - 0.1836617795578009, - 0.35345838578380223 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.3790024771587162, - 2.6826427421373844 - ], - [ - [ - 0.06674465249683595, - 0.00758974355089074 - ], - [ - 0.00758974355089074, - 0.010941337467642094 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.7975207192048467, - 2.648804929884559 - ], - [ - [ - 0.04179490653730112, - -0.0008804918773778667 - ], - [ - -0.0008804918773778667, - 0.01894703835622372 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.7892103116263098, - 2.3889827969424746 - ], - [ - [ - 0.06981215874510405, - 0.024495486092706104 - ], - [ - 0.024495486092706104, - 0.027595894601570723 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.1287723581911502, - 2.4952538058261218 - ], - [ - [ - 0.11769569342965194, - 0.07645116135114065 - ], - [ - 0.07645116135114065, - 0.065064246723973 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.22719090006201909, - 2.6852639094221327 - ], - [ - [ - 0.13242007925831814, - 0.056489945231852114 - ], - [ - 0.056489945231852114, - 0.029720713515433376 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.05282649341510476, - 0.15569013461077372, - 0.206778262380251, - 0.236528454211177, - 0.18223041239033358, - 0.16594624299235985 - ] - }, - "name" : "s5", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.37837853799417037, - 1.9535350619771967 - ], - [ - [ - 0.26729462581803703, - 0.023834012228917192 - ], - [ - 0.023834012228917192, - 0.019464694084064562 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.3628415782399637, - 1.882172769495759 - ], - [ - [ - 0.04119078854334496, - 0.017149261387412688 - ], - [ - 0.017149261387412688, - 0.011349351100975813 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.3888621692858933, - 1.2541746323347118 - ], - [ - [ - 0.020433744554810482, - 0.0029332886583703184 - ], - [ - 0.0029332886583703184, - 0.025889612335607905 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.3031665380644082, - 2.609142943680803 - ], - [ - [ - 0.030235908165502784, - 0.00688599472900813 - ], - [ - 0.00688599472900813, - 0.009563047637178582 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.4556877684873955, - 2.2578724539085577 - ], - [ - [ - 0.02586577881762999, - -0.0033532114740001855 - ], - [ - -0.0033532114740001855, - 0.0185089705075455 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.4997726603855883, - 1.6418327355886864 - ], - [ - [ - 0.018527977466202616, - -0.011652790434438093 - ], - [ - -0.011652790434438093, - 0.03073007379679976 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.03751890216561287, - 0.10206134092761178, - 0.1854134309677487, - 0.1894060230436204, - 0.3040325602355418, - 0.1815677426598645 - ] - }, - "name" : "s6", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.4740324405476108, - 0.6793356020400678 - ], - [ - [ - 0.013905656749014823, - 0.006420313244265778 - ], - [ - 0.006420313244265778, - 0.008342592294139022 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.5620295029158417, - 0.22561342375481364 - ], - [ - [ - 0.005204980461551485, - -0.0020663244535487064 - ], - [ - -0.0020663244535487064, - 0.008063411540066746 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.7716484475658794, - 0.16873818660385048 - ], - [ - [ - 0.012352779905889531, - -0.0013755633485664613 - ], - [ - -0.0013755633485664613, - 0.011300981390999335 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.4651043007270745, - 1.026455711264284 - ], - [ - [ - 0.00688365227691836, - -0.003938893327530027 - ], - [ - -0.003938893327530027, - 0.03984346718847703 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.5748281706469278, - 0.48604160042681127 - ], - [ - [ - 0.009143480605500373, - 0.004306935648931213 - ], - [ - 0.004306935648931213, - 0.006000425915229577 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.6980558196885895, - 0.06807770131674543 - ], - [ - [ - 0.004762177519084378, - -0.0021409877499768883 - ], - [ - -0.0021409877499768883, - 0.0071358845829416475 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.17452297772797398, - 0.1564050862026991, - 0.06420378105501827, - 0.11432912555049494, - 0.11843511112374087, - 0.3721039183400729 - ] - }, - "name" : "s7", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.5534316362387994, - 0.02655264130395254 - ], - [ - [ - 0.0004693389742928, - -0.0012999181332825945 - ], - [ - -0.0012999181332825945, - 0.0036491658666729706 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.357020419307743, - -0.0824126544020036 - ], - [ - [ - 0.009363037520695788, - 0.004633242410237834 - ], - [ - 0.004633242410237834, - 0.011270036730538184 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.7051351097590806, - -0.6215527193992321 - ], - [ - [ - 0.017599234861051288, - 0.005450663781536869 - ], - [ - 0.005450663781536869, - 0.0021551046026794325 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.6364852923638427, - -0.044048301751014235 - ], - [ - [ - 0.0050971342125390685, - 0.0018493714322927802 - ], - [ - 0.0018493714322927802, - 0.002483906320083705 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.630408855867652, - -0.03326221511233886 - ], - [ - [ - 0.003660551774188115, - 2.7839712620614068e-05 - ], - [ - 2.7839712620614068e-05, - 0.0029400596278572 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.5036571396644298, - -0.07131536179085843 - ], - [ - [ - 0.015760468496151668, - -0.0038552173452561654 - ], - [ - -0.0038552173452561654, - 0.010432750299370035 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.026433581297436223, - 0.40478320786727573, - 0.020778393438191635, - 0.0279295007818252, - 0.18545106515382637, - 0.33462425146144487 - ] - }, - "name" : "s8", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.22001807739303675, - -0.5510589124880456 - ], - [ - [ - 0.02334847655813331, - -0.004366984526417163 - ], - [ - -0.004366984526417163, - 0.0009210130799342035 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.332187281714675, - -0.43382961819635274 - ], - [ - [ - 0.008201239239184694, - -0.006012106623468407 - ], - [ - -0.006012106623468407, - 0.008354143108727218 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.571533125460306, - -0.8552583662902093 - ], - [ - [ - 0.011995534469637104, - -0.0013104076444251755 - ], - [ - -0.0013104076444251755, - 0.00858446781645628 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.4011364058019415, - -0.2891306775276589 - ], - [ - [ - 0.011540272484551903, - -0.005263876044788044 - ], - [ - -0.005263876044788044, - 0.006554348283738964 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.5312838775209263, - -0.6587188612640342 - ], - [ - [ - 0.011036211699371655, - 0.001725216285079576 - ], - [ - 0.001725216285079576, - 0.008160203511353681 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.7140896201781348, - -1.0610312733004146 - ], - [ - [ - 0.008739938643323801, - -0.002805384008638865 - ], - [ - -0.002805384008638865, - 0.018365124873027987 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.02862975033062507, - 0.22647819911453607, - 0.14816865293166337, - 0.2667991965397493, - 0.22639525563746818, - 0.10352894544595789 - ] - }, - "name" : "s9", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.5105644941198295, - -1.2305492591141955 - ], - [ - [ - 0.004749475788737469, - 0.002050500566991411 - ], - [ - 0.002050500566991411, - 0.04316921033379957 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.255725857814887, - -1.5446182662306835 - ], - [ - [ - 0.026241416123267865, - 0.01478664464139937 - ], - [ - 0.01478664464139937, - 0.04664604938329416 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.14022090509142604, - -0.33254752904572826 - ], - [ - [ - 0.005273306066163566, - -0.008298136833628936 - ], - [ - -0.008298136833628936, - 0.01316608572961744 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.719193655046859, - -1.4379862003230002 - ], - [ - [ - 0.002073854511166349, - -0.002742151231187398 - ], - [ - -0.002742151231187398, - 0.04816844574856781 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 1.1590821800846507, - -1.7466028581489552 - ], - [ - [ - 0.014747086584620156, - -0.011844186012822677 - ], - [ - -0.011844186012822677, - 0.027873689016353086 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.7109316574583632, - -1.8869854422613985 - ], - [ - [ - 0.024808150562480792, - 0.008765438428741447 - ], - [ - 0.008765438428741447, - 0.019455758832003775 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.2856114120630684, - 0.28308472510601773, - 0.017769495152708197, - 0.04927756973406105, - 0.09771647666781708, - 0.2665403212763277 - ] - }, - "name" : "sa", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.6274848405878357, - -1.5878127727753963 - ], - [ - [ - 0.04461649670550253, - 0.020476732666448533 - ], - [ - 0.020476732666448533, - 0.058773676044906215 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.47860341763292646, - -1.7093325598133895 - ], - [ - [ - 0.057807426968676724, - -0.007055143197524462 - ], - [ - -0.007055143197524462, - 0.008092166100741916 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.30863937771264216, - 0.09269790039565494 - ], - [ - [ - 0.012731612848978545, - 0.007793900375732349 - ], - [ - 0.007793900375732349, - 0.00883232068529274 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.0972377485439256, - -1.9031433578885442 - ], - [ - [ - 0.06064974976990788, - 0.0017259124741102452 - ], - [ - 0.0017259124741102452, - 0.010983164541769893 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.8033144155651418, - -1.4035069149041042 - ], - [ - [ - 0.03408267248054692, - -0.030768420553735003 - ], - [ - -0.030768420553735003, - 0.040718807954605966 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.202747597679054, - -1.271448775230189 - ], - [ - [ - 0.028477324512242153, - -0.021547363076713313 - ], - [ - -0.021547363076713313, - 0.029281734934733013 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.02845363700506843, - 0.24001139345706027, - 0.023146740536511844, - 0.3425606860717868, - 0.20223506406910532, - 0.16359247886046732 - ] - }, - "name" : "sb", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.6812410250550898, - -0.9945457610707594 - ], - [ - [ - 0.25479027265829696, - -0.03021309651999369 - ], - [ - -0.03021309651999369, - 0.03397285049274198 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.3129927964705252, - -0.16168205055707718 - ], - [ - [ - 0.015057270942274808, - -0.007038088751222924 - ], - [ - -0.007038088751222924, - 0.015381935935929037 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.0016200435132603, - 0.3606466242202415 - ], - [ - [ - 0.015548259605274027, - 0.007247893638893246 - ], - [ - 0.007247893638893246, - 0.017288633364408706 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.33799326807055, - -0.6598444215797216 - ], - [ - [ - 0.028319071510993228, - -0.0156378747936057 - ], - [ - -0.0156378747936057, - 0.05953841883916908 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.1847189834600382, - 0.10045234444702131 - ], - [ - [ - 0.004772794086241709, - -0.0015325263062055004 - ], - [ - -0.0015325263062055004, - 0.012584705914371512 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.17553169365773555, - 0.3924972869757415 - ], - [ - [ - 0.037304127305721, - -0.02936384742629144 - ], - [ - -0.02936384742629144, - 0.023113677052574295 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.05128493088858949, - 0.1681225485333926, - 0.19636121806243992, - 0.41091646234515955, - 0.1611832566737433, - 0.012131583496675116 - ] - }, - "name" : "sc", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.7934725617660205, - 0.6057363908307537 - ], - [ - [ - 0.022317149452309556, - 0.017270744296022513 - ], - [ - 0.017270744296022513, - 0.019440313652649362 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.34422628066895583, - 0.7741574696053061 - ], - [ - [ - 0.029296682638318364, - -0.0012923157435852566 - ], - [ - -0.0012923157435852566, - 0.001303708455642681 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.24011736694469782, - 0.8352333796266742 - ], - [ - [ - 0.012602321854845036, - -0.007070638507265635 - ], - [ - -0.007070638507265635, - 0.0042305232423694005 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.5112241644884947, - 0.7733676695608179 - ], - [ - [ - 0.012977039996842802, - 0.014421684717871062 - ], - [ - 0.014421684717871062, - 0.016846084267525076 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.1300621937205689, - 0.9117360378240474 - ], - [ - [ - 0.0061139293597976384, - -0.0011443314309826115 - ], - [ - -0.0011443314309826115, - 0.0025633483132631177 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.26356860955487416, - -0.11822672222508471 - ], - [ - [ - 0.03692893214011667, - 0.04897592388304517 - ], - [ - 0.04897592388304517, - 0.08061039808471612 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.25226825036022715, - 0.20777225879885397, - 0.12165630988431426, - 0.16386373910817, - 0.23103872278256257, - 0.023400719065872048 - ] - }, - "name" : "sd", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.012395575352788629, - 0.7336957741001034 - ], - [ - [ - 0.0011177986113019142, - 0.0005003791701335271 - ], - [ - 0.0005003791701335271, - 0.0051864782602025665 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.025976282878549212, - 0.3822149530730406 - ], - [ - [ - 4.1810756305687986e-05, - 0.0002194029348793227 - ], - [ - 0.0002194029348793227, - 0.00891933965456976 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.011866064213497939, - 0.22083011482320739 - ], - [ - [ - 6.100617171635189e-05, - 0.0002678672774896578 - ], - [ - 0.0002678672774896578, - 0.001807150764050995 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.02869871776862829, - 0.5852158826109061 - ], - [ - [ - 0.00021443141519116601, - 0.0003216005073519097 - ], - [ - 0.0003216005073519097, - 0.0017408561305467566 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.17461690762901225, - 0.2514823980968933 - ], - [ - [ - 0.0034610096436457956, - 0.00032290752784165937 - ], - [ - 0.00032290752784165937, - 0.0010489564679564834 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.006001353547590089, - 0.07393375903339867 - ], - [ - [ - 2.615909187470745e-06, - -1.55650565231611e-05 - ], - [ - -1.55650565231611e-05, - 9.261444767639299e-05 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.266676633488028, - 0.39952289581990325, - 0.13431031737396196, - 0.16847400398737397, - 0.022793919240991996, - 0.008222230089740885 - ] - }, - "name" : "se", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.011706586553209407, - 0.1989837093457438 - ], - [ - [ - 0.0002600320975194991, - -0.00029313164420553166 - ], - [ - -0.00029313164420553166, - 0.0024962987875945795 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.008059035002137047, - 0.0992659766574861 - ], - [ - [ - 3.744246935806723e-05, - 9.928793854680249e-05 - ], - [ - 9.928793854680249e-05, - 0.001962695396083612 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.003268675851413411, - -0.002295302662178956 - ], - [ - [ - 3.7018881612523695e-06, - -6.735304708917524e-06 - ], - [ - -6.735304708917524e-06, - 1.3633012802624129e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.009705606541981859, - 0.027462412576589528 - ], - [ - [ - 6.052174517849812e-05, - -5.759867819033617e-05 - ], - [ - -5.759867819033617e-05, - 0.00016922833112391984 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.01608905718932907, - -0.009103617652634023 - ], - [ - [ - 4.119722164536434e-05, - -4.393164079829657e-05 - ], - [ - -4.393164079829657e-05, - 4.754374832223868e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.008464601421229494, - 0.0054369377577025355 - ], - [ - [ - 9.09394879661089e-07, - 1.5980155780514924e-06 - ], - [ - 1.5980155780514924e-06, - 1.659535960468529e-05 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.07690178053789785, - 0.5140104720236519, - 0.03871726531407183, - 0.3497768962596432, - 0.012599366947750955, - 0.007994218916984158 - ] - }, - "name" : "sf", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.002947583836233708, - -0.014085898772608942 - ], - [ - [ - 4.997074058857972e-05, - -8.58222254446471e-06 - ], - [ - -8.58222254446471e-06, - 1.473968229440846e-06 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.0041513491224198, - -0.007647914449541538 - ], - [ - [ - 6.606795414204124e-07, - 1.0092758949966294e-06 - ], - [ - 1.0092758949966294e-06, - 1.8211327225964687e-06 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.002020321438389336, - 0.0008782123665955889 - ], - [ - [ - 6.691004922583928e-06, - 4.827908728558241e-06 - ], - [ - 4.827908728558241e-06, - 4.3013178018881445e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.012137137898650814, - -0.008375104312481335 - ], - [ - [ - 3.071356221976164e-05, - 3.823122114811344e-06 - ], - [ - 3.823122114811344e-06, - 2.2483755341498392e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.005654610382815709, - -0.004301478239111403 - ], - [ - [ - 1.1346987302296028e-05, - 8.50428539002298e-07 - ], - [ - 8.50428539002298e-07, - 1.982007198574262e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.03605909079596208, - 0.07148204905896231 - ], - [ - [ - 6.7373778081189e-05, - 0.00032885659460106276 - ], - [ - 0.00032885659460106276, - 0.0016087706772518074 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.015951886264093124, - 0.015105197825686702, - 0.12478863497276392, - 0.5425359219930025, - 0.28871900383477556, - 0.012899355109678242 - ] - }, - "name" : "sg", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.0030421922096838315, - -0.004983047756316794 - ], - [ - [ - 2.5388189529300666e-06, - 1.6662182400089128e-06 - ], - [ - 1.6662182400089128e-06, - 1.3974181670069181e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.0006891750873637185, - -0.023903324906292465 - ], - [ - [ - 8.592436317923748e-06, - 5.969231204280878e-06 - ], - [ - 5.969231204280878e-06, - 6.0356529818711753e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.00990770516409004, - -0.06548456494158666 - ], - [ - [ - 1.3425855338144182e-05, - 2.3857093652616646e-05 - ], - [ - 2.3857093652616646e-05, - 4.866444332689878e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.010644732660706657, - -0.0011766082735908438 - ], - [ - [ - 9.512269139964253e-06, - 9.467325294638514e-07 - ], - [ - 9.467325294638514e-07, - 1.600528810496113e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.023928791341342744, - -0.01561464325459599 - ], - [ - [ - 0.0001084371879755833, - -4.092967588426552e-05 - ], - [ - -4.092967588426552e-05, - 1.7222730136650855e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.003948364136135996, - -0.048653509366815985 - ], - [ - [ - 5.440840489830764e-06, - 1.1776988260303396e-05 - ], - [ - 1.1776988260303396e-05, - 4.503133973133043e-05 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.07749333198376236, - 0.14792779821990945, - 0.006667880710577348, - 0.7173907035746235, - 0.016389009797243507, - 0.03413127571388383 - ] - }, - "name" : "sh", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.00855309776292331, - -0.018720305179355533 - ], - [ - [ - 2.36943338149435e-05, - 2.0252127722154588e-08 - ], - [ - 2.0252127722154588e-08, - 3.135219784048359e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.0063462159670564094, - -0.15701879393762805 - ], - [ - [ - 4.0307162355742754e-05, - 0.00011171772883148373 - ], - [ - 0.00011171772883148373, - 0.0003198554188811831 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.0031340257796457, - -0.2625252780799887 - ], - [ - [ - 2.056012571551924e-05, - 1.985574070914933e-05 - ], - [ - 1.985574070914933e-05, - 0.0005521767143884687 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.001096303969822012, - -0.0818957166084949 - ], - [ - [ - 1.8099808781001736e-05, - 2.9311724254782684e-05 - ], - [ - 2.9311724254782684e-05, - 0.0012231672058566035 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.0058586239365255215, - -0.16846403672668755 - ], - [ - [ - 9.73628308927989e-06, - -6.648409009806088e-05 - ], - [ - -6.648409009806088e-05, - 0.0015275003624832357 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.02291759533796144, - -0.2207060942972471 - ], - [ - [ - 5.111665452316568e-05, - 0.00034093405971383456 - ], - [ - 0.00034093405971383456, - 0.003987596145140463 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.08994178819778052, - 0.07351179508208565, - 0.06244957074980986, - 0.5936558561413317, - 0.14645658286589883, - 0.03398440696309336 - ] - }, - "name" : "si", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - 0.013444488577260268, - -0.23255242838514759 - ], - [ - [ - 5.5600568718479655e-05, - 1.7381435894772431e-06 - ], - [ - 1.7381435894772431e-06, - 0.004193485419936131 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.0920026324033031, - -0.5207111728114413 - ], - [ - [ - 0.0015532869522256445, - 0.0007467235092565076 - ], - [ - 0.0007467235092565076, - 0.0027105368280336815 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.20672667144580556, - -0.6910829719625321 - ], - [ - [ - 0.0015163237032995685, - 0.0019359488028411385 - ], - [ - 0.0019359488028411385, - 0.00426248811370334 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.016100408725562388, - -0.3913423549141934 - ], - [ - [ - 0.0007881653302823009, - 1.1857681461475886e-05 - ], - [ - 1.1857681461475886e-05, - 0.002072827331631612 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.05669523200720021, - -0.5962986090135582 - ], - [ - [ - 0.0018470421184541093, - -0.0008152529337090934 - ], - [ - -0.0008152529337090934, - 0.0009201531486591058 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.15343483753856943, - -0.8386801128318258 - ], - [ - [ - 0.0015850434925874155, - -0.001057439206268052 - ], - [ - -0.001057439206268052, - 0.0016099470497745287 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.3768873989448247, - 0.1285610861980284, - 0.015653237979986773, - 0.3160056411613814, - 0.11188006103504143, - 0.051012574680737326 - ] - }, - "name" : "sj", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.016597492215702336, - -0.20090549807072197 - ], - [ - [ - 1e-08, - 0.0 - ], - [ - 0.0, - 1.0000000006993149e-08 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.4052272220702588, - -0.9923595015869292 - ], - [ - [ - 0.008882623712320308, - 0.01298397436235799 - ], - [ - 0.01298397436235799, - 0.021123202021512945 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.5021480123069786, - -1.1393587170654256 - ], - [ - [ - 0.009967129184108798, - 0.004424440892051373 - ], - [ - 0.004424440892051373, - 0.0032069321486745185 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.22975551796855637, - -0.7872531954140234 - ], - [ - [ - 0.009177736030227812, - 0.015656789355453973 - ], - [ - 0.015656789355453973, - 0.04019626125632157 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.5798863801165027, - -1.3354247966315143 - ], - [ - [ - 0.0010854152196530268, - 0.0010985750278557508 - ], - [ - 0.0010985750278557508, - 0.004010608326210521 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.8648721613913096, - -1.5417399206271434 - ], - [ - [ - 0.002256965805541192, - 0.002354901005786698 - ], - [ - 0.002354901005786698, - 0.004183943703880117 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.0058305303966059065, - 0.24177814201453038, - 0.1279143739918462, - 0.49506237957654053, - 0.08459765593693747, - 0.044816918083539506 - ] - }, - "name" : "sk", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.3334905961859544, - -0.6163663967713614 - ], - [ - [ - 0.01363302082086174, - 0.023225413694902622 - ], - [ - 0.023225413694902622, - 0.04134479638828502 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.0426283110875887, - -1.3987196428273911 - ], - [ - [ - 0.029812025764636884, - 0.015565742950590478 - ], - [ - 0.015565742950590478, - 0.013043293495956433 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.6944509974998825, - -1.1210264419712077 - ], - [ - [ - 0.008710225292427078, - -0.008755899259920808 - ], - [ - -0.008755899259920808, - 0.011710017271952821 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.7064555551915874, - -1.2965635672187297 - ], - [ - [ - 0.015852080969489743, - 0.014804995364899032 - ], - [ - 0.014804995364899032, - 0.016589743014523498 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.411364378766222, - -1.319524482039155 - ], - [ - [ - 0.028752531636784897, - 0.00030218047346973633 - ], - [ - 0.00030218047346973633, - 0.003403571703287115 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -2.023827205061829, - -0.7656001556246845 - ], - [ - [ - 0.002623653177397475, - -0.002308240351209803 - ], - [ - -0.002308240351209803, - 0.006288418830639329 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.02363642597864892, - 0.3467744505521859, - 0.10054266136968587, - 0.2405642858915019, - 0.25873057951055145, - 0.029751596697426032 - ] - }, - "name" : "sl", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.7124565891176353, - -0.7910923896774367 - ], - [ - [ - 0.09535441903488277, - 0.07301444944897137 - ], - [ - 0.07301444944897137, - 0.07899836448129899 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -2.2251850106763613, - 0.07394890668712056 - ], - [ - [ - 0.016037852204344043, - -0.00987638330345994 - ], - [ - -0.00987638330345994, - 0.007419033573225893 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -2.402482940826013, - 0.7454669475539399 - ], - [ - [ - 0.005038477683478805, - -0.003571682407117534 - ], - [ - -0.003571682407117534, - 0.0025318987239228556 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.885738706778285, - -0.6714757586859583 - ], - [ - [ - 0.039479366237407075, - -0.06255542221869939 - ], - [ - -0.06255542221869939, - 0.12175678537358108 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -2.3972105609592043, - 0.43925304213430816 - ], - [ - [ - 0.010107331333982998, - 0.002740478784648492 - ], - [ - 0.002740478784648492, - 0.003270742554789797 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -2.4729926443765233, - 0.5609942121971084 - ], - [ - [ - 0.00025919039113621073, - -0.0012813147164814377 - ], - [ - -0.0012813147164814377, - 0.006422842895965328 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.07997240231766853, - 0.1319279826542064, - 0.019877259755614718, - 0.72221357187315, - 0.03875836917173945, - 0.007250414227620797 - ] - }, - "name" : "sm", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -2.544833965107347, - 1.618805225916382 - ], - [ - [ - 1.000000135867e-08, - 5.627795411297711e-16 - ], - [ - 5.627795411297711e-16, - 1.0000000233110919e-08 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -2.371823473812686, - 1.2591795814536557 - ], - [ - [ - 0.008598847406341339, - -0.004105503060165918 - ], - [ - -0.004105503060165918, - 0.025009833491207452 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -2.544833965107347, - 1.618805225916382 - ], - [ - [ - 9.999999998947156e-06, - 5.264226173475608e-16 - ], - [ - 5.264226173475608e-16, - 1e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -2.2892986732550864, - 0.6194531479611614 - ], - [ - [ - 0.054575480462267706, - -0.09821945706306685 - ], - [ - -0.09821945706306685, - 0.21922975406084483 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -2.399446872962747, - 1.5197776978767612 - ], - [ - [ - 9.999999999111817e-06, - 4.440921249909714e-16 - ], - [ - 4.440921249909714e-16, - 1e-05 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -0.5945997733426932, - -0.3499900991769726 - ], - [ - [ - 0.08344322330990675, - 0.08221263351095641 - ], - [ - 0.08221263351095641, - 0.10038070975481765 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.00861133727386538, - 0.20652173067274274, - 0.002301517962848081, - 0.6259121295820457, - 0.01091279076294852, - 0.14574049374554962 - ] - }, - "name" : "sn", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : { - "class" : "GeneralMixtureModel", - "distributions" : [ - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -2.4372081037711713, - 1.659943481234789 - ], - [ - [ - 0.016402613255750733, - -0.023434311305960184 - ], - [ - -0.023434311305960184, - 0.06292426623689859 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -2.2682583159737804, - 1.9629275725787798 - ], - [ - [ - 0.023587822928866336, - 0.007041571905583579 - ], - [ - 0.007041571905583579, - 0.022098984029621738 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -2.3309820403090016, - 1.8527430623460173 - ], - [ - [ - 7.990393104332352e-05, - 0.00025862791934941516 - ], - [ - 0.00025862791934941516, - 0.0008371102622276192 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.0396306445103913, - -0.6605041056380266 - ], - [ - [ - 1.000000011102278e-08, - -1.1102278105951476e-16 - ], - [ - -1.1102278105951476e-16, - 1.000000011102278e-08 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.1971467376206921, - -0.34995204603968894 - ], - [ - [ - 0.00597502346254105, - 0.022512325325468433 - ], - [ - 0.022512325325468433, - 0.08962496010498687 - ] - ] - ], - "frozen" : false - }, - { - "class" : "Distribution", - "name" : "MultivariateGaussianDistribution", - "parameters" : [ - [ - -1.3154820083538519, - 0.521184550015688 - ], - [ - [ - 0.19894593672011907, - -0.11154677973908222 - ], - [ - -0.11154677973908222, - 0.3955516917271641 - ] - ] - ], - "frozen" : false - } - ], - "weights" : [ - 0.33092646420557786, - 0.4760260282760996, - 0.03076047241271292, - 0.015425351922206646, - 0.060077344843005163, - 0.08678433834039775 - ] - }, - "name" : "so", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : null, - "name" : "None-start", - "weight" : 1.0 - }, - { - "class" : "State", - "distribution" : null, - "name" : "None-end", - "weight" : 1.0 - } - ], - "end_index" : 26, - "start_index" : 25, - "silent_index" : 25, - "edges" : [ - [ - 0, - 0, - 0.8985916191900636, - 0.8894199738424869, - null - ], - [ - 0, - 1, - 0.10135262683378896, - 0.11058002615330285, - null - ], - [ - 0, - 4, - 5.5753976147506955e-05, - 0.1, - null - ], - [ - 1, - 1, - 0.9301709474176229, - 0.9310938027557745, - null - ], - [ - 1, - 2, - 0.06982905258237711, - 0.06890619724418152, - null - ], - [ - 2, - 2, - 0.9067539925144981, - 0.9055991868180906, - null - ], - [ - 2, - 3, - 0.09324600748550194, - 0.09440081318190942, - null - ], - [ - 3, - 3, - 0.9458926896007699, - 0.9464678350976422, - null - ], - [ - 3, - 4, - 0.054107310399230106, - 0.053532164902357765, - null - ], - [ - 4, - 0, - 0.1088243618146912, - 0.12267949920575863, - null - ], - [ - 4, - 4, - 0.8505382019167489, - 0.8163434546984181, - null - ], - [ - 4, - 5, - 0.04063743626855986, - 0.1, - null - ], - [ - 5, - 5, - 0.6592310932490484, - 0.6671045969840651, - null - ], - [ - 5, - 6, - 0.3407689067509516, - 0.3328954030159349, - null - ], - [ - 6, - 6, - 0.665535060624416, - 0.6656677869101361, - null - ], - [ - 6, - 7, - 0.3344649393755839, - 0.33433221308986383, - null - ], - [ - 7, - 7, - 0.6596686580668369, - 0.655692457853935, - null - ], - [ - 7, - 8, - 0.340331341933163, - 0.34430754214606507, - null - ], - [ - 8, - 8, - 0.6968792967694533, - 0.6985282246720477, - null - ], - [ - 8, - 9, - 0.3031207032305468, - 0.3014717753279522, - null - ], - [ - 9, - 9, - 0.6622212622441626, - 0.6619376200270657, - null - ], - [ - 9, - 10, - 0.3377787377558374, - 0.33806237997293437, - null - ], - [ - 10, - 10, - 0.6656375469874473, - 0.6665875411683188, - null - ], - [ - 10, - 11, - 0.33436245301255285, - 0.3334124588316812, - null - ], - [ - 11, - 11, - 0.6642423783644619, - 0.664329261905452, - null - ], - [ - 11, - 12, - 0.3357576216355382, - 0.33567073809454795, - null - ], - [ - 12, - 12, - 0.6479670187643027, - 0.6497460922647158, - null - ], - [ - 12, - 13, - 0.3520329812356973, - 0.3502539077352843, - null - ], - [ - 13, - 13, - 0.6601259934820228, - 0.6606413985265989, - null - ], - [ - 13, - 14, - 0.3398740065179771, - 0.3393586014734011, - null - ], - [ - 14, - 14, - 0.6754495148726938, - 0.6733325596634472, - null - ], - [ - 14, - 15, - 0.32455048512730617, - 0.32666744033655276, - null - ], - [ - 15, - 15, - 0.5197516025590413, - 0.5156775233428476, - null - ], - [ - 15, - 16, - 0.4802483974409588, - 0.4843224766571524, - null - ], - [ - 16, - 16, - 0.7687659995284701, - 0.7652559656099109, - null - ], - [ - 16, - 17, - 0.23123400047152992, - 0.234744034390089, - null - ], - [ - 17, - 17, - 0.7647396848536714, - 0.7683858962949026, - null - ], - [ - 17, - 18, - 0.23526031514632867, - 0.23161410370509738, - null - ], - [ - 18, - 18, - 0.5707411570111889, - 0.5743826177168254, - null - ], - [ - 18, - 19, - 0.42925884298881106, - 0.42561738228317453, - null - ], - [ - 19, - 19, - 0.6511777781216394, - 0.6510468003333086, - null - ], - [ - 19, - 20, - 0.3488222218783606, - 0.34895319966669147, - null - ], - [ - 20, - 20, - 0.6633646129417968, - 0.6618270613273677, - null - ], - [ - 20, - 21, - 0.3366353870582031, - 0.3381729386726323, - null - ], - [ - 21, - 21, - 0.6671963211308947, - 0.6625049281952917, - null - ], - [ - 21, - 22, - 0.3328036788691052, - 0.33749507180470817, - null - ], - [ - 22, - 22, - 0.41605630375112324, - 0.41719415757252454, - null - ], - [ - 22, - 23, - 0.5839436962488768, - 0.5828058424274754, - null - ], - [ - 23, - 23, - 0.3038225677497977, - 0.3670539809200027, - null - ], - [ - 23, - 24, - 0.6961774322502025, - 0.6329460190799971, - null - ], - [ - 24, - 24, - 0.2523425663833279, - 0.10532573175022351, - null - ], - [ - 24, - 0, - 0.038671944751991476, - 0.1, - null - ], - [ - 24, - 5, - 0.7089854888646807, - 0.1, - null - ], - [ - 25, - 3, - 1.0, - 1.0, - null - ] - ], - "distribution ties" : [] -} \ No newline at end of file diff --git a/tests/test_examples/snapshot/test_segmentation_hmm_training_right_sensor.json b/tests/test_examples/snapshot/test_segmentation_hmm_training_right_sensor.json index 6348c470..b7cf68cb 100644 --- a/tests/test_examples/snapshot/test_segmentation_hmm_training_right_sensor.json +++ b/tests/test_examples/snapshot/test_segmentation_hmm_training_right_sensor.json @@ -93,7 +93,7 @@ { "s_id":14, "start":3567, - "end":3802 + "end":3816 }, { "s_id":15, @@ -167,8 +167,8 @@ }, { "s_id":29, - "start":6977, - "end":7246 + "start":6966, + "end":7273 } ] } diff --git a/tests/test_examples/test_all_examples.py b/tests/test_examples/test_all_examples.py index 539614b4..72304c55 100644 --- a/tests/test_examples/test_all_examples.py +++ b/tests/test_examples/test_all_examples.py @@ -6,8 +6,8 @@ from gaitmap.utils.consts import SF_ACC from tests._hmm_test_helpers import ( - import_legacy_hmm_backend, load_pretrained_inference_stride_list_snapshot, + require_trainable_hmm_backend, ) from tests.conftest import compare_algo_objects @@ -276,21 +276,12 @@ def test_roth_hmm_stride_segmentation() -> None: ) -def test_segmentation_hmm_training() -> None: - import_legacy_hmm_backend() +def test_segmentation_hmm_training(snapshot) -> None: + require_trainable_hmm_backend() from examples.stride_segmentation.segmentation_hmm_training import hmm - # Training is not deterministic enough for an exact snapshot across machines/backends. - # We keep it anchored to the same pretrained inference reference and allow small boundary drift. for sensor in ["left_sensor", "right_sensor"]: - expected = load_pretrained_inference_stride_list_snapshot(sensor) - actual = hmm.stride_list_[sensor] - expected_duration = expected["end"] - expected["start"] - actual_duration = actual["end"] - actual["start"] - - assert abs(len(actual) - len(expected)) <= 1 - assert abs(actual_duration.median() - expected_duration.median()) <= 20 - assert abs(actual["end"].iloc[-1] - expected["end"].iloc[-1]) <= 50 + snapshot.assert_match(hmm.stride_list_[sensor], sensor) def test_zupt_dependency() -> None: diff --git a/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py b/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py index 284c71df..6390be02 100644 --- a/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py +++ b/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py @@ -4,14 +4,18 @@ import pytest pytest.importorskip("gaitmap_mad.stride_segmentation.hmm.modern") +import torch from gaitmap_mad.stride_segmentation.hmm import PreTrainedRothSegmentationModel from gaitmap_mad.stride_segmentation.hmm._backend_common import prepare_predict_data +from gaitmap_mad.stride_segmentation.hmm._state import FlatHmmState, GaussianEmissionState, HmmGraphState from gaitmap_mad.stride_segmentation.hmm.modern import PomegranateModernHmmBackend +from gaitmap_mad.stride_segmentation.hmm.modern._state import flat_hmm_state_to_pomegranate_modern_model from gaitmap_mad.stride_segmentation.hmm.scipy._utils import log_emission_probabilities from gaitmap.example_data import get_healthy_example_imu_data from gaitmap.utils.coordinate_conversion import convert_left_foot_to_fbf + def _viterbi_decode_with_end_probs(model, log_emissions: np.ndarray) -> np.ndarray: with np.errstate(divide="ignore"): transition_log_probs = np.log(np.asarray(model.compiled.graph.transition_probs, dtype=float)) @@ -87,6 +91,36 @@ def test_modern_native_viterbi_matches_full_length_end_probability_decode() -> N log_emissions = log_emission_probabilities(model.model, observations) expected_native = _viterbi_decode_with_end_probs(model.model, log_emissions) - assert len(native) == len(canonical) + 1 + assert len(native) == len(canonical) np.testing.assert_array_equal(native, expected_native) - assert not np.array_equal(canonical, native[:-1]) + np.testing.assert_array_equal(canonical, expected_native) + + +def test_modern_runtime_model_uses_float64_for_training() -> None: + """Modern runtime models should train without dtype mismatches on float64 data.""" + state = FlatHmmState( + graph=HmmGraphState( + transition_probs=np.array([[0.9, 0.1], [0.2, 0.8]], dtype=np.float64), + start_probs=np.array([1.0, 0.0], dtype=np.float64), + end_probs=np.array([0.0, 1.0], dtype=np.float64), + ), + emissions=( + GaussianEmissionState( + mean=np.array([0.0], dtype=np.float64), + covariance=np.array([[1.0]], dtype=np.float64), + ), + GaussianEmissionState( + mean=np.array([1.0], dtype=np.float64), + covariance=np.array([[1.0]], dtype=np.float64), + ), + ), + state_names=("s0", "s1"), + name="dtype_regression", + ) + runtime_model = flat_hmm_state_to_pomegranate_modern_model(state) + training_data = np.array([[[0.1], [0.2], [1.1]]], dtype=np.float64) + priors = np.array([[[1.0, 0.0], [1.0, 0.0], [0.0, 1.0]]], dtype=np.float64) + + assert runtime_model.dtype == torch.float64 + + runtime_model.fit(training_data, priors=priors) From 70fe6f98629147cf441a4075c34e04aa8f85bc1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 13:31:11 +0100 Subject: [PATCH 21/28] Simplify HMM test coverage --- .../segmentation_hmm_training.py | 4 +- .../stride_segmentation/hmm/legacy/_utils.py | 2 +- .../hmm/modern/_backend.py | 1 - tests/test_examples/test_all_examples.py | 4 +- .../test_hmm_backend_modern_runtime.py | 126 --------------- .../test_hmm_backend_selection.py | 67 -------- .../test_stride_segmentation/test_roth_hmm.py | 148 ++---------------- 7 files changed, 23 insertions(+), 329 deletions(-) delete mode 100644 tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py delete mode 100644 tests/test_stride_segmentation/test_hmm_backend_selection.py diff --git a/examples/stride_segmentation/segmentation_hmm_training.py b/examples/stride_segmentation/segmentation_hmm_training.py index c4370f56..e648bb0e 100644 --- a/examples/stride_segmentation/segmentation_hmm_training.py +++ b/examples/stride_segmentation/segmentation_hmm_training.py @@ -199,7 +199,9 @@ # Note: We are using a high snap_to_min_win_ms here to get consistent output agaisnt all HMM backends. # They have slight inconsitencies in some edge cases and we want to make sure this example provides the same results for consistent snapshots. -hmm = HmmStrideSegmentation(segmentation_model, snap_to_min_win_ms=300).segment(bf_data, sampling_rate_hz=sampling_rate_hz) +hmm = HmmStrideSegmentation(segmentation_model, snap_to_min_win_ms=300).segment( + bf_data, sampling_rate_hz=sampling_rate_hz +) hmm.stride_list_ # %% diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py index 010232a9..0cce8129 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py @@ -20,7 +20,7 @@ from tpcp import BaseTpcpObject from tpcp._hash import custom_hash -from gaitmap_mad.stride_segmentation.hmm._repr_utils import ShortenedHMMPrint as ShortenedHMMPrint # noqa: F401 +from gaitmap_mad.stride_segmentation.hmm._repr_utils import ShortenedHMMPrint as ShortenedHMMPrint from gaitmap_mad.stride_segmentation.hmm._repr_utils import is_serialized_hmm_state from gaitmap_mad.stride_segmentation.hmm._utils import _DataToShortError, cluster_data_by_labels diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py index de651184..c2021ba4 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_backend.py @@ -8,7 +8,6 @@ import numpy as np import pandas as pd import torch -from pomegranate.hmm import DenseHMM from gaitmap_mad.stride_segmentation.hmm._backend_base import BaseHmmBackend, BaseTrainableHmm from gaitmap_mad.stride_segmentation.hmm._backend_common import ( diff --git a/tests/test_examples/test_all_examples.py b/tests/test_examples/test_all_examples.py index 72304c55..01cd9fec 100644 --- a/tests/test_examples/test_all_examples.py +++ b/tests/test_examples/test_all_examples.py @@ -270,7 +270,9 @@ def test_multi_process() -> None: def test_roth_hmm_stride_segmentation() -> None: from examples.stride_segmentation.roth_hmm_stride_segmentation import hmm_seg - assert_frame_equal(hmm_seg.stride_list_["left_sensor"], load_pretrained_inference_stride_list_snapshot("left_sensor")) + assert_frame_equal( + hmm_seg.stride_list_["left_sensor"], load_pretrained_inference_stride_list_snapshot("left_sensor") + ) assert_frame_equal( hmm_seg.stride_list_["right_sensor"], load_pretrained_inference_stride_list_snapshot("right_sensor") ) diff --git a/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py b/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py deleted file mode 100644 index 6390be02..00000000 --- a/tests/test_stride_segmentation/test_hmm_backend_modern_runtime.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Tests for the optional native pomegranate 1.x inference path.""" - -import numpy as np -import pytest - -pytest.importorskip("gaitmap_mad.stride_segmentation.hmm.modern") -import torch -from gaitmap_mad.stride_segmentation.hmm import PreTrainedRothSegmentationModel -from gaitmap_mad.stride_segmentation.hmm._backend_common import prepare_predict_data -from gaitmap_mad.stride_segmentation.hmm._state import FlatHmmState, GaussianEmissionState, HmmGraphState -from gaitmap_mad.stride_segmentation.hmm.modern import PomegranateModernHmmBackend -from gaitmap_mad.stride_segmentation.hmm.modern._state import flat_hmm_state_to_pomegranate_modern_model -from gaitmap_mad.stride_segmentation.hmm.scipy._utils import log_emission_probabilities - -from gaitmap.example_data import get_healthy_example_imu_data -from gaitmap.utils.coordinate_conversion import convert_left_foot_to_fbf - - -def _viterbi_decode_with_end_probs(model, log_emissions: np.ndarray) -> np.ndarray: - with np.errstate(divide="ignore"): - transition_log_probs = np.log(np.asarray(model.compiled.graph.transition_probs, dtype=float)) - start_log_probs = np.log(np.asarray(model.compiled.graph.start_probs, dtype=float)) - end_log_probs = np.log(np.asarray(model.compiled.graph.end_probs, dtype=float)) - - n_samples, n_states = log_emissions.shape - dp = np.full((n_samples, n_states), -np.inf, dtype=float) - pointers = np.zeros((n_samples, n_states), dtype=int) - - dp[0] = start_log_probs + log_emissions[0] - for sample_idx in range(1, n_samples): - scores = dp[sample_idx - 1][:, None] + transition_log_probs - pointers[sample_idx] = np.argmax(scores, axis=0) - dp[sample_idx] = scores[pointers[sample_idx], np.arange(n_states)] + log_emissions[sample_idx] - - path = np.zeros(n_samples, dtype=int) - path[-1] = int(np.argmax(dp[-1] + end_log_probs)) - for sample_idx in range(n_samples - 1, 0, -1): - path[sample_idx - 1] = pointers[sample_idx, path[sample_idx]] - return path - - -def _get_pretrained_feature_data(): - model = PreTrainedRothSegmentationModel() - data = convert_left_foot_to_fbf(get_healthy_example_imu_data()["left_sensor"]) - feature_data, _ = model._transform([data], None, sampling_rate_hz=100) - return model, feature_data[0] - - -def test_modern_native_map_matches_canonical_backend() -> None: - """The native MAP path should match the canonical decoder exactly.""" - model, feature_data = _get_pretrained_feature_data() - - canonical = PomegranateModernHmmBackend().predict( - model.model, - feature_data, - expected_columns=model.data_columns, - algorithm="map", - verbose=model.verbose, - ) - native = PomegranateModernHmmBackend(inference_implementation="native").predict( - model.model, - feature_data, - expected_columns=model.data_columns, - algorithm="map", - verbose=model.verbose, - ) - - np.testing.assert_array_equal(canonical, native) - - -def test_modern_native_viterbi_matches_full_length_end_probability_decode() -> None: - """The native Viterbi path should use the full-length end-probability decode.""" - model, feature_data = _get_pretrained_feature_data() - - canonical = PomegranateModernHmmBackend().predict( - model.model, - feature_data, - expected_columns=model.data_columns, - algorithm="viterbi", - verbose=model.verbose, - ) - native = PomegranateModernHmmBackend(inference_implementation="native").predict( - model.model, - feature_data, - expected_columns=model.data_columns, - algorithm="viterbi", - verbose=model.verbose, - ) - - observations = prepare_predict_data(feature_data, model.data_columns, len(model.model.compiled.state_names)) - log_emissions = log_emission_probabilities(model.model, observations) - expected_native = _viterbi_decode_with_end_probs(model.model, log_emissions) - - assert len(native) == len(canonical) - np.testing.assert_array_equal(native, expected_native) - np.testing.assert_array_equal(canonical, expected_native) - - -def test_modern_runtime_model_uses_float64_for_training() -> None: - """Modern runtime models should train without dtype mismatches on float64 data.""" - state = FlatHmmState( - graph=HmmGraphState( - transition_probs=np.array([[0.9, 0.1], [0.2, 0.8]], dtype=np.float64), - start_probs=np.array([1.0, 0.0], dtype=np.float64), - end_probs=np.array([0.0, 1.0], dtype=np.float64), - ), - emissions=( - GaussianEmissionState( - mean=np.array([0.0], dtype=np.float64), - covariance=np.array([[1.0]], dtype=np.float64), - ), - GaussianEmissionState( - mean=np.array([1.0], dtype=np.float64), - covariance=np.array([[1.0]], dtype=np.float64), - ), - ), - state_names=("s0", "s1"), - name="dtype_regression", - ) - runtime_model = flat_hmm_state_to_pomegranate_modern_model(state) - training_data = np.array([[[0.1], [0.2], [1.1]]], dtype=np.float64) - priors = np.array([[[1.0, 0.0], [1.0, 0.0], [0.0, 1.0]]], dtype=np.float64) - - assert runtime_model.dtype == torch.float64 - - runtime_model.fit(training_data, priors=priors) diff --git a/tests/test_stride_segmentation/test_hmm_backend_selection.py b/tests/test_stride_segmentation/test_hmm_backend_selection.py deleted file mode 100644 index f2cc31af..00000000 --- a/tests/test_stride_segmentation/test_hmm_backend_selection.py +++ /dev/null @@ -1,67 +0,0 @@ -import importlib -from pathlib import Path -from types import SimpleNamespace - -from gaitmap_mad.stride_segmentation.hmm import _backend_base as backend_base - - -class _FakeLegacyBackend(backend_base.BaseHmmBackend): - def __init__(self) -> None: - backend_base.BaseHmmBackend.__init__(self, backend_id="pomegranate-legacy") - - -class _FakeModernBackend(backend_base.BaseHmmBackend): - def __init__(self) -> None: - backend_base.BaseHmmBackend.__init__(self, backend_id="pomegranate-modern") - self.inference_implementation = "native" - - -class _FakeScipyBackend(backend_base.BaseHmmBackend): - def __init__(self) -> None: - backend_base.BaseHmmBackend.__init__(self, backend_id="scipy-inference") - - -def _get_default_backend(monkeypatch, *, modern_available: bool, legacy_available: bool): - def _fake_import_module(module_name: str): - if module_name == "gaitmap_mad.stride_segmentation.hmm.modern": - if not modern_available: - raise ImportError("modern backend unavailable") - return SimpleNamespace(PomegranateModernHmmBackend=_FakeModernBackend) - if module_name == "gaitmap_mad.stride_segmentation.hmm.legacy": - if not legacy_available: - raise ImportError("legacy backend unavailable") - return SimpleNamespace(PomegranateLegacyHmmBackend=_FakeLegacyBackend) - if module_name == "gaitmap_mad.stride_segmentation.hmm.scipy": - return SimpleNamespace(ScipyHmmInferenceBackend=_FakeScipyBackend) - return importlib.import_module(module_name) - - monkeypatch.setattr(backend_base, "import_module", _fake_import_module) - return backend_base.get_default_hmm_backend() - - -def test_default_backend_without_pomegranate(monkeypatch) -> None: - backend = _get_default_backend(monkeypatch, modern_available=False, legacy_available=False) - - assert isinstance(backend, _FakeScipyBackend) - - -def test_default_backend_with_legacy_pomegranate(monkeypatch) -> None: - backend = _get_default_backend(monkeypatch, modern_available=False, legacy_available=True) - - assert isinstance(backend, _FakeLegacyBackend) - - -def test_default_backend_with_modern_pomegranate(monkeypatch) -> None: - backend = _get_default_backend(monkeypatch, modern_available=True, legacy_available=True) - - assert isinstance(backend, _FakeModernBackend) - - -def test_packaged_pretrained_model_uses_migrated_state_format() -> None: - model_json = Path( - "packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_pre_trained_models/fallriskpd_at_lab_model.json" - ).read_text(encoding="utf8") - - assert "SimpleHmm" not in model_json - assert '"stride_model"' not in model_json - assert '"transition_model"' not in model_json diff --git a/tests/test_stride_segmentation/test_roth_hmm.py b/tests/test_stride_segmentation/test_roth_hmm.py index 71a00e07..95bc1428 100644 --- a/tests/test_stride_segmentation/test_roth_hmm.py +++ b/tests/test_stride_segmentation/test_roth_hmm.py @@ -1,14 +1,10 @@ import json from importlib import import_module from types import SimpleNamespace -from unittest.mock import patch import numpy as np import pandas as pd import pytest -from numpy.testing import assert_almost_equal, assert_array_equal -from pandas.testing import assert_frame_equal - from gaitmap_mad.stride_segmentation.hmm import ( CompositeHmmConfig, HMMState, @@ -20,8 +16,9 @@ RothSegmentationHmm, get_default_hmm_backend, ) -from gaitmap_mad.stride_segmentation.hmm._utils import estimate_sequence_boundary_probs from gaitmap_mad.stride_segmentation.hmm.scipy import ScipyHmmInferenceBackend +from numpy.testing import assert_almost_equal, assert_array_equal +from pandas.testing import assert_frame_equal from tpcp._hash import custom_hash from gaitmap.data_transform import SlidingWindowMean @@ -35,7 +32,6 @@ from gaitmap.utils.exceptions import ValidationError from tests._hmm_test_helpers import ( import_legacy_hmm_backend, - import_modern_hmm_backend, load_pretrained_inference_stride_list_snapshot, ) from tests.mixins.test_algorithm_mixin import TestAlgorithmMixin @@ -61,14 +57,9 @@ def _require_legacy_backend(): import_legacy_hmm_backend() backend_module = import_module("gaitmap_mad.stride_segmentation.hmm.legacy._backend") - state_module = import_module("gaitmap_mad.stride_segmentation.hmm.legacy._state") - utils_module = import_module("gaitmap_mad.stride_segmentation.hmm.legacy._utils") return SimpleNamespace( - backend_module=backend_module, backend_class=backend_module.PomegranateLegacyHmmBackend, initialize_hmm=backend_module.initialize_hmm, - hmm_state_to_pomegranate_model=state_module.hmm_state_to_pomegranate_model, - predict=utils_module.predict, ) @@ -91,7 +82,9 @@ def _trainable_backend_params(): params.append(pytest.param(PomegranateModernHmmBackend(), id="pomegranate-modern")) if not params: params.append( - pytest.param(None, id="no-trainable-backend", marks=pytest.mark.skip(reason="requires a trainable HMM backend")) + pytest.param( + None, id="no-trainable-backend", marks=pytest.mark.skip(reason="requires a trainable HMM backend") + ) ) return params @@ -406,104 +399,12 @@ def test_different_architectures(self, architecture) -> None: class TestRothSegmentationHmm: - def test_boundary_prob_estimation_uses_empirical_counts(self) -> None: - start_probs, end_probs = estimate_sequence_boundary_probs( - [np.array([0, 1, 2]), np.array([1, 2, 2]), np.array([1, 0, 2])], - 3, - ) - - assert_array_equal(start_probs, np.array([1 / 3, 2 / 3, 0.0])) - assert_array_equal(end_probs, np.array([0.0, 0.0, 1.0])) - - def test_legacy_combined_model_uses_global_end_probabilities(self) -> None: - legacy = _require_legacy_backend() - backend = legacy.backend_class() - model_config = CompositeHmmConfig( - modules=( - HmmSubModelConfig( - name="transition", - role="transition", - n_states=2, - n_gmm_components=1, - architecture="left-right-loose", - ), - HmmSubModelConfig( - name="stride", - role="stride", - n_states=2, - n_gmm_components=1, - architecture="left-right-strict", - ), - ) - ) - module_offsets = {"transition": 0, "stride": 2} - trained_models = { - "transition": SimpleNamespace( - model=legacy.initialize_hmm( - [np.random.rand(12, 1)], - [np.tile(np.arange(2), 6)], - n_states=2, - n_gmm_components=1, - architecture="left-right-loose", - verbose=False, - ) - ), - "stride": SimpleNamespace( - model=legacy.initialize_hmm( - [np.random.rand(12, 1)], - [np.tile(np.arange(2), 6)], - n_states=2, - n_gmm_components=1, - architecture="left-right-strict", - verbose=False, - ) - ), - } - - combined = backend._create_combined_model( - trained_models=trained_models, - labels_train_sequence=[ - np.array([0, 1, 2, 3]), - np.array([0, 1, 2, 3]), - np.array([0, 1, 2, 2]), - ], - distributions=[ - distribution - for model in trained_models.values() - for distribution in legacy.backend_module.get_model_distributions(model.model) - ], - model_config=model_config, - module_offsets=module_offsets, - initialization="labels", - verbose=False, - ) - - end_probs = combined.dense_transition_matrix()[:-2, -1] - - assert end_probs[2] > 0 - assert end_probs[3] > 0 - assert end_probs[0] == 0 - assert end_probs[1] == 0 - def test_predict_without_model_raises_error(self) -> None: with pytest.raises(ValueError) as e: RothSegmentationHmm().predict(pd.DataFrame(np.random.rand(100, 3)), sampling_rate_hz=100) assert "No trained model for prediction available!" in str(e.value) - def test_self_optimize_calls_self_optimize_with_info(self) -> None: - data, labels = ( - [pd.DataFrame(np.random.rand(100, 3))], - [_stride_list_to_region_list(pd.DataFrame({"start": [0], "end": [100]}))], - ) - - with patch.object(RothSegmentationHmm, "self_optimize_with_info") as mock: - instance = RothSegmentationHmm() - mock.return_value = (instance, None) - instance.self_optimize(data, labels, sampling_rate_hz=100) - - mock.assert_called_once_with(data, labels, sampling_rate_hz=100) - def test_self_optimize_with_info_returns_history(self) -> None: data, labels = ( [pd.DataFrame(np.random.rand(120, 6), columns=BF_COLS)], @@ -757,8 +658,7 @@ def test_pretrained_inference_backend_matches_pomegranate_hidden_states( comparison_result.hidden_state_sequence_feature_space_, ) - def test_trained_model_roundtrip_matches_original_hidden_states(self) -> None: - legacy = _require_legacy_backend() + def test_trained_model_json_roundtrip_preserves_hidden_states(self) -> None: data_sequence = [pd.DataFrame(np.random.rand(120, 6), columns=BF_COLS)] region_list_sequence = [_stride_list_to_region_list(pd.DataFrame({"start": [0, 40, 70], "end": [30, 70, 100]}))] instance = RothSegmentationHmm( @@ -766,36 +666,20 @@ def test_trained_model_roundtrip_matches_original_hidden_states(self) -> None: ).set_params( hmm_config__feature_transform__sampling_rate_feature_space_hz=100, ) - captured_model = {} - original_converter = legacy.backend_module.pomegranate_model_to_hmm_state - - def _capture_and_convert(model, *args, **kwargs): - captured_model["raw_model"] = model - return original_converter(model, *args, **kwargs) - - with patch.object(legacy.backend_module, "pomegranate_model_to_hmm_state", side_effect=_capture_and_convert): - instance.self_optimize(data_sequence, region_list_sequence, sampling_rate_hz=100) + instance.self_optimize(data_sequence, region_list_sequence, sampling_rate_hz=100) + restored = RothSegmentationHmm.from_json(instance.to_json()) - runtime_model = legacy.hmm_state_to_pomegranate_model(instance.model) - feature_data, _ = instance._transform(data_sequence, None, sampling_rate_hz=100) - feature_data = feature_data[0] + original_result = instance.predict(data_sequence[0], sampling_rate_hz=100) + restored_result = restored.predict(data_sequence[0], sampling_rate_hz=100) - raw_sequence = legacy.predict( - captured_model["raw_model"], - feature_data, - expected_columns=instance.data_columns, - algorithm=instance.algo_predict, - ) - roundtrip_sequence = legacy.predict( - runtime_model, - feature_data, - expected_columns=instance.data_columns, - algorithm=instance.algo_predict, + assert_array_equal(original_result.hidden_state_sequence_, restored_result.hidden_state_sequence_) + assert_array_equal( + original_result.hidden_state_sequence_feature_space_, + restored_result.hidden_state_sequence_feature_space_, ) - - assert_array_equal(raw_sequence, roundtrip_sequence) - assert instance.model.trained_with.backend_id == "pomegranate-legacy" + assert instance.model.trained_with.backend_id.startswith("pomegranate-") assert instance.model.trained_with.backend_version is not None + assert isinstance(restored.backend, _default_backend_type()) @pytest.mark.parametrize("inference_backend", _runtime_inference_backend_params()) def test_trained_inference_backend_matches_pomegranate_hidden_states(self, inference_backend) -> None: From 501b3cf06a911ff63cde8e325f513bed165a2785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 13:37:10 +0100 Subject: [PATCH 22/28] Fix HMM lint issues --- .../stride_segmentation/hmm/legacy/_utils.py | 4 +- .../stride_segmentation/hmm/modern/_state.py | 89 ++++++++++++------- 2 files changed, 58 insertions(+), 35 deletions(-) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py index 0cce8129..0d31aca3 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py @@ -20,10 +20,12 @@ from tpcp import BaseTpcpObject from tpcp._hash import custom_hash -from gaitmap_mad.stride_segmentation.hmm._repr_utils import ShortenedHMMPrint as ShortenedHMMPrint +from gaitmap_mad.stride_segmentation.hmm import _repr_utils from gaitmap_mad.stride_segmentation.hmm._repr_utils import is_serialized_hmm_state from gaitmap_mad.stride_segmentation.hmm._utils import _DataToShortError, cluster_data_by_labels +ShortenedHMMPrint = _repr_utils.ShortenedHMMPrint + def _add_transition(model, a, b, probability, pseudocount, group) -> None: pseudocount = pseudocount or probability diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py index 1985ec99..72d09475 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/modern/_state.py @@ -64,51 +64,61 @@ def _stabilize_covariance(covariance: Any, covariance_type: str, min_cov: Any) - return torch.maximum(stabilized, min_cov) -def _patch_distribution_numerics(distribution: Any) -> None: - if getattr(distribution, "_gaitmap_min_cov_patch", False): - return - if hasattr(distribution, "distributions"): - for child in distribution.distributions: - _patch_distribution_numerics(child) - if not hasattr(distribution, "covs") or not hasattr(distribution, "from_summaries"): - return +def _iter_child_distributions(distribution: Any) -> tuple[Any, ...]: + return tuple(getattr(distribution, "distributions", ())) + + +def _supports_numerics_patch(distribution: Any) -> bool: + return hasattr(distribution, "covs") and hasattr(distribution, "from_summaries") - original_reset_cache = distribution._reset_cache - def _reset_cache_with_stable_covariance(self) -> Any: +def _resolve_min_cov(distribution: Any) -> Any: + if distribution.min_cov is None: + return _MODERN_MIN_COVARIANCE + return distribution.min_cov + + +def _covariances_from_summaries(distribution: Any, min_cov: Any) -> Any: + if distribution.covariance_type == "full": + v = distribution._xw_sum.unsqueeze(0) * distribution._xw_sum.unsqueeze(1) + covs = distribution._xxw_sum / distribution._w_sum - v / distribution._w_sum**2.0 + covs = 0.5 * (covs + covs.transpose(-1, -2)) + if min_cov is not None: + covs = covs + min_cov * torch.eye(covs.shape[-1], dtype=covs.dtype, device=covs.device) + return covs + if distribution.covariance_type in ["diag", "sphere"]: + covs = distribution._xxw_sum / distribution._w_sum - distribution._xw_sum**2.0 / distribution._w_sum**2.0 + if distribution.covariance_type == "sphere": + covs = covs.mean(dim=-1) + if min_cov is not None: + covs = torch.maximum(covs, min_cov) + return covs + raise ValueError(f"Unsupported covariance type `{distribution.covariance_type}`.") + + +def _reset_cache_with_stable_covariance(original_reset_cache: Any) -> Any: + def _patched(self) -> Any: if getattr(self, "_initialized", False): - min_cov = _MODERN_MIN_COVARIANCE if self.min_cov is None else self.min_cov + min_cov = _resolve_min_cov(self) with torch.no_grad(): self.covs.copy_(_stabilize_covariance(self.covs, self.covariance_type, min_cov)) return original_reset_cache() - def _from_summaries_with_min_cov(self) -> Any: + return _patched + + +def _from_summaries_with_min_cov() -> Any: + def _patched(self) -> Any: # Mirror the upstream PR #1143 fix that applies `min_cov` during the # Normal M-step. Current releases store `min_cov` but do not use it. if self.frozen is True: return None means = self._xw_sum / self._w_sum - min_cov = ( - None - if self.min_cov is None - else torch.as_tensor(self.min_cov, dtype=self.covs.dtype, device=self.covs.device) - ) - - if self.covariance_type == "full": - v = self._xw_sum.unsqueeze(0) * self._xw_sum.unsqueeze(1) - covs = self._xxw_sum / self._w_sum - v / self._w_sum**2.0 - covs = 0.5 * (covs + covs.transpose(-1, -2)) - if min_cov is not None: - covs = covs + min_cov * torch.eye(covs.shape[-1], dtype=covs.dtype, device=covs.device) - elif self.covariance_type in ["diag", "sphere"]: - covs = self._xxw_sum / self._w_sum - self._xw_sum**2.0 / self._w_sum**2.0 - if self.covariance_type == "sphere": - covs = covs.mean(dim=-1) - if min_cov is not None: - covs = torch.maximum(covs, min_cov) - else: # pragma: no cover - mirrors pomegranate's supported covariance types - raise ValueError(f"Unsupported covariance type `{self.covariance_type}`.") + min_cov = None + if self.min_cov is not None: + min_cov = torch.as_tensor(self.min_cov, dtype=self.covs.dtype, device=self.covs.device) + covs = _covariances_from_summaries(self, min_cov) if not torch.isfinite(means).all() or not torch.isfinite(covs).all(): self._reset_cache() @@ -119,8 +129,19 @@ def _from_summaries_with_min_cov(self) -> Any: self._reset_cache() return None - distribution._reset_cache = MethodType(_reset_cache_with_stable_covariance, distribution) - distribution.from_summaries = MethodType(_from_summaries_with_min_cov, distribution) + return _patched + + +def _patch_distribution_numerics(distribution: Any) -> None: + if getattr(distribution, "_gaitmap_min_cov_patch", False): + return + for child in _iter_child_distributions(distribution): + _patch_distribution_numerics(child) + if not _supports_numerics_patch(distribution): + return + + distribution._reset_cache = MethodType(_reset_cache_with_stable_covariance(distribution._reset_cache), distribution) + distribution.from_summaries = MethodType(_from_summaries_with_min_cov(), distribution) distribution._gaitmap_min_cov_patch = True From 4231bb171a0c7099c2f33b548b4adbc518177a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 13:44:30 +0100 Subject: [PATCH 23/28] Install HMM extras in CI test jobs --- .github/workflows/test-and-lint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml index c04b09da..c6723e8d 100644 --- a/.github/workflows/test-and-lint.yml +++ b/.github/workflows/test-and-lint.yml @@ -25,9 +25,9 @@ jobs: - name: Install dependencies (all extras) if: ${{ matrix.python-version == '3.9' }} run: uv sync --group dev --all-extras - - name: Install dependencies (stats extra) + - name: Install dependencies (stats + hmm extras) if: ${{ matrix.python-version != '3.9' }} - run: uv sync --group dev --extra stats + run: uv sync --group dev --extra stats --extra hmm - name: Testing run: uv run poe test - name: Upload coverage reports to Codecov From d9e279698cf78f27417018eb4f945052412d0997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 13:46:20 +0100 Subject: [PATCH 24/28] Install all extras in CI test jobs --- .github/workflows/test-and-lint.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml index c6723e8d..3d3f978a 100644 --- a/.github/workflows/test-and-lint.yml +++ b/.github/workflows/test-and-lint.yml @@ -23,11 +23,7 @@ jobs: cache-dependency-glob: "uv.lock" python-version: ${{ matrix.python-version }} - name: Install dependencies (all extras) - if: ${{ matrix.python-version == '3.9' }} run: uv sync --group dev --all-extras - - name: Install dependencies (stats + hmm extras) - if: ${{ matrix.python-version != '3.9' }} - run: uv sync --group dev --extra stats --extra hmm - name: Testing run: uv run poe test - name: Upload coverage reports to Codecov From 104778b1a73cd1e5195b209d5307841fecd601cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 13:58:30 +0100 Subject: [PATCH 25/28] Fix modern pomegranate compatibility --- gaitmap/base.py | 18 +++++++++++------- .../stride_segmentation/hmm/_backend_base.py | 9 +++++++++ .../stride_segmentation/hmm/_repr_utils.py | 7 ++++--- .../stride_segmentation/hmm/legacy/_utils.py | 3 ++- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/gaitmap/base.py b/gaitmap/base.py index 5c08fc2b..6364534b 100644 --- a/gaitmap/base.py +++ b/gaitmap/base.py @@ -43,14 +43,17 @@ def _hint_tuples(item): def _import_hidden_markov_model(): - return import_module("pomegranate.hmm").HiddenMarkovModel + try: + return import_module("pomegranate.hmm").HiddenMarkovModel + except (ImportError, AttributeError): + return None class _CustomEncoder(json.JSONEncoder): def encode(self, o: Any) -> str: return super().encode(_hint_tuples(o)) - def default(self, o): # noqa: C901, PLR0911 + def default(self, o): # noqa: PLR0911 if isinstance(o, _BaseSerializable): return o._to_json_dict() if isinstance(o, Rotation): @@ -63,11 +66,7 @@ def default(self, o): # noqa: C901, PLR0911 return {"_obj_type": "DataFrame", "df": o.to_json(orient="split")} if isinstance(o, pd.Series): return {"_obj_type": "Series", "df": o.to_json(orient="split")} - try: - hidden_markov_model = _import_hidden_markov_model() - except ImportError: - hidden_markov_model = None - + hidden_markov_model = _import_hidden_markov_model() if hidden_markov_model is not None and isinstance(o, hidden_markov_model): warnings.warn( "Exporting `pomegranate.hmm.HiddenMarkovModel` objects to json can sometimes not provide perfect " @@ -103,6 +102,11 @@ def _custom_deserialize(json_obj): # pylint: disable=too-many-return-statements return pd.read_json(json_obj["df"], orient="split", typ=typ) if json_obj["_obj_type"] == "HiddenMarkovModel": hidden_markov_model = _import_hidden_markov_model() + if hidden_markov_model is None: + raise ImportError( + "Loading serialized `pomegranate.hmm.HiddenMarkovModel` objects requires legacy " + "`pomegranate 0.x` with `HiddenMarkovModel` support." + ) with np.errstate(divide="ignore"): # Sometimes probabilities are zero which can lead to warnings when the log-probabilities are # calculated. diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py index 1c679168..abc42576 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py @@ -8,6 +8,7 @@ import numpy as np import pandas as pd +from tpcp import make_optimize_safe from typing_extensions import Self from gaitmap.base import _BaseSerializable @@ -38,6 +39,14 @@ def self_optimize_with_info( ) -> HmmTrainingResult[Self]: raise NotImplementedError + @make_optimize_safe + def self_optimize( + self, + data_sequence: Sequence[pd.DataFrame | np.ndarray], + labels_sequence: Sequence[np.ndarray], + ) -> Self: + return self.self_optimize_with_info(data_sequence, labels_sequence)[0] + class BaseHmmBackend(_BaseSerializable): """Base abstraction for backend-specific HMM primitives.""" diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_repr_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_repr_utils.py index 6af63626..8546491d 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_repr_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_repr_utils.py @@ -25,17 +25,18 @@ class ShortenedHMMPrint(BaseTpcpObject): """Mixin class to better format HMM models when printing them.""" def __repr_parameter__(self, name: str, value: Any) -> str: + legacy_hmm = getattr(pg, "HiddenMarkovModel", None) if pg is not None else None if name == "model": if is_serialized_hmm_state(value): n_states = len(value.compiled.state_names) if getattr(value, "compiled", None) is not None else "?" backend = getattr(getattr(value, "trained_with", None), "backend_id", "?") return f"{name}=HMMState[backend={backend}, states={n_states}](...)" - if pg is not None and isinstance(value, pg.HiddenMarkovModel): + if legacy_hmm is not None and isinstance(value, legacy_hmm): return f"{name}=HiddenMarkovModel[name={value.name}](...)" if ( - pg is not None + legacy_hmm is not None and isinstance(value, CloneFactory) - and isinstance(value.default_value, pg.HiddenMarkovModel) + and isinstance(value.default_value, legacy_hmm) ): return f"{name}=cf(HiddenMarkovModel[name={value.get_value().name}](...))" return super().__repr_parameter__(name, value) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py index 0d31aca3..bba08780 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py @@ -80,7 +80,8 @@ class _HackyClonableHMMFix(BaseTpcpObject): @classmethod def __clone_param__(cls, param_name: str, value: Any) -> Any: - if isinstance(value, pg.HiddenMarkovModel): + legacy_hmm = getattr(pg, "HiddenMarkovModel", None) if pg is not None else None + if legacy_hmm is not None and isinstance(value, legacy_hmm): return _clone_model(value) if is_serialized_hmm_state(value): return type(value).from_json(value.to_json()) From 446f7e0b3087f022ed7dd96c544552144ee62818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 14:14:02 +0100 Subject: [PATCH 26/28] Remove raw legacy HMM serialization --- gaitmap/base.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/gaitmap/base.py b/gaitmap/base.py index 6364534b..bb8d901f 100644 --- a/gaitmap/base.py +++ b/gaitmap/base.py @@ -2,7 +2,6 @@ import json import warnings -from importlib import import_module from typing import Any, Optional, TypeVar, Union import numpy as np @@ -42,13 +41,6 @@ def _hint_tuples(item): return item -def _import_hidden_markov_model(): - try: - return import_module("pomegranate.hmm").HiddenMarkovModel - except (ImportError, AttributeError): - return None - - class _CustomEncoder(json.JSONEncoder): def encode(self, o: Any) -> str: return super().encode(_hint_tuples(o)) @@ -66,15 +58,6 @@ def default(self, o): # noqa: PLR0911 return {"_obj_type": "DataFrame", "df": o.to_json(orient="split")} if isinstance(o, pd.Series): return {"_obj_type": "Series", "df": o.to_json(orient="split")} - hidden_markov_model = _import_hidden_markov_model() - if hidden_markov_model is not None and isinstance(o, hidden_markov_model): - warnings.warn( - "Exporting `pomegranate.hmm.HiddenMarkovModel` objects to json can sometimes not provide perfect " - "round-trips. I.e. sometimes values (in particular weightings of distributions) might change " - "slightly in the re-imported model due to rounding issue. " - "This is a limitation of the underlying pomegrante library." - ) - return {"_obj_type": "HiddenMarkovModel", "hmm": json.loads(o.to_json())} if o is tpcp.NOTHING: return {"_obj_type": "EmptyDefault"} if isinstance(o, Memory): @@ -100,18 +83,6 @@ def _custom_deserialize(json_obj): # pylint: disable=too-many-return-statements if json_obj["_obj_type"] in ["Series", "DataFrame"]: typ = "series" if json_obj["_obj_type"] == "Series" else "frame" return pd.read_json(json_obj["df"], orient="split", typ=typ) - if json_obj["_obj_type"] == "HiddenMarkovModel": - hidden_markov_model = _import_hidden_markov_model() - if hidden_markov_model is None: - raise ImportError( - "Loading serialized `pomegranate.hmm.HiddenMarkovModel` objects requires legacy " - "`pomegranate 0.x` with `HiddenMarkovModel` support." - ) - with np.errstate(divide="ignore"): - # Sometimes probabilities are zero which can lead to warnings when the log-probabilities are - # calculated. - # We ignore these warnings here to avoid clutter in the output. - return hidden_markov_model.from_dict(json_obj["hmm"]) if json_obj["_obj_type"] == "EmptyDefault": return tpcp.NOTHING if json_obj["_obj_type"] == "Tuple": From ae62c3625980b76035b2967fb0199cc42754b724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Wed, 11 Mar 2026 14:31:45 +0100 Subject: [PATCH 27/28] Tighten legacy HMM import boundaries --- .../stride_segmentation/hmm/_backend_base.py | 2 - .../stride_segmentation/hmm/_repr_utils.py | 42 ----------------- .../hmm/_segmentation_model.py | 10 ++++- .../hmm/legacy/__init__.py | 24 +++++++--- .../hmm/legacy/_backend.py | 13 ++---- .../stride_segmentation/hmm/legacy/_state.py | 8 +--- .../stride_segmentation/hmm/legacy/_utils.py | 45 ++++++++++++------- 7 files changed, 61 insertions(+), 83 deletions(-) delete mode 100644 packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_repr_utils.py diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py index abc42576..0e3026bf 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_backend_base.py @@ -8,7 +8,6 @@ import numpy as np import pandas as pd -from tpcp import make_optimize_safe from typing_extensions import Self from gaitmap.base import _BaseSerializable @@ -39,7 +38,6 @@ def self_optimize_with_info( ) -> HmmTrainingResult[Self]: raise NotImplementedError - @make_optimize_safe def self_optimize( self, data_sequence: Sequence[pd.DataFrame | np.ndarray], diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_repr_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_repr_utils.py deleted file mode 100644 index 8546491d..00000000 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_repr_utils.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Backend-neutral helpers for HMM repr/clone support.""" - -from __future__ import annotations - -from typing import Any - -try: - import pomegranate as pg -except ImportError: # pragma: no cover - exercised in environments without pomegranate - pg = None - -from tpcp import BaseTpcpObject, CloneFactory - - -def is_serialized_hmm_state(value: Any) -> bool: - return ( - hasattr(value, "compiled") - and hasattr(value, "trained_with") - and callable(getattr(value, "to_json", None)) - and callable(getattr(type(value), "from_json", None)) - ) - - -class ShortenedHMMPrint(BaseTpcpObject): - """Mixin class to better format HMM models when printing them.""" - - def __repr_parameter__(self, name: str, value: Any) -> str: - legacy_hmm = getattr(pg, "HiddenMarkovModel", None) if pg is not None else None - if name == "model": - if is_serialized_hmm_state(value): - n_states = len(value.compiled.state_names) if getattr(value, "compiled", None) is not None else "?" - backend = getattr(getattr(value, "trained_with", None), "backend_id", "?") - return f"{name}=HMMState[backend={backend}, states={n_states}](...)" - if legacy_hmm is not None and isinstance(value, legacy_hmm): - return f"{name}=HiddenMarkovModel[name={value.name}](...)" - if ( - legacy_hmm is not None - and isinstance(value, CloneFactory) - and isinstance(value.default_value, legacy_hmm) - ): - return f"{name}=cf(HiddenMarkovModel[name={value.get_value().name}](...))" - return super().__repr_parameter__(name, value) diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py index 5dc09a89..45c578d0 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/_segmentation_model.py @@ -20,7 +20,6 @@ from gaitmap_mad.stride_segmentation.hmm._backend import BaseHmmBackend, BaseTrainableHmm, get_default_hmm_backend from gaitmap_mad.stride_segmentation.hmm._config import CompositeHmmConfig, HmmSubModelConfig, RothHmmConfig from gaitmap_mad.stride_segmentation.hmm._hmm_feature_transform import RothHmmFeatureTransformer -from gaitmap_mad.stride_segmentation.hmm._repr_utils import ShortenedHMMPrint from gaitmap_mad.stride_segmentation.hmm._state import HMMState from gaitmap_mad.stride_segmentation.hmm._utils import ( _DataToShortError, @@ -264,7 +263,7 @@ def self_optimize_with_info( raise NotImplementedError -class RothSegmentationHmm(BaseSegmentationHmm, ShortenedHMMPrint): +class RothSegmentationHmm(BaseSegmentationHmm): """A hierarchical HMM model for stride segmentation proposed by Roth et al. [1]_. This model uses individually trained HMM submodules that are combined into one final segmentation HMM. @@ -325,6 +324,13 @@ class RothSegmentationHmm(BaseSegmentationHmm, ShortenedHMMPrint): feature_space_data_: pd.DataFrame hidden_state_sequence_feature_space_: np.ndarray + def __repr_parameter__(self, name: str, value: Any) -> str: + if name == "model" and isinstance(value, HMMState): + n_states = len(value.compiled.state_names) if getattr(value, "compiled", None) is not None else "?" + backend = getattr(getattr(value, "trained_with", None), "backend_id", "?") + return f"{name}=HMMState[backend={backend}, states={n_states}](...)" + return super().__repr_parameter__(name, value) + @classmethod def _from_json_dict(cls, json_dict: dict) -> Self: params = json_dict["params"].copy() diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py index 1df26701..5340bc70 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/__init__.py @@ -1,11 +1,19 @@ """Legacy pomegranate backend package.""" -from gaitmap_mad.stride_segmentation.hmm.legacy._backend import ( - PomegranateLegacyHmmBackend, - _get_pomegranate_version, - initialize_hmm, - pg, -) +from __future__ import annotations + +from importlib import import_module +from importlib.metadata import PackageNotFoundError, version + +import pomegranate as pg + + +def _get_pomegranate_version() -> str | None: + try: + return version("pomegranate") + except PackageNotFoundError: + return None + if getattr(pg, "HiddenMarkovModel", None) is None: raise ImportError( @@ -13,4 +21,8 @@ f"Installed version: {_get_pomegranate_version() or 'not installed'}." ) +_backend = import_module("gaitmap_mad.stride_segmentation.hmm.legacy._backend") +PomegranateLegacyHmmBackend = _backend.PomegranateLegacyHmmBackend +initialize_hmm = _backend.initialize_hmm + __all__ = ["PomegranateLegacyHmmBackend", "initialize_hmm"] diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py index e0661836..f20ba477 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_backend.py @@ -9,15 +9,7 @@ import numpy as np import pandas as pd - -try: - import pomegranate as pg -except ImportError: # pragma: no cover - exercised in environments without pomegranate - pg = None -try: - from pomegranate.hmm import History -except (ImportError, AttributeError): - History = Any +import pomegranate as pg from tpcp import OptiPara, make_optimize_safe from typing_extensions import Self @@ -41,6 +33,7 @@ pomegranate_model_to_hmm_state, ) from gaitmap_mad.stride_segmentation.hmm.legacy._utils import ( + History, ShortenedHMMPrint, _clone_model, _HackyClonableHMMFix, @@ -62,7 +55,7 @@ def _get_pomegranate_version() -> str | None: def _require_legacy_pomegranate(): legacy_hmm = getattr(pg, "HiddenMarkovModel", None) - if pg is None or legacy_hmm is None: + if legacy_hmm is None: raise ImportError("The legacy HMM backend requires pomegranate 0.x with `HiddenMarkovModel` support.") return pg diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_state.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_state.py index 376bfdac..4db35e8d 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_state.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_state.py @@ -6,11 +6,7 @@ from typing import Any import numpy as np - -try: - import pomegranate as pg -except ImportError: # pragma: no cover - exercised in environments without pomegranate - pg = None +import pomegranate as pg from gaitmap_mad.stride_segmentation.hmm._state import ( BackendInfo, @@ -35,7 +31,7 @@ def _get_pomegranate_version() -> str | None: def _require_legacy_pomegranate(): legacy_hmm = getattr(pg, "HiddenMarkovModel", None) - if pg is None or legacy_hmm is None: + if legacy_hmm is None: raise ImportError("The legacy HMM backend requires pomegranate 0.x with `HiddenMarkovModel` support.") return pg diff --git a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py index bba08780..125019ed 100644 --- a/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py +++ b/packages/gaitmap_mad/src/gaitmap_mad/stride_segmentation/hmm/legacy/_utils.py @@ -8,23 +8,39 @@ import numpy as np import pandas as pd - -try: - import pomegranate as pg -except ImportError: # pragma: no cover - exercised in environments without pomegranate - pg = None -try: - from pomegranate.hmm import History -except (ImportError, AttributeError): - History = Any -from tpcp import BaseTpcpObject +import pomegranate as pg +from pomegranate.hmm import History +from tpcp import BaseTpcpObject, CloneFactory from tpcp._hash import custom_hash -from gaitmap_mad.stride_segmentation.hmm import _repr_utils -from gaitmap_mad.stride_segmentation.hmm._repr_utils import is_serialized_hmm_state from gaitmap_mad.stride_segmentation.hmm._utils import _DataToShortError, cluster_data_by_labels -ShortenedHMMPrint = _repr_utils.ShortenedHMMPrint +if getattr(pg, "HiddenMarkovModel", None) is None: + raise ImportError("The legacy HMM backend requires `pomegranate 0.x` with `HiddenMarkovModel` support.") + + +def is_serialized_hmm_state(value: Any) -> bool: + return ( + hasattr(value, "compiled") + and hasattr(value, "trained_with") + and callable(getattr(value, "to_json", None)) + and callable(getattr(type(value), "from_json", None)) + ) + + +class ShortenedHMMPrint(BaseTpcpObject): + """Mixin class to better format legacy HMM models when printing them.""" + + def __repr_parameter__(self, name: str, value: Any) -> str: + if name == "model" and isinstance(value, pg.HiddenMarkovModel): + return f"{name}=HiddenMarkovModel[name={value.name}](...)" + if ( + name == "model" + and isinstance(value, CloneFactory) + and isinstance(value.default_value, pg.HiddenMarkovModel) + ): + return f"{name}=cf(HiddenMarkovModel[name={value.get_value().name}](...))" + return super().__repr_parameter__(name, value) def _add_transition(model, a, b, probability, pseudocount, group) -> None: @@ -80,8 +96,7 @@ class _HackyClonableHMMFix(BaseTpcpObject): @classmethod def __clone_param__(cls, param_name: str, value: Any) -> Any: - legacy_hmm = getattr(pg, "HiddenMarkovModel", None) if pg is not None else None - if legacy_hmm is not None and isinstance(value, legacy_hmm): + if isinstance(value, pg.HiddenMarkovModel): return _clone_model(value) if is_serialized_hmm_state(value): return type(value).from_json(value.to_json()) From 58f2e9efbac9337d96a4922829fe36f0cdd0ff92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20K=C3=BCderle?= Date: Sun, 29 Mar 2026 12:57:31 +0200 Subject: [PATCH 28/28] Proper uv layout --- _tasks.py | 2 +- .../segmentation_hmm_training.py | 15 +++++++++++---- pyproject.toml | 9 +++++---- src/__init__.py | 0 {gaitmap => src/gaitmap}/__init__.py | 0 .../gaitmap}/_event_detection_common/__init__.py | 0 .../_event_detection_mixin.py | 0 {gaitmap => src/gaitmap}/base.py | 0 .../gaitmap}/data_transform/__init__.py | 0 {gaitmap => src/gaitmap}/data_transform/_base.py | 0 .../gaitmap}/data_transform/_feature_transform.py | 0 .../gaitmap}/data_transform/_filter.py | 0 .../gaitmap}/data_transform/_scaler.py | 0 .../gaitmap}/evaluation_utils/__init__.py | 0 .../gaitmap}/evaluation_utils/event_detection.py | 0 .../gaitmap}/evaluation_utils/parameter_errors.py | 0 .../gaitmap}/evaluation_utils/scores.py | 0 .../evaluation_utils/stride_segmentation.py | 0 .../gaitmap}/event_detection/__init__.py | 0 .../event_detection/_herzer_event_detection.py | 0 {gaitmap => src/gaitmap}/example_data.py | 0 .../gaitmap}/gait_detection/__init__.py | 0 {gaitmap => src/gaitmap}/parameters/__init__.py | 0 .../gaitmap}/parameters/_spatial_parameters.py | 0 .../gaitmap}/parameters/_temporal_parameters.py | 0 .../gaitmap}/preprocessing/__init__.py | 0 .../preprocessing/sensor_alignment/__init__.py | 0 .../sensor_alignment/_gravity_alignment.py | 0 .../sensor_alignment/_mulisensor_alignment.py | 0 .../sensor_alignment/_pca_alignment.py | 0 .../gaitmap}/stride_segmentation/__init__.py | 0 .../_roi_stride_segmentation.py | 0 .../gaitmap}/stride_segmentation/_utils.py | 0 .../gaitmap}/stride_segmentation/hmm.py | 0 .../trajectory_reconstruction/__init__.py | 0 .../_region_level_trajectory.py | 0 .../_stride_level_trajectory.py | 0 .../_trajectory_wrapper.py | 0 .../orientation_methods/__init__.py | 0 .../orientation_methods/_madgwick.py | 0 .../_simple_gyro_integration.py | 0 .../position_methods/__init__.py | 0 .../_forward_backwards_integration.py | 0 .../trajectory_methods/__init__.py | 0 .../trajectory_methods/_kalman_numba_funcs.py | 0 .../trajectory_methods/_rts_kalman.py | 0 {gaitmap => src/gaitmap}/utils/__init__.py | 0 {gaitmap => src/gaitmap}/utils/_algo_helper.py | 0 .../gaitmap}/utils/_datatype_validation_helper.py | 0 {gaitmap => src/gaitmap}/utils/_gaitmap_mad.py | 0 {gaitmap => src/gaitmap}/utils/_types.py | 0 {gaitmap => src/gaitmap}/utils/array_handling.py | 0 {gaitmap => src/gaitmap}/utils/consts.py | 0 .../gaitmap}/utils/coordinate_conversion.py | 0 {gaitmap => src/gaitmap}/utils/datatype_helper.py | 0 {gaitmap => src/gaitmap}/utils/exceptions.py | 0 .../gaitmap}/utils/fast_quaternion_math.py | 0 {gaitmap => src/gaitmap}/utils/rotations.py | 0 .../gaitmap}/utils/signal_processing.py | 0 .../gaitmap}/utils/static_moment_detection.py | 0 .../gaitmap}/utils/stride_list_conversion.py | 0 {gaitmap => src/gaitmap}/utils/vector_math.py | 0 .../gaitmap}/zupt_detection/__init__.py | 0 {gaitmap => src/gaitmap}/zupt_detection/_base.py | 0 .../zupt_detection/_combo_zupt_detector.py | 0 .../_moving_window_zupt_detector.py | 0 .../zupt_detection/_stride_event_zupt_detector.py | 0 67 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 src/__init__.py rename {gaitmap => src/gaitmap}/__init__.py (100%) rename {gaitmap => src/gaitmap}/_event_detection_common/__init__.py (100%) rename {gaitmap => src/gaitmap}/_event_detection_common/_event_detection_mixin.py (100%) rename {gaitmap => src/gaitmap}/base.py (100%) rename {gaitmap => src/gaitmap}/data_transform/__init__.py (100%) rename {gaitmap => src/gaitmap}/data_transform/_base.py (100%) rename {gaitmap => src/gaitmap}/data_transform/_feature_transform.py (100%) rename {gaitmap => src/gaitmap}/data_transform/_filter.py (100%) rename {gaitmap => src/gaitmap}/data_transform/_scaler.py (100%) rename {gaitmap => src/gaitmap}/evaluation_utils/__init__.py (100%) rename {gaitmap => src/gaitmap}/evaluation_utils/event_detection.py (100%) rename {gaitmap => src/gaitmap}/evaluation_utils/parameter_errors.py (100%) rename {gaitmap => src/gaitmap}/evaluation_utils/scores.py (100%) rename {gaitmap => src/gaitmap}/evaluation_utils/stride_segmentation.py (100%) rename {gaitmap => src/gaitmap}/event_detection/__init__.py (100%) rename {gaitmap => src/gaitmap}/event_detection/_herzer_event_detection.py (100%) rename {gaitmap => src/gaitmap}/example_data.py (100%) rename {gaitmap => src/gaitmap}/gait_detection/__init__.py (100%) rename {gaitmap => src/gaitmap}/parameters/__init__.py (100%) rename {gaitmap => src/gaitmap}/parameters/_spatial_parameters.py (100%) rename {gaitmap => src/gaitmap}/parameters/_temporal_parameters.py (100%) rename {gaitmap => src/gaitmap}/preprocessing/__init__.py (100%) rename {gaitmap => src/gaitmap}/preprocessing/sensor_alignment/__init__.py (100%) rename {gaitmap => src/gaitmap}/preprocessing/sensor_alignment/_gravity_alignment.py (100%) rename {gaitmap => src/gaitmap}/preprocessing/sensor_alignment/_mulisensor_alignment.py (100%) rename {gaitmap => src/gaitmap}/preprocessing/sensor_alignment/_pca_alignment.py (100%) rename {gaitmap => src/gaitmap}/stride_segmentation/__init__.py (100%) rename {gaitmap => src/gaitmap}/stride_segmentation/_roi_stride_segmentation.py (100%) rename {gaitmap => src/gaitmap}/stride_segmentation/_utils.py (100%) rename {gaitmap => src/gaitmap}/stride_segmentation/hmm.py (100%) rename {gaitmap => src/gaitmap}/trajectory_reconstruction/__init__.py (100%) rename {gaitmap => src/gaitmap}/trajectory_reconstruction/_region_level_trajectory.py (100%) rename {gaitmap => src/gaitmap}/trajectory_reconstruction/_stride_level_trajectory.py (100%) rename {gaitmap => src/gaitmap}/trajectory_reconstruction/_trajectory_wrapper.py (100%) rename {gaitmap => src/gaitmap}/trajectory_reconstruction/orientation_methods/__init__.py (100%) rename {gaitmap => src/gaitmap}/trajectory_reconstruction/orientation_methods/_madgwick.py (100%) rename {gaitmap => src/gaitmap}/trajectory_reconstruction/orientation_methods/_simple_gyro_integration.py (100%) rename {gaitmap => src/gaitmap}/trajectory_reconstruction/position_methods/__init__.py (100%) rename {gaitmap => src/gaitmap}/trajectory_reconstruction/position_methods/_forward_backwards_integration.py (100%) rename {gaitmap => src/gaitmap}/trajectory_reconstruction/trajectory_methods/__init__.py (100%) rename {gaitmap => src/gaitmap}/trajectory_reconstruction/trajectory_methods/_kalman_numba_funcs.py (100%) rename {gaitmap => src/gaitmap}/trajectory_reconstruction/trajectory_methods/_rts_kalman.py (100%) rename {gaitmap => src/gaitmap}/utils/__init__.py (100%) rename {gaitmap => src/gaitmap}/utils/_algo_helper.py (100%) rename {gaitmap => src/gaitmap}/utils/_datatype_validation_helper.py (100%) rename {gaitmap => src/gaitmap}/utils/_gaitmap_mad.py (100%) rename {gaitmap => src/gaitmap}/utils/_types.py (100%) rename {gaitmap => src/gaitmap}/utils/array_handling.py (100%) rename {gaitmap => src/gaitmap}/utils/consts.py (100%) rename {gaitmap => src/gaitmap}/utils/coordinate_conversion.py (100%) rename {gaitmap => src/gaitmap}/utils/datatype_helper.py (100%) rename {gaitmap => src/gaitmap}/utils/exceptions.py (100%) rename {gaitmap => src/gaitmap}/utils/fast_quaternion_math.py (100%) rename {gaitmap => src/gaitmap}/utils/rotations.py (100%) rename {gaitmap => src/gaitmap}/utils/signal_processing.py (100%) rename {gaitmap => src/gaitmap}/utils/static_moment_detection.py (100%) rename {gaitmap => src/gaitmap}/utils/stride_list_conversion.py (100%) rename {gaitmap => src/gaitmap}/utils/vector_math.py (100%) rename {gaitmap => src/gaitmap}/zupt_detection/__init__.py (100%) rename {gaitmap => src/gaitmap}/zupt_detection/_base.py (100%) rename {gaitmap => src/gaitmap}/zupt_detection/_combo_zupt_detector.py (100%) rename {gaitmap => src/gaitmap}/zupt_detection/_moving_window_zupt_detector.py (100%) rename {gaitmap => src/gaitmap}/zupt_detection/_stride_event_zupt_detector.py (100%) diff --git a/_tasks.py b/_tasks.py index 8d27bf43..483a552b 100644 --- a/_tasks.py +++ b/_tasks.py @@ -44,7 +44,7 @@ def update_version(version) -> None: .stdout.decode() .strip() ) - update_version_strings(HERE / "gaitmap/__init__.py", new_version) + update_version_strings(HERE / "src/gaitmap/__init__.py", new_version) # Update the gaitmap_mad version as well subprocess.run( ["uv", "version", new_version, "--project", str(HERE / "packages/gaitmap_mad"), "--frozen"], diff --git a/examples/stride_segmentation/segmentation_hmm_training.py b/examples/stride_segmentation/segmentation_hmm_training.py index e648bb0e..1ccd444d 100644 --- a/examples/stride_segmentation/segmentation_hmm_training.py +++ b/examples/stride_segmentation/segmentation_hmm_training.py @@ -121,7 +121,7 @@ # invoke the training process. # Again, all configurable parameters are exposed for demonstration purpose. # These parameters should again work for most usecases. -from gaitmap.stride_segmentation.hmm import RothSegmentationHmm +from gaitmap.stride_segmentation.hmm import PreTrainedRothSegmentationModel, RothSegmentationHmm segmentation_model = RothSegmentationHmm( hmm_config=RothHmmConfig( @@ -167,9 +167,16 @@ # The model will internally perform the feature transformation of the dataset, train the individual sub models and # finally combine them to a flatted segmentation model. -segmentation_model = segmentation_model.self_optimize( - data_train_sequence, region_list_sequence, sampling_rate_hz=sampling_rate_hz -) +if segmentation_model.backend.backend_id == "scipy-inference": + print( + "Skipping HMM training because the current environment only provides the " + "SciPy inference backend. Falling back to the packaged pre-trained model." + ) + segmentation_model = PreTrainedRothSegmentationModel() +else: + segmentation_model = segmentation_model.self_optimize( + data_train_sequence, region_list_sequence, sampling_rate_hz=sampling_rate_hz + ) # %% # Inspecting the Results diff --git a/pyproject.toml b/pyproject.toml index cf2cc3b0..3dc184e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,8 +82,8 @@ _auto_fix = "ruff check . --fix-only --show-fixes --exit-zero" _auto_fix_unsafe = "ruff check . --fix-only --show-fixes --exit-zero --unsafe-fixes" format = ["_auto_fix", "_format"] format_unsafe = ["_auto_fix_unsafe", "_format"] -lint = { cmd = "ruff check gaitmap packages/gaitmap_mad --fix", help = "Lint all files with ruff." } -_lint_ci = "ruff check gaitmap packages/gaitmap_mad --output-format=github" +lint = { cmd = "ruff check src/gaitmap packages/gaitmap_mad --fix", help = "Lint all files with ruff." } +_lint_ci = "ruff check src/gaitmap packages/gaitmap_mad --output-format=github" _check_format = "ruff format . --check" ci_check = { sequence = ["_check_format", "_lint_ci"], help = "Check all potential format and linting issues." } test = { cmd = "pytest --cov=gaitmap --cov-report=term-missing --cov-report=xml", help = "Run Pytest with coverage." } @@ -96,5 +96,6 @@ version = { "script" = "_tasks:task_update_version()", help = "Bump version in a bump_dev = { script = "_tasks:task_bump_all_dev()", help = "Update all dev dependencies to their @latest version." } [build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +requires = ["uv_build>=0.10.8,<0.11.0"] +build-backend = "uv_build" + diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gaitmap/__init__.py b/src/gaitmap/__init__.py similarity index 100% rename from gaitmap/__init__.py rename to src/gaitmap/__init__.py diff --git a/gaitmap/_event_detection_common/__init__.py b/src/gaitmap/_event_detection_common/__init__.py similarity index 100% rename from gaitmap/_event_detection_common/__init__.py rename to src/gaitmap/_event_detection_common/__init__.py diff --git a/gaitmap/_event_detection_common/_event_detection_mixin.py b/src/gaitmap/_event_detection_common/_event_detection_mixin.py similarity index 100% rename from gaitmap/_event_detection_common/_event_detection_mixin.py rename to src/gaitmap/_event_detection_common/_event_detection_mixin.py diff --git a/gaitmap/base.py b/src/gaitmap/base.py similarity index 100% rename from gaitmap/base.py rename to src/gaitmap/base.py diff --git a/gaitmap/data_transform/__init__.py b/src/gaitmap/data_transform/__init__.py similarity index 100% rename from gaitmap/data_transform/__init__.py rename to src/gaitmap/data_transform/__init__.py diff --git a/gaitmap/data_transform/_base.py b/src/gaitmap/data_transform/_base.py similarity index 100% rename from gaitmap/data_transform/_base.py rename to src/gaitmap/data_transform/_base.py diff --git a/gaitmap/data_transform/_feature_transform.py b/src/gaitmap/data_transform/_feature_transform.py similarity index 100% rename from gaitmap/data_transform/_feature_transform.py rename to src/gaitmap/data_transform/_feature_transform.py diff --git a/gaitmap/data_transform/_filter.py b/src/gaitmap/data_transform/_filter.py similarity index 100% rename from gaitmap/data_transform/_filter.py rename to src/gaitmap/data_transform/_filter.py diff --git a/gaitmap/data_transform/_scaler.py b/src/gaitmap/data_transform/_scaler.py similarity index 100% rename from gaitmap/data_transform/_scaler.py rename to src/gaitmap/data_transform/_scaler.py diff --git a/gaitmap/evaluation_utils/__init__.py b/src/gaitmap/evaluation_utils/__init__.py similarity index 100% rename from gaitmap/evaluation_utils/__init__.py rename to src/gaitmap/evaluation_utils/__init__.py diff --git a/gaitmap/evaluation_utils/event_detection.py b/src/gaitmap/evaluation_utils/event_detection.py similarity index 100% rename from gaitmap/evaluation_utils/event_detection.py rename to src/gaitmap/evaluation_utils/event_detection.py diff --git a/gaitmap/evaluation_utils/parameter_errors.py b/src/gaitmap/evaluation_utils/parameter_errors.py similarity index 100% rename from gaitmap/evaluation_utils/parameter_errors.py rename to src/gaitmap/evaluation_utils/parameter_errors.py diff --git a/gaitmap/evaluation_utils/scores.py b/src/gaitmap/evaluation_utils/scores.py similarity index 100% rename from gaitmap/evaluation_utils/scores.py rename to src/gaitmap/evaluation_utils/scores.py diff --git a/gaitmap/evaluation_utils/stride_segmentation.py b/src/gaitmap/evaluation_utils/stride_segmentation.py similarity index 100% rename from gaitmap/evaluation_utils/stride_segmentation.py rename to src/gaitmap/evaluation_utils/stride_segmentation.py diff --git a/gaitmap/event_detection/__init__.py b/src/gaitmap/event_detection/__init__.py similarity index 100% rename from gaitmap/event_detection/__init__.py rename to src/gaitmap/event_detection/__init__.py diff --git a/gaitmap/event_detection/_herzer_event_detection.py b/src/gaitmap/event_detection/_herzer_event_detection.py similarity index 100% rename from gaitmap/event_detection/_herzer_event_detection.py rename to src/gaitmap/event_detection/_herzer_event_detection.py diff --git a/gaitmap/example_data.py b/src/gaitmap/example_data.py similarity index 100% rename from gaitmap/example_data.py rename to src/gaitmap/example_data.py diff --git a/gaitmap/gait_detection/__init__.py b/src/gaitmap/gait_detection/__init__.py similarity index 100% rename from gaitmap/gait_detection/__init__.py rename to src/gaitmap/gait_detection/__init__.py diff --git a/gaitmap/parameters/__init__.py b/src/gaitmap/parameters/__init__.py similarity index 100% rename from gaitmap/parameters/__init__.py rename to src/gaitmap/parameters/__init__.py diff --git a/gaitmap/parameters/_spatial_parameters.py b/src/gaitmap/parameters/_spatial_parameters.py similarity index 100% rename from gaitmap/parameters/_spatial_parameters.py rename to src/gaitmap/parameters/_spatial_parameters.py diff --git a/gaitmap/parameters/_temporal_parameters.py b/src/gaitmap/parameters/_temporal_parameters.py similarity index 100% rename from gaitmap/parameters/_temporal_parameters.py rename to src/gaitmap/parameters/_temporal_parameters.py diff --git a/gaitmap/preprocessing/__init__.py b/src/gaitmap/preprocessing/__init__.py similarity index 100% rename from gaitmap/preprocessing/__init__.py rename to src/gaitmap/preprocessing/__init__.py diff --git a/gaitmap/preprocessing/sensor_alignment/__init__.py b/src/gaitmap/preprocessing/sensor_alignment/__init__.py similarity index 100% rename from gaitmap/preprocessing/sensor_alignment/__init__.py rename to src/gaitmap/preprocessing/sensor_alignment/__init__.py diff --git a/gaitmap/preprocessing/sensor_alignment/_gravity_alignment.py b/src/gaitmap/preprocessing/sensor_alignment/_gravity_alignment.py similarity index 100% rename from gaitmap/preprocessing/sensor_alignment/_gravity_alignment.py rename to src/gaitmap/preprocessing/sensor_alignment/_gravity_alignment.py diff --git a/gaitmap/preprocessing/sensor_alignment/_mulisensor_alignment.py b/src/gaitmap/preprocessing/sensor_alignment/_mulisensor_alignment.py similarity index 100% rename from gaitmap/preprocessing/sensor_alignment/_mulisensor_alignment.py rename to src/gaitmap/preprocessing/sensor_alignment/_mulisensor_alignment.py diff --git a/gaitmap/preprocessing/sensor_alignment/_pca_alignment.py b/src/gaitmap/preprocessing/sensor_alignment/_pca_alignment.py similarity index 100% rename from gaitmap/preprocessing/sensor_alignment/_pca_alignment.py rename to src/gaitmap/preprocessing/sensor_alignment/_pca_alignment.py diff --git a/gaitmap/stride_segmentation/__init__.py b/src/gaitmap/stride_segmentation/__init__.py similarity index 100% rename from gaitmap/stride_segmentation/__init__.py rename to src/gaitmap/stride_segmentation/__init__.py diff --git a/gaitmap/stride_segmentation/_roi_stride_segmentation.py b/src/gaitmap/stride_segmentation/_roi_stride_segmentation.py similarity index 100% rename from gaitmap/stride_segmentation/_roi_stride_segmentation.py rename to src/gaitmap/stride_segmentation/_roi_stride_segmentation.py diff --git a/gaitmap/stride_segmentation/_utils.py b/src/gaitmap/stride_segmentation/_utils.py similarity index 100% rename from gaitmap/stride_segmentation/_utils.py rename to src/gaitmap/stride_segmentation/_utils.py diff --git a/gaitmap/stride_segmentation/hmm.py b/src/gaitmap/stride_segmentation/hmm.py similarity index 100% rename from gaitmap/stride_segmentation/hmm.py rename to src/gaitmap/stride_segmentation/hmm.py diff --git a/gaitmap/trajectory_reconstruction/__init__.py b/src/gaitmap/trajectory_reconstruction/__init__.py similarity index 100% rename from gaitmap/trajectory_reconstruction/__init__.py rename to src/gaitmap/trajectory_reconstruction/__init__.py diff --git a/gaitmap/trajectory_reconstruction/_region_level_trajectory.py b/src/gaitmap/trajectory_reconstruction/_region_level_trajectory.py similarity index 100% rename from gaitmap/trajectory_reconstruction/_region_level_trajectory.py rename to src/gaitmap/trajectory_reconstruction/_region_level_trajectory.py diff --git a/gaitmap/trajectory_reconstruction/_stride_level_trajectory.py b/src/gaitmap/trajectory_reconstruction/_stride_level_trajectory.py similarity index 100% rename from gaitmap/trajectory_reconstruction/_stride_level_trajectory.py rename to src/gaitmap/trajectory_reconstruction/_stride_level_trajectory.py diff --git a/gaitmap/trajectory_reconstruction/_trajectory_wrapper.py b/src/gaitmap/trajectory_reconstruction/_trajectory_wrapper.py similarity index 100% rename from gaitmap/trajectory_reconstruction/_trajectory_wrapper.py rename to src/gaitmap/trajectory_reconstruction/_trajectory_wrapper.py diff --git a/gaitmap/trajectory_reconstruction/orientation_methods/__init__.py b/src/gaitmap/trajectory_reconstruction/orientation_methods/__init__.py similarity index 100% rename from gaitmap/trajectory_reconstruction/orientation_methods/__init__.py rename to src/gaitmap/trajectory_reconstruction/orientation_methods/__init__.py diff --git a/gaitmap/trajectory_reconstruction/orientation_methods/_madgwick.py b/src/gaitmap/trajectory_reconstruction/orientation_methods/_madgwick.py similarity index 100% rename from gaitmap/trajectory_reconstruction/orientation_methods/_madgwick.py rename to src/gaitmap/trajectory_reconstruction/orientation_methods/_madgwick.py diff --git a/gaitmap/trajectory_reconstruction/orientation_methods/_simple_gyro_integration.py b/src/gaitmap/trajectory_reconstruction/orientation_methods/_simple_gyro_integration.py similarity index 100% rename from gaitmap/trajectory_reconstruction/orientation_methods/_simple_gyro_integration.py rename to src/gaitmap/trajectory_reconstruction/orientation_methods/_simple_gyro_integration.py diff --git a/gaitmap/trajectory_reconstruction/position_methods/__init__.py b/src/gaitmap/trajectory_reconstruction/position_methods/__init__.py similarity index 100% rename from gaitmap/trajectory_reconstruction/position_methods/__init__.py rename to src/gaitmap/trajectory_reconstruction/position_methods/__init__.py diff --git a/gaitmap/trajectory_reconstruction/position_methods/_forward_backwards_integration.py b/src/gaitmap/trajectory_reconstruction/position_methods/_forward_backwards_integration.py similarity index 100% rename from gaitmap/trajectory_reconstruction/position_methods/_forward_backwards_integration.py rename to src/gaitmap/trajectory_reconstruction/position_methods/_forward_backwards_integration.py diff --git a/gaitmap/trajectory_reconstruction/trajectory_methods/__init__.py b/src/gaitmap/trajectory_reconstruction/trajectory_methods/__init__.py similarity index 100% rename from gaitmap/trajectory_reconstruction/trajectory_methods/__init__.py rename to src/gaitmap/trajectory_reconstruction/trajectory_methods/__init__.py diff --git a/gaitmap/trajectory_reconstruction/trajectory_methods/_kalman_numba_funcs.py b/src/gaitmap/trajectory_reconstruction/trajectory_methods/_kalman_numba_funcs.py similarity index 100% rename from gaitmap/trajectory_reconstruction/trajectory_methods/_kalman_numba_funcs.py rename to src/gaitmap/trajectory_reconstruction/trajectory_methods/_kalman_numba_funcs.py diff --git a/gaitmap/trajectory_reconstruction/trajectory_methods/_rts_kalman.py b/src/gaitmap/trajectory_reconstruction/trajectory_methods/_rts_kalman.py similarity index 100% rename from gaitmap/trajectory_reconstruction/trajectory_methods/_rts_kalman.py rename to src/gaitmap/trajectory_reconstruction/trajectory_methods/_rts_kalman.py diff --git a/gaitmap/utils/__init__.py b/src/gaitmap/utils/__init__.py similarity index 100% rename from gaitmap/utils/__init__.py rename to src/gaitmap/utils/__init__.py diff --git a/gaitmap/utils/_algo_helper.py b/src/gaitmap/utils/_algo_helper.py similarity index 100% rename from gaitmap/utils/_algo_helper.py rename to src/gaitmap/utils/_algo_helper.py diff --git a/gaitmap/utils/_datatype_validation_helper.py b/src/gaitmap/utils/_datatype_validation_helper.py similarity index 100% rename from gaitmap/utils/_datatype_validation_helper.py rename to src/gaitmap/utils/_datatype_validation_helper.py diff --git a/gaitmap/utils/_gaitmap_mad.py b/src/gaitmap/utils/_gaitmap_mad.py similarity index 100% rename from gaitmap/utils/_gaitmap_mad.py rename to src/gaitmap/utils/_gaitmap_mad.py diff --git a/gaitmap/utils/_types.py b/src/gaitmap/utils/_types.py similarity index 100% rename from gaitmap/utils/_types.py rename to src/gaitmap/utils/_types.py diff --git a/gaitmap/utils/array_handling.py b/src/gaitmap/utils/array_handling.py similarity index 100% rename from gaitmap/utils/array_handling.py rename to src/gaitmap/utils/array_handling.py diff --git a/gaitmap/utils/consts.py b/src/gaitmap/utils/consts.py similarity index 100% rename from gaitmap/utils/consts.py rename to src/gaitmap/utils/consts.py diff --git a/gaitmap/utils/coordinate_conversion.py b/src/gaitmap/utils/coordinate_conversion.py similarity index 100% rename from gaitmap/utils/coordinate_conversion.py rename to src/gaitmap/utils/coordinate_conversion.py diff --git a/gaitmap/utils/datatype_helper.py b/src/gaitmap/utils/datatype_helper.py similarity index 100% rename from gaitmap/utils/datatype_helper.py rename to src/gaitmap/utils/datatype_helper.py diff --git a/gaitmap/utils/exceptions.py b/src/gaitmap/utils/exceptions.py similarity index 100% rename from gaitmap/utils/exceptions.py rename to src/gaitmap/utils/exceptions.py diff --git a/gaitmap/utils/fast_quaternion_math.py b/src/gaitmap/utils/fast_quaternion_math.py similarity index 100% rename from gaitmap/utils/fast_quaternion_math.py rename to src/gaitmap/utils/fast_quaternion_math.py diff --git a/gaitmap/utils/rotations.py b/src/gaitmap/utils/rotations.py similarity index 100% rename from gaitmap/utils/rotations.py rename to src/gaitmap/utils/rotations.py diff --git a/gaitmap/utils/signal_processing.py b/src/gaitmap/utils/signal_processing.py similarity index 100% rename from gaitmap/utils/signal_processing.py rename to src/gaitmap/utils/signal_processing.py diff --git a/gaitmap/utils/static_moment_detection.py b/src/gaitmap/utils/static_moment_detection.py similarity index 100% rename from gaitmap/utils/static_moment_detection.py rename to src/gaitmap/utils/static_moment_detection.py diff --git a/gaitmap/utils/stride_list_conversion.py b/src/gaitmap/utils/stride_list_conversion.py similarity index 100% rename from gaitmap/utils/stride_list_conversion.py rename to src/gaitmap/utils/stride_list_conversion.py diff --git a/gaitmap/utils/vector_math.py b/src/gaitmap/utils/vector_math.py similarity index 100% rename from gaitmap/utils/vector_math.py rename to src/gaitmap/utils/vector_math.py diff --git a/gaitmap/zupt_detection/__init__.py b/src/gaitmap/zupt_detection/__init__.py similarity index 100% rename from gaitmap/zupt_detection/__init__.py rename to src/gaitmap/zupt_detection/__init__.py diff --git a/gaitmap/zupt_detection/_base.py b/src/gaitmap/zupt_detection/_base.py similarity index 100% rename from gaitmap/zupt_detection/_base.py rename to src/gaitmap/zupt_detection/_base.py diff --git a/gaitmap/zupt_detection/_combo_zupt_detector.py b/src/gaitmap/zupt_detection/_combo_zupt_detector.py similarity index 100% rename from gaitmap/zupt_detection/_combo_zupt_detector.py rename to src/gaitmap/zupt_detection/_combo_zupt_detector.py diff --git a/gaitmap/zupt_detection/_moving_window_zupt_detector.py b/src/gaitmap/zupt_detection/_moving_window_zupt_detector.py similarity index 100% rename from gaitmap/zupt_detection/_moving_window_zupt_detector.py rename to src/gaitmap/zupt_detection/_moving_window_zupt_detector.py diff --git a/gaitmap/zupt_detection/_stride_event_zupt_detector.py b/src/gaitmap/zupt_detection/_stride_event_zupt_detector.py similarity index 100% rename from gaitmap/zupt_detection/_stride_event_zupt_detector.py rename to src/gaitmap/zupt_detection/_stride_event_zupt_detector.py