Skip to content

Commit

Permalink
RELEASE v0.5.6 squashed commits
Browse files Browse the repository at this point in the history
  • Loading branch information
mh105 committed Aug 9, 2024
1 parent eba3a93 commit 95a66f9
Show file tree
Hide file tree
Showing 26 changed files with 2,355 additions and 171 deletions.
7 changes: 4 additions & 3 deletions .github/workflows/pr-pytest-conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ jobs:
cache-environment: true

- name: Package install with pip
run: |
python --version
pip install .
run: pip install .

- name: "- Check python version"
run: python --version

- name: Lint with flake8
run: |
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/push-pytest-conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ jobs:
cache-environment: true

- name: Package install with pip
run: |
python --version
pip install .
run: pip install .

- name: "- Check python version"
run: python --version

- name: Lint with flake8
run: |
Expand Down
64 changes: 45 additions & 19 deletions .github/workflows/release-install-tests.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Release install tests
name: Release install

on:
workflow_dispatch:
Expand All @@ -10,11 +10,11 @@ on:

env:
PACKAGE_NAME: somata
RELEASE_JSON: https://api.github.com/repos/mh105/somata/releases/latest
RELEASE_HTTP: https://github.com/mh105/somata/releases/latest

jobs:
install-unix:
name: install-on-${{ matrix.os }}
pip-install:
name: pip-install-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
Expand All @@ -26,12 +26,7 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: "Curl the latest release version"
run: |
latest_version=$(curl -s ${{ env.RELEASE_JSON }} | grep "tag_name" | cut -d '"' -f4 | cut -c2-)
echo "latest_version=$latest_version" >> $GITHUB_ENV
- name: "[Test 1] Create environment for 'pip install' on Unix"
- name: "Create environment for 'pip install' on Unix"
if: runner.os != 'Windows'
uses: mamba-org/setup-micromamba@v1
with:
Expand All @@ -40,27 +35,38 @@ jobs:
pip
cmdstanpy # avoids having to run 'install_cmdstan' after pip install

- name: "[Test 1] Create environment for 'pip install' on Windows"
- name: "Create environment for 'pip install' on Windows"
if: runner.os == 'Windows'
uses: mamba-org/setup-micromamba@v1
with:
environment-name: pip_env
create-args:
pip
cmdstanpy # avoids having to run 'install_cmdstan' after pip install
cmdstanpy
numpy<2
spectrum # avoids building 'spectrum' on Windows that requires VC++ v14+

- name: "- Pip install ${{ env.PACKAGE_NAME }}"
run: pip install ${{ env.PACKAGE_NAME }}

- name: "- Pip list"
run: pip list

- name: "- Get the latest release version"
run: |
python --version
pip list
latest_version=$(python -c "import requests; url = '${{ env.RELEASE_HTTP }}'; r = requests.get(url); print(r.url.split('/')[-1])" | awk '{print substr($0, 2)}')
if [ -n "$latest_version" ]; then
echo "latest_version extracted from GitHub releases: $latest_version"
echo "latest_version=$latest_version" >> $GITHUB_ENV
else
echo "failed to retrieve latest_version."
exit 1
fi
- name: "- Verify installed package version"
run: |
installed_version=$(pip show ${{ env.PACKAGE_NAME }} | grep Version | cut -d' ' -f2)
installed_version=$(pip show ${{ env.PACKAGE_NAME }} | grep Version | awk '{print $2}')
if [ "$installed_version" != "${{ env.latest_version }}" ]; then
echo "Installed version from PyPI ($installed_version) does not match the latest release version on GitHub (${{ env.latest_version }})."
exit 1
Expand All @@ -71,9 +77,22 @@ jobs:
- name: "- Run tests with pytest"
run: |
pip install pytest
pytest
pytest -vv
- name: "[Test 2] Install ${{ env.PACKAGE_NAME }} with 'conda create'"
conda-install:
name: conda-install-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
defaults:
run:
shell: bash -el {0}

steps:
- uses: actions/checkout@v4

- name: "Install ${{ env.PACKAGE_NAME }} with 'conda create'"
uses: mamba-org/setup-micromamba@v1
with:
environment-name: conda_env
Expand All @@ -82,10 +101,17 @@ jobs:
-c conda-forge
${{ env.PACKAGE_NAME }}

- name: "- Micromamba list"
- name: "- Get the latest release version"
run: |
python --version
micromamba list
latest_version=$(python -c "import requests; url = '${{ env.RELEASE_HTTP }}'; r = requests.get(url); print(r.url.split('/')[-1])" | awk '{print substr($0, 2)}')
if [ -n "$latest_version" ]; then
echo "latest_version extracted from GitHub releases: $latest_version"
echo "latest_version=$latest_version" >> $GITHUB_ENV
else
echo "failed to retrieve latest_version."
exit 1
fi
- name: "- Verify installed package version"
run: |
Expand All @@ -100,4 +126,4 @@ jobs:
- name: "- Run tests with pytest"
run: |
micromamba install pytest
pytest
pytest -vv
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ Basic state-space models are introduced as class objects for flexible manipulati
Classical exact and approximate inference algorithms are implemented and interfaced as class methods.
Advanced neural oscillator modeling techniques are brought together to work synergistically.

[![Release install tests](https://github.com/mh105/somata/actions/workflows/release-install-tests.yml/badge.svg?kill_cache)](https://github.com/mh105/somata/actions/workflows/release-install-tests.yml)
[![Version](https://img.shields.io/badge/Version-0.5.5-green?kill_cache)](https://github.com/mh105/somata/releases)
[![Last-Update](https://anaconda.org/conda-forge/somata/badges/latest_release_date.svg?kill_cache)](https://anaconda.org/conda-forge/somata)
[![License: BSD 3-Clause Clear](https://img.shields.io/badge/License-BSD%203--Clause%20Clear-lightgrey.svg?kill_cache)](https://spdx.org/licenses/BSD-3-Clause-Clear.html)
[![DOI](https://zenodo.org/badge/556083594.svg?kill_cache)](https://zenodo.org/badge/latestdoi/556083594)
[![Release install tests](https://github.com/mh105/somata/actions/workflows/release-install-tests.yml/badge.svg)](https://github.com/mh105/somata/actions/workflows/release-install-tests.yml)
[![Version](https://img.shields.io/badge/Version-0.5.6-green)](https://github.com/mh105/somata/releases)
[![Last-Update](https://anaconda.org/conda-forge/somata/badges/latest_release_date.svg)](https://anaconda.org/conda-forge/somata)
[![License: BSD 3-Clause Clear](https://img.shields.io/badge/License-BSD%203--Clause%20Clear-lightgrey.svg)](https://spdx.org/licenses/BSD-3-Clause-Clear.html)
[![DOI](https://zenodo.org/badge/556083594.svg)](https://zenodo.org/badge/latestdoi/556083594)

---

Expand Down Expand Up @@ -483,7 +483,7 @@ Gen(1)<2440>
```

### For more in-depth working examples with the basic models in somata
Look at the demo script [basic_models_demo_04092024.py](examples/basic_models_demo_04092024.py) and execute the code in this file _line by line_ to get familiar with the class objects and methods of `somata` basic models.
We provide [a Jupyter notebook including three exercises](examples/somata_getting_started.ipynb) to help new users to get started with `somata`. After going through the exercises, one will become familiar with how the `somata` basic models are structured and intended to be used for state-space modeling. Running Kalman filtering, fixed-interval smoothing, and EM algorithm on `somata` basic models are also demonstrated in the notebook. The [iOsc+ and dOsc oscillator search methods](#3-oscillator-search-algorithms) described in the next section are showcased in the notebook as well, which serve as examples of algorithms that can be easily built in `somata`.

---

Expand Down Expand Up @@ -560,9 +560,9 @@ Soulat, H., Stephen, E. P., Beck, A. M., & Purdon, P. L. (2022). State space met

### 3. Oscillator Search Algorithms

There are two similarly flavored univariate oscillator search methods in this module: iterative oscillator search (iOsc) and decomposition oscillator search (dOsc) algorithms.
There are two similarly flavored univariate oscillator search methods in this module: iterative oscillator search (iOsc+) and decomposition oscillator search (dOsc) algorithms.

- iOsc: for a well-commented example script, see [IterOsc_example.py](examples/IterOsc_example.py).
- iOsc+: for a well-commented example script, see [IterOsc_example.py](examples/IterOsc_example.py).

_**N.B.:** We recommend downsampling to 120 Hz or less, depending on the oscillations present in your data. Highly oversampled data will make it more difficult to identify oscillatory components, increase the computational time, and could also introduce high frequency noise._

Expand Down
13 changes: 13 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Release log

## 0.5.6
*August 2024*

#### New features

- Add more example Jupyter notebooks to demonstrate package syntaxes and algorithms

#### Closed issues

- SOMATA basic model classes now perform more reliably and consistently, including automatic parsing of parameters to generate components
- Multitaper spectrogram implementation used functions deprecated in Numpy 2.0
- Observation matrix and noise covariance ignored if initializing through components

## 0.5.5
*July 2024*

Expand Down
55 changes: 31 additions & 24 deletions examples/DecOsc_example.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
# Author: Mingjian He <[email protected]>
from somata.oscillator_search import DecomposedOscillatorModel as DecOsc
from somata.oscillator_search.helper_functions import random, simulate_matsuda, sim_to_osc_object
from somata import OscillatorModel as Osc
import numpy as np

random.seed(1) # to ensure reproducible results
fs = 100 # sampling frequency (Hz)
osc1 = {'a': 0.996, 'q': 0.4, 'f': 0.1} # set simulating parameters for slow oscillation
osc2 = {'a': 0.95, 'q': 0.2, 'f': 10} # set simulating parameters for alpha oscillation
y, param_list, ob_noise = simulate_matsuda([osc1, osc2], R=1.2, Fs=fs, T=10)
sim_osc0, sim_x0 = sim_to_osc_object(y, param_list) # save simulations as Osc object to pass into plotting functions
# Simulate 10-s data with two oscillators, one slow and one alpha frequency
np.random.seed(1) # to ensure reproducible results
o1 = Osc(a=[0.996, 0.95], freq=[0.1, 10], sigma2=[0.4, 0.2], R=1.2, Fs=100)
x, y = o1.simulate(duration=10)

# Initialize Decomposed Oscillator object
do1 = DecOsc(y, fs, noise_start=None, osc_range=7)
# noise_start determines the frequency above which is used to estimate the observation noise; default: Nyquist - 20 Hz
# osc_range is maximum number of total oscillators, set to default
# Initialize a DecomposedOscillatorModel object
do1 = DecOsc(y, o1.Fs, noise_start=None, osc_range=7)
# noise_start determines the frequency above which is used to estimate the observation noise; default: (Nyquist - 20 Hz)
# osc_range is maximum number of total oscillators; default: 7

# Run through decomposed oscillators to fit model
do1.iterate(plot_fit=True)
# plot_fit=True plots fitted spectrum at each decomposition of oscillators
# Plot multitaper spectrogram, mean spectrum, and raw data time trace
_ = do1.plot_mtm()
_ = do1.plot_trace()

# Plot frequency domain (from parameters and from estimated x_t_n)
for version in ['theoretical', 'actual']:
_ = do1.get_knee_osc().visualize_freq(version, y=y, sim_osc=sim_osc0, sim_x=sim_x0)
# Run through the set of decomposed oscillators to find the best model
do1.iterate(plot_fit=True) # this is the dOsc algorithm
# plot_fit=True plots fitted theoretical spectra during each iteration

# Plot time domain estimated x_t
_ = do1.get_knee_osc().visualize_time(y=y, sim_x=sim_x0)
# Inspect the selected model
print(do1.get_knee_osc())

# Plot likelihood and selected model (may not be the highest likelihood)
# Plot log-likelihood and the selected model (may not be the highest)
_ = do1.plot_log_likelihoods()

# Other helpful plotting methods
_ = do1.plot_mtm() # plot multitaper spectrogram and mean spectrum
_ = do1.plot_trace() # plot raw time trace
_ = do1.plot_fit_spectra() # plot fitted spectra (equivalent to calling .visualize_freq(version) manually)
# Plot estimated hidden states x_t in the frequency domain
_ = do1.plot_fit_spectra(sim_osc=o1, sim_x=x[:, 1:])
# sim_osc is the true OscillatorModel used for the data generation (optional)
# sim_x is the true hidden states x_t underlying the data generation (optional)

# Plot estimated hidden states x_t in the time domain
_ = do1.plot_fit_traces(sim_x=x[:, 1:])
# sim_x is the true hidden states x_t underlying the data generation (optional)

# Plot diagnostics of residuals and run statistical tests on autocorrelations and normality
do1.diagnose_residual()
67 changes: 37 additions & 30 deletions examples/IterOsc_example.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,46 @@
# Author: Mingjian He <[email protected]>
from somata.oscillator_search import IterativeOscillatorModel as IterOsc
from somata.oscillator_search.helper_functions import (
random, simulate_matsuda, sim_to_osc_object, innovations_wrapper)

random.seed(1) # to ensure reproducible results
fs = 100 # sampling frequency (Hz)
osc1 = {'a': 0.996, 'q': 0.4, 'f': 0.1} # set simulating parameters for slow oscillation
osc2 = {'a': 0.95, 'q': 0.2, 'f': 10} # set simulating parameters for alpha oscillation
y, param_list, ob_noise = simulate_matsuda([osc1, osc2], R=1.2, Fs=fs, T=10)
sim_osc0, sim_x0 = sim_to_osc_object(y, param_list) # save simulations as Osc object to pass into plotting functions

# Initialize Iterative Oscillator object
io1 = IterOsc(y, fs, noise_start=None, osc_range=7)
# noise_start determines the frequency above which is used to estimate the observation noise; default: Nyquist - 20 Hz
# osc_range is maximum number of total oscillators, set to default

# Run through iterations to fit model
io1.iterate(freq_res=1, plot_fit=True, verbose=True)
from somata.oscillator_search.helper_functions import innovations_wrapper
from somata import OscillatorModel as Osc
import numpy as np

# Simulate 10-s data with two oscillators, one slow and one alpha frequency
np.random.seed(1) # to ensure reproducible results
o1 = Osc(a=[0.996, 0.95], freq=[0.1, 10], sigma2=[0.4, 0.2], R=1.2, Fs=100)
x, y = o1.simulate(duration=10)

# Initialize an IterativeOscillatorModel object
io1 = IterOsc(y, o1.Fs, noise_start=None, osc_range=7)
# noise_start determines the frequency above which is used to estimate the observation noise; default: (Nyquist - 20 Hz)
# osc_range is maximum number of total oscillators; default: 7

# Plot multitaper spectrogram, mean spectrum, and raw data time trace
_ = io1.plot_mtm()
_ = io1.plot_trace()

# Run through iterations to find the best model
io1.iterate(freq_res=1, plot_fit=True, verbose=True) # this is the iOsc+ algorithm
# freq_res is the minimal resolution in Hz from existing frequencies when adding a new oscillator
# plot_fit=True plots innovation spectrum and AR fitting during iterations
# plot_fit=True plots innovation spectrum and AR fitting during each iteration
# verbose=True prints parameters throughout the method

# Plot frequency domain (from parameters and from estimated x_t_n)
for version in ['theoretical', 'actual']:
_ = io1.get_knee_osc().visualize_freq(version, y=y, sim_osc=sim_osc0, sim_x=sim_x0)
# Inspect the selected model
print(io1.get_knee_osc())

# Plot time domain estimated x_t
_ = io1.get_knee_osc().visualize_time(y=y, sim_x=sim_x0)

# Plot likelihood and selected model (may not be the highest likelihood)
# Plot log-likelihood and the selected model (may not be the highest)
_ = io1.plot_log_likelihoods()

# Plot fitting at a specific iteration and the innovation spectrum
_ = innovations_wrapper(io1, 0) # this should look like the first plot produced by io1.iterate()
_ = innovations_wrapper(io1, 0) # the same as the first plot produced by io1.iterate()

# Plot estimated hidden states x_t in the frequency domain
_ = io1.plot_fit_spectra(sim_osc=o1, sim_x=x[:, 1:])
# sim_osc is the true OscillatorModel used for the data generation (optional)
# sim_x is the true hidden states x_t underlying the data generation (optional)

# Plot estimated hidden states x_t in the time domain
_ = io1.plot_fit_traces(sim_x=x[:, 1:])
# sim_x is the true hidden states x_t underlying the data generation (optional)

# Other helpful plotting methods
_ = io1.plot_mtm() # plot multitaper spectrogram and mean spectrum
_ = io1.plot_trace() # plot raw time trace
_ = io1.plot_fit_spectra() # plot fitted spectra (equivalent to calling .visualize_freq(version) manually)
# Plot diagnostics of residuals and run statistical tests on autocorrelations and normality
io1.diagnose_residual()
Loading

0 comments on commit 95a66f9

Please sign in to comment.