spark-spectroscopy 2.0.0__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.
- spark_spectroscopy-2.0.0/PKG-INFO +17 -0
- spark_spectroscopy-2.0.0/pyproject.toml +33 -0
- spark_spectroscopy-2.0.0/setup.cfg +4 -0
- spark_spectroscopy-2.0.0/spark/__init__.py +4 -0
- spark_spectroscopy-2.0.0/spark/core/__init__.py +4 -0
- spark_spectroscopy-2.0.0/spark/core/core_analysis.py +290 -0
- spark_spectroscopy-2.0.0/spark/core/diagnostics.py +184 -0
- spark_spectroscopy-2.0.0/spark/core/fit.py +348 -0
- spark_spectroscopy-2.0.0/spark/core/fitting/__init__.py +4 -0
- spark_spectroscopy-2.0.0/spark/core/fitting/global_fitter.py +382 -0
- spark_spectroscopy-2.0.0/spark/core/fitting/optimizer.py +112 -0
- spark_spectroscopy-2.0.0/spark/core/models.py +202 -0
- spark_spectroscopy-2.0.0/spark/data/__init__.py +4 -0
- spark_spectroscopy-2.0.0/spark/data/dataset.py +102 -0
- spark_spectroscopy-2.0.0/spark/data/importer.py +556 -0
- spark_spectroscopy-2.0.0/spark/gui/GlobalFitClassGui.py +3692 -0
- spark_spectroscopy-2.0.0/spark/gui/__init__.py +4 -0
- spark_spectroscopy-2.0.0/spark/gui/dialogs.py +325 -0
- spark_spectroscopy-2.0.0/spark/gui/flups.py +1452 -0
- spark_spectroscopy-2.0.0/spark/gui/maps_from_timescans.py +393 -0
- spark_spectroscopy-2.0.0/spark/gui/model_builder.py +396 -0
- spark_spectroscopy-2.0.0/spark/gui/plotters/__init__.py +4 -0
- spark_spectroscopy-2.0.0/spark/gui/plotters/explorers.py +497 -0
- spark_spectroscopy-2.0.0/spark/gui/plotters/publication.py +791 -0
- spark_spectroscopy-2.0.0/spark/gui/tas.py +799 -0
- spark_spectroscopy-2.0.0/spark/gui/utils.py +83 -0
- spark_spectroscopy-2.0.0/spark/main.py +329 -0
- spark_spectroscopy-2.0.0/spark_spectroscopy.egg-info/PKG-INFO +17 -0
- spark_spectroscopy-2.0.0/spark_spectroscopy.egg-info/SOURCES.txt +31 -0
- spark_spectroscopy-2.0.0/spark_spectroscopy.egg-info/dependency_links.txt +1 -0
- spark_spectroscopy-2.0.0/spark_spectroscopy.egg-info/entry_points.txt +2 -0
- spark_spectroscopy-2.0.0/spark_spectroscopy.egg-info/requires.txt +5 -0
- spark_spectroscopy-2.0.0/spark_spectroscopy.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spark-spectroscopy
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Professional Python suite for ultrafast transient absorption and fluorescence spectroscopy analysis.
|
|
5
|
+
Author-email: Alejandro Serrano Capote <tu.correo@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: numpy>=1.20.0
|
|
14
|
+
Requires-Dist: scipy>=1.6.0
|
|
15
|
+
Requires-Dist: matplotlib>=3.3.0
|
|
16
|
+
Requires-Dist: PyQt5>=5.15.0
|
|
17
|
+
Requires-Dist: h5py>=3.0.0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spark-spectroscopy"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Professional Python suite for ultrafast transient absorption and fluorescence spectroscopy analysis."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Alejandro Serrano Capote", email = "tu.correo@example.com" }
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
19
|
+
]
|
|
20
|
+
requires-python = ">=3.8"
|
|
21
|
+
dependencies = [
|
|
22
|
+
"numpy>=1.20.0",
|
|
23
|
+
"scipy>=1.6.0",
|
|
24
|
+
"matplotlib>=3.3.0",
|
|
25
|
+
"PyQt5>=5.15.0",
|
|
26
|
+
"h5py>=3.0.0"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
spark-gui = "spark.main:main"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
include = ["spark", "spark.*"]
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# core_analysis.py
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from scipy.interpolate import interp1d
|
|
5
|
+
from scipy.optimize import curve_fit
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------------------------
|
|
10
|
+
# Models and Correction Functions
|
|
11
|
+
# ---------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
def eV_a_nm(E_eV):
|
|
14
|
+
"""
|
|
15
|
+
Converts energy in electron-volts (eV) to wavelength in nanometers (nm).
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
E_eV (numpy.ndarray or float): Energy in eV.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
numpy.ndarray or float: Corresponding wavelength in nm.
|
|
22
|
+
"""
|
|
23
|
+
# Prevent division by zero by temporarily replacing 0 with 1
|
|
24
|
+
E_eV_safe = np.where(E_eV == 0, 1, E_eV)
|
|
25
|
+
return 1239.841984 / E_eV_safe
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def t0_model(w, a, b, c, d):
|
|
29
|
+
"""
|
|
30
|
+
Proposed non-linear model for time-zero (t0) dispersion correction.
|
|
31
|
+
|
|
32
|
+
Formula: t0 = a * sqrt((b*w^2 - 1) / (c*w^2 - 1)) + d
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
w (array-like): Wavelength points.
|
|
36
|
+
a, b, c, d (float): Fitting parameters.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
numpy.ndarray: Computed t0 values. Returns NaN where the expression is mathematically invalid.
|
|
40
|
+
"""
|
|
41
|
+
w = np.asarray(w, dtype=float)
|
|
42
|
+
num = b * w**2 - 1.0
|
|
43
|
+
den = c * w**2 - 1.0
|
|
44
|
+
|
|
45
|
+
out = np.full_like(w, np.nan, dtype=float)
|
|
46
|
+
|
|
47
|
+
# Suppress warnings for expected invalid operations (like negative roots or division by zero)
|
|
48
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
49
|
+
ratio = num / den
|
|
50
|
+
valid = (den != 0) & (ratio >= 0)
|
|
51
|
+
out[valid] = a * np.sqrt(ratio[valid]) + d
|
|
52
|
+
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def apply_t0_correction_poly(popt, WL, TD, data):
|
|
57
|
+
"""
|
|
58
|
+
Applies a polynomial time-zero (t0) correction to the dataset.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
popt (array-like): Polynomial coefficients (must be of length 5, degree 4).
|
|
62
|
+
WL (numpy.ndarray): Wavelength array.
|
|
63
|
+
TD (numpy.ndarray): Time Delay array.
|
|
64
|
+
data (numpy.ndarray): 2D array of the main data matrix.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
tuple: A tuple containing:
|
|
68
|
+
- corrected (numpy.ndarray): The t0-corrected 2D data matrix.
|
|
69
|
+
- t0_lambda (numpy.ndarray): The calculated t0 offset for each wavelength.
|
|
70
|
+
"""
|
|
71
|
+
coeffs = np.asarray(popt)
|
|
72
|
+
if coeffs.size != 5:
|
|
73
|
+
raise ValueError("Polynomial coefficients must have length 5.")
|
|
74
|
+
|
|
75
|
+
t0_lambda = np.polyval(coeffs, WL)
|
|
76
|
+
corrected = np.zeros_like(data)
|
|
77
|
+
|
|
78
|
+
for i, wl in enumerate(WL):
|
|
79
|
+
delay_corr = TD - t0_lambda[i]
|
|
80
|
+
|
|
81
|
+
# SOLUTION: Strictly use 0.0 to fill out-of-bounds data to prevent extrapolation artifacts
|
|
82
|
+
f = interp1d(delay_corr, data[i, :], kind='linear', bounds_error=False, fill_value=0.0)
|
|
83
|
+
corrected[i, :] = f(TD)
|
|
84
|
+
|
|
85
|
+
return corrected, t0_lambda
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def apply_t0_correction_nonlinear(popt, WL, TD, data):
|
|
89
|
+
"""
|
|
90
|
+
Applies a non-linear time-zero (t0) correction to the dataset using `t0_model`.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
popt (list or array-like): Optimized parameters [a, b, c, d] for the non-linear model.
|
|
94
|
+
WL (numpy.ndarray): Wavelength array.
|
|
95
|
+
TD (numpy.ndarray): Time Delay array.
|
|
96
|
+
data (numpy.ndarray): 2D array of the main data matrix.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
tuple: A tuple containing:
|
|
100
|
+
- corrected (numpy.ndarray): The t0-corrected 2D data matrix.
|
|
101
|
+
- t0_lambda (numpy.ndarray): The calculated t0 offset for each wavelength.
|
|
102
|
+
"""
|
|
103
|
+
t0_lambda = t0_model(WL, *popt)
|
|
104
|
+
corrected = data.copy()
|
|
105
|
+
|
|
106
|
+
for i, t0_val in enumerate(t0_lambda):
|
|
107
|
+
if np.isfinite(t0_val):
|
|
108
|
+
delay_corr = TD - t0_val
|
|
109
|
+
|
|
110
|
+
# SOLUTION: Strictly use 0.0 to fill out-of-bounds data to prevent extrapolation artifacts
|
|
111
|
+
f = interp1d(delay_corr, data[i, :], kind='linear', bounds_error=False, fill_value=0.0)
|
|
112
|
+
corrected[i, :] = f(TD)
|
|
113
|
+
else:
|
|
114
|
+
# If the calculated t0 is not finite, leave the data uncorrected for that wavelength
|
|
115
|
+
corrected[i, :] = data[i, :]
|
|
116
|
+
|
|
117
|
+
return corrected, t0_lambda
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def fit_t0(w_points, t0_points, WL, TD, data, min_points_nonlinear=4, mode='auto'):
|
|
121
|
+
"""
|
|
122
|
+
Fits time-zero (t0) dispersion based on user-selected points (w_points, t0_points).
|
|
123
|
+
|
|
124
|
+
The function attempts to fit the non-linear model (`t0_model`) if there are enough points.
|
|
125
|
+
If the non-linear fit fails or lacks sufficient points, it falls back to a polynomial
|
|
126
|
+
fit of up to degree 4.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
w_points (array-like): Wavelengths (nm) of the chosen data points.
|
|
130
|
+
t0_points (array-like): Corresponding delays (ps) of the chosen data points.
|
|
131
|
+
WL (numpy.ndarray): Full wavelength array (from read_csv_file or load_from_paths).
|
|
132
|
+
TD (numpy.ndarray): Full time delay array.
|
|
133
|
+
data (numpy.ndarray): Full 2D data matrix.
|
|
134
|
+
min_points_nonlinear (int): Minimum number of points required to attempt the non-linear model. Defaults to 4.
|
|
135
|
+
mode (str): Fitting mode strategy. 'auto' (default), 'nonlinear' (force non-linear), or 'poly' (force polynomial).
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
dict: A dictionary containing the fitting results:
|
|
139
|
+
- 'method': String indicating the method used ('nonlinear' or 'polyX').
|
|
140
|
+
- 'popt': The optimized coefficients/parameters.
|
|
141
|
+
- 'fit_x': High-resolution X array used for plotting the fit line.
|
|
142
|
+
- 'fit_y': High-resolution Y array representing the fit curve.
|
|
143
|
+
- 'corrected': The newly corrected 2D data matrix.
|
|
144
|
+
- 't0_lambda': The calculated t0 vector for every wavelength in WL.
|
|
145
|
+
"""
|
|
146
|
+
w = np.asarray(w_points, dtype=float)
|
|
147
|
+
t0 = np.asarray(t0_points, dtype=float)
|
|
148
|
+
|
|
149
|
+
if w.size < 2:
|
|
150
|
+
raise ValueError("At least 2 points are required for fitting (ideally >= 4 for the non-linear model).")
|
|
151
|
+
|
|
152
|
+
# High-resolution array strictly for smooth plotting of the fit curve
|
|
153
|
+
fit_x = np.linspace(np.min(w), np.max(w), 400)
|
|
154
|
+
|
|
155
|
+
# =======================================================
|
|
156
|
+
# --- Forced Mode: Polynomial ---------------------------
|
|
157
|
+
# =======================================================
|
|
158
|
+
if mode == 'poly':
|
|
159
|
+
if w.size < 5:
|
|
160
|
+
deg = min(4, max(1, w.size - 1))
|
|
161
|
+
else:
|
|
162
|
+
deg = 4
|
|
163
|
+
|
|
164
|
+
coeffs = np.polyfit(w, t0, deg)
|
|
165
|
+
|
|
166
|
+
# Ensure coefficients array is strictly length 5 (pad with leading zeros)
|
|
167
|
+
if coeffs.size < 5:
|
|
168
|
+
coeffs = np.concatenate([np.zeros(5 - coeffs.size), coeffs])
|
|
169
|
+
|
|
170
|
+
fit_y = np.polyval(coeffs, fit_x)
|
|
171
|
+
corrected, t0_lambda = apply_t0_correction_poly(coeffs, WL, TD, data)
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
'method': f'poly{deg}',
|
|
175
|
+
'popt': coeffs,
|
|
176
|
+
'fit_x': fit_x,
|
|
177
|
+
'fit_y': fit_y,
|
|
178
|
+
'corrected': corrected,
|
|
179
|
+
't0_lambda': t0_lambda
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# =======================================================
|
|
183
|
+
# --- Forced Mode: Non-Linear ---------------------------
|
|
184
|
+
# =======================================================
|
|
185
|
+
if mode == 'nonlinear':
|
|
186
|
+
try:
|
|
187
|
+
wmin = np.min(w)
|
|
188
|
+
# Initial parameter estimates (p0)
|
|
189
|
+
a0 = (np.nanmax(t0) - np.nanmin(t0)) / 2.0 if np.isfinite(t0).any() else 0.0
|
|
190
|
+
d0 = np.nanmedian(t0)
|
|
191
|
+
min_required = 1.0 / (wmin**2) if wmin != 0 else 1e-8
|
|
192
|
+
b0 = min_required * 1.1
|
|
193
|
+
c0 = min_required * 1.2
|
|
194
|
+
|
|
195
|
+
p0 = [a0, b0, c0, d0]
|
|
196
|
+
|
|
197
|
+
# Bound constraints to ensure stability
|
|
198
|
+
bounds = ([-np.inf, min_required, min_required, -np.inf],
|
|
199
|
+
[np.inf, np.inf, np.inf, np.inf])
|
|
200
|
+
|
|
201
|
+
popt, pcov = curve_fit(
|
|
202
|
+
t0_model, w, t0,
|
|
203
|
+
p0=p0, bounds=bounds,
|
|
204
|
+
maxfev=20000, method="trf"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
fit_y = t0_model(fit_x, *popt)
|
|
208
|
+
corrected, t0_lambda = apply_t0_correction_nonlinear(popt, WL, TD, data)
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
'method': 'nonlinear',
|
|
212
|
+
'popt': popt,
|
|
213
|
+
'fit_x': fit_x,
|
|
214
|
+
'fit_y': fit_y,
|
|
215
|
+
'corrected': corrected,
|
|
216
|
+
't0_lambda': t0_lambda
|
|
217
|
+
}
|
|
218
|
+
except Exception as e:
|
|
219
|
+
raise RuntimeError(f"Non-linear fit failed: {e}")
|
|
220
|
+
|
|
221
|
+
# =======================================================
|
|
222
|
+
# --- Auto Mode (Original Behavior) ---------------------
|
|
223
|
+
# =======================================================
|
|
224
|
+
try_nl = (w.size >= min_points_nonlinear)
|
|
225
|
+
|
|
226
|
+
if try_nl:
|
|
227
|
+
try:
|
|
228
|
+
wmin = np.min(w)
|
|
229
|
+
# Initial parameter estimates (p0)
|
|
230
|
+
a0 = (np.nanmax(t0) - np.nanmin(t0)) / 2.0 if np.isfinite(t0).any() else 0.0
|
|
231
|
+
d0 = np.nanmedian(t0)
|
|
232
|
+
min_required = 1.0 / (wmin**2) if wmin != 0 else 1e-8
|
|
233
|
+
b0 = min_required * 1.1
|
|
234
|
+
c0 = min_required * 1.2
|
|
235
|
+
|
|
236
|
+
p0 = [a0, b0, c0, d0]
|
|
237
|
+
|
|
238
|
+
# Bound constraints to ensure stability
|
|
239
|
+
bounds = ([-np.inf, min_required, min_required, -np.inf],
|
|
240
|
+
[np.inf, np.inf, np.inf, np.inf])
|
|
241
|
+
|
|
242
|
+
popt, pcov = curve_fit(
|
|
243
|
+
t0_model, w, t0,
|
|
244
|
+
p0=p0, bounds=bounds,
|
|
245
|
+
maxfev=20000, method="trf"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
fit_y = t0_model(fit_x, *popt)
|
|
249
|
+
|
|
250
|
+
# Check if the curve is stable before accepting it
|
|
251
|
+
if np.all(np.isfinite(fit_y)):
|
|
252
|
+
corrected, t0_lambda = apply_t0_correction_nonlinear(popt, WL, TD, data)
|
|
253
|
+
return {
|
|
254
|
+
'method': 'nonlinear',
|
|
255
|
+
'popt': popt,
|
|
256
|
+
'fit_x': fit_x,
|
|
257
|
+
'fit_y': fit_y,
|
|
258
|
+
'corrected': corrected,
|
|
259
|
+
't0_lambda': t0_lambda
|
|
260
|
+
}
|
|
261
|
+
except Exception:
|
|
262
|
+
pass # If the non-linear fit fails, proceed to polynomial fallback
|
|
263
|
+
|
|
264
|
+
# --- Polynomial Fallback ---
|
|
265
|
+
# Determine the degree based on the number of available points
|
|
266
|
+
if w.size < 5:
|
|
267
|
+
deg = min(3, max(1, w.size - 1))
|
|
268
|
+
else:
|
|
269
|
+
deg = 4
|
|
270
|
+
|
|
271
|
+
coeffs = np.polyfit(w, t0, deg)
|
|
272
|
+
|
|
273
|
+
# Ensure coefficients array is strictly length 5
|
|
274
|
+
if coeffs.size < 5:
|
|
275
|
+
coeffs = np.concatenate([np.zeros(5 - coeffs.size), coeffs])
|
|
276
|
+
|
|
277
|
+
fit_y = np.polyval(coeffs, fit_x)
|
|
278
|
+
corrected, t0_lambda = apply_t0_correction_poly(coeffs, WL, TD, data)
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
'method': f'poly{deg}',
|
|
282
|
+
'popt': coeffs,
|
|
283
|
+
'fit_x': fit_x,
|
|
284
|
+
'fit_y': fit_y,
|
|
285
|
+
'corrected': corrected,
|
|
286
|
+
't0_lambda': t0_lambda
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import matplotlib.colors as mcolors
|
|
4
|
+
|
|
5
|
+
def analyze(result, param_names=None):
|
|
6
|
+
"""
|
|
7
|
+
Imprime un resumen profesional con los resultados del ajuste y
|
|
8
|
+
las estadísticas del motor de optimización (SciPy).
|
|
9
|
+
"""
|
|
10
|
+
print("="*50)
|
|
11
|
+
print(" SPARK FIT DIAGNOSTICS")
|
|
12
|
+
print("="*50)
|
|
13
|
+
|
|
14
|
+
# 1. Estadísticas de convergencia
|
|
15
|
+
res = result.scipy_res
|
|
16
|
+
print("[Convergence Status]")
|
|
17
|
+
print(f" Message: {res.message}")
|
|
18
|
+
print(f" Success: {res.success}")
|
|
19
|
+
print(f" Cost (Sum of Squares): {res.cost:.4e}")
|
|
20
|
+
print(f" Function Evaluations: {res.nfev}")
|
|
21
|
+
print("-" * 50)
|
|
22
|
+
|
|
23
|
+
# 2. Parámetros optimizados
|
|
24
|
+
print("[Optimized Parameters]")
|
|
25
|
+
if param_names is None:
|
|
26
|
+
# Nombres por defecto si no se especifican
|
|
27
|
+
param_names = ["w (IRF FWHM)", "t0 (Time Zero)"] + [f"Tau {i+1}" for i in range(len(result.x)-2)]
|
|
28
|
+
|
|
29
|
+
for name, val in zip(param_names, result.x):
|
|
30
|
+
print(f" {name:<15}: {val:>10.4f}")
|
|
31
|
+
print("="*50)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def plot_spectra(dataset, target_times):
|
|
35
|
+
"""
|
|
36
|
+
Dibuja los espectros a los tiempos exactos (o más cercanos) solicitados.
|
|
37
|
+
target_times: lista de tiempos en ps, ej: [0.1, 1.0, 10.0]
|
|
38
|
+
"""
|
|
39
|
+
plt.figure(figsize=(8, 5))
|
|
40
|
+
|
|
41
|
+
for t in target_times:
|
|
42
|
+
# Encontrar el índice del tiempo más cercano en el dataset
|
|
43
|
+
idx = np.abs(dataset.td - t).argmin()
|
|
44
|
+
actual_t = dataset.td[idx]
|
|
45
|
+
|
|
46
|
+
plt.plot(dataset.wl, dataset.data[:, idx], label=f'{actual_t:.2f} ps')
|
|
47
|
+
|
|
48
|
+
plt.axhline(0, color='black', linestyle='--', linewidth=0.8)
|
|
49
|
+
plt.xlabel("Wavelength (nm)")
|
|
50
|
+
plt.ylabel("ΔA")
|
|
51
|
+
plt.title("Transient Spectra")
|
|
52
|
+
plt.legend()
|
|
53
|
+
plt.tight_layout()
|
|
54
|
+
plt.show()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def plot_kinetics_fit(dataset, result, target_wavelengths, use_symlog=True):
|
|
58
|
+
"""
|
|
59
|
+
Dibuja los datos experimentales (puntos) y el ajuste (línea)
|
|
60
|
+
para las longitudes de onda solicitadas.
|
|
61
|
+
target_wavelengths: lista de wls en nm, ej: [450, 500, 600]
|
|
62
|
+
"""
|
|
63
|
+
plt.figure(figsize=(8, 5))
|
|
64
|
+
|
|
65
|
+
for w in target_wavelengths:
|
|
66
|
+
# Encontrar el índice de la longitud de onda más cercana
|
|
67
|
+
idx = np.abs(dataset.wl - w).argmin()
|
|
68
|
+
actual_w = dataset.wl[idx]
|
|
69
|
+
|
|
70
|
+
# Puntos experimentales (scatter)
|
|
71
|
+
p = plt.plot(dataset.td, dataset.data[idx, :], 'o', markersize=4, alpha=0.6, label=f'Data {actual_w:.1f} nm')
|
|
72
|
+
color = p[0].get_color()
|
|
73
|
+
|
|
74
|
+
# Línea del Fit
|
|
75
|
+
plt.plot(dataset.td, result.fitres[idx, :], '-', color=color, linewidth=2, label=f'Fit {actual_w:.1f} nm')
|
|
76
|
+
|
|
77
|
+
plt.axhline(0, color='black', linestyle='--', linewidth=0.8)
|
|
78
|
+
|
|
79
|
+
# Escala logarítmica para los tiempos (muy útil en espectroscopía)
|
|
80
|
+
if use_symlog:
|
|
81
|
+
plt.xscale('symlog', linthresh=1.0)
|
|
82
|
+
|
|
83
|
+
plt.xlabel("Delay Time (ps)")
|
|
84
|
+
plt.ylabel("ΔA")
|
|
85
|
+
plt.title("Kinetics & Global Fit")
|
|
86
|
+
plt.legend()
|
|
87
|
+
plt.tight_layout()
|
|
88
|
+
plt.show()
|
|
89
|
+
|
|
90
|
+
def plot_associated_spectra(dataset, result, num_species=None):
|
|
91
|
+
"""
|
|
92
|
+
Dibuja los espectros asociados (DAS o SAS) extraídos por el ajuste VarPro.
|
|
93
|
+
Extrae automáticamente las constantes de tiempo (taus) para la leyenda.
|
|
94
|
+
"""
|
|
95
|
+
plt.figure(figsize=(8, 5))
|
|
96
|
+
|
|
97
|
+
# Si no le pasas el número de especies, lo deduce de los parámetros
|
|
98
|
+
if num_species is None:
|
|
99
|
+
num_species = len(result.x) - 2 # Restamos w y t0
|
|
100
|
+
|
|
101
|
+
taus = result.x[2 : 2 + num_species]
|
|
102
|
+
|
|
103
|
+
# Dibujamos las especies cinéticas
|
|
104
|
+
for i in range(num_species):
|
|
105
|
+
plt.plot(dataset.wl, result.amplitudes[i, :], linewidth=2,
|
|
106
|
+
label=f'Especie {i+1} ($\\tau$ = {taus[i]:.2f} ps)')
|
|
107
|
+
|
|
108
|
+
# Si la matriz de amplitudes tiene más filas que especies, es que hay un Artefacto Coherente
|
|
109
|
+
if result.amplitudes.shape[0] > num_species:
|
|
110
|
+
# Dibujamos el artefacto (o sus derivadas) con un estilo diferente para no confundir
|
|
111
|
+
plt.plot(dataset.wl, result.amplitudes[num_species, :],
|
|
112
|
+
linestyle=':', color='gray', label='Coherent Artifact')
|
|
113
|
+
|
|
114
|
+
plt.axhline(0, color='black', linestyle='--', linewidth=0.8)
|
|
115
|
+
plt.xlabel("Wavelength (nm)")
|
|
116
|
+
plt.ylabel("Amplitud ($\\Delta$A)")
|
|
117
|
+
plt.title("Associated Spectra (DAS / SAS)")
|
|
118
|
+
plt.legend()
|
|
119
|
+
plt.tight_layout()
|
|
120
|
+
plt.show()
|
|
121
|
+
|
|
122
|
+
def plot_single_map(dataset, title="2D Map", use_symlog=True):
|
|
123
|
+
"""
|
|
124
|
+
Dibuja un único mapa 2D a partir de un objeto SpectroscopyDataset.
|
|
125
|
+
Ideal para visualizar el archivo crudo o el efecto del pre-procesado.
|
|
126
|
+
"""
|
|
127
|
+
plt.figure(figsize=(8, 5))
|
|
128
|
+
|
|
129
|
+
# Centramos el colorbar en 0 para visualizar bien las absorciones/emisiones
|
|
130
|
+
max_val = np.nanmax(np.abs(dataset.data))
|
|
131
|
+
norm = mcolors.TwoSlopeNorm(vmin=-max_val, vcenter=0, vmax=max_val)
|
|
132
|
+
|
|
133
|
+
# Usamos pcolormesh que es ideal para ejes no lineales
|
|
134
|
+
im = plt.pcolormesh(dataset.td, dataset.wl, dataset.data,
|
|
135
|
+
cmap='jet', norm=norm, shading='nearest')
|
|
136
|
+
|
|
137
|
+
if use_symlog:
|
|
138
|
+
plt.gca().set_xscale('symlog', linthresh=1.0)
|
|
139
|
+
|
|
140
|
+
plt.xlabel("Delay Time (ps)")
|
|
141
|
+
plt.ylabel("Wavelength (nm)")
|
|
142
|
+
plt.title(title)
|
|
143
|
+
plt.colorbar(im, label="Amplitud ($\\Delta$A)")
|
|
144
|
+
plt.tight_layout()
|
|
145
|
+
plt.show()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def plot_fit_maps(dataset, result, use_symlog=True, vmin=None, vmax=None):
|
|
149
|
+
"""
|
|
150
|
+
Dibuja un panel 1x3 comparando: Datos, Ajuste Global y Matriz de Residuales.
|
|
151
|
+
"""
|
|
152
|
+
fig, axes = plt.subplots(1, 3, figsize=(15, 5), sharex=True, sharey=True)
|
|
153
|
+
|
|
154
|
+
# Si no se dan límites, calculamos la escala simétrica respecto al máximo de los datos
|
|
155
|
+
if vmin is None or vmax is None:
|
|
156
|
+
max_val = np.nanmax(np.abs(dataset.data))
|
|
157
|
+
vmin, vmax = -max_val, max_val
|
|
158
|
+
|
|
159
|
+
norm = mcolors.TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
|
|
160
|
+
cmap = 'jet'
|
|
161
|
+
|
|
162
|
+
# 1. Datos experimentales
|
|
163
|
+
axes[0].pcolormesh(dataset.td, dataset.wl, dataset.data, cmap=cmap, norm=norm, shading='nearest')
|
|
164
|
+
axes[0].set_title("Processed Data")
|
|
165
|
+
|
|
166
|
+
# 2. Superficie ajustada (Fit)
|
|
167
|
+
axes[1].pcolormesh(dataset.td, dataset.wl, result.fitres, cmap=cmap, norm=norm, shading='nearest')
|
|
168
|
+
axes[1].set_title("Global Fit")
|
|
169
|
+
|
|
170
|
+
# 3. Residuales (Error)
|
|
171
|
+
im = axes[2].pcolormesh(dataset.td, dataset.wl, result.resid, cmap=cmap, norm=norm, shading='nearest')
|
|
172
|
+
axes[2].set_title("Residuals")
|
|
173
|
+
|
|
174
|
+
for ax in axes:
|
|
175
|
+
if use_symlog:
|
|
176
|
+
ax.set_xscale('symlog', linthresh=1.0)
|
|
177
|
+
ax.set_xlabel("Delay Time (ps)")
|
|
178
|
+
|
|
179
|
+
axes[0].set_ylabel("Wavelength (nm)")
|
|
180
|
+
|
|
181
|
+
# Añadir una única barra de color compartida para todo el panel
|
|
182
|
+
fig.colorbar(im, ax=axes.ravel().tolist(), label="Amplitud ($\\Delta$A)", aspect=30, pad=0.02)
|
|
183
|
+
|
|
184
|
+
plt.show()
|