funcnodes-span 0.3.0__tar.gz → 0.3.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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: funcnodes-span
3
- Version: 0.3.0
4
- Summary: Spectral Peak ANalysis (SPAN) for funcnodes
3
+ Version: 0.3.2
4
+ Summary: SPectral ANalysis (SPAN) for funcnodes
5
5
  License: MIT
6
6
  Author: Kourosh Rezaei
7
7
  Author-email: kouroshrezaei90@gmail.com
@@ -25,5 +25,5 @@ Project-URL: source, https://github.com/Linkdlab/funcnodes_span
25
25
  Project-URL: tracker, https://github.com/Linkdlab/funcnodes_span/issues
26
26
  Description-Content-Type: text/markdown
27
27
 
28
- Spectral Peak ANalysis (SPAN) for funcnodes
28
+ SPectral ANalysis (SPAN) for funcnodes
29
29
 
@@ -0,0 +1 @@
1
+ SPectral ANalysis (SPAN) for funcnodes
@@ -7,7 +7,7 @@ from .baseline import BASELINE_NODE_SHELF as BASELINE
7
7
  from funcnodes_lmfit import NODE_SHELF as LMFIT_NODE_SHELF
8
8
  from .curves import CURVES_NODE_SHELF
9
9
 
10
- __version__ = "0.3.0"
10
+ __version__ = "0.3.2"
11
11
 
12
12
  NODE_SHELF = fn.Shelf(
13
13
  name="Spectral Analysis",
@@ -47,7 +47,7 @@ def knee_point_detection(x, y) -> Tuple[int, float, float]:
47
47
 
48
48
 
49
49
  def estimate_noise(
50
- x, y, is_baseline_region: Optional[np.ndarray] = None, std_factor=3
50
+ x, y, is_baseline_region: Optional[np.ndarray] = None, std_factor: float = 3
51
51
  ) -> float:
52
52
  """
53
53
  Estimate the noise level in a signal.
@@ -16,6 +16,6 @@ estimate_noise_node = fn.NodeDecorator(
16
16
  CURVES_NODE_SHELF = fn.Shelf(
17
17
  nodes=[knee_point_detection_node, estimate_noise_node],
18
18
  subshelves=[],
19
- name="Smoothing",
20
- description="Smoothing of the spectra",
19
+ name="Curves",
20
+ description="Analysis of curves and spectra",
21
21
  )
@@ -5,6 +5,12 @@ from lmfit.model import ModelResult
5
5
  from lmfit.models import GaussianModel
6
6
  from .peaks import PeakProperties
7
7
  from funcnodes_lmfit.model import AUTOMODELMAP
8
+ import funcnodes as fn
9
+
10
+ AutoModelEnum = fn.DataEnum("AutoModelEnum", AUTOMODELMAP)
11
+ # class AutoModelEnum(fn.DataEnum):
12
+ # GaussianModel = AUTOMODELMAP["GaussianModel"]
13
+ # LorentzianModel = AUTOMODELMAP["LorentzianModel"]
8
14
 
9
15
 
10
16
  def group_signals(
@@ -38,6 +44,7 @@ def group_signals(
38
44
  # Assign each peak to its respective group based on start and end indices
39
45
  for pi, p in enumerate(peaks):
40
46
  ingroup = (baseline_cut >= p.i_index) & (baseline_cut <= p.f_index)
47
+
41
48
  if ingroup.sum() == 0:
42
49
  ingroup[(baseline_cut >= p.i_index).argmax()] = True
43
50
 
@@ -57,6 +64,9 @@ def group_signals(
57
64
  else:
58
65
  current_peak_group.update(pg)
59
66
 
67
+ if len(current_peak_group) > 0:
68
+ connected_peaks.append(list(current_peak_group))
69
+
60
70
  # Return a list of grouped PeakProperties
61
71
  return [[peaks[pi] for pi in cp] for cp in connected_peaks if len(cp) > 0]
62
72
 
@@ -105,12 +115,11 @@ def fit_local_peak(
105
115
  Returns:
106
116
  Model: The fitted model for the peak.
107
117
  """
118
+ x = np.asarray(x)
119
+ y = np.asarray(y)
108
120
 
109
- if isinstance(model_class, str):
110
- model_class = AUTOMODELMAP[model_class]
111
-
112
- if isinstance(incomplete_peak_model_class, str):
113
- incomplete_peak_model_class = AUTOMODELMAP[incomplete_peak_model_class]
121
+ model_class = AutoModelEnum.v(model_class)
122
+ incomplete_peak_model_class = AutoModelEnum.v(incomplete_peak_model_class)
114
123
 
115
124
  pf = f"p{peak.id}_"
116
125
  model = model_class(prefix=pf) # Initialize the model
@@ -129,16 +138,11 @@ def fit_local_peak(
129
138
  if peak.yfull is None:
130
139
  peak.yfull = y
131
140
 
132
- if isinstance(model_class, str):
133
- model_class = AUTOMODELMAP[model_class]
134
-
135
- if isinstance(incomplete_peak_model_class, str):
136
- incomplete_peak_model_class = AUTOMODELMAP[incomplete_peak_model_class]
137
-
138
141
  if np.abs(yf[-1] - yf[0]) > incomplete_threshold * (local_max - local_min):
139
142
  # Handle incomplete peak by extending the range and filling gaps
140
143
  m_complete = incomplete_peak_model_class()
141
144
  guess = m_complete.guess(data=yf, x=xf)
145
+ guess["center"].set(min=min(xf), max=max(xf))
142
146
  fr_complete = m_complete.fit(data=yf, x=xf, params=guess, iter_cb=iter_cb)
143
147
 
144
148
  fwhm = 2 * np.sqrt(2 * np.log(2)) * fr_complete.params["sigma"].value
@@ -226,11 +230,11 @@ def fit_peak_group(
226
230
  Model: The fitted model for the group of peaks.
227
231
  """
228
232
 
229
- if isinstance(model_class, str):
230
- model_class = AUTOMODELMAP[model_class]
233
+ x = np.asarray(x)
234
+ y = np.asarray(y)
231
235
 
232
- if isinstance(incomplete_peak_model_class, str):
233
- incomplete_peak_model_class = AUTOMODELMAP[incomplete_peak_model_class]
236
+ model_class = AutoModelEnum.v(model_class)
237
+ incomplete_peak_model_class = AutoModelEnum.v(incomplete_peak_model_class)
234
238
 
235
239
  groupmodel = None
236
240
  most_left = min([p.i_index for p in peaks]) # Find the leftmost index in the group
@@ -278,12 +282,12 @@ def fit_peaks(
278
282
  peaks: List[PeakProperties],
279
283
  x: np.ndarray,
280
284
  y: np.ndarray,
281
- model_class: Type[Model] = GaussianModel,
285
+ model_class: AutoModelEnum = AutoModelEnum.GaussianModel,
282
286
  filter_negatives: bool = True,
283
287
  baseline_factor: float = 0.001,
284
288
  incomplete_threshold: float = 0.8,
285
289
  incomplete_x_extend: float = 2.0,
286
- incomplete_peak_model_class: Type[Model] = GaussianModel,
290
+ incomplete_peak_model_class: AutoModelEnum = AutoModelEnum.GaussianModel,
287
291
  iter_cb=None,
288
292
  ) -> Tuple[List[PeakProperties], Model, ModelResult]:
289
293
  """
@@ -337,21 +341,24 @@ def fit_peaks(
337
341
  Returns:
338
342
  Model: The fitted model for the global signal.
339
343
  """
340
- if isinstance(model_class, str):
341
- model_class = AUTOMODELMAP[model_class]
342
-
343
- if isinstance(incomplete_peak_model_class, str):
344
- incomplete_peak_model_class = AUTOMODELMAP[incomplete_peak_model_class]
344
+ model_class = AutoModelEnum.v(model_class)
345
+ incomplete_peak_model_class = AutoModelEnum.v(incomplete_peak_model_class)
346
+ x = np.asarray(x)
347
+ y = np.asarray(y)
345
348
 
346
349
  if len(peaks) == 0:
347
350
  raise ValueError("No peaks provided for fitting.")
348
351
 
349
352
  # Group peaks based on the baseline factor
350
353
  connected_peaks = group_signals(x, y, peaks, baseline_factor=baseline_factor)
354
+ if len(connected_peaks) == 0:
355
+ raise ValueError("No peaks after grouping.")
351
356
 
352
357
  global_model = None
353
358
  # Fit each peak group
354
359
  for pg in connected_peaks:
360
+ if len(pg) == 0:
361
+ continue
355
362
  m = fit_peak_group(
356
363
  x,
357
364
  y,
@@ -369,6 +376,9 @@ def fit_peaks(
369
376
  # Combine models for all peak groups
370
377
  global_model += m
371
378
 
379
+ if global_model is None:
380
+ raise ValueError("No model created")
381
+
372
382
  # Fit the global model to the full data
373
383
  global_fit = global_model.fit(data=y, x=x, iter_cb=iter_cb)
374
384
 
@@ -381,3 +391,25 @@ def fit_peaks(
381
391
  peak.model = get_submodel_by_prefix(global_model, f"p{peak.id}_")
382
392
 
383
393
  return peaks, global_model, global_fit
394
+
395
+
396
+ def peaks_from_fitted(fitted_peaks: List[PeakProperties]) -> List[PeakProperties]:
397
+ peaks = []
398
+ from .peak_analysis import peak_finder # noaq Avoid circular import
399
+
400
+ for p in fitted_peaks:
401
+ model = p.model
402
+ mparams = model.make_params()
403
+ y = model.eval(x=p.xfull, params=mparams)
404
+ pf: List[PeakProperties] = peak_finder.o_func(y=y, x=p.xfull)[0]
405
+ if len(pf) != 1:
406
+ raise ValueError("Expected one peak")
407
+ pf = pf[0]
408
+ pf._id = f"fitted_{p.id}"
409
+ pf.model = model
410
+ p.add_serializable_property("model", str(model))
411
+ for k, v in mparams.items():
412
+ p.add_serializable_property(k.replace(model.prefix, ""), v.value)
413
+ peaks.append(pf)
414
+
415
+ return peaks
@@ -2,7 +2,7 @@ from funcnodes import NodeDecorator, Shelf
2
2
  import funcnodes as fn
3
3
  import numpy as np
4
4
  from exposedfunctionality import controlled_wrapper
5
- from typing import Optional, List, Tuple, Union
5
+ from typing import Optional, List, Tuple
6
6
  from scipy.signal import find_peaks
7
7
  from scipy.stats import norm
8
8
  from scipy import signal, interpolate
@@ -12,7 +12,13 @@ import plotly.graph_objs as go
12
12
  from .normalization import density_normalization
13
13
  from .peaks import PeakProperties
14
14
 
15
- from .fitting import fit_peaks, AUTOMODELMAP, fit_local_peak
15
+ from .fitting import fit_peaks, AUTOMODELMAP, fit_local_peak, peaks_from_fitted
16
+
17
+ # make enum from AUTOMODELMAP
18
+ AutoModelEnum = fn.DataEnum(
19
+ "AutoModelEnum",
20
+ AUTOMODELMAP,
21
+ )
16
22
 
17
23
 
18
24
  @NodeDecorator(
@@ -217,32 +223,32 @@ def interpolation_1d(
217
223
  def force_peak_finder(
218
224
  x: np.array,
219
225
  y: np.array,
220
- basic_peaks: Union[List[PeakProperties], PeakProperties],
226
+ basic_peak: PeakProperties,
221
227
  ) -> List[PeakProperties]:
222
- # """
223
- # Identify and return the two peaks around the main peak in the given peaks dictionary.
224
-
225
- # Parameters:
226
- # - peaks (dict): A dictionary containing peak information.
227
- # It should have the keys 'peaks' and 'data'.
228
- # 'peaks' should contain a list of dictionaries with keys 'Initial index', 'Index',
229
- # and 'Ending index'.
230
- # 'data' should contain arrays 'x' and 'y'.
231
-
232
- # Returns:
233
- # - dict: A dictionary containing information about the two identified peaks.
234
- # """
235
- if isinstance(basic_peaks, (list, np.ndarray, tuple)):
236
- if len(basic_peaks) != 1:
237
- raise ValueError(
238
- "This method accepts one and only one main peak as an input."
239
- )
240
- basic_peaks = basic_peaks[0]
228
+ """
229
+ Breaks down a given main peak into two individual peaks.
230
+
231
+ The function works by calculating the first and second derivatives of the signal and
232
+ finding the local maxima and minima of the derivatives. It then determines which peak
233
+ is on the left and right side of the main peak by comparing the distance between the
234
+ main peak and the closest peak on either side.
235
+
236
+ Parameters:
237
+ - x (np.array): The x-values of the signal.
238
+ - y (np.array): The y-values of the signal.
239
+ - basic_peak (PeakProperties): The main peak.
240
+
241
+ Returns:
242
+ - List[PeakProperties]: A list of two PeakProperties objects, one for each of the
243
+ individual peaks.
244
+ """
245
+ if not isinstance(basic_peak, PeakProperties):
246
+ raise TypeError("The basic peak must be a single PeakProperties object.")
241
247
 
242
- peaks = copy.deepcopy(basic_peaks)
243
- main_peak_i_index = peaks.i_index
244
- main_peak_r_index = peaks.index
245
- main_peak_f_index = peaks.f_index
248
+ peak = copy.deepcopy(basic_peak)
249
+ main_peak_i_index = peak.i_index
250
+ main_peak_r_index = peak.index
251
+ main_peak_f_index = peak.f_index
246
252
  y_array = y
247
253
  x_array = x
248
254
  # Calculate first and second derivatives
@@ -260,9 +266,9 @@ def force_peak_finder(
260
266
  max_pp = signal.argrelmax(y_array_pp)[0]
261
267
  # min_pp = signal.argrelmin(y_array_pp)[0]
262
268
 
263
- # main_peak_i_index = peaks.i_index
264
- # main_peak_r_index = peaks.index
265
- # main_peak_f_index = peaks.f_index
269
+ # main_peak_i_index = peak.i_index
270
+ # main_peak_r_index = peak.index
271
+ # main_peak_f_index = peak.f_index
266
272
 
267
273
  # Determine which peak is on the left and right side of the main peak
268
274
  if (
@@ -300,10 +306,11 @@ def force_peak_finder(
300
306
  peak_lst = []
301
307
  peak_lst.append([peak1["I.Index"], peak1["R.Index"], peak1["F.Index"]])
302
308
  peak_lst.append([peak2["I.Index"], peak2["R.Index"], peak2["F.Index"]])
309
+
303
310
  peak_properties_list = []
304
311
  for peak_nr, peak in enumerate(peak_lst):
305
312
  peak_properties = PeakProperties(
306
- id=basic_peaks.id + f"_{peak_nr + 1}",
313
+ id=basic_peak.id + f"_{peak_nr + 1}",
307
314
  i_index=peak[0],
308
315
  index=peak[1],
309
316
  f_index=peak[2],
@@ -547,18 +554,47 @@ fit_peak_node = fn.NodeDecorator(
547
554
  id="span.peaks.fit_peak",
548
555
  name="Fit Peak",
549
556
  outputs=[{"name": "fitted_peak"}, {"name": "model"}, {"name": "fit_result"}],
550
- # separate_process=True,
557
+ separate_process=True,
551
558
  )(fit_local_peak)
552
559
 
560
+
553
561
  fit_peaks_node = fn.NodeDecorator(
554
562
  id="span.peaks.fit_peaks",
555
563
  name="Fit Peaks",
556
564
  outputs=[{"name": "fitted_peaks"}, {"name": "model"}, {"name": "fit_results"}],
557
- default_io_options={
558
- "modelname": {"value_options": {"options": list(AUTOMODELMAP.keys())}},
559
- },
560
- separate_process=True,
565
+ # separate_process=True,
561
566
  )(fit_peaks)
567
+ # @fn.controlled_wrapper(fit_peaks, wrapper_attribute="__fnwrapped__")
568
+ # def fit_peaks_node(
569
+ # peaks: List[PeakProperties],
570
+ # x: np.ndarray,
571
+ # y: np.ndarray,
572
+ # model_class: AutoModelEnum = AutoModelEnum.v("GaussianModel"),
573
+ # filter_negatives: bool = True,
574
+ # baseline_factor: float = 0.001,
575
+ # incomplete_threshold: float = 0.8,
576
+ # incomplete_x_extend: float = 2.0,
577
+ # incomplete_peak_model_class: AutoModelEnum = AutoModelEnum.v("GaussianModel"),
578
+ # ) -> Tuple[List[PeakProperties], Model, ModelResult]:
579
+ # return fit_peaks(
580
+ # peaks,
581
+ # x,
582
+ # y,
583
+ # model_class=AutoModelEnum.v(model_class),
584
+ # filter_negatives=filter_negatives,
585
+ # baseline_factor=baseline_factor,
586
+ # incomplete_threshold=incomplete_threshold,
587
+ # incomplete_x_extend=incomplete_x_extend,
588
+ # incomplete_peak_model_class=AutoModelEnum.v(incomplete_peak_model_class),
589
+ # )
590
+
591
+
592
+ peaks_from_fitted_node = fn.NodeDecorator(
593
+ id="span.peaks.peaks_from_fitted",
594
+ name="Peaks from fitted",
595
+ description="converts the fit data in peaks to regular peaks",
596
+ outputs=[{"name": "peaks"}],
597
+ )(peaks_from_fitted)
562
598
 
563
599
  PEAKS_NODE_SHELF = Shelf(
564
600
  nodes=[
@@ -568,6 +604,7 @@ PEAKS_NODE_SHELF = Shelf(
568
604
  plot_peaks,
569
605
  fit_peaks_node,
570
606
  fit_peak_node,
607
+ peaks_from_fitted_node,
571
608
  plot_fitted_peaks,
572
609
  plot_peak,
573
610
  ],
@@ -93,12 +93,14 @@ def _smooth(
93
93
 
94
94
  if mode not in _SMOOTHING_MAPPER.keys():
95
95
  raise ValueError(f"Unsupported smoothing mode: {mode}")
96
-
96
+ y = np.asarray(y)
97
97
  if x is not None:
98
+ x = np.asarray(x)
98
99
  med_xdiff = np.nanmedian(np.diff(x))
99
100
  window = window / med_xdiff
100
101
  window = int(window)
101
-
102
+ if window == 0:
103
+ return y.copy()
102
104
  return _SMOOTHING_MAPPER[mode](y, window)
103
105
 
104
106
 
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "funcnodes-span"
3
- version = "0.3.0"
4
- description = "Spectral Peak ANalysis (SPAN) for funcnodes"
3
+ version = "0.3.2"
4
+ description = "SPectral ANalysis (SPAN) for funcnodes"
5
5
  authors = ["Kourosh Rezaei <kouroshrezaei90@gmail.com>"]
6
6
  readme = "README.md"
7
7
  license = "MIT"
@@ -28,7 +28,7 @@ funcnodes-lmfit = "*"
28
28
  [tool.poetry.group.dev.dependencies]
29
29
  pytest = "*"
30
30
  pre-commit = "*"
31
- funcnodes-module = "*"
31
+ funcnodes-module = "^0.1.19"
32
32
  pooch = "*"
33
33
 
34
34
 
@@ -1 +0,0 @@
1
- Spectral Peak ANalysis (SPAN) for funcnodes
File without changes