Skip to content
Merged

Test #51

Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/actions/install_package/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ runs:
- name: Install Dependencies
shell: bash
run: |
uv sync
uv sync --refresh
echo "python-version=${{ steps.setup_python.outputs.python-version }}" >> "$GITHUB_OUTPUT"
21 changes: 12 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
[project]
name = "funcnodes-span"
version = "0.3.7"
version = "1.0.0"
description = "SPectral ANalysis (SPAN) for funcnodes"
readme = "README.md"
classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",]
requires-python = ">=3.11"
dependencies = [
"scipy",
"lmfit",
"funcnodes",
"funcnodes_numpy",
"funcnodes_pandas",
"funcnodes_plotly",
"funcnodes>=1.0.0",
"funcnodes-core>=1.0.4",
"funcnodes-numpy",
"funcnodes-pandas",
"funcnodes-plotly",
"pybaselines",
"funcnodes-lmfit>=0.2.0",
"numba (>=0.61.0,<1.0.0)",
Expand Down Expand Up @@ -46,11 +47,13 @@ download = "https://pypi.org/project/funcnodes-span/#files"
module = "funcnodes_span"
shelf = "funcnodes_span:NODE_SHELF"

[tool.setuptools.package-dir]
"" = "src"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/funcnodes_span"]

[tool.setuptools.packages.find]
where = [ "src",]

[tool.commitizen]
name = "cz_conventional_commits"
Expand Down
2 changes: 1 addition & 1 deletion src/funcnodes_span/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from funcnodes_lmfit import NODE_SHELF as LMFIT_NODE_SHELF
from .curves import CURVES_NODE_SHELF

__version__ = "0.3.6"
__version__ = "1.0.0"

NODE_SHELF = fn.Shelf(
name="Spectral Analysis",
Expand Down
131 changes: 114 additions & 17 deletions src/funcnodes_span/peak_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from funcnodes_span._curves import estimate_noise
import numpy as np
from exposedfunctionality import controlled_wrapper
from typing import Optional, List, Tuple
from typing import Annotated, Literal, Optional, List, Tuple
from scipy.signal import find_peaks
from scipy import signal, interpolate
from scipy.ndimage import gaussian_filter1d
Expand Down Expand Up @@ -32,25 +32,122 @@
@NodeDecorator(
id="span.basics.peaks",
name="Peak finder",
outputs=[{"name": "peaks"}, {"name": "norm_x"}, {"name": "norm_y"}],
)
@controlled_wrapper(find_peaks, wrapper_attribute="__fnwrapped__")
def peak_finder(
y: np.ndarray,
x: Optional[np.ndarray] = None,
on: Optional[np.ndarray] = None,
noise_level: Optional[int] = None,
height: Optional[float] = None,
threshold: Optional[float] = None,
distance: Optional[float] = None,
prominence: Optional[float] = None,
width: Optional[float] = None,
wlen: Optional[int] = None,
rel_height: float = 0.05,
width_at_rel_height: float = 0.5,
plateau_size: Optional[int] = None,
) -> Tuple[List[PeakProperties], np.ndarray, np.ndarray]:
""" """
y: Annotated[
np.ndarray,
fn.InputMeta(
description="Original 1D signal to analyze for peaks. If `x` is provided, must have same length."
),
],
x: Annotated[
Optional[np.ndarray],
fn.InputMeta(
description="Optional x-axis values (strictly increasing). If given, widths/distances/wlen/plateau_size are interpreted in x-units and signals are density-normalized; indices remain aligned."
),
] = None,
on: Annotated[
Optional[np.ndarray],
fn.InputMeta(
hidden=True,
description="Optional signal to use for detection (e.g., smoothed/denoised). Measurements/centering can still use the original `y`."
),
] = None,
index_source: Annotated[
Literal["original", "on", "centroid"],
fn.InputMeta(
hidden=True,
description="How to choose the peak center within [i,f]: 'original' uses argmax on `y`; 'on' keeps detection index; 'centroid' uses center-of-mass on `y`.",
),
] = "original",
noise_level: Annotated[
Optional[int],
fn.InputMeta(
hidden=True,
description="Scales synthetic noise added for valley detection. Effective σ ≈ estimate_noise(x,y)/noise_level. Larger values = less added noise. Default ~5000.",
),
] = None,
height: Annotated[
Optional[float],
fn.InputMeta(
hidden=True,
description="Minimum peak height for detection on the detection signal (`on` if given, else `y`). If None, uses `rel_height * max(detection signal)`.",
),
] = None,
threshold: Annotated[
Optional[float],
fn.InputMeta(
hidden=True,
description="Required vertical step to neighbors for a peak (same units as detection signal). See `scipy.signal.find_peaks`.",
),
] = None,
distance: Annotated[
Optional[float],
fn.InputMeta(
hidden=True,
description="Minimum horizontal distance between neighboring peaks. Interpreted in x-units if `x` is provided, otherwise in samples.",
),
] = None,
prominence: Annotated[
Optional[float],
fn.InputMeta(
hidden=True,
description="Minimum required prominence of peaks, computed on the detection signal. See `scipy.signal.find_peaks`.",
),
] = None,
width: Annotated[
Optional[float],
fn.InputMeta(
hidden=True,
description="Minimum peak width measured at `width_at_rel_height` of peak height. In x-units if `x` is provided, else in samples.",
),
] = None,
wlen: Annotated[
Optional[int],
fn.InputMeta(
hidden=True,
description="Window length used for prominence calculation. If `x` is provided, specify in x-units (internally converted to samples); otherwise in samples.",
),
] = None,
rel_height: Annotated[
float,
fn.InputMeta(
description="Fallback height factor. If `height` is None, detection height = `rel_height * max(detection signal)`."
),
] = 0.05,
width_at_rel_height: Annotated[
float,
fn.InputMeta(
hidden=True,
description="Relative height at which peak width is measured. 0.5 corresponds to FWHM. Passed to `find_peaks(rel_height=...)`.",
),
] = 0.5,
plateau_size: Annotated[
Optional[int],
fn.InputMeta(
hidden=True,
description="Minimum length of a flat-top (plateau) at the peak. In x-units if `x` is provided, else in samples. See `scipy.signal.find_peaks`.",
),
] = None,
) -> Tuple[
Annotated[List[PeakProperties], fn.OutputMeta(description="Peak properties",name="peaks")],
Annotated[np.ndarray, fn.OutputMeta(description="Normalized X-axis values",name="norm_x")],
Annotated[np.ndarray, fn.OutputMeta(description="Normalized Y-axis values",name="norm_y")],
]:
# Tuple[List[PeakProperties], np.ndarray, np.ndarray]:
"""
Detect peaks on an optional processed signal and report peak bounds and center.

Parameters
- y: Original signal for reporting measurements.
- x: Optional x/axis values. If provided, density normalization preserves indices.
- on: Optional signal to use for detection (e.g., smoothed/denoised).
- index_source: How to choose peak center inside detected bounds:
* "original": argmax on original `_y` within [i,f] (default).
* "on": keep detection index as-is.
* "centroid": center-of-mass on `_y` within [i,f].
"""
peak_lst = []

y = np.array(y, dtype=float)
Expand Down
5 changes: 5 additions & 0 deletions tests/test_peak_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,18 @@ async def test_plot_peak(self):
plotter = plot_peak()

plotter.inputs["peak"].connect(idxnode.outputs["element"])
print("AAA",peaks.inputs["y"].is_connected(),plotter.inputs["y"].is_connected())
plotter.inputs["y"].forwards_from(peaks.inputs["y"])
plotter.inputs["x"].forwards_from(peaks.inputs["x"])

await fn.run_until_complete(peaks, plotter, idxnode)

self.assertIsInstance(plotter.outputs["figure"].value, go.Figure)

async def test_peak_finder_annotations(self):
n = peak_finder()
assert n["on"].hidden


class TestInterpolation(unittest.IsolatedAsyncioTestCase):
async def test_default(self):
Expand Down
Loading
Loading