Skip to content

Commit

Permalink
update docstring and coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
Koaha committed Nov 28, 2024
1 parent 9a427aa commit cc57c1c
Show file tree
Hide file tree
Showing 11 changed files with 384 additions and 79 deletions.
22 changes: 22 additions & 0 deletions docs/source/_static/sidebar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.wy-nav-side {
background-color: #f8f9fa; /* Light background */
}
.wy-nav-content {
padding: 1em;
}
.wy-menu-vertical {
border-right: 1px solid #ddd;
}
.wy-menu-vertical li a {
font-size: 0.95em;
color: #007bff; /* Link color */
}
.wy-menu-vertical li a:hover {
text-decoration: underline;
}
.wy-menu-vertical li.toctree-l1 > a {
font-weight: bold; /* Top-level items */
}
.wy-menu-vertical li.toctree-l2 > a {
margin-left: 10px; /* Indent for second-level items */
}
5 changes: 5 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
# Substitute project name into .rst files when |project_name| is used
rst_epilog = '.. |project_name| replace:: %s' % project

# Add CSS files
html_css_files = [
'sidebar.css',
]


# -- Extensions configuration ------------------------------------------------

Expand Down
56 changes: 27 additions & 29 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
@@ -1,52 +1,50 @@
Welcome to vital_sqi's documentation!
Welcome to vital_sqi's Documentation!
=====================================

.. image:: ./_static/imgs/logo.png
:alt: Vital_SQI logo
:width: 200px
:align: center

.. note::
Vital_SQI is a Python library designed for analyzing physiological signals (like ECG and PPG) and computing Signal Quality Indices (SQI).


The code of the project is on `GitHub <https://github.com/Oucru-Innovations/vital-sqi>`_.

Getting Started
---------------
.. toctree::
:maxdepth: 2

usage/installation
usage/introduction
usage/contributions
usage/development
usage/quickstart

Data Manipulation
-----------------
🚀 Getting Started
------------------
.. toctree::
:maxdepth: 2
:caption: Basics

_examples/notebooks/Data_manipulation_ECG_PPG
usage/installation 💻 Installation Guide
usage/introduction 📖 Introduction
usage/quickstart ⚡ Quickstart Tutorial
usage/contributions 🤝 Contributing to Vital_SQI
usage/development 🔧 Development Guide

Pipeline
--------
🛠️ Tutorials and Examples
-------------------------
.. toctree::
:maxdepth: 2
:caption: Tutorials

_examples/notebooks/SQI_pipeline
_examples/notebooks/Data_manipulation_ECG_PPG 🛠️ Data Manipulation with ECG & PPG
_examples/notebooks/SQI_pipeline 🔄 Building SQI Pipelines

Documentation
-------------
📚 Documentation
----------------
.. toctree::
:maxdepth: 2
:caption: API Reference

docstring/modules
docstring/vital_sqi.pipeline
docstring/vital_sqi.sqi
docstring/modules 📘 Module Overview
docstring/vital_sqi.pipeline 🔄 Pipeline Module
docstring/vital_sqi.sqi 📊 SQI Module

Indices and tables
==================
🔍 Indices and Tables
=====================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
* :ref:`genindex` - General Index
* :ref:`modindex` - Module Index
* :ref:`search` - Search
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "vitalsqi_toolkit"
version = "1.0.1"
version = "1.0.2"
description = "A toolkit for signal quality analysis of ECG and PPG signals"
readme = "README.md"
requires-python = ">=3.7"
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

setup(
name = 'vitalsqi_toolkit',
version = '1.0.1',
version = '1.0.2',
packages = find_packages(include = ["vital_sqi", "vital_sqi.*"]),
description = "Signal quality control pipeline for electrocardiogram and "
"photoplethysmogram",
Expand Down
1 change: 1 addition & 0 deletions tests/common/test_generate_template.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from scipy.special import erf


class TestPPGDualDoubleFrequencyTemplate(object):
def test_on_ppg_dual_double_frequency_template(self):
pass
Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
from webdriver_manager.firefox import GeckoDriverManager
from dash.testing.composite import DashComposite


def pytest_ignore_collect(path):
if "vital_sqi/app" in str(path):
return True


def pytest_addoption(parser):
"""Add a command-line option for selecting the browser."""
parser.addoption(
Expand Down
180 changes: 170 additions & 10 deletions tests/sqi/test_hrv_sqi.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ def test_sdsd_sqi(self, valid_nn_intervals, short_nn_intervals):
assert np.isnan(sdsd_sqi(["invalid_input"]))

# # Test with invalid input
# with pytest.raises(ValueError, match="diff requires input that is at least one dimensional"):
# sdsd_sqi("invalid_input")
with pytest.warns(UserWarning):
res = sdsd_sqi("invalid_input")
assert np.isnan(res) # Ensure the function returns NaN for invalid inputs

def test_rmssd_sqi(self, valid_nn_intervals, short_nn_intervals):
# Test with valid NN intervals
Expand All @@ -73,14 +74,21 @@ def test_rmssd_sqi(self, valid_nn_intervals, short_nn_intervals):
assert np.isnan(rmssd_sqi(["invalid_input"]))

# Test with invalid input
# with pytest.raises(ValueError, match="diff requires input that is at least one dimensional"):
# rmssd_sqi("invalid_input")
with pytest.warns(UserWarning):
res = rmssd_sqi("invalid_input")
assert np.isnan(res) # Ensure the function returns NaN for invalid inputs

def test_cvsd_sqi(self, valid_nn_intervals):
assert cvsd_sqi(valid_nn_intervals) == pytest.approx(0.01994, rel=1e-1)
with pytest.warns(UserWarning):
res = cvsd_sqi("invalid_input")
assert np.isnan(res) # Ensure the function returns NaN for invalid inputs

def test_cvnn_sqi(self, valid_nn_intervals):
assert cvnn_sqi(valid_nn_intervals) == pytest.approx(0.0182, rel=1e-1)
with pytest.warns(UserWarning):
res = cvnn_sqi("invalid_input")
assert np.isnan(res) # Ensure the function returns NaN for invalid inputs

def test_median_nn_sqi(self, valid_nn_intervals, empty_nn_intervals):
assert median_nn_sqi(valid_nn_intervals) == pytest.approx(810, rel=1e1)
Expand All @@ -91,6 +99,9 @@ def test_pnn_sqi(self, valid_nn_intervals, short_nn_intervals):
100.0, rel=1e-2
)
assert np.isnan(pnn_sqi(short_nn_intervals))
with pytest.warns(UserWarning):
res = pnn_sqi("invalid_input")
assert np.isnan(res) # Ensure the function returns NaN for invalid inputs

def test_hr_sqi(self, valid_nn_intervals):
assert hr_sqi(valid_nn_intervals, stat="mean") == pytest.approx(74.26, rel=1e-2)
Expand Down Expand Up @@ -147,6 +158,70 @@ def test_frequency_sqi(self, valid_nn_intervals):
)
)

# Test all metrics
length = 500
base_rate = 600
variability = 50
synthetic_nn_intervals = base_rate + np.random.randint(
-variability, variability, size=length
)
freq_min = 0.04
freq_max = 0.15
result_peak = frequency_sqi(
synthetic_nn_intervals, freq_min=freq_min, freq_max=freq_max, metric="peak"
)
assert result_peak >= 0 or np.isnan(result_peak)
result_absolute = frequency_sqi(
synthetic_nn_intervals,
freq_min=freq_min,
freq_max=freq_max,
metric="absolute",
)
assert result_absolute >= 0 or np.isnan(result_absolute)
result_log = frequency_sqi(
synthetic_nn_intervals, freq_min=freq_min, freq_max=freq_max, metric="log"
)
assert result_log >= 0 or np.isnan(result_log)
result_normalized = frequency_sqi(
synthetic_nn_intervals,
freq_min=freq_min,
freq_max=freq_max,
metric="normalized",
)
assert result_normalized >= 0 or np.isnan(result_normalized)
result_relative = frequency_sqi(
synthetic_nn_intervals,
freq_min=freq_min,
freq_max=freq_max,
metric="relative",
)
assert result_relative >= 0 or np.isnan(result_relative)

def generate_nn_intervals(self, length=500, base_rate=600, variability=50):
"""
Generate synthetic NN intervals mimicking heart rate variability.
Parameters:
----------
length : int
Number of intervals to generate.
base_rate : int
Base NN interval in milliseconds.
variability : int
Maximum variability in NN interval.
Returns:
-------
list
A list of synthetic NN intervals.
"""
np.random.seed(42) # For reproducibility
# Generate random variations around the base rate
nn_intervals = base_rate + np.random.randint(
-variability, variability, size=length
)
return nn_intervals.tolist()

def test_lf_hf_ratio_sqi(self, valid_nn_intervals, empty_nn_intervals):
"""Test LF/HF ratio calculation."""
# Valid case
Expand Down Expand Up @@ -181,6 +256,23 @@ def test_lf_hf_ratio_sqi(self, valid_nn_intervals, empty_nn_intervals):
)
)

length = 500
base_rate = 600
variability = 50
synthetic_nn_intervals = base_rate + np.random.randint(
-variability, variability, size=length
)
ratio = lf_hf_ratio_sqi(
synthetic_nn_intervals, lf_range=(1e-3, 1e3), hf_range=(1e-4, 1e4)
)
print(ratio)
assert not np.isnan(ratio)

very_high_hf_ratio = lf_hf_ratio_sqi(
synthetic_nn_intervals, lf_range=(0.5, 1.0), hf_range=(10.0, 20.0)
)
assert np.isnan(very_high_hf_ratio)

def test_poincare_features_sqi(self, valid_nn_intervals, short_nn_intervals):
features = poincare_features_sqi(valid_nn_intervals)
assert features["sd1"] >= 0
Expand All @@ -190,22 +282,23 @@ def test_poincare_features_sqi(self, valid_nn_intervals, short_nn_intervals):
features = poincare_features_sqi(short_nn_intervals)
for key in features:
assert np.isnan(features[key])
with pytest.warns(UserWarning):
res = poincare_features_sqi("invalid_input")
assert np.isnan(
res["sd1"]
) # Ensure the function returns NaN for invalid inputs.res['sd1']

def test_get_all_features_hrva(self):
signal = np.sin(np.linspace(0, 2 * np.pi, 1000)) # Simulated signal
sample_rate = 100

# Valid case
features = (
get_all_features_hrva(signal, sample_rate=sample_rate)
)
features = get_all_features_hrva(signal, sample_rate=sample_rate)
assert isinstance(features, dict)

# Edge case: Invalid peak detection
invalid_signal = [0] * 1000 # Flat signal, no peaks
features = (
get_all_features_hrva(invalid_signal, sample_rate=sample_rate)
)
features = get_all_features_hrva(invalid_signal, sample_rate=sample_rate)
assert features == {}

# Edge case: Invalid wave type
Expand All @@ -217,3 +310,70 @@ def test_get_all_features_hrva(self):
# Edge case: Invalid sample rate
with pytest.raises(Exception, match="Sample rate must be a positive number."):
get_all_features_hrva(signal, sample_rate=-100)

with pytest.warns(UserWarning):
res = get_all_features_hrva("invalid_input")
assert len(res) == 0

def test_hr_sqi_invalid_stat(self, valid_nn_intervals):
result = hr_sqi(valid_nn_intervals, stat="invalid")
assert np.isnan(result), "Expected NaN for invalid stat input"

def test_hr_range_sqi_edge_cases(self):
nn_intervals = [800, 810, 820, 830]
assert hr_range_sqi(nn_intervals, range_min=900, range_max=1000) == 100.0
assert hr_range_sqi(nn_intervals, range_min=500, range_max=700) == 100.0

def test_frequency_sqi_empty_band_powers(self):
nn_intervals = [
800,
810,
820,
830,
] # Adjust as necessary to force empty band_powers
result = frequency_sqi(
nn_intervals, freq_min=0.4, freq_max=0.5, metric="absolute"
)
assert np.isnan(result)

def test_lf_hf_ratio_sqi_invalid_range(self, valid_nn_intervals):
result = lf_hf_ratio_sqi(
valid_nn_intervals, lf_range=(0.5, 0.4), hf_range=(0.15, 0.1)
)
assert np.isnan(result)

def test_poincare_features_sqi_insufficient_data(self):
features = poincare_features_sqi([800]) # Insufficient intervals
for key in features:
assert np.isnan(features[key])

def test_get_all_features_hrva_invalid_inputs(self):
signal = np.sin(np.linspace(0, 2 * np.pi, 1000))
sample_rate = 100

# Invalid peak detection method
result = get_all_features_hrva(
signal, sample_rate=sample_rate, rpeak_method=999
)
assert (
result is not None
), "Expected a default dictionary for invalid peak detection method"

# Invalid sample rate
with pytest.raises(ValueError, match="Sample rate must be a positive number"):
get_all_features_hrva(signal, sample_rate=-100)

def test_get_all_features_hrva_rr_interval_failure(self):
invalid_signal = [0] * 1000 # Flat signal, no peaks
features = get_all_features_hrva(invalid_signal, sample_rate=100)
assert features == {}

def test_get_all_features_hrva_feature_extraction_failure(self):
invalid_signal = np.sin(np.linspace(0, 2 * np.pi, 10)) # Too short for HRV
features = get_all_features_hrva(invalid_signal, sample_rate=100)
assert features == {}

def test_get_all_features_hrva_final_block(self):
invalid_signal = [0] * 1000
features = get_all_features_hrva(invalid_signal, sample_rate=100)
assert features == {}
Loading

0 comments on commit cc57c1c

Please sign in to comment.