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.
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/PKG-INFO +3 -3
- funcnodes_span-0.3.2/README.md +1 -0
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/funcnodes_span/__init__.py +1 -1
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/funcnodes_span/_curves.py +1 -1
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/funcnodes_span/curves.py +2 -2
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/funcnodes_span/fitting.py +54 -22
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/funcnodes_span/peak_analysis.py +72 -35
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/funcnodes_span/smoothing.py +4 -2
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/pyproject.toml +3 -3
- funcnodes_span-0.3.0/README.md +0 -1
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/LICENSE +0 -0
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/funcnodes_span/_baseline.py +0 -0
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/funcnodes_span/_smoothing.py +0 -0
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/funcnodes_span/baseline.py +0 -0
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/funcnodes_span/normalization.py +0 -0
- {funcnodes_span-0.3.0 → funcnodes_span-0.3.2}/funcnodes_span/peaks.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: funcnodes-span
|
|
3
|
-
Version: 0.3.
|
|
4
|
-
Summary:
|
|
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
|
-
|
|
28
|
+
SPectral ANalysis (SPAN) for funcnodes
|
|
29
29
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SPectral ANalysis (SPAN) for funcnodes
|
|
@@ -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="
|
|
20
|
-
description="
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
230
|
-
|
|
233
|
+
x = np.asarray(x)
|
|
234
|
+
y = np.asarray(y)
|
|
231
235
|
|
|
232
|
-
|
|
233
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
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
|
-
|
|
226
|
+
basic_peak: PeakProperties,
|
|
221
227
|
) -> List[PeakProperties]:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
243
|
-
main_peak_i_index =
|
|
244
|
-
main_peak_r_index =
|
|
245
|
-
main_peak_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 =
|
|
264
|
-
# main_peak_r_index =
|
|
265
|
-
# main_peak_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=
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
description = "
|
|
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
|
|
funcnodes_span-0.3.0/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Spectral Peak ANalysis (SPAN) for funcnodes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|