Skip to content

Commit

Permalink
[10] Add coverage to CI pipeline (#12)
Browse files Browse the repository at this point in the history
* BLD:
- first test of a GitHub CI for testing

* FIX:
- fixed wrong pip install and python versions in CI ?

* FIX:
- fixed loading of test files that do not contain test but dependencies that cannot be loaded ?

* BUG:
- escaped imports of unaccessible dependencies for test-file generation in GitHub CI tests by making the `if __name__ == "__main__"` include these imports and function definitions

* TST:
- test whether Cython build can be removed from GitHub actions for testing ?

* TST:
- reverted removal of Cython for testing

* PKG:
- moved the full package back into an `src`-folder

BLD:
- updated `pyproject.toml` and `setup.py` to account for the movement to the `src`-folder
- removed dedicated Cython build from CI test pipeline

* BLD:
- changed CI pipeline target branches

* BLD:
- augmented CI checks with format, type, and lint checks

* BUG:
- fixed CI pipeline wrong folder include ?

BLD:
- added isort check

* BUG:
- fixed broken `isort` usage in CI?

* FIX:
- fixed missing `isort` dependency in CI?
- fixed wrong import sort order in Cython Hermite functions

* FIX:
- fixed missing `colorama` dependency for `isort` in CI ?

* FIX:
- type-ignored Cython import that was not properly resolved by `pyright` ?

* FIX:
- fixed missing import of Cython module for `pyright`  in CI ?

* FIX:
- again trying to resolve the wrong import error of Cython module by `pyright` in CI ?

* FIX:
- fixed wrong `pyright` Cython import error of Cython import in CI

* BLD:
- removed pushes to `develop` from the GitHub CI actions

* BLD:
- added missing comma to the name of the GitHub CI action

* [10 develop] Add coverage to CI pipeline (#11)

* DOC:
- added Python versions and `black` code style to `README`

* DOC:
- added `isort` badge to `README`

* TST:
- added `--no-jit`-flag to `pytest` to enable proper coverage of Numba functions

* tmp:
- first test of CI with coverage report ?

* BUG:
- fixed failure of `pytest-xdist` and `pytest-cov` in GitHub CI (works locally) ?

* BUG:
- fixed accidentally placed `\` for `./tests` in coverage CI action ?

* BUG:
- added codecov to CI pipeline ?

* TST:
- tried to readd `pytest-xdist` for coverage reports ?

* wip:
- reset example Jupyter notebook number 3

* MAINT:
- made `_get_num_workers` a function of the `_utils`-model

TST:
- increased coverage to 100% by testing `np.float32` x-values for the Hermite functions was well as super negative numbers of requested workers

* DOC:
- added setup, installation, and development instructions to `README`

* DOC:
- switched back from `README.rst` to `README.md`

BLD:
- made CI pipeline push actions apply to the main branch only
  • Loading branch information
MothNik committed Jul 21, 2024
1 parent 776e941 commit 75808fa
Show file tree
Hide file tree
Showing 12 changed files with 471 additions and 190 deletions.
9 changes: 8 additions & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,11 @@ jobs:
- name: Run tests
run: |
pytest -n=auto -x
pytest --cov=robust_hermite_ft ./tests -n="auto" --cov-report=xml -x --no-jit
- name: Upload coverage report
uses: codecov/codecov-action@v4.0.1
with:
file: ./coverage.xml
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
134 changes: 134 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# `robust_hermite_ft`

[![python-3.9](https://img.shields.io/badge/python-3.9-blue.svg)](https://www.python.org/downloads/release/python-390/)
[![python-3.10](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/release/python-3100/)
[![python-3.11](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/downloads/release/python-3110/)
[![python-3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3120/)
[![code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![code style: isort](https://img.shields.io/badge/code%20style-isort-000000.svg)](https://pycqa.github.io/isort/)
[![codecov](https://codecov.io/gh/MothNik/robust_hermite_ft/branch/10-improve-and-add-coverage-to-CI/graph/badge.svg)](https://codecov.io/gh/MothNik/robust_hermite_ft/branch/10-improve-and-add-coverage-to-CI)
<br><br>

You want to compute the Fourier transform of a signal, but your signal can be corrupted by outliers? If so, this package is for you even though you will have to say goodbye to the _"fast"_ in _Fast Fourier Transform_ 🏃🙅‍♀️

🏗️🚧 👷👷‍♂️👷‍♀️🏗️🚧

Currently under construction. Please check back later.

## ⚙️ Setup and 🪛 Development

### 🎁 Installation

Currently, the package is not yet available on PyPI. To install it, you can clone the repository

```bash
git clone https://github.com/MothNik/robust_hermite_ft.git
```

and from within the repositories root directory, install it with

```bash
pip install -e .
```

for normal use or

```bash
pip install -e .["dev"]
```

for development which will also install the development dependencies.

⚠️ **Warning**: This will require a C-compiler to be installed on your system to
compile the Cython code.

### 🔎 Code quality

The following checks for `black`, `isort`, `pyright`, `ruff`, and
`cython-lint` - that are also part of the CI pipeline - can be run with

```bash
black --check --diff --color ./examples ./src ./tests
isort --check --diff --color ./examples ./src ./tests
pyright
ruff check ./examples ./src ./tests
cython-lint src/robust_hermite_ft/hermite_functions/_c_hermite.pyx
```

### ✅❌ Tests

To run the tests - almost like in the CI pipeline - you can use

```bash
pytest --cov=robust_hermite_ft ./tests -n="auto" --cov-report=xml -x --no-jit
```

for parallelized testing whose coverage report will be stored in the folder
`./htmlcov`.

## 〰️ Hermite functions

Being the eigenfunctions of the Fourier transform, Hermite functions are excellent
candidates for the basis functions for a Least Squares Regression approach to the Fourier
transform. However, their evaluation can be a bit tricky.

The module `hermite_functions` offers a numerically stable way to evaluate Hermite
functions or arbitrary order $n$ and argument - that can be scaled with a factor
$\alpha$:

<p align="center">
<img src="docs/hermite_functions/DilatedHermiteFunctions_DifferentScales.png" width="1000px" />
</p>

The Hermite functions are defined as

<p align="left">
<img src="docs/hermite_functions/equations/DilatedHermiteFunctions.png" width="500px" />
</p>

with the Hermite polynomials

<p align="left">
<img src="docs/hermite_functions/equations/DilatedHermitePolynomials.png" width="681px" />
</p>

By making use of logarithm tricks, the evaluation that might involve infinitely high
polynomial values and at the same time infinitely small Gaussians - that are on top of
that scaled by an infinitely high factorial - can be computed safely and yield accurate
results.

For doing so, the relation between the dilated and the non-dilated Hermite functions

<p align="left">
<img src="docs/hermite_functions/equations/HermiteFunctions_UndilatedToDilated.png" width="321px" />
</p>

and the recurrence relation for the Hermite functions

<p align="left">
<img src="docs/hermite_functions/equations/HermiteFunctions_RecurrenceRelation.png" width="699px" />
</p>

are used, but not directly. Instead, the latest evaluated Hermite function is kept at a
value of either -1, 0, or +1 during the recursion and the logarithm of a correction
factor is tracked and applied when the respective Hermite function is finally evaluated
and stored. This approach is based on [[1]](#references).

The implementation is tested against a symbolic evaluation with `sympy` that uses 200
digits of precision and it can be shown that even orders as high as 2,000 can still be
computed even though neither the polynomial, the Gaussian nor the factorial can be
evaluated for this anymore. The factorial for example would already have overflown for
orders of 170 in `float64`-precision.

<p align="center">
<img src="docs/hermite_functions/DilatedHermiteFunctions_Stability.png" width="1000px" />
</p>

As a sanity check, their orthogonality is part of the tests together with a test for
the fact that the absolute values of the Hermite functions for real input cannot exceed
the value $\frac{1}{\pi^{-\frac{1}{4}}\cdot\sqrt{\alpha}}$.

## References

- [1] Bunck B. F., A fast algorithm for evaluation of normalized Hermite
functions, BIT Numer Math (2009), 49, pp. 281–295, DOI: [https://doi.org/10.1007/s10543-009-0216-1](https://doi.org/10.1007/s10543-009-0216-1)
79 changes: 0 additions & 79 deletions README.rst

This file was deleted.

71 changes: 4 additions & 67 deletions examples/03_hermite_functions_performance.ipynb

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions src/robust_hermite_ft/_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
Module :mod:`_utils`
This module provides utility functionalities that are used throughout the package, e.g.,
- handling of Numba-related tasks
"""

# === Imports ===

from .numba_helpers import ( # noqa: F401
NUMBA_NO_JIT_ARGV,
NUMBA_NO_JIT_ENV_KEY,
NumbaJitActions,
do_numba_normal_jit_action,
no_jit,
)
from .parallel_helpers import _get_num_workers # noqa: F401
82 changes: 82 additions & 0 deletions src/robust_hermite_ft/_utils/numba_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""
Module :mod:`_utils.numba_helpers`
This module implements auxiliary functionalities to handle Numba-related tasks, such as
- checking whether Numba ``jit``-compilation has been explicitly specified to take no
effect, e.g., for test coverage
"""

# === Imports ===

import os
from enum import Enum
from typing import Callable

# === Models ===

# an Enum that specifies the possible actions that can be taken regarding Numba
# ``jit``-compilation


class NumbaJitActions(Enum):
"""
Specifies the possible actions that can be taken regarding Numba
``jit``-compilation.
"""

NORMAL = "0"
DEACTIVATE = "1"


# === Constants ===

# the runtime argument that is used to specify that Numba ``jit``-compilation should
# take no effect
NUMBA_NO_JIT_ARGV = "--no-jit"

# the environment variable that is used to specify that Numba ``jit``-compilation should
# take no effect
NUMBA_NO_JIT_ENV_KEY = "CUSTOM_NUMBA_NO_JIT"


# whether the environment variable is set to specify that Numba ``jit``-compilation
# should take effect or not in the current runtime environment
do_numba_normal_jit_action = (
os.environ.get(NUMBA_NO_JIT_ENV_KEY, NumbaJitActions.NORMAL.value)
== NumbaJitActions.NORMAL.value
)


# === Functions ===


def no_jit(*args, **kwargs) -> Callable:
"""
Fake decorator that can be used to make sure that Numba ``jit``-compilation has no
effect.
Parameters
----------
func : :class:`Callable`
The function that is decorated.
args : :class:`tuple`
The fake positional arguments.
kwargs : :class:`dict`
The fake keyword arguments.
Returns
-------
decorated_func : :class:`Callable`
The decorated function.
"""

def decorator(func: Callable) -> Callable:
return func

return decorator
50 changes: 50 additions & 0 deletions src/robust_hermite_ft/_utils/parallel_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
Module :mod:`_utils.parallel_helpers`
This module provides functionalities to handle parallel computations, e.g.,
- obtaining the number of threads available for the process
"""

# === Imports ===

import psutil

# === Functions ===


def _get_num_workers(workers: int) -> int:
"""
Gets the number of available workers for the process calling this function.
Parameters
----------
workers : :class:`int`
Number of workers requested.
Returns
-------
workers : :class:`int`
Number of workers available.
"""

# the number of workers may not be less than -1
if workers < -1:
raise ValueError(
f"Expected 'workers' to be greater or equal to -1 but got {workers}."
)

# then, the maximum number of workers is determined ...
# NOTE: the following does not count the number of total threads, but the number of
# threads available to the process calling this function
process = psutil.Process()
max_workers = len(process.cpu_affinity()) # type: ignore
del process

# ... and overwrites the number of workers if it is set to -1
workers = max_workers if workers == -1 else workers

# the number of workers is limited between 1 and the number of available threads
return max(1, min(workers, max_workers))
Loading

0 comments on commit 75808fa

Please sign in to comment.