Source code for osl_dynamics.models

"""Generative models.

This subpackage contains all the models implemented in osl-dynamics. Each
model module (e.g. ``hmm.py``, ``dynemo.py``) defines a ``Config`` dataclass
and a ``Model`` class.

Code structure
--------------

The code is organised into three layers:

**1. Base layer** (``mod_base.py``)

- :py:class:`~osl_dynamics.models.mod_base.BaseModelConfig` — Common
  configuration shared by all models (learning rate, batch size, number of
  modes/states, etc.).
- :py:class:`~osl_dynamics.models.mod_base.ModelBase` — Abstract base class
  that wraps a Keras model. Provides the training loop (``fit``),
  initialisation, checkpointing, and attribute delegation to the underlying
  Keras model. Subclasses must implement ``build_model()``.

**2. Inference layer** (``inf_mod_base.py``)

Two parallel branches extend ``ModelBase`` for different inference paradigms:

- **Variational inference** — For models with continuous latent variables
  (mode mixing coefficients inferred by an RNN). Adds KL annealing and
  alpha temperature handling.

  - :py:class:`~osl_dynamics.models.inf_mod_base.VariationalInferenceModelConfig`
  - :py:class:`~osl_dynamics.models.inf_mod_base.VariationalInferenceModelBase`
  - Used by: DyNeMo, M-DyNeMo, SC-DyNeMo, DIVE, DyNeStE.

- **Markov state inference** — For models with discrete hidden states
  (state sequence inferred by the Baum-Welch algorithm). Adds transition
  probability learning and state initialisation.

  - :py:class:`~osl_dynamics.models.inf_mod_base.MarkovStateInferenceModelConfig`
  - :py:class:`~osl_dynamics.models.inf_mod_base.MarkovStateInferenceModelBase`
  - Used by: HMM, HMM-Poisson, HIVE.

**3. Full model**

Each model combines a ``Config`` (via multiple inheritance from
``BaseModelConfig`` + an inference config) and a ``Model`` (inheriting from
the appropriate inference base class):

.. list-table::
   :header-rows: 1
   :widths: 20 15 50

   * - Model
     - Inference
     - Description
   * - :py:mod:`~osl_dynamics.models.hmm`
     - Markov
     - Hidden Markov Model with MVN observations.
       See :doc:`model description </models/hmm>`.
   * - :py:mod:`~osl_dynamics.models.hmm_poi`
     - Markov
     - HMM with Poisson observations.
   * - :py:mod:`~osl_dynamics.models.hive`
     - Markov
     - HMM with Integrated Variability Estimation
       (session-specific parameters via embeddings).
       See :doc:`model description </models/hive>`.
   * - :py:mod:`~osl_dynamics.models.dynemo`
     - Variational
     - Dynamic Network Modes (continuous mode mixing via RNN).
       See :doc:`model description </models/dynemo>`.
   * - :py:mod:`~osl_dynamics.models.mdynemo`
     - Variational
     - Multi-Dynamic Network Modes (separate dynamics for
       power and connectivity).
       See :doc:`model description </models/mdynemo>`.
   * - :py:mod:`~osl_dynamics.models.sc_dynemo`
     - Variational
     - Single-Channel DyNeMo (extends DyNeMo).
   * - :py:mod:`~osl_dynamics.models.dive`
     - Variational
     - DyNeMo with Integrated Variability Estimation.
   * - :py:mod:`~osl_dynamics.models.dyneste`
     - Variational
     - Dynamic Network States (discrete states with
       non-Markovian temporal model).
       See :doc:`model description </models/dyneste>`.

**Utilities** (``obs_mod.py``)

Shared functions for getting/setting observation model parameters
(means, covariances, embeddings, regularizers).

Tutorials
---------
- :doc:`HMM Training </tutorials_build/3-2_hmm_training>`
- :doc:`DyNeMo Training </tutorials_build/3-3_dynemo_training>`
- :doc:`Getting Inferred Parameters </tutorials_build/3-4_hmm_dynemo_get_inf_params>`

Python example scripts
----------------------
- `Simulation <https://github.com/OHBA-analysis/osl-dynamics/tree/main/examples/simulation>`_
- `MEG analysis <https://github.com/OHBA-analysis/osl-dynamics/tree/main/examples/meg_analysis>`_
- `fMRI analysis <https://github.com/OHBA-analysis/osl-dynamics/tree/main/examples/fmri>`_
"""

import re

import yaml

_TENSORFLOW_PACKAGES = ("tensorflow", "tensorflow_probability", "tf_keras", "keras")

_TENSORFLOW_INSTALL_MESSAGE = """\
Could not import '{package}', which osl-dynamics needs to build and train \
models. The rest of osl-dynamics (data loading, preprocessing, analysis and \
simulation) can be used without it.

To install the packages needed for model training:

    pip install "osl-dynamics[tf]"

or, if you have an NVIDIA GPU:

    pip install "osl-dynamics[tf-cuda]"

Alternatively, use one of the conda environment files described here:
https://osl-dynamics.readthedocs.io/en/latest/install.html\
"""


def _missing_tensorflow_package(error):
    name = getattr(error, "name", None) or ""
    if not name:
        match = re.search(r"No module named '([^']+)'", str(error))
        name = match.group(1) if match else ""
    name = name.split(".")[0]
    return name if name in _TENSORFLOW_PACKAGES else None


try:
    from osl_dynamics.models import (
        dynemo,
        mdynemo,
        sc_dynemo,
        hmm,
        hmm_poi,
        hive,
        dive,
        dyneste,
    )
except ImportError as error:
    _package = _missing_tensorflow_package(error)
    if _package is None:
        raise
    raise ImportError(_TENSORFLOW_INSTALL_MESSAGE.format(package=_package)) from error


[docs] models = { "DyNeMo": dynemo.Model, "M-DyNeMo": mdynemo.Model, "SC-DyNeMo": sc_dynemo.Model, "HMM": hmm.Model, "HMM-Poisson": hmm_poi.Model, "HIVE": hive.Model, "DIVE": dive.Model, "DyNeStE": dyneste.Model, }
from osl_dynamics.utils import misc
[docs] def load(dirname, single_gpu=True): """Load model. Parameters ---------- dirname : str Path to directory where the config.yml and weights are stored. single_gpu : bool, optional Should we compile the model on a single GPU? Returns ------- model : osl-dynamics model Model object. """ with open(f"{dirname}/config.yml", "r") as file: config_dict = yaml.load(file, misc.NumpyLoader) if "model_name" not in config_dict: raise ValueError( "Either use a specific `Model.load` method or " "provide a `model_name` field in config" ) try: model_type = models[config_dict["model_name"]] except KeyError: raise NotImplementedError( f"{config_dict['model_name']} was not found. " f"Options are {', '.join(models.keys())}" ) return model_type.load(dirname, single_gpu=single_gpu)