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.
Files changed (33) hide show
  1. spark_spectroscopy-2.0.0/PKG-INFO +17 -0
  2. spark_spectroscopy-2.0.0/pyproject.toml +33 -0
  3. spark_spectroscopy-2.0.0/setup.cfg +4 -0
  4. spark_spectroscopy-2.0.0/spark/__init__.py +4 -0
  5. spark_spectroscopy-2.0.0/spark/core/__init__.py +4 -0
  6. spark_spectroscopy-2.0.0/spark/core/core_analysis.py +290 -0
  7. spark_spectroscopy-2.0.0/spark/core/diagnostics.py +184 -0
  8. spark_spectroscopy-2.0.0/spark/core/fit.py +348 -0
  9. spark_spectroscopy-2.0.0/spark/core/fitting/__init__.py +4 -0
  10. spark_spectroscopy-2.0.0/spark/core/fitting/global_fitter.py +382 -0
  11. spark_spectroscopy-2.0.0/spark/core/fitting/optimizer.py +112 -0
  12. spark_spectroscopy-2.0.0/spark/core/models.py +202 -0
  13. spark_spectroscopy-2.0.0/spark/data/__init__.py +4 -0
  14. spark_spectroscopy-2.0.0/spark/data/dataset.py +102 -0
  15. spark_spectroscopy-2.0.0/spark/data/importer.py +556 -0
  16. spark_spectroscopy-2.0.0/spark/gui/GlobalFitClassGui.py +3692 -0
  17. spark_spectroscopy-2.0.0/spark/gui/__init__.py +4 -0
  18. spark_spectroscopy-2.0.0/spark/gui/dialogs.py +325 -0
  19. spark_spectroscopy-2.0.0/spark/gui/flups.py +1452 -0
  20. spark_spectroscopy-2.0.0/spark/gui/maps_from_timescans.py +393 -0
  21. spark_spectroscopy-2.0.0/spark/gui/model_builder.py +396 -0
  22. spark_spectroscopy-2.0.0/spark/gui/plotters/__init__.py +4 -0
  23. spark_spectroscopy-2.0.0/spark/gui/plotters/explorers.py +497 -0
  24. spark_spectroscopy-2.0.0/spark/gui/plotters/publication.py +791 -0
  25. spark_spectroscopy-2.0.0/spark/gui/tas.py +799 -0
  26. spark_spectroscopy-2.0.0/spark/gui/utils.py +83 -0
  27. spark_spectroscopy-2.0.0/spark/main.py +329 -0
  28. spark_spectroscopy-2.0.0/spark_spectroscopy.egg-info/PKG-INFO +17 -0
  29. spark_spectroscopy-2.0.0/spark_spectroscopy.egg-info/SOURCES.txt +31 -0
  30. spark_spectroscopy-2.0.0/spark_spectroscopy.egg-info/dependency_links.txt +1 -0
  31. spark_spectroscopy-2.0.0/spark_spectroscopy.egg-info/entry_points.txt +2 -0
  32. spark_spectroscopy-2.0.0/spark_spectroscopy.egg-info/requires.txt +5 -0
  33. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()