Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fourier #300

Draft
wants to merge 27 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a017437
first commit
BalzaniEdoardo Jan 23, 2025
c075b9a
Merge branch 'main' into fourier
BalzaniEdoardo Jan 23, 2025
32e90b7
added fourier basis
BalzaniEdoardo Jan 23, 2025
ef96185
linted
BalzaniEdoardo Jan 23, 2025
413ebf0
Update deploy-pure-python.yml
BalzaniEdoardo Jan 23, 2025
a8246d8
Merge branch 'development' into fourier
BalzaniEdoardo Jan 28, 2025
df910f8
Update README.md
BalzaniEdoardo Feb 1, 2025
55c2f85
linted
BalzaniEdoardo Feb 1, 2025
a3bee71
fixed readme
BalzaniEdoardo Feb 1, 2025
dd6929f
linted with black new version
BalzaniEdoardo Feb 1, 2025
36435fb
Merge pull request #303 from flatironinstitute/BalzaniEdoardo-patch-1
BalzaniEdoardo Feb 1, 2025
24b2b57
Update CONTRIBUTING.md
arnabiswas Feb 4, 2025
8361cf8
first attempt add link checks
BalzaniEdoardo Feb 4, 2025
15bb1ce
push ci
BalzaniEdoardo Feb 4, 2025
d220a80
add
BalzaniEdoardo Feb 4, 2025
e57033c
add
BalzaniEdoardo Feb 4, 2025
1694ed0
added
BalzaniEdoardo Feb 4, 2025
b6ef893
use a md check action
BalzaniEdoardo Feb 4, 2025
7cc9601
try config file to only search files not in the static website (those…
BalzaniEdoardo Feb 4, 2025
a222a15
add conf
BalzaniEdoardo Feb 4, 2025
dc1fe25
remove config and add file path option
BalzaniEdoardo Feb 4, 2025
e5d1106
do not use action
BalzaniEdoardo Feb 4, 2025
70ecb43
try to set error flag
BalzaniEdoardo Feb 4, 2025
7188503
fix all links and make sure ci fails
BalzaniEdoardo Feb 4, 2025
6d9a72e
fix relative path in bash script
BalzaniEdoardo Feb 4, 2025
07a9b22
Merge pull request #304 from arnabiswas/patch-1
BalzaniEdoardo Feb 4, 2025
ad5ac6e
Merge branch 'main' into fourier
BalzaniEdoardo Feb 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,28 @@ jobs:
- name: Check links
run: ./bash_scripts/prevent_absolute_links_to_docs.sh

check-links:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install markdown-link-check
run: npm install -g markdown-link-check

- name: Make .sh executable
run: chmod +x bash_scripts/check_markdown_links.sh

- name: Check links in root Markdown files
run: ./bash_scripts/check_markdown_links.sh

check:
if: ${{ !github.event.pull_request.draft }}
needs:
- tox_tests
- prevent_docs_absolute_links
- tox_check
- check-links
runs-on: ubuntu-latest
steps:
- name: Decide whether all tests and notebooks succeeded
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/deploy-pure-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
name: Build and test package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
# this is necessary for setuptools_scm to work properly with github
# actions, see https://github.com/pypa/setuptools_scm/issues/480 and
# https://stackoverflow.com/a/68959339
Expand Down Expand Up @@ -70,4 +70,4 @@ jobs:
name: artifact
path: dist
- name: Publish package to pypi
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@release/v1
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Developers are encouraged to contribute to various areas of development. This co

Feel free to work on any section of code that you believe you can improve. More importantly, remember to thoroughly test all your classes and functions, and to provide clear, detailed comments within your code. This not only aids others in using the library, but also facilitates future maintenance and further development.

For more detailed information about NeMoS modules, including design choices and implementation details, visit the [`For Developers`](https://nemos.readthedocs.io/en/latest/developers_notes/) section of the package documentation.
For more detailed information about NeMoS modules, including design choices and implementation details, visit the [`For Developers`](https://nemos.readthedocs.io/en/latest/developers_notes/README.html) section of the package documentation.

## Contributing to the code

Expand Down Expand Up @@ -46,7 +46,7 @@ pip install -e .[dev]
```

> [!NOTE]
> In order to install `nemos` in editable mode you will need a Python virtual environment. Please see our documentation [here](https://nemos.readthedocs.io/en/latest/installation/) that provides guidance on how to create and activate a virtual environment.
> In order to install `nemos` in editable mode you will need a Python virtual environment. Please see our documentation [here](https://nemos.readthedocs.io/en/latest/installation.html) that provides guidance on how to create and activate a virtual environment.

3) Add the upstream branch:

Expand Down Expand Up @@ -184,7 +184,7 @@ it must have an `.py` extension, and it must be contained within the `tests` dir
add it to the tests-to-run.

> [!NOTE]
> If you have many variants on a test you wish to run, you should make use of pytest's `parameterize` mark. See the official documentation [here](https://docs.pytest.org/en/stable/how-to/parametrize.html) and NeMoS [`test_error_invalid_entry`](https://github.com/flatironinstitute/nemos/blob/main/tests/test_vallidation.py#L27) for a concrete implementation.
> If you have many variants on a test you wish to run, you should make use of pytest's `parameterize` mark. See the official documentation [here](https://docs.pytest.org/en/stable/how-to/parametrize.html) and NeMoS [`test_error_invalid_entry`](https://github.com/flatironinstitute/nemos/blob/main/tests/test_validation.py) for a concrete implementation.

> [!NOTE]
> If you are using an object that gets used in multiple tests (such as a model with certain data, regularizer, or solver), you should use pytest's `fixtures` to avoid having to load or instantiate the object multiple times. Look at our `conftest.py` to see already available fixtures for your tests. See the official documentation [here](https://docs.pytest.org/en/stable/how-to/fixtures.html).
Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ We provide a **Poisson GLM** for analyzing spike counts, and a **Gamma GLM** for
The package is under active development and more methods will be added in the future.

For those looking to get a better grasp of the Generalized Linear Model, we recommend checking out the
Neuromatch Academy's lesson [here](https://compneuro.neuromatch.io/tutorials/W1D3_GeneralizedLinearModels/student/W1D3_Tutorial1.htmls) and Jonathan Pillow's tutorial
Neuromatch Academy's lesson [here](https://compneuro.neuromatch.io/tutorials/W1D3_GeneralizedLinearModels/student/W1D3_Tutorial1.html) and Jonathan Pillow's tutorial
from Cosyne 2018 [here](https://www.youtube.com/watch?v=NFeGW5ljUoI&t=424s).

## Overview
Expand Down Expand Up @@ -65,9 +65,9 @@ In this example, we'll construct a time-series of features using the basis objec
import nemos as nmo

# Instantiate the basis
basis_1 = nmo.basis.MSplineBasis(n_basis_funcs=5)
basis_2 = nmo.basis.CyclicBSplineBasis(n_basis_funcs=6)
basis_3 = nmo.basis.MSplineBasis(n_basis_funcs=7)
basis_1 = nmo.basis.MSplineEval(n_basis_funcs=5)
basis_2 = nmo.basis.CyclicBSplineEval(n_basis_funcs=6)
basis_3 = nmo.basis.MSplineEval(n_basis_funcs=7)

basis = basis_1 * basis_2 + basis_3

Expand Down Expand Up @@ -99,7 +99,7 @@ ll = glm.score(X, y)
<img src="docs/assets/glm_population_scheme.svg" width="84%">

This second example demonstrates feature construction by convolving the simultaneously recorded population spike counts with a bank of filters, utilizing the basis in `conv` mode.
The figure above show the GLM scheme for a single neuron, however in NeMoS you can fit jointly the whole population with the [`PopulationGLM`](https://nemos.readthedocs.io/en/latest/generated/how_to_guide/plot_04_population_glm/) object.
The figure above show the GLM scheme for a single neuron, however in NeMoS you can fit jointly the whole population with the [`PopulationGLM`](https://nemos.readthedocs.io/en/latest/how_to_guide/plot_03_population_glm.html) object.

#### Feature Representation

Expand All @@ -111,7 +111,7 @@ import nemos as nmo

# generate 5 basis functions of 100 time-bins,
# and convolve the counts with the basis.
X = nmo.basis.RaisedCosineBasisLog(5, mode="conv", window_size=100
X = nmo.basis.RaisedCosineLogConv(5, window_size=100
).compute_features(spike_counts)
```
#### Population GLM
Expand All @@ -127,8 +127,8 @@ firing_rate = glm.predict(X)
ll = glm.score(X, spike_counts)
```

For a deeper dive, see our [Quickstart](https://nemos.readthedocs.io/en/latest/quickstart/) guide and consider using [pynapple](https://github.com/pynapple-org/pynapple) for data exploration and preprocessing. When initializing the GLM object, you may optionally specify an [observation
model](https://nemos.readthedocs.io/en/latest/reference/nemos/observation_models/) and a [regularizer](https://nemos.readthedocs.io/en/latest/reference/nemos/regularizer/).
For a deeper dive, see our [Quickstart](https://nemos.readthedocs.io/en/latest/quickstart.html) guide and consider using [pynapple](https://github.com/pynapple-org/pynapple) for data exploration and preprocessing. When initializing the GLM object, you may optionally specify an [observation
model](https://nemos.readthedocs.io/en/latest/api_reference.html#the-nemos-observation-models-module) and a [regularizer](https://nemos.readthedocs.io/en/latest/api_reference.html#the-nemos-regularizer-module).

> **Note: Multi-epoch Convolution**
>
Expand All @@ -149,7 +149,7 @@ Run the following `pip` command in your virtual environment.
python -m pip install nemos
```

For more details, including specifics for GPU users and developers, refer to NeMoS [docs](https://nemos.readthedocs.io/en/latest/installation/).
For more details, including specifics for GPU users and developers, refer to NeMoS [docs](https://nemos.readthedocs.io/en/latest/installation.html).


## Disclaimer
Expand Down
35 changes: 35 additions & 0 deletions bash_scripts/check_markdown_links.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash

# Fail script if any command fails
set -e

# Find markdown-link-check path
MARKDOWN_LINK_CHECK=$(which markdown-link-check || echo "")

# If markdown-link-check is not found, print an error and exit
if [[ -z "$MARKDOWN_LINK_CHECK" ]]; then
echo "❌ ERROR: markdown-link-check command not found. Make sure it is installed globally."
exit 1
fi

echo "🔍 Checking Markdown links in root directory..."

# Initialize an error flag
ERROR=0
LOG_FILE=$(mktemp) # Temporary file to store output

# Find all Markdown files in the root directory and check links
for file in $(find . -maxdepth 1 -name "*.md"); do
echo "📂 Checking $file..."

# Run markdown-link-check and capture output
$MARKDOWN_LINK_CHECK "$file" 2>&1 | tee -a "$LOG_FILE"
done

# Check if "ERROR:" appears in the log file
if grep -q "ERROR:" "$LOG_FILE"; then
echo "🚨 Link check failed! Please fix broken links."
exit 1
else
echo "✅ All links are valid."
fi
1 change: 1 addition & 0 deletions src/nemos/basis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
BSplineEval,
CyclicBSplineConv,
CyclicBSplineEval,
FourierEval,
HistoryConv,
IdentityEval,
MSplineConv,
Expand Down
162 changes: 162 additions & 0 deletions src/nemos/basis/_fourier_basis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import abc
from typing import Optional, Tuple

import numpy as np
from numpy.typing import ArrayLike, NDArray
from pynapple import Tsd, TsdFrame, TsdTensor

from ..type_casting import support_pynapple
from ..typing import FeatureMatrix
from ._basis import Basis, check_transform_input, min_max_rescale_samples
from ._basis_mixin import AtomicBasisMixin


class FourierBasis(Basis, AtomicBasisMixin, abc.ABC):
"""Fourier Basis.

Parameters
----------
n_basis_funcs :
The number of basis functions.
include_constant:
Include the constant term, which corresponds to 0 frequency. Default is False.
mode :
The mode of operation. 'eval' for evaluation at sample points,
'conv' for convolutional operation.
label :
The label of the basis, intended to be descriptive of the task variable being processed.
For example: velocity, position, spike_counts.

"""

def __init__(
self,
n_frequencies: int,
include_constant: bool = False,
mode="eval",
label: Optional[str] = "RaisedCosineBasisLinear",
) -> None:

self.include_constant = include_constant
self._n_input_dimensionality = 1

# this sets the _n_basis_funcs too
self.n_frequencies = n_frequencies

AtomicBasisMixin.__init__(self, n_basis_funcs=self._n_basis_funcs)
super().__init__(
mode=mode,
label=label,
)

@property
def include_constant(self):
return bool(self._include_constant)

@include_constant.setter
def include_constant(self, value):
if not isinstance(value, bool):
raise TypeError(
f"`include_constant` must be a boolean. {value} provided instead!"
)
# store as int (used in slicing)
self._include_constant = int(value)

@support_pynapple(conv_type="numpy")
@check_transform_input
def _evaluate( # call these _evaluate
self,
sample_pts: ArrayLike | Tsd | TsdFrame | TsdTensor,
) -> FeatureMatrix:
"""Generate basis functions with given samples.

Parameters
----------
sample_pts :
Spacing for basis functions, holding elements on interval [0, 1].
`sample_pts` is a n-dimensional (n >= 1) array with first axis being the samples, i.e.
`sample_pts.shape[0] == n_samples`.

Raises
------
ValueError
If the sample provided do not lie in [0,1].

"""

# scale to 0, 1
sample_pts: NDArray = min_max_rescale_samples(
np.copy(sample_pts), getattr(self, "bounds", None)
)[0]
# first sample in 0, last sample in 2 pi - 2 pi / n_samples.
sample_pts *= 2 * np.pi * (1.0 - 1.0 / sample_pts.shape[0])

# reshape samples
shape = sample_pts.shape
sample_pts = sample_pts.reshape(
-1,
)

# compute angles
angles = np.outer(sample_pts, self._frequencies)
out = np.concatenate(
[np.cos(angles), -np.sin(angles[:, int(self._include_constant) :])], axis=1
)
return out.reshape(*shape, out.shape[1])

def evaluate_on_grid(self, n_samples: int) -> Tuple[NDArray, NDArray]:
"""Evaluate the basis set on a grid of equi-spaced sample points.

Parameters
----------
n_samples :
The number of points in the uniformly spaced grid. A higher number of
samples will result in a more detailed visualization of the basis functions.

Returns
-------
X :
Array of shape (n_samples,) containing the equi-spaced sample
points where we've evaluated the basis.
basis_funcs :
Fourier basis, shape (n_samples, n_basis_funcs)
"""
return super().evaluate_on_grid(n_samples)

@property
def n_basis_funcs(self) -> tuple | None:
"""Read-only property for Fourier basis."""
return super().n_basis_funcs

@property
def n_frequencies(self) -> int:
"""Number of frequencies for the basis."""
return int(self._frequencies[-1])

@n_frequencies.setter
def n_frequencies(self, value: int):
if not isinstance(value, int):
raise TypeError(f"`value` must be an integer. {value} provided instead!")
if value <= 0:
raise ValueError(
f"`value` must be a positive integer. {value} provided instead!"
)
self._n_basis_funcs = 2 * value + self._include_constant
self._frequencies = np.arange(
1 - self._include_constant, value + 1, dtype=float
)

def _check_n_basis_min(self) -> None:
"""Check that the user required enough basis elements.
Checks that the number of basis is at least 1.

Raises
------
ValueError
If a negative number of basis is provided.
"""
if self.n_basis_funcs < 0:
raise ValueError(
f"Object class {self.__class__.__name__} requires >= 1 basis elements. "
f"{self.n_basis_funcs} basis elements specified instead"
)
Loading