Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5f4d21d
MAINT: Add nd implementation of modpoly
derb12 Mar 24, 2026
2f3fbd4
MAINT: Add nd versions for all other polynomial methods
derb12 Mar 24, 2026
6cac7e6
MAINT: Unify _register methods for 1D and 2D
derb12 Mar 25, 2026
0b693a8
OTH: Make nd class a mixin so methods can be inherited
derb12 Mar 25, 2026
82ae913
MAINT: rename _register to _handle_io
derb12 Mar 26, 2026
4a68343
TST: Add tests for nd _handle_io
derb12 Mar 26, 2026
4aaf0ca
MAINT: Generalize _avg_opening for 1d or 2d
derb12 Mar 26, 2026
525c24c
TST: Remove tests for ensuring methods are wrapped
derb12 Mar 26, 2026
aeba33a
MAINT: Move mor, imor, and tophat to nd mixin
derb12 Mar 26, 2026
aae8ee7
Merge branch 'development' into merge_1d2d
derb12 Mar 28, 2026
d92bba8
MAINT: Standardize solve and direct_solve calls
derb12 Mar 28, 2026
b496dd1
MAINT: Add factorize and factorized_solve for 2D solvers
derb12 Mar 29, 2026
0baeca5
MAINT: Simplify result objects
derb12 Mar 29, 2026
bbed6bd
TST: Add tests for direct_solve for penalized objects
derb12 Mar 29, 2026
cd0bd07
MAINT: Simplify PenalizedSystem solve
derb12 Mar 29, 2026
2d837ae
MAINT: Add _setup_pls method
derb12 Mar 29, 2026
1a08cc7
MAINT: Move asls to an nd pls mixin method
derb12 Apr 1, 2026
3cf8be9
TST: Ensure pls result object tests disallow subclasses
derb12 Apr 1, 2026
1e5b700
MAINT: Move airpls to an nd pls mixin method
derb12 Apr 1, 2026
de42bf9
MAINT: Move arpls to an nd pls mixin method
derb12 Apr 1, 2026
023ff09
MAINT: Move iarpls to an nd pls mixin method
derb12 Apr 1, 2026
f5905f7
MAINT: Move psalsa to an nd pls mixin method
derb12 Apr 2, 2026
141cde7
MAINT: Move derpsalsa to an nd pls mixin method
derb12 Apr 2, 2026
7a13a85
MAINT: Move brpls to an nd pls mixin method
derb12 Apr 2, 2026
3657909
MAINT: Move lsrpls to an nd pls mixin method
derb12 Apr 2, 2026
ed9eeba
MAINT: Move mixture_model to an nd pls mixin method
derb12 Apr 2, 2026
9a6141a
MAINT: Move irsqr to an nd pls mixin method
derb12 Apr 2, 2026
a9decd1
DOC: Fix mask example for updated solve call
derb12 Apr 2, 2026
83948a6
MAINT: Guard against spline degree of None
derb12 Apr 3, 2026
78d0b75
MAINT: Move penalized_poly license statement to nd
derb12 Apr 3, 2026
1249dc5
MAINT: Update CI to include numba for 3.14t
derb12 Apr 4, 2026
7e03e71
TST: Ensure sorting works properly for batched input
derb12 Apr 4, 2026
0699bcc
MAINT: clean up returns for methods that inherit nd
derb12 Apr 4, 2026
2634558
TST: Add back tests for ensuring methods are wrapped
derb12 Apr 4, 2026
231f08d
MAINT: Rename _pls to pls to match conventions
derb12 Apr 4, 2026
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/workflows/python-test-latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
fail-fast: false
matrix:
# Choose the latest stable python version
python-version: ['3.13']
python-version: ['3.14']

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,11 @@ jobs:
run: pytest .

- name: Install optional dependencies
id: install-optional
# uncomment the if statement below to allow skipping versions
if: matrix.python-version != '3.14t'
#if: matrix.python-version != '3.14t'
run: python -m pip install .[full]

- name: Test with optional dependencies
if: steps.install-optional.outcome == 'success'
run: pytest .


Expand Down
2 changes: 1 addition & 1 deletion LICENSES_bundled.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ licensed sources, which are listed below.

Source: ported MATLAB code from https://www.mathworks.com/matlabcentral/fileexchange/27429-background-correction
(last accessed March 18, 2021)
Function: pybaselines.polynomial.penalized_poly
File: pybaselines._nd.polynomial.py
License: 2-clause BSD

Copyright (c) 2012, Vincent Mazet
Expand Down
4 changes: 1 addition & 3 deletions docs/examples/general/plot_masked_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,7 @@ def masked_arpls(y, mask=None, lam=1e5, diff_order=2, tol=1e-3, max_iter=50, wei
weights[~mask] = 0
whittaker_system = PenalizedSystem(len(y), lam=lam, diff_order=diff_order)
for _ in range(max_iter):
baseline = whittaker_system.solve(
whittaker_system.add_diagonal(weights), weights * y_fit,
)
baseline = whittaker_system.solve(y_fit, weights)
# need to ignore the problem regions in y since they would otherwise affect
# the arpls weighting; could alternatively do:
# _arpls(np.interp(x, x[mask], y[mask]), baseline) to approximate
Expand Down
140 changes: 110 additions & 30 deletions pybaselines/_algorithm_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
ParameterWarning, SortingWarning, _determine_sorts, _inverted_sort, _sort_array,
estimate_window, pad_edges
)
from .results import PSplineResult, WhittakerResult


class _Algorithm:
Expand Down Expand Up @@ -269,10 +270,10 @@ def _return_results(self, baseline, params, dtype, sort_keys=(), skip_sorting=Fa
return baseline, params

@classmethod
def _register(cls, func=None, *, sort_keys=(), ensure_1d=True, skip_sorting=False,
require_unique_x=False):
def _handle_io(cls, func=None, *, sort_keys=(), ensure_dims=True, skip_sorting=False,
require_unique=False, reshape_keys=None):
"""
Wraps a baseline function to validate inputs and correct outputs.
Wraps a baseline method to validate inputs and correct outputs.

The input data is converted to a numpy array, validated to ensure the length is
consistent, and ordered to match the input x ordering. The outputs are corrected
Expand All @@ -281,19 +282,22 @@ def _register(cls, func=None, *, sort_keys=(), ensure_1d=True, skip_sorting=Fals
Parameters
----------
func : Callable, optional
The function that is being decorated. Default is None, which returns a partial function.
The method that is being decorated. Default is None, which returns a partial function.
sort_keys : tuple, optional
The keys within the output parameter dictionary that will need sorting to match the
sort order of :attr:`.x`. Default is ().
ensure_1d : bool, optional
sort order of ``self.x``. Default is ().
ensure_dims : bool, optional
If True (default), will raise an error if the shape of `array` is not a one dimensional
array with shape (N,) or a two dimensional array with shape (N, 1) or (1, N).
skip_sorting : bool, optional
If True, will skip sorting the inputs and outputs, which is useful for algorithms that
use other algorithms so that sorting is already internally done. Default is False.
require_unique_x : bool, optional
If True, will check ``self.x`` to ensure all values are unique and will raise an error
require_unique : bool, optional
If True, will check `self.x` to ensure all values are unique and will raise an error
if non-unique values are present. Default is False, which skips the check.
reshape_keys : None, optional
Not used within this method, simply added to have the same call signature
as `_Algorithm2D._handle_io`.

Returns
-------
Expand All @@ -305,8 +309,8 @@ def _register(cls, func=None, *, sort_keys=(), ensure_1d=True, skip_sorting=Fals
"""
if func is None:
return partial(
cls._register, sort_keys=sort_keys, ensure_1d=ensure_1d, skip_sorting=skip_sorting,
require_unique_x=require_unique_x
cls._handle_io, sort_keys=sort_keys, ensure_dims=ensure_dims,
skip_sorting=skip_sorting, require_unique=require_unique
)

@wraps(func)
Expand All @@ -316,19 +320,19 @@ def inner(self, data=None, *args, **kwargs):
raise TypeError('"data" and "x_data" cannot both be None')
input_y = True
y, self.x = _yx_arrays(
data, check_finite=self._check_finite, ensure_1d=ensure_1d
data, check_finite=self._check_finite, ensure_1d=ensure_dims
)
self._size = y.shape[-1]
else:
if require_unique_x and not self._validated_x:
if require_unique and not self._validated_x:
if np.any(self.x[1:] == self.x[:-1]):
raise ValueError('x-values must be unique for the selected method')
else:
self._validated_x = True
if data is not None:
input_y = True
y = _check_sized_array(
data, self._size, check_finite=self._check_finite, ensure_1d=ensure_1d,
data, self._size, check_finite=self._check_finite, ensure_1d=ensure_dims,
name='data'
)
else:
Expand Down Expand Up @@ -391,7 +395,7 @@ def _setup_whittaker(self, y, lam=1, diff_order=2, weights=None, copy_weights=Fa
----------
y : numpy.ndarray, shape (N,)
The y-values of the measured data, already converted to a numpy
array by :meth:`~._Algorithm._register`.
array by :meth:`~._Algorithm._handle_io`.
lam : float, optional
The smoothing parameter, lambda. Typical values are between 10 and
1e8, but it strongly depends on the penalized least square method
Expand Down Expand Up @@ -445,8 +449,8 @@ def _setup_whittaker(self, y, lam=1, diff_order=2, weights=None, copy_weights=Fa
self._size, weights, copy_input=copy_weights, check_finite=self._check_finite,
dtype=float
)
if self._sort_order is not None and weights is not None:
weight_array = weight_array[self._sort_order]
if weights is not None:
weight_array = _sort_array(weight_array, self._sort_order)

allow_lower = allow_lower and self.banded_solver < 4
allow_penta = self.banded_solver < 3
Expand All @@ -459,15 +463,15 @@ def _setup_whittaker(self, y, lam=1, diff_order=2, weights=None, copy_weights=Fa
return y, weight_array, whittaker_system

def _setup_polynomial(self, y, weights=None, poly_order=2, calc_vander=True,
calc_pinv=False, copy_weights=False):
calc_pinv=False, copy_weights=False, max_cross=None):
"""
Sets the starting parameters for doing polynomial fitting.

Parameters
----------
y : numpy.ndarray, shape (N,)
The y-values of the measured data, already converted to a numpy
array by :meth:`~._Algorithm._register`.
array by :meth:`~._Algorithm._handle_io`.
weights : array-like, shape (N,), optional
The weighting array. If None (default), then will be an array with
size equal to N and all values set to 1.
Expand All @@ -481,6 +485,9 @@ def _setup_polynomial(self, y, weights=None, poly_order=2, calc_vander=True,
copy_weights : boolean, optional
If True, will copy the array of input weights. Only needed if the
algorithm changes the weights in-place. Default is False.
max_cross : None, optional
Not used within this method, simply added to have the same call signature
as `_Algorithm2D._setup_polynomial`.

Returns
-------
Expand Down Expand Up @@ -509,8 +516,8 @@ def _setup_polynomial(self, y, weights=None, poly_order=2, calc_vander=True,
self._size, weights, copy_input=copy_weights, check_finite=self._check_finite,
dtype=float
)
if self._sort_order is not None and weights is not None:
weight_array = weight_array[self._sort_order]
if weights is not None:
weight_array = _sort_array(weight_array, self._sort_order)

if calc_vander:
if self._polynomial is None:
Expand Down Expand Up @@ -542,7 +549,7 @@ def _setup_spline(self, y, weights=None, spline_degree=3, num_knots=10,
----------
y : numpy.ndarray, shape (N,)
The y-values of the measured data, already converted to a numpy
array by :meth:`~._Algorithm._register`.
array by :meth:`~._Algorithm._handle_io`.
weights : array-like, shape (N,), optional
The weighting array. If None (default), then will be an array with
size equal to N and all values set to 1.
Expand Down Expand Up @@ -597,8 +604,8 @@ def _setup_spline(self, y, weights=None, spline_degree=3, num_knots=10,
self._size, weights, dtype=float, order='C', copy_input=copy_weights,
check_finite=self._check_finite
)
if self._sort_order is not None and weights is not None:
weight_array = weight_array[self._sort_order]
if weights is not None:
weight_array = _sort_array(weight_array, self._sort_order)

if not make_basis:
return y, weight_array
Expand All @@ -623,6 +630,79 @@ def _setup_spline(self, y, weights=None, spline_degree=3, num_knots=10,

return y, weight_array, pspline

def _setup_pls(self, y, weights=None, spline_degree=None, num_knots=10,
diff_order=2, lam=1, allow_lower=True, reverse_diags=False,
copy_weights=False, num_eigens=None):
"""
Sets the starting parameters for methods using penalized least squares.

Depending on the input of `spline_degree`, will dispatch to either
`_setup_whittaker` or `_setup_spline`.

Parameters
----------
y : numpy.ndarray, shape (N,)
The y-values of the measured data, already converted to a numpy
array by :meth:`~._Algorithm._handle_io`.
weights : array-like, shape (N,), optional
The weighting array. If None (default), then will be an array with
size equal to N and all values set to 1.
spline_degree : int or None, optional
If None (default), denotes that the system is using Whittaker smoothing.
Otherwise, the system is a penalized spline with a spline degree of `spline_degree`.
num_knots : int, optional
The number of interior knots for the splines. Only used if `spline_degree` is
not None. Default is 10.
diff_order : int, optional
The integer differential order for the penalty; must be greater than 0.
Default is 2.
lam : float, optional
The smoothing parameter, lambda. Typical values are between 10 and
1e8, but it strongly depends on `diff_order` and the data size.
Default is 1.
allow_lower : boolean, optional
If True (default), will include only the lower non-zero diagonals of
the squared difference matrix. If False, will include all non-zero diagonals.
reverse_diags : boolean, optional
If True, will reverse the order of the diagonals of the penalty matrix.
Default is False.
copy_weights : boolean, optional
If True, will copy the array of input weights. Only needed if the
algorithm changes the weights in-place. Default is False.
num_eigens : None, optional
Not used within this method, simply added to have the same call signature
as `_Algorithm2D._setup_pls`.

Returns
-------
y : numpy.ndarray, shape (N,)
The y-values of the measured data, converted to a numpy array.
weight_array : numpy.ndarray, shape (N,)
The weight array for fitting the spline to the data.
penalized_system : PenalizedSystem or PSpline
The object for solving the penalized least squared system. If `spline_degree`
is None, returns a PenalizedSystem object;, otherwise, returns a PSpline.
result_class : WhittakerResult or PSplineResult
The result class for defining the solution. If `spline_degree`
is None, returns WhittakerResult; otherwise, returns PSplineResult.

"""
if spline_degree is None:
y, weight_array, penalized_system = self._setup_whittaker(
y, lam=lam, diff_order=diff_order, weights=weights, copy_weights=copy_weights,
allow_lower=allow_lower, reverse_diags=reverse_diags
)
result_class = WhittakerResult
else:
y, weight_array, penalized_system = self._setup_spline(
y, lam=lam, diff_order=diff_order, weights=weights, copy_weights=copy_weights,
allow_lower=allow_lower, reverse_diags=reverse_diags,
spline_degree=spline_degree, num_knots=num_knots, penalized=True, make_basis=True
)
result_class = PSplineResult

return y, weight_array, penalized_system, result_class

def _setup_morphology(self, y, half_window=None, window_kwargs=None, **kwargs):
"""
Sets the starting parameters for morphology-based methods.
Expand All @@ -631,7 +711,7 @@ def _setup_morphology(self, y, half_window=None, window_kwargs=None, **kwargs):
----------
y : numpy.ndarray, shape (N,)
The y-values of the measured data, already converted to a numpy
array by :meth:`~._Algorithm._register`.
array by :meth:`~._Algorithm._handle_io`.
half_window : int, optional
The half-window used for the morphology functions. If a value is input,
then that value will be used. Default is None, which will optimize the
Expand Down Expand Up @@ -686,7 +766,7 @@ def _setup_smooth(self, y, half_window=None, pad_type='half', window_multiplier=
----------
y : numpy.ndarray, shape (N,)
The y-values of the measured data, already converted to a numpy
array by :meth:`~._Algorithm._register`.
array by :meth:`~._Algorithm._handle_io`.
half_window : int, optional
The half-window used for the smoothing functions. Used
to pad the left and right edges of the data to reduce edge
Expand Down Expand Up @@ -753,7 +833,7 @@ def _setup_classification(self, y, weights=None, **kwargs):
----------
y : numpy.ndarray, shape (N,)
The y-values of the measured data, already converted to a numpy
array by :meth:`~._Algorithm._register`.
array by :meth:`~._Algorithm._handle_io`.
weights : array-like, shape (N,), optional
The weighting array. If None (default), then will be an array with
size equal to N and all values set to 1.
Expand All @@ -772,8 +852,8 @@ def _setup_classification(self, y, weights=None, **kwargs):
weight_array = _check_optional_array(
self._size, weights, dtype=bool, check_finite=self._check_finite
)
if self._sort_order is not None and weights is not None:
weight_array = weight_array[self._sort_order]
if weights is not None:
weight_array = _sort_array(weight_array, self._sort_order)

return y, weight_array

Expand Down Expand Up @@ -853,7 +933,7 @@ def _setup_optimizer(self, y, method, modules, method_kwargs=None, copy_kwargs=T
----------
y : numpy.ndarray, shape (N,)
The y-values of the measured data, already converted to a numpy
array by :meth:`~._Algorithm._register`.
array by :meth:`~._Algorithm._handle_io`.
method : str
The string name of the desired function, like 'asls'. Case does not matter.
modules : Sequence[module, ...]
Expand Down Expand Up @@ -911,7 +991,7 @@ def _setup_misc(self, y):
----------
y : numpy.ndarray, shape (N,)
The y-values of the measured data, already converted to a numpy
array by :meth:`~._Algorithm._register`.
array by :meth:`~._Algorithm._handle_io`.

Returns
-------
Expand Down
Loading