funcnodes-span 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {funcnodes_span-0.2.0 → funcnodes_span-0.2.2}/PKG-INFO +2 -2
- {funcnodes_span-0.2.0 → funcnodes_span-0.2.2}/funcnodes_span/__init__.py +1 -1
- {funcnodes_span-0.2.0 → funcnodes_span-0.2.2}/funcnodes_span/baseline.py +23 -55
- funcnodes_span-0.2.2/funcnodes_span/normalization.py +161 -0
- {funcnodes_span-0.2.0 → funcnodes_span-0.2.2}/funcnodes_span/peak_analysis.py +78 -42
- {funcnodes_span-0.2.0 → funcnodes_span-0.2.2}/funcnodes_span/smoothing.py +57 -52
- {funcnodes_span-0.2.0 → funcnodes_span-0.2.2}/pyproject.toml +2 -2
- funcnodes_span-0.2.0/funcnodes_span/normalization.py +0 -56
- {funcnodes_span-0.2.0 → funcnodes_span-0.2.2}/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: funcnodes-span
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary:
|
|
5
5
|
Home-page: https://linkdlab.de/
|
|
6
6
|
Author: Kourosh Rezaei
|
|
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
12
|
Requires-Dist: Numba
|
|
13
13
|
Requires-Dist: funcnodes
|
|
14
|
-
Requires-Dist: funcnodes-core (>=0.1.
|
|
14
|
+
Requires-Dist: funcnodes-core (>=0.1.4)
|
|
15
15
|
Requires-Dist: funcnodes_numpy
|
|
16
16
|
Requires-Dist: funcnodes_pandas
|
|
17
17
|
Requires-Dist: funcnodes_plotly
|
|
@@ -5,7 +5,7 @@ from .smoothing import SMOOTH_NODE_SHELF as SMOOTH
|
|
|
5
5
|
from .peak_analysis import PEAKS_NODE_SHELF as PEAK
|
|
6
6
|
from .baseline import BASELINE_NODE_SHELF as BASELINE
|
|
7
7
|
|
|
8
|
-
__version__ = "0.2.
|
|
8
|
+
__version__ = "0.2.2"
|
|
9
9
|
|
|
10
10
|
NODE_SHELF = fn.Shelf(
|
|
11
11
|
name="Spectral Analysis",
|
|
@@ -1,20 +1,16 @@
|
|
|
1
|
+
import funcnodes as fn
|
|
1
2
|
from funcnodes import NodeDecorator, Shelf
|
|
2
3
|
from exposedfunctionality import controlled_wrapper
|
|
3
4
|
import numpy as np
|
|
4
|
-
from enum import Enum
|
|
5
5
|
from typing import Optional, Tuple, Union, List
|
|
6
6
|
import pybaselines
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class CostFunction(
|
|
9
|
+
class CostFunction(fn.DataEnum):
|
|
10
10
|
asymmetric_indec = "asymmetric_indec"
|
|
11
11
|
asymmetric_truncated_quadratic = "asymmetric_truncated_quadratic"
|
|
12
12
|
asymmetric_huber = "asymmetric_huber"
|
|
13
13
|
|
|
14
|
-
@classmethod
|
|
15
|
-
def default(cls):
|
|
16
|
-
return cls.asymmetric_indec.value
|
|
17
|
-
|
|
18
14
|
|
|
19
15
|
@NodeDecorator(
|
|
20
16
|
"pybaselines.polynomial.goldindec",
|
|
@@ -33,7 +29,7 @@ def _goldindec(
|
|
|
33
29
|
tol: float = 0.001,
|
|
34
30
|
max_iter: int = 250,
|
|
35
31
|
weights: Optional[np.ndarray] = None,
|
|
36
|
-
cost_function: CostFunction = CostFunction.
|
|
32
|
+
cost_function: CostFunction = CostFunction.asymmetric_indec,
|
|
37
33
|
peak_ratio: float = 0.5,
|
|
38
34
|
alpha_factor: float = 0.99,
|
|
39
35
|
tol_2: float = 0.001,
|
|
@@ -41,8 +37,7 @@ def _goldindec(
|
|
|
41
37
|
max_iter_2: int = 100,
|
|
42
38
|
return_coef: bool = False,
|
|
43
39
|
) -> Tuple[np.ndarray, np.ndarray, dict]:
|
|
44
|
-
|
|
45
|
-
cost_function = cost_function.value
|
|
40
|
+
cost_function = CostFunction.v(cost_function)
|
|
46
41
|
baseline, params = pybaselines.polynomial.goldindec(
|
|
47
42
|
data,
|
|
48
43
|
x_data=x_data,
|
|
@@ -186,7 +181,7 @@ def _modpoly(
|
|
|
186
181
|
return baseline_corrected, baseline, params
|
|
187
182
|
|
|
188
183
|
|
|
189
|
-
class PenalizedPolyCostFunction(
|
|
184
|
+
class PenalizedPolyCostFunction(fn.DataEnum):
|
|
190
185
|
asymmetric_truncated_quadratic = "asymmetric_truncated_quadratic"
|
|
191
186
|
symmetric_truncated_quadratic = "symmetric_truncated_quadratic"
|
|
192
187
|
asymmetric_huber = "asymmetric_huber"
|
|
@@ -194,10 +189,6 @@ class PenalizedPolyCostFunction(Enum):
|
|
|
194
189
|
asymmetric_indec = "asymmetric_indec"
|
|
195
190
|
symmetric_indec = "symmetric_indec"
|
|
196
191
|
|
|
197
|
-
@classmethod
|
|
198
|
-
def default(cls):
|
|
199
|
-
return cls.asymmetric_truncated_quadratic.value
|
|
200
|
-
|
|
201
192
|
|
|
202
193
|
@NodeDecorator(
|
|
203
194
|
"pybaselines.polynomial.penalized_poly",
|
|
@@ -218,13 +209,12 @@ def _penalized_poly(
|
|
|
218
209
|
max_iter: int = 250,
|
|
219
210
|
tol: float = 1e-3,
|
|
220
211
|
weights: Optional[np.ndarray] = None,
|
|
221
|
-
cost_function: PenalizedPolyCostFunction = PenalizedPolyCostFunction.
|
|
212
|
+
cost_function: PenalizedPolyCostFunction = PenalizedPolyCostFunction.asymmetric_truncated_quadratic,
|
|
222
213
|
threshold: Optional[float] = None,
|
|
223
214
|
alpha_factor: float = 0.99,
|
|
224
215
|
return_coef: bool = False,
|
|
225
216
|
) -> Tuple[np.ndarray, np.ndarray, dict]:
|
|
226
|
-
|
|
227
|
-
cost_function = cost_function.value
|
|
217
|
+
cost_function = PenalizedPolyCostFunction.v(cost_function)
|
|
228
218
|
baseline, params = pybaselines.polynomial.penalized_poly(
|
|
229
219
|
data,
|
|
230
220
|
x_data=x_data,
|
|
@@ -1540,15 +1530,11 @@ def _noise_median(
|
|
|
1540
1530
|
return baseline_corrected, baseline, params
|
|
1541
1531
|
|
|
1542
1532
|
|
|
1543
|
-
class Side(
|
|
1533
|
+
class Side(fn.DataEnum):
|
|
1544
1534
|
both = "both"
|
|
1545
1535
|
left = "left"
|
|
1546
1536
|
right = "right"
|
|
1547
1537
|
|
|
1548
|
-
@classmethod
|
|
1549
|
-
def default(cls):
|
|
1550
|
-
return cls.both.value
|
|
1551
|
-
|
|
1552
1538
|
|
|
1553
1539
|
@NodeDecorator(
|
|
1554
1540
|
"pybaselines.smooth.ria",
|
|
@@ -1566,13 +1552,12 @@ def _ria(
|
|
|
1566
1552
|
half_window: Optional[int] = None,
|
|
1567
1553
|
max_iter: int = 500,
|
|
1568
1554
|
tol: float = 0.01,
|
|
1569
|
-
side: Side = Side.
|
|
1555
|
+
side: Side = Side.both,
|
|
1570
1556
|
width_scale: float = 0.1,
|
|
1571
1557
|
height_scale: float = 1.0,
|
|
1572
1558
|
sigma_scale: float = 1.0 / 12.0,
|
|
1573
1559
|
) -> Tuple[np.ndarray, np.ndarray, dict]:
|
|
1574
|
-
|
|
1575
|
-
side = side.value
|
|
1560
|
+
side = Side.v(side)
|
|
1576
1561
|
baseline, params = pybaselines.smooth.ria(
|
|
1577
1562
|
data,
|
|
1578
1563
|
x_data=x_data,
|
|
@@ -1936,14 +1921,10 @@ CLASSIFICATION_NODE_SHELF = Shelf(
|
|
|
1936
1921
|
)
|
|
1937
1922
|
|
|
1938
1923
|
|
|
1939
|
-
class Method(
|
|
1924
|
+
class Method(fn.DataEnum):
|
|
1940
1925
|
modpoly = "modpoly"
|
|
1941
1926
|
imodpoly = "imodpoly"
|
|
1942
1927
|
|
|
1943
|
-
@classmethod
|
|
1944
|
-
def default(cls):
|
|
1945
|
-
return cls.modpoly.value
|
|
1946
|
-
|
|
1947
1928
|
|
|
1948
1929
|
@NodeDecorator(
|
|
1949
1930
|
"pybaselines.optimizers.adaptive_minmax",
|
|
@@ -1961,14 +1942,13 @@ def _adaptive_minmax(
|
|
|
1961
1942
|
data: np.ndarray,
|
|
1962
1943
|
x_data: Optional[np.ndarray] = None,
|
|
1963
1944
|
poly_order: Optional[Union[int, List[int]]] = None,
|
|
1964
|
-
method: Method = Method.
|
|
1945
|
+
method: Method = Method.modpoly,
|
|
1965
1946
|
constrained_fraction: Union[float, List[float]] = 0.01,
|
|
1966
1947
|
constrained_weight: Union[float, List[float]] = 100000.0,
|
|
1967
1948
|
estimation_poly_order: int = 2,
|
|
1968
1949
|
weights: Optional[np.ndarray] = None,
|
|
1969
1950
|
) -> Tuple[np.ndarray, np.ndarray, dict]:
|
|
1970
|
-
|
|
1971
|
-
method = method.value
|
|
1951
|
+
method = Method.v(method)
|
|
1972
1952
|
baseline, params = pybaselines.optimizers.adaptive_minmax(
|
|
1973
1953
|
data,
|
|
1974
1954
|
x_data=x_data,
|
|
@@ -1983,7 +1963,7 @@ def _adaptive_minmax(
|
|
|
1983
1963
|
return baseline_corrected, baseline, params
|
|
1984
1964
|
|
|
1985
1965
|
|
|
1986
|
-
class MethodColab(
|
|
1966
|
+
class MethodColab(fn.DataEnum):
|
|
1987
1967
|
airpls = "airpls"
|
|
1988
1968
|
arpls = "arpls"
|
|
1989
1969
|
asls = "asls"
|
|
@@ -1994,10 +1974,6 @@ class MethodColab(Enum):
|
|
|
1994
1974
|
iasls = "iasls"
|
|
1995
1975
|
psalsa = "psalsa"
|
|
1996
1976
|
|
|
1997
|
-
@classmethod
|
|
1998
|
-
def default(cls):
|
|
1999
|
-
return cls.asls.value
|
|
2000
|
-
|
|
2001
1977
|
|
|
2002
1978
|
@NodeDecorator(
|
|
2003
1979
|
"pybaselines.optimizers.collab_pls",
|
|
@@ -2015,10 +1991,9 @@ def _collab_pls(
|
|
|
2015
1991
|
data: np.ndarray,
|
|
2016
1992
|
x_data: Optional[np.ndarray] = None,
|
|
2017
1993
|
average_dataset: bool = True,
|
|
2018
|
-
method: MethodColab = MethodColab.
|
|
1994
|
+
method: MethodColab = MethodColab.asls,
|
|
2019
1995
|
) -> Tuple[np.ndarray, np.ndarray, dict]:
|
|
2020
|
-
|
|
2021
|
-
method = method.value
|
|
1996
|
+
method = MethodColab.v(method)
|
|
2022
1997
|
baseline, params = pybaselines.optimizers.collab_pls(
|
|
2023
1998
|
data,
|
|
2024
1999
|
x_data=x_data,
|
|
@@ -2029,7 +2004,7 @@ def _collab_pls(
|
|
|
2029
2004
|
return baseline_corrected, baseline, params
|
|
2030
2005
|
|
|
2031
2006
|
|
|
2032
|
-
class MethodAll(
|
|
2007
|
+
class MethodAll(fn.DataEnum):
|
|
2033
2008
|
goldindec = "goldindec"
|
|
2034
2009
|
imodpoly = "imodpoly"
|
|
2035
2010
|
loess = "loess"
|
|
@@ -2076,10 +2051,6 @@ class MethodAll(Enum):
|
|
|
2076
2051
|
rubberband = "rubberband"
|
|
2077
2052
|
std_distribution = "std_distribution"
|
|
2078
2053
|
|
|
2079
|
-
@classmethod
|
|
2080
|
-
def default(cls):
|
|
2081
|
-
return cls.asls.value
|
|
2082
|
-
|
|
2083
2054
|
|
|
2084
2055
|
@NodeDecorator(
|
|
2085
2056
|
"pybaselines.optimizers.custom_bc",
|
|
@@ -2097,10 +2068,9 @@ def _custom_bc(
|
|
|
2097
2068
|
sampling: Union[int, np.ndarray] = 1,
|
|
2098
2069
|
lam: Optional[float] = None,
|
|
2099
2070
|
diff_order: int = 2,
|
|
2100
|
-
method: MethodAll = MethodAll.
|
|
2071
|
+
method: MethodAll = MethodAll.asls,
|
|
2101
2072
|
) -> Tuple[np.ndarray, np.ndarray, dict]:
|
|
2102
|
-
|
|
2103
|
-
method = method.value
|
|
2073
|
+
method = MethodAll.v(method)
|
|
2104
2074
|
baseline, params = pybaselines.optimizers.custom_bc(
|
|
2105
2075
|
data,
|
|
2106
2076
|
x_data=x_data,
|
|
@@ -2128,19 +2098,17 @@ def _custom_bc(
|
|
|
2128
2098
|
def _optimize_extended_range(
|
|
2129
2099
|
data: np.ndarray,
|
|
2130
2100
|
x_data: Optional[np.ndarray] = None,
|
|
2131
|
-
side: Side = Side.
|
|
2101
|
+
side: Side = Side.both,
|
|
2132
2102
|
width_scale: float = 0.1,
|
|
2133
2103
|
height_scale: float = 1.0,
|
|
2134
2104
|
sigma_scale: float = 1.0 / 12.0,
|
|
2135
2105
|
min_value: float = 2.0,
|
|
2136
2106
|
max_value: float = 8.0,
|
|
2137
2107
|
step: int = 1,
|
|
2138
|
-
method: MethodAll = MethodAll.
|
|
2108
|
+
method: MethodAll = MethodAll.asls,
|
|
2139
2109
|
) -> Tuple[np.ndarray, np.ndarray, dict]:
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
if isinstance(side, Side):
|
|
2143
|
-
side = side.value
|
|
2110
|
+
method = MethodAll.v(method)
|
|
2111
|
+
side = Side.v(side)
|
|
2144
2112
|
baseline, params = pybaselines.optimizers.optimize_extended_range(
|
|
2145
2113
|
data,
|
|
2146
2114
|
x_data=x_data,
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from typing import Literal, Tuple
|
|
2
|
+
from funcnodes import NodeDecorator, Shelf
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy.interpolate import interp1d
|
|
5
|
+
from exposedfunctionality import controlled_wrapper
|
|
6
|
+
import funcnodes as fn
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NormMode(fn.DataEnum):
|
|
10
|
+
ZERO_ONE = "zero_one"
|
|
11
|
+
MINUS_ONE_ONE = "minus_one_one"
|
|
12
|
+
SUM_ABS = "sum_abs"
|
|
13
|
+
SUM = "sum"
|
|
14
|
+
EUCLIDEAN = "euclidean"
|
|
15
|
+
MEAN_STD = "mean_std"
|
|
16
|
+
MAX = "max"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@NodeDecorator(id="span.basics.norm", name="Normalization node")
|
|
20
|
+
def _norm(array: np.ndarray, mode: NormMode = NormMode.ZERO_ONE) -> np.ndarray:
|
|
21
|
+
# """
|
|
22
|
+
# Apply different normalizations to the array.
|
|
23
|
+
|
|
24
|
+
# Args:
|
|
25
|
+
# array (np.ndarray): The input array to be normalized.
|
|
26
|
+
# mode (NormMode): The normalization mode to apply. Defaults to NormMode.ZERO_ONE.
|
|
27
|
+
|
|
28
|
+
# Returns:
|
|
29
|
+
# np.ndarray: The normalized array.
|
|
30
|
+
|
|
31
|
+
# Raises:
|
|
32
|
+
# ValueError: If an unsupported normalization mode is provided.
|
|
33
|
+
# """
|
|
34
|
+
mode = NormMode.v(mode)
|
|
35
|
+
normalization_methods = {
|
|
36
|
+
NormMode.ZERO_ONE.value: lambda x: (x - np.amin(x)) / (np.amax(x) - np.amin(x)),
|
|
37
|
+
NormMode.MINUS_ONE_ONE.value: lambda x: 2
|
|
38
|
+
* ((x - np.amin(x)) / (np.amax(x) - np.amin(x)))
|
|
39
|
+
- 1,
|
|
40
|
+
NormMode.SUM_ABS.value: lambda x: x / np.abs(x).sum(),
|
|
41
|
+
NormMode.SUM.value: lambda x: x / x.sum(),
|
|
42
|
+
NormMode.EUCLIDEAN.value: lambda x: x / np.sqrt((x**2).sum()),
|
|
43
|
+
NormMode.MEAN_STD.value: lambda x: (x - x.mean()) / x.std(),
|
|
44
|
+
NormMode.MAX.value: lambda x: x / x.max(),
|
|
45
|
+
}
|
|
46
|
+
if mode not in normalization_methods.keys():
|
|
47
|
+
raise ValueError(f"Unsupported normalization mode: {mode}")
|
|
48
|
+
return normalization_methods[mode](array)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def density_normalization(
|
|
52
|
+
x: np.ndarray,
|
|
53
|
+
y: np.ndarray,
|
|
54
|
+
num_points: int = None,
|
|
55
|
+
distance_estimation: Literal["median", "mean", "min", "max"] = "median",
|
|
56
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
57
|
+
"""
|
|
58
|
+
Redistributes the x and y coordinates to have evenly spaced x-values while retaining original points.
|
|
59
|
+
|
|
60
|
+
Parameters:
|
|
61
|
+
x (np.ndarray): Uneven, unsorted x coordinates.
|
|
62
|
+
y (np.ndarray): Corresponding y values.
|
|
63
|
+
num_points (int, optional): Total number of points in the final grid.
|
|
64
|
+
Must be greater than or equal to the number of unique original x values.
|
|
65
|
+
If None, it is estimated based on the specified distance estimation method.
|
|
66
|
+
distance_estimation (Literal["median", "mean", "min", "max"], optional):
|
|
67
|
+
Method to estimate the spacing between points when num_points is not provided.
|
|
68
|
+
Options include "median", "mean", "min", "max". Defaults to "median".
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Tuple[np.ndarray, np.ndarray]:
|
|
72
|
+
- x_new: Evenly spaced x coordinates including original x points.
|
|
73
|
+
- y_new: Corresponding interpolated y values."""
|
|
74
|
+
# Convert inputs to numpy arrays
|
|
75
|
+
x = np.array(x)
|
|
76
|
+
y = np.array(y)
|
|
77
|
+
|
|
78
|
+
# if x is already evenly spaced, return the input
|
|
79
|
+
if len(np.unique(np.diff(x))) == 1:
|
|
80
|
+
return x, y
|
|
81
|
+
|
|
82
|
+
# Sort the data based on x values
|
|
83
|
+
sorted_indices = np.argsort(x)
|
|
84
|
+
x_sorted = x[sorted_indices]
|
|
85
|
+
y_sorted = y[sorted_indices]
|
|
86
|
+
|
|
87
|
+
# Handle duplicate x-values by averaging their y-values
|
|
88
|
+
unique_x, indices, counts = np.unique(
|
|
89
|
+
x_sorted, return_index=True, return_counts=True
|
|
90
|
+
)
|
|
91
|
+
y_unique = np.array([y_sorted[i : i + c].mean() for i, c in zip(indices, counts)])
|
|
92
|
+
|
|
93
|
+
# Determine number of points for the evenly spaced grid
|
|
94
|
+
|
|
95
|
+
if num_points is None:
|
|
96
|
+
distance_func = {
|
|
97
|
+
"mean": np.mean,
|
|
98
|
+
"median": np.median,
|
|
99
|
+
"min": np.min,
|
|
100
|
+
"max": np.max,
|
|
101
|
+
}.get(distance_estimation, np.median) # Default to median if key not found
|
|
102
|
+
|
|
103
|
+
unique_diffs = np.diff(unique_x)
|
|
104
|
+
|
|
105
|
+
# Handle the case where there is only one unique_x point
|
|
106
|
+
if len(unique_diffs) == 0:
|
|
107
|
+
num_points = 1
|
|
108
|
+
else:
|
|
109
|
+
estimated_diff = distance_func(unique_diffs)
|
|
110
|
+
total_range = unique_x.max() - unique_x.min()
|
|
111
|
+
|
|
112
|
+
# Prevent division by zero
|
|
113
|
+
if estimated_diff == 0:
|
|
114
|
+
num_points = len(unique_x)
|
|
115
|
+
else:
|
|
116
|
+
num_points = int(total_range / estimated_diff)
|
|
117
|
+
else:
|
|
118
|
+
num_points = int(num_points)
|
|
119
|
+
# Create evenly spaced x-values
|
|
120
|
+
x_new = np.linspace(unique_x.min(), unique_x.max(), num_points)
|
|
121
|
+
|
|
122
|
+
# Create an interpolation function
|
|
123
|
+
interp_func = interp1d(unique_x, y_unique, kind="linear", fill_value="extrapolate")
|
|
124
|
+
|
|
125
|
+
# Compute the interpolated y-values
|
|
126
|
+
y_new = interp_func(x_new)
|
|
127
|
+
|
|
128
|
+
return x_new, y_new
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@NodeDecorator(
|
|
132
|
+
id="span.norm.density",
|
|
133
|
+
name="Density normalization node",
|
|
134
|
+
description="Redistributes the x and y coordinates to have evenly spaced x-values while retaining original points.",
|
|
135
|
+
outputs=[
|
|
136
|
+
{
|
|
137
|
+
"name": "x_new",
|
|
138
|
+
"description": "Evenly spaced x coordinates including original x points.",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"name": "y_new",
|
|
142
|
+
"description": "Corresponding interpolated y values.",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
)
|
|
146
|
+
@controlled_wrapper(density_normalization, wrapper_attribute="__fnwrapped__")
|
|
147
|
+
def density_normalization_node(
|
|
148
|
+
x: np.ndarray,
|
|
149
|
+
y: np.ndarray,
|
|
150
|
+
num_points: int = None,
|
|
151
|
+
distance_estimation: Literal["median", "mean", "min", "max"] = "median",
|
|
152
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
153
|
+
return density_normalization(x, y, num_points, distance_estimation)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
NORM_NODE_SHELF = Shelf(
|
|
157
|
+
nodes=[_norm, density_normalization_node],
|
|
158
|
+
subshelves=[],
|
|
159
|
+
name="Normalization",
|
|
160
|
+
description="Normalization of the spectra",
|
|
161
|
+
)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from funcnodes import NodeDecorator, Shelf
|
|
2
|
+
import funcnodes as fn
|
|
2
3
|
import numpy as np
|
|
3
|
-
from enum import Enum
|
|
4
4
|
from exposedfunctionality import controlled_wrapper
|
|
5
|
-
from typing import Optional, List, Tuple
|
|
5
|
+
from typing import Optional, List, Tuple, Dict
|
|
6
6
|
from scipy.signal import find_peaks
|
|
7
7
|
from scipy.stats import norm
|
|
8
8
|
from scipy import signal, interpolate
|
|
@@ -13,6 +13,15 @@ import plotly.graph_objs as go
|
|
|
13
13
|
from plotly.subplots import make_subplots
|
|
14
14
|
import re
|
|
15
15
|
from dataclasses import dataclass
|
|
16
|
+
from .normalization import density_normalization
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class FittinInfo:
|
|
21
|
+
model_name: str
|
|
22
|
+
best_values: dict
|
|
23
|
+
data: np.ndarray
|
|
24
|
+
userkws: dict
|
|
16
25
|
|
|
17
26
|
|
|
18
27
|
@dataclass
|
|
@@ -35,8 +44,8 @@ class PeakProperties:
|
|
|
35
44
|
width: float
|
|
36
45
|
_is_fitted: bool = False
|
|
37
46
|
_is_force_fitted: bool = False
|
|
38
|
-
fitting_data: Optional[
|
|
39
|
-
fitting_info: Optional[
|
|
47
|
+
fitting_data: Optional[Dict[str, np.ndarray]] = None
|
|
48
|
+
fitting_info: Optional[FittinInfo] = None
|
|
40
49
|
|
|
41
50
|
|
|
42
51
|
def compute_peak_properties(
|
|
@@ -46,25 +55,27 @@ def compute_peak_properties(
|
|
|
46
55
|
peak_nr: int,
|
|
47
56
|
is_fitted: bool = False,
|
|
48
57
|
is_force_fitted: bool = False,
|
|
49
|
-
fitting_data: Optional[
|
|
50
|
-
fitting_info: Optional[
|
|
58
|
+
fitting_data: Optional[Dict[str, np.ndarray]] = None,
|
|
59
|
+
fitting_info: Optional[FittinInfo] = None,
|
|
51
60
|
) -> PeakProperties:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
"""
|
|
62
|
+
Compute various properties of a given peak.
|
|
63
|
+
|
|
64
|
+
Parameters:
|
|
65
|
+
- x_array: np.ndarray - The array of x-values (e.g., time or wavelength).
|
|
66
|
+
- y_array: np.ndarray - The array of y-values (e.g., intensity).
|
|
67
|
+
- peak_indices: List[int] - A list containing the start index, peak index, and end index of the peak.
|
|
68
|
+
- peak_nr: int - The identifier number of the peak.
|
|
69
|
+
- is_fitted: bool = False - A flag indicating whether the peak is fitted or not.
|
|
70
|
+
- is_force_fitted: bool = False - A flag indicating whether the peak is forced fitted or not.
|
|
71
|
+
- fitting_data: Optional[Dict[str, np.ndarray]] = None - A dictionary containing the fitting
|
|
72
|
+
data if the peak is fitted.
|
|
73
|
+
- fitting_info: Optional[FittinInfo] = None - A dictionary containing the fitting information
|
|
74
|
+
if the peak is fitted.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
- peak_properties: PeakProperties - A dictionary containing various properties of the peak.
|
|
78
|
+
"""
|
|
68
79
|
|
|
69
80
|
i_index, index, f_index = peak_indices
|
|
70
81
|
|
|
@@ -198,7 +209,7 @@ def peak_finder(
|
|
|
198
209
|
wlen: Optional[int] = None,
|
|
199
210
|
rel_height: float = 0.5,
|
|
200
211
|
plateau_size: Optional[int] = None,
|
|
201
|
-
) ->
|
|
212
|
+
) -> List[PeakProperties]:
|
|
202
213
|
peak_lst = []
|
|
203
214
|
x_array = np.array(x_array, dtype=float)
|
|
204
215
|
y_array = np.array(y_array, dtype=float)
|
|
@@ -208,13 +219,36 @@ def peak_finder(
|
|
|
208
219
|
distance = float(distance) if distance is not None else None
|
|
209
220
|
prominence = float(prominence) if prominence is not None else None
|
|
210
221
|
width = float(width) if width is not None else None
|
|
211
|
-
wlen =
|
|
222
|
+
wlen = float(wlen) if wlen is not None else None
|
|
212
223
|
rel_height = float(rel_height)
|
|
213
|
-
plateau_size =
|
|
224
|
+
plateau_size = float(plateau_size) if plateau_size is not None else None
|
|
214
225
|
|
|
215
226
|
height = 0.05 * np.max(y_array) if height is None else height
|
|
216
227
|
noise_level = 5000 if noise_level is None else noise_level
|
|
217
228
|
|
|
229
|
+
if x_array is not None:
|
|
230
|
+
x_array, y_array = density_normalization(
|
|
231
|
+
x_array,
|
|
232
|
+
y_array,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
xdiff = x_array[1] - x_array[0]
|
|
236
|
+
# if x is given width is based on the x scale and has to be converted to index
|
|
237
|
+
if width is not None:
|
|
238
|
+
width = width / xdiff
|
|
239
|
+
|
|
240
|
+
# same for distance
|
|
241
|
+
if distance is not None:
|
|
242
|
+
distance = distance / xdiff
|
|
243
|
+
|
|
244
|
+
# same for wlen
|
|
245
|
+
if wlen is not None:
|
|
246
|
+
wlen = wlen / xdiff
|
|
247
|
+
|
|
248
|
+
# same for plateau_size
|
|
249
|
+
if plateau_size is not None:
|
|
250
|
+
plateau_size = plateau_size / xdiff
|
|
251
|
+
|
|
218
252
|
# Make a copy of the input array
|
|
219
253
|
y_array_copy = np.copy(y_array)
|
|
220
254
|
|
|
@@ -225,8 +259,8 @@ def peak_finder(
|
|
|
225
259
|
prominence=prominence,
|
|
226
260
|
height=height,
|
|
227
261
|
distance=distance,
|
|
228
|
-
width=width,
|
|
229
|
-
wlen=wlen,
|
|
262
|
+
width=max(1, width) if width is not None else None,
|
|
263
|
+
wlen=int(wlen) if wlen is not None else None,
|
|
230
264
|
rel_height=rel_height,
|
|
231
265
|
plateau_size=plateau_size,
|
|
232
266
|
)
|
|
@@ -331,7 +365,7 @@ def interpolation_1d(
|
|
|
331
365
|
return x_interpolated, y_interpolated
|
|
332
366
|
|
|
333
367
|
|
|
334
|
-
class FittingModel(
|
|
368
|
+
class FittingModel(fn.DataEnum):
|
|
335
369
|
ComplexConstant = "Complex Constant"
|
|
336
370
|
Gaussian = "Gaussian"
|
|
337
371
|
Gaussian2D = "Gaussian-2D"
|
|
@@ -356,12 +390,8 @@ class FittingModel(Enum):
|
|
|
356
390
|
Rectangle = "Rectangle"
|
|
357
391
|
Expression = "Expression"
|
|
358
392
|
|
|
359
|
-
@classmethod
|
|
360
|
-
def default(cls):
|
|
361
|
-
return cls.Gaussian.value
|
|
362
|
-
|
|
363
393
|
|
|
364
|
-
class BaselineModel(
|
|
394
|
+
class BaselineModel(fn.DataEnum):
|
|
365
395
|
# Polynomial = "Polynomial"
|
|
366
396
|
Linear = "Linear"
|
|
367
397
|
Spline = "Spline"
|
|
@@ -374,13 +404,13 @@ class BaselineModel(Enum):
|
|
|
374
404
|
return cls.Exponential.value
|
|
375
405
|
|
|
376
406
|
|
|
377
|
-
@NodeDecorator(id="span.basics.fit", name="Fit 1D")
|
|
407
|
+
@NodeDecorator(id="span.basics.fit", name="Fit 1D", separate_process=True)
|
|
378
408
|
def fit_1D(
|
|
379
409
|
x_array: np.ndarray,
|
|
380
410
|
y_array: np.ndarray,
|
|
381
411
|
basic_peaks: List[PeakProperties],
|
|
382
|
-
main_model: FittingModel = FittingModel.
|
|
383
|
-
baseline_model: BaselineModel = BaselineModel.
|
|
412
|
+
main_model: FittingModel = FittingModel.Gaussian,
|
|
413
|
+
baseline_model: BaselineModel = BaselineModel.Exponential,
|
|
384
414
|
) -> List[PeakProperties]:
|
|
385
415
|
# """
|
|
386
416
|
# Fit a 1D model to the given data.
|
|
@@ -405,10 +435,9 @@ def fit_1D(
|
|
|
405
435
|
x_array = np.array(x_array)
|
|
406
436
|
y_array = np.array(y_array)
|
|
407
437
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
baseline_model = baseline_model.value
|
|
438
|
+
main_model = FittingModel.v(main_model)
|
|
439
|
+
|
|
440
|
+
baseline_model = BaselineModel.v(baseline_model)
|
|
412
441
|
peaks = copy.deepcopy(basic_peaks)
|
|
413
442
|
y = y_array
|
|
414
443
|
x = x_array
|
|
@@ -473,8 +502,15 @@ def fit_1D(
|
|
|
473
502
|
|
|
474
503
|
out = f.fit(y, pars, x=x)
|
|
475
504
|
com = out.eval_components(x=x)
|
|
476
|
-
info_dict = out.__dict__
|
|
477
|
-
info_dict["model_name"] = main_model
|
|
505
|
+
# info_dict = out.__dict__
|
|
506
|
+
# info_dict["model_name"] = main_model
|
|
507
|
+
|
|
508
|
+
info_dict = FittinInfo(
|
|
509
|
+
model_name=main_model,
|
|
510
|
+
best_values=out.best_values,
|
|
511
|
+
data=out.data,
|
|
512
|
+
userkws=out.userkws,
|
|
513
|
+
)
|
|
478
514
|
|
|
479
515
|
peak_properties_list = []
|
|
480
516
|
|
|
@@ -1,28 +1,70 @@
|
|
|
1
|
+
from typing import Dict, Callable
|
|
1
2
|
from funcnodes import NodeDecorator, Shelf
|
|
2
3
|
import numpy as np
|
|
3
4
|
import pandas as pd
|
|
4
|
-
from enum import Enum
|
|
5
5
|
from scipy.signal import savgol_filter, medfilt
|
|
6
6
|
from scipy.ndimage import gaussian_filter1d
|
|
7
7
|
import warnings
|
|
8
|
+
import funcnodes as fn
|
|
8
9
|
|
|
9
10
|
warnings.filterwarnings("ignore")
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
class SmoothMode(fn.DataEnum):
|
|
12
14
|
SAVITZKY_GOLAY = "savgol"
|
|
13
15
|
GAUSSIAN = "gaussian"
|
|
14
16
|
MOVING_AVERAGE = "ma"
|
|
15
17
|
EXPONENTIAL_MOVING_AVERAGE = "ema"
|
|
16
18
|
MEDIAN = "median"
|
|
17
|
-
|
|
18
|
-
@classmethod
|
|
19
|
-
def default(cls) -> 'SmoothMode':
|
|
20
|
-
"""Returns the default smoothing mode."""
|
|
21
|
-
return cls.SAVITZKY_GOLAY.value
|
|
22
19
|
|
|
23
|
-
@NodeDecorator("span.basics.smooth", name="Smoothing")
|
|
24
20
|
|
|
25
|
-
def
|
|
21
|
+
def smooth_savgol(x: np.ndarray, window: int) -> np.ndarray:
|
|
22
|
+
return savgol_filter(x, window, 2)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def smooth_gaussian(x: np.ndarray, window: int) -> np.ndarray:
|
|
26
|
+
return gaussian_filter1d(x, window)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def smooth_ma(x: np.ndarray, window: int) -> np.ndarray:
|
|
30
|
+
if x.ndim > 1:
|
|
31
|
+
n, m = x.shape
|
|
32
|
+
result = np.zeros((n, m))
|
|
33
|
+
for i in range(n):
|
|
34
|
+
result[i, :] = np.convolve(x[i, :], np.ones(window) / window, mode="same")
|
|
35
|
+
return result
|
|
36
|
+
else:
|
|
37
|
+
return np.convolve(x, np.ones(window) / window, mode="same")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def smooth_ema(x: np.ndarray, window: int) -> np.ndarray:
|
|
41
|
+
if x.ndim > 1:
|
|
42
|
+
n, m = x.shape
|
|
43
|
+
result = np.zeros((n, m))
|
|
44
|
+
for i in range(n):
|
|
45
|
+
result[i, :] = pd.Series(x[i, :]).ewm(span=window).mean().values
|
|
46
|
+
return result
|
|
47
|
+
else:
|
|
48
|
+
return pd.Series(x).ewm(span=window).mean().values
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def smooth_median(x: np.ndarray, window: int) -> np.ndarray:
|
|
52
|
+
return medfilt(x, window)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_SMOOTHING_MAPPER: Dict[str, Callable[[np.ndarray, int], np.ndarray]] = {
|
|
56
|
+
SmoothMode.SAVITZKY_GOLAY.value: smooth_savgol,
|
|
57
|
+
SmoothMode.GAUSSIAN.value: smooth_gaussian,
|
|
58
|
+
SmoothMode.MOVING_AVERAGE.value: smooth_ma,
|
|
59
|
+
SmoothMode.EXPONENTIAL_MOVING_AVERAGE.value: smooth_ema,
|
|
60
|
+
SmoothMode.MEDIAN.value: smooth_median,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@NodeDecorator("span.basics.smooth", name="Smoothing")
|
|
65
|
+
def _smooth(
|
|
66
|
+
array: np.ndarray, mode: SmoothMode = SmoothMode.SAVITZKY_GOLAY, window: int = 5
|
|
67
|
+
) -> np.ndarray:
|
|
26
68
|
# """
|
|
27
69
|
# Apply different smoothing techniques to the input array.
|
|
28
70
|
|
|
@@ -37,49 +79,12 @@ def _smooth(array: np.ndarray, mode: SmoothMode = SmoothMode.default(), window:
|
|
|
37
79
|
# Raises:
|
|
38
80
|
# ValueError: If an unsupported smoothing mode is provided.
|
|
39
81
|
# """
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return savgol_filter(x, window, 2)
|
|
44
|
-
|
|
45
|
-
def smooth_gaussian(x: np.ndarray) -> np.ndarray:
|
|
46
|
-
return gaussian_filter1d(x, window)
|
|
47
|
-
|
|
48
|
-
def smooth_ma(x: np.ndarray) -> np.ndarray:
|
|
49
|
-
if x.ndim > 1:
|
|
50
|
-
n, m = x.shape
|
|
51
|
-
result = np.zeros((n, m))
|
|
52
|
-
for i in range(n):
|
|
53
|
-
result[i, :] = np.convolve(x[i, :], np.ones(window) / window, mode="same")
|
|
54
|
-
return result
|
|
55
|
-
else:
|
|
56
|
-
return np.convolve(x, np.ones(window) / window, mode="same")
|
|
57
|
-
|
|
58
|
-
def smooth_ema(x: np.ndarray) -> np.ndarray:
|
|
59
|
-
if x.ndim > 1:
|
|
60
|
-
n, m = x.shape
|
|
61
|
-
result = np.zeros((n, m))
|
|
62
|
-
for i in range(n):
|
|
63
|
-
result[i, :] = pd.Series(x[i, :]).ewm(span=window).mean().values
|
|
64
|
-
return result
|
|
65
|
-
else:
|
|
66
|
-
return pd.Series(x).ewm(span=window).mean().values
|
|
67
|
-
|
|
68
|
-
def smooth_median(x: np.ndarray) -> np.ndarray:
|
|
69
|
-
return medfilt(x, window)
|
|
70
|
-
|
|
71
|
-
smoothing_methods = {
|
|
72
|
-
SmoothMode.SAVITZKY_GOLAY.value: smooth_savgol,
|
|
73
|
-
SmoothMode.GAUSSIAN.value: smooth_gaussian,
|
|
74
|
-
SmoothMode.MOVING_AVERAGE.value: smooth_ma,
|
|
75
|
-
SmoothMode.EXPONENTIAL_MOVING_AVERAGE.value: smooth_ema,
|
|
76
|
-
SmoothMode.MEDIAN.value: smooth_median
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if mode not in smoothing_methods.keys():
|
|
82
|
+
mode = SmoothMode.v(mode)
|
|
83
|
+
|
|
84
|
+
if mode not in _SMOOTHING_MAPPER.keys():
|
|
80
85
|
raise ValueError(f"Unsupported smoothing mode: {mode}")
|
|
81
|
-
|
|
82
|
-
return
|
|
86
|
+
|
|
87
|
+
return _SMOOTHING_MAPPER[mode](array, window)
|
|
83
88
|
|
|
84
89
|
|
|
85
90
|
# @NodeDecorator("span.basics.smooth.savgol", name="Savgol")
|
|
@@ -187,4 +192,4 @@ SMOOTH_NODE_SHELF = Shelf(
|
|
|
187
192
|
subshelves=[],
|
|
188
193
|
name="Smoothing",
|
|
189
194
|
description="Smoothing of the spectra",
|
|
190
|
-
)
|
|
195
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "funcnodes-span"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = ["Kourosh Rezaei <kouroshrezaei90@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -15,7 +15,7 @@ funcnodes_numpy = "*"
|
|
|
15
15
|
funcnodes_pandas = "*"
|
|
16
16
|
funcnodes_plotly = "*"
|
|
17
17
|
|
|
18
|
-
funcnodes-core = ">=0.1.
|
|
18
|
+
funcnodes-core = ">=0.1.4"
|
|
19
19
|
pybaselines = "*"
|
|
20
20
|
Numba = "*"
|
|
21
21
|
pentapy = "*"
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
from funcnodes import NodeDecorator, Shelf
|
|
2
|
-
import numpy as np
|
|
3
|
-
from enum import Enum
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class NormMode(Enum):
|
|
7
|
-
ZERO_ONE = "zero_one"
|
|
8
|
-
MINUS_ONE_ONE = "minus_one_one"
|
|
9
|
-
SUM_ABS = "sum_abs"
|
|
10
|
-
SUM = "sum"
|
|
11
|
-
EUCLIDEAN = "euclidean"
|
|
12
|
-
MEAN_STD = "mean_std"
|
|
13
|
-
MAX = "max"
|
|
14
|
-
|
|
15
|
-
@classmethod
|
|
16
|
-
def default(cls) -> "NormMode":
|
|
17
|
-
"""Returns the default normalization mode."""
|
|
18
|
-
return cls.ZERO_ONE.value
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@NodeDecorator(id="span.basics.norm", name="Normalization node")
|
|
22
|
-
def _norm(array: np.ndarray, mode: NormMode = NormMode.default()) -> np.ndarray:
|
|
23
|
-
# """
|
|
24
|
-
# Apply different normalizations to the array.
|
|
25
|
-
|
|
26
|
-
# Args:
|
|
27
|
-
# array (np.ndarray): The input array to be normalized.
|
|
28
|
-
# mode (NormMode): The normalization mode to apply. Defaults to NormMode.ZERO_ONE.
|
|
29
|
-
|
|
30
|
-
# Returns:
|
|
31
|
-
# np.ndarray: The normalized array.
|
|
32
|
-
|
|
33
|
-
# Raises:
|
|
34
|
-
# ValueError: If an unsupported normalization mode is provided.
|
|
35
|
-
# """
|
|
36
|
-
if isinstance(mode, NormMode):
|
|
37
|
-
mode = mode.value
|
|
38
|
-
normalization_methods = {
|
|
39
|
-
NormMode.ZERO_ONE.value: lambda x: (x - np.amin(x)) / (np.amax(x) - np.amin(x)),
|
|
40
|
-
NormMode.MINUS_ONE_ONE.value: lambda x: 2 * ((x - np.amin(x)) / (np.amax(x) - np.amin(x))) - 1,
|
|
41
|
-
NormMode.SUM_ABS.value: lambda x: x / np.abs(x).sum(),
|
|
42
|
-
NormMode.SUM.value: lambda x: x / x.sum(),
|
|
43
|
-
NormMode.EUCLIDEAN.value: lambda x: x / np.sqrt((x**2).sum()),
|
|
44
|
-
NormMode.MEAN_STD.value: lambda x: (x - x.mean()) / x.std(),
|
|
45
|
-
NormMode.MAX.value: lambda x: x / x.max()
|
|
46
|
-
}
|
|
47
|
-
if mode not in normalization_methods.keys():
|
|
48
|
-
raise ValueError(f"Unsupported normalization mode: {mode}")
|
|
49
|
-
return normalization_methods[mode](array)
|
|
50
|
-
|
|
51
|
-
NORM_NODE_SHELF = Shelf(
|
|
52
|
-
nodes=[_norm],
|
|
53
|
-
subshelves=[],
|
|
54
|
-
name="Normalization",
|
|
55
|
-
description="Normalization of the spectra",
|
|
56
|
-
)
|
|
File without changes
|