Skip to content

Commit 57095c5

Browse files
committed
add pybaselines in dependencies, pump to v26.36.1
1 parent 127bf59 commit 57095c5

5 files changed

Lines changed: 72 additions & 9 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies = [
4040
"openpyxl >=3.1.5",
4141
"pandas",
4242
"pyarrow >= 15.0.0",
43+
"pybaselines >= 1.2.1",
4344
"matplotlib >=3.6.2, <3.10.9",
4445
"pyside6",
4546
"scipy",

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ matplotlib>=3.6.2,<3.10.9
22
openpyxl>=3.1.2
33
pandas>=3.0.1
44
pyarrow>=15.0.0
5+
pybaselines>=1.2.1
56
pyqt_toast_notification==1.3.3
67
PySide6==6.10.2
78
PySide6_Addons==6.10.2

spectroview/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# The app uses PySide6 throughout, but superqt (used for QLabeledDoubleRangeSlider)
77
os.environ.setdefault("QT_API", "pyside6")
88

9-
VERSION = "26.35.4"
9+
VERSION = "26.36.1"
1010

1111

1212
TEXT_EXPIRE = (

spectroview/fit_engine/baseline.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
import numpy as np
22

3+
4+
class BaselineEvaluationError(RuntimeError):
5+
"""Raised when an automatic baseline method cannot be evaluated."""
6+
7+
8+
def _evaluation_error(mode: str, exc: Exception) -> BaselineEvaluationError:
9+
if isinstance(exc, ImportError):
10+
detail = (
11+
"the required 'pybaselines' package is not installed correctly"
12+
)
13+
else:
14+
detail = str(exc) or exc.__class__.__name__
15+
return BaselineEvaluationError(
16+
f"Automatic baseline method '{mode}' failed: {detail}."
17+
)
18+
19+
320
_INTERNAL_METHODS = {
421
None: {'label': 'None', 'use_points': False},
522
'Linear': {'label': 'Linear Interpolation', 'use_points': True, 'sigma_kwarg': 'sigma', 'category': 'Manual'},
@@ -90,17 +107,17 @@ def eval_baseline(x: np.ndarray, y: np.ndarray, config: dict) -> np.ndarray:
90107
lam = 10 ** config.get("coef", 5.0)
91108
b, _ = baseline_fitter.arpls(y, lam=lam)
92109
return b
93-
except Exception:
94-
return np.zeros_like(x)
110+
except Exception as exc:
111+
raise _evaluation_error(mode, exc) from exc
95112
elif mode == 'sonneveld_vesser':
96113
try:
97114
from pybaselines.classification import Classification
98115
baseline_fitter = Classification(x_data=x)
99116
niter = config.get("coef", 100)
100117
b, _ = baseline_fitter.dietrich(y, num_iter=int(niter)) # Just an approximation for Sonneveld-Vesser
101118
return b
102-
except Exception:
103-
return np.zeros_like(x)
119+
except Exception as exc:
120+
raise _evaluation_error(mode, exc) from exc
104121
else:
105122
try:
106123
from pybaselines import Baseline
@@ -122,9 +139,9 @@ def eval_baseline(x: np.ndarray, y: np.ndarray, config: dict) -> np.ndarray:
122139
b, _ = func(y, **kwargs)
123140
return b
124141
else:
125-
return np.zeros_like(x)
126-
except Exception:
127-
return np.zeros_like(x)
142+
raise ValueError(f"unsupported automatic baseline method '{mode}'")
143+
except Exception as exc:
144+
raise _evaluation_error(mode, exc) from exc
128145

129146
def eval_baseline_batch(x: np.ndarray, Y: np.ndarray, config: dict) -> np.ndarray:
130147
"""Evaluate baseline for a batch of spectra (N, M)."""

tests/unit/fit_engine/test_baseline.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
"""Unit tests for fit_engine/baseline.py - single and batched baseline evaluation."""
2+
import builtins
3+
from pathlib import Path
4+
25
import numpy as np
36
import pytest
47

58
from spectroview.fit_engine.baseline import (
6-
eval_baseline, eval_baseline_batch, get_baseline_method_meta,
9+
BaselineEvaluationError, eval_baseline, eval_baseline_batch,
10+
get_baseline_method_meta,
711
)
812

913

@@ -86,6 +90,46 @@ def test_order_capped_by_number_of_points(self, x, linear_y):
8690
np.testing.assert_allclose(baseline, linear_y, atol=1e-6)
8791

8892

93+
class TestEvalBaselineAutomatic:
94+
@pytest.mark.parametrize("mode", ["airpls", "arpls", "asls", "modpoly"])
95+
def test_supported_method_returns_a_real_baseline(self, x, mode):
96+
background = 8.0 + 0.02 * x + 0.0005 * (x - 50.0) ** 2
97+
peak = 30.0 * np.exp(-0.5 * ((x - 55.0) / 4.0) ** 2)
98+
y = background + peak
99+
config = {"mode": mode, "coef": 5.0, "order_max": 2}
100+
101+
baseline = eval_baseline(x, y, config)
102+
103+
assert baseline.shape == y.shape
104+
assert np.all(np.isfinite(baseline))
105+
assert np.any(baseline != 0)
106+
107+
def test_missing_pybaselines_is_not_silently_treated_as_zero(
108+
self, x, linear_y, monkeypatch):
109+
real_import = builtins.__import__
110+
111+
def reject_pybaselines(name, *args, **kwargs):
112+
if name == "pybaselines" or name.startswith("pybaselines."):
113+
raise ModuleNotFoundError("No module named 'pybaselines'")
114+
return real_import(name, *args, **kwargs)
115+
116+
monkeypatch.setattr(builtins, "__import__", reject_pybaselines)
117+
118+
with pytest.raises(BaselineEvaluationError, match="pybaselines"):
119+
eval_baseline(x, linear_y, {"mode": "airpls", "coef": 5.0})
120+
121+
def test_pybaselines_is_declared_as_a_direct_dependency(self):
122+
project_root = Path(__file__).parents[3]
123+
pyproject = (project_root / "pyproject.toml").read_text(encoding="utf-8")
124+
requirements = (project_root / "requirements.txt").read_text(encoding="utf-8")
125+
126+
assert '"pybaselines ' in pyproject
127+
assert any(
128+
line.lower().startswith("pybaselines")
129+
for line in requirements.splitlines()
130+
)
131+
132+
89133
class TestEvalBaselineBatchMatchesPerSpectrumLoop:
90134
"""eval_baseline_batch has a fully-vectorized fast path for Linear and
91135
Polynomial; it must match calling eval_baseline() row-by-row."""

0 commit comments

Comments
 (0)