pyphyschemtools 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

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 (29) hide show
  1. pyphyschemtools/.__init__.py.swp +0 -0
  2. pyphyschemtools/.ipynb_checkpoints/Chem3D-checkpoint.py +835 -0
  3. pyphyschemtools/.ipynb_checkpoints/PeriodicTable-checkpoint.py +294 -0
  4. pyphyschemtools/.ipynb_checkpoints/aithermo-checkpoint.py +349 -0
  5. pyphyschemtools/.ipynb_checkpoints/core-checkpoint.py +120 -0
  6. pyphyschemtools/.ipynb_checkpoints/spectra-checkpoint.py +471 -0
  7. pyphyschemtools/.ipynb_checkpoints/survey-checkpoint.py +1048 -0
  8. pyphyschemtools/.ipynb_checkpoints/sympyUtilities-checkpoint.py +51 -0
  9. pyphyschemtools/.ipynb_checkpoints/tools4AS-checkpoint.py +964 -0
  10. pyphyschemtools/Chem3D.py +12 -8
  11. pyphyschemtools/ML.py +6 -4
  12. pyphyschemtools/PeriodicTable.py +9 -4
  13. pyphyschemtools/__init__.py +3 -3
  14. pyphyschemtools/aithermo.py +5 -6
  15. pyphyschemtools/core.py +7 -6
  16. pyphyschemtools/spectra.py +78 -58
  17. pyphyschemtools/survey.py +0 -449
  18. pyphyschemtools/sympyUtilities.py +9 -9
  19. pyphyschemtools/tools4AS.py +12 -8
  20. {pyphyschemtools-0.1.0.dist-info → pyphyschemtools-0.1.1.dist-info}/METADATA +2 -2
  21. {pyphyschemtools-0.1.0.dist-info → pyphyschemtools-0.1.1.dist-info}/RECORD +29 -20
  22. /pyphyschemtools/{icons-logos-banner → icons_logos_banner}/Logo_pyPhysChem_border.svg +0 -0
  23. /pyphyschemtools/{icons-logos-banner → icons_logos_banner}/__init__.py +0 -0
  24. /pyphyschemtools/{icons-logos-banner → icons_logos_banner}/logo.png +0 -0
  25. /pyphyschemtools/{icons-logos-banner → icons_logos_banner}/tools4pyPC_banner.png +0 -0
  26. /pyphyschemtools/{icons-logos-banner → icons_logos_banner}/tools4pyPC_banner.svg +0 -0
  27. {pyphyschemtools-0.1.0.dist-info → pyphyschemtools-0.1.1.dist-info}/WHEEL +0 -0
  28. {pyphyschemtools-0.1.0.dist-info → pyphyschemtools-0.1.1.dist-info}/licenses/LICENSE +0 -0
  29. {pyphyschemtools-0.1.0.dist-info → pyphyschemtools-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,120 @@
1
+ ############################################################
2
+ # Text Utilities
3
+ ############################################################
4
+ from .visualID_Eng import fg, bg, hl
5
+
6
+ def centerTitle(content=None):
7
+ '''
8
+ centers and renders as HTML a text in the notebook
9
+ font size = 16px, background color = dark grey, foreground color = white
10
+ '''
11
+ from IPython.display import display, HTML
12
+ display(HTML(f"<div style='text-align:center; font-weight: bold; font-size:16px;background-color: #343132;color: #ffffff'>{content}</div>"))
13
+
14
+
15
+ def centertxt(content=None,font='sans', size=12,weight="normal",bgc="#000000",fgc="#ffffff"):
16
+ '''
17
+ centers and renders as HTML a text in the notebook
18
+
19
+ input:
20
+ - content = the text to render (default: None)
21
+ - font = font family (default: 'sans', values allowed = 'sans-serif' | 'serif' | 'monospace' | 'cursive' | 'fantasy' | ...)
22
+ - size = font size (default: 12)
23
+ - weight = font weight (default: 'normal', values allowed = 'normal' | 'bold' | 'bolder' | 'lighter' | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 )
24
+ - bgc = background color (name or hex code, default = '#ffffff')
25
+ - fgc = foreground color (name or hex code, default = '#000000')
26
+ '''
27
+ from IPython.display import display, HTML
28
+ display(HTML(f"<div style='text-align:center; font-family: {font}; font-weight: {weight}; font-size:{size}px;background-color: {bgc};color: {fgc}'>{content}</div>"))
29
+
30
+
31
+ def smart_trim(img):
32
+ """
33
+ Determines the bounding box of the meaningful content in an image.
34
+
35
+ This function automatically detects if the image has transparency (Alpha channel).
36
+ If it does, it calculates the bounding box based on non-transparent pixels.
37
+ If the image is opaque, it assumes a white background and calculates the
38
+ bounding box by detecting differences from a pure white canvas.
39
+
40
+ Args:
41
+ img (PIL.Image.Image): The source image object.
42
+
43
+ Returns:
44
+ tuple: A 4-tuple (left, upper, right, lower) defining the crop box,
45
+ or None if the image is uniform/empty.
46
+ """
47
+ import sys
48
+ from pathlib import Path
49
+ from PIL import Image, ImageOps
50
+
51
+ if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info):
52
+ return img.getbbox()
53
+ else:
54
+ bg = Image.new("RGB", img.size, (255, 255, 255))
55
+ diff = ImageOps.difference(img.convert("RGB"), bg)
56
+ return diff.getbbox()
57
+
58
+ def crop_images(input_files, process_folder=False):
59
+ """
60
+ Trims whitespace or transparency from image files and saves the results.
61
+
62
+ If process_folder is True, input_files is treated as a directory path,
63
+ and all images within (excluding those ending in -C) are processed.
64
+ Otherwise, input_files is treated as a single file path or a list of paths.
65
+
66
+ The function preserves original image metadata (DPI, ICC profiles).
67
+
68
+ Args:
69
+ input_files (str, Path, or list): File path(s) or a directory path.
70
+ process_folder (bool): If True, treats input_files as a directory to crawl.
71
+
72
+ Returns:
73
+ None: Prints status messages to the console for each file processed.
74
+ """
75
+ import sys
76
+ from pathlib import Path
77
+ from PIL import Image, ImageOps
78
+
79
+ files_to_process = []
80
+
81
+ if process_folder:
82
+ folder_path = Path(input_files)
83
+ if folder_path.is_dir():
84
+ # On récupère png, jpg, jpeg (insensible à la casse)
85
+ # On exclut les fichiers finissant déjà par -C
86
+ extensions = ('*.png', '*.jpg', '*.jpeg', '*.PNG', '*.JPG', '*.JPEG')
87
+ for ext in extensions:
88
+ for f in folder_path.glob(ext):
89
+ if not f.stem.endswith('-C'):
90
+ files_to_process.append(f)
91
+ else:
92
+ print(f"❌ Error: {input_files} is not a valid directory.")
93
+ return
94
+ else:
95
+ # Logique existante pour fichier unique ou liste
96
+ if isinstance(input_files, (str, Path)):
97
+ files_to_process = [Path(input_files)]
98
+ else:
99
+ files_to_process = [Path(f) for f in input_files]
100
+
101
+ for path in files_to_process:
102
+ if not path.exists() or path.is_dir():
103
+ continue
104
+
105
+ output_path = path.with_name(f"{path.stem}-C{path.suffix}")
106
+
107
+ try:
108
+ with Image.open(path) as img:
109
+ info = img.info.copy()
110
+ bbox = smart_trim(img)
111
+
112
+ if bbox:
113
+ img_trimmed = img.crop(bbox)
114
+ img_trimmed.save(output_path, **info)
115
+ print(f"✅ Saved: {output_path.name}")
116
+ else:
117
+ print(f"⚠️ Skipping {path.name}: No content detected.")
118
+
119
+ except Exception as e:
120
+ print(f"❌ Error processing {path.name}: {e}")
@@ -0,0 +1,471 @@
1
+ ############################################################
2
+ # Absorption spectra
3
+ ############################################################
4
+ from .visualID_Eng import fg, bg, hl
5
+ from .core import centerTitle, centertxt
6
+
7
+ import numpy as np
8
+ import matplotlib.pyplot as plt
9
+ import scipy.constants as sc
10
+
11
+ class SpectrumSimulator:
12
+
13
+ def __init__(self, sigma_ev=0.3, plotWH=(12,8), \
14
+ fontSize_axisText=14, fontSize_axisLabels=14, fontSize_legends=12,
15
+ fontsize_peaks=12,
16
+ colorS='#3e89be',colorVT='#469cd6'
17
+ ):
18
+ """
19
+ Initializes the spectrum simulator
20
+
21
+ Args:
22
+ - sigma_ev (float): Gaussian half-width at half-maximum in electron-volts (eV).
23
+ Default is 0.3 eV (GaussView default is 0.4 eV).
24
+ - plotWH (tuple(int,int)): Width and Height of the matplotlib figures in inches. Default is (12,8).
25
+ - colorS: color of the simulated spectrum (default ='#3e89be')
26
+ - colorVT: color of the vertical transition line (default = '#469cd6')
27
+
28
+ Returns:
29
+ None: This method initializes the instance attributes.
30
+ Calculates:
31
+ sigmanm = half-width of the Gaussian band, in nm
32
+
33
+ """
34
+ self.sigma_ev = sigma_ev
35
+ # Conversion constante eV -> nm sigma
36
+ self.ev2nm_const = (sc.h * sc.c) * 1e9 / sc.e
37
+ self.sigmanm = self.ev2nm_const / self.sigma_ev
38
+ self.plotW = plotWH[0]
39
+ self.plotH = plotWH[1]
40
+ self.colorS = colorS
41
+ self.colorVT = colorVT
42
+ self.fig = None
43
+ self.graph = None
44
+ self.fontSize_axisText = fontSize_axisText
45
+ self.fontSize_axisLabels = fontSize_axisLabels
46
+ self.fontSize_legends = fontSize_legends
47
+ self.fontsize_peaks = fontsize_peaks
48
+
49
+ print(f"sigma = {sigma_ev} eV -> sigmanm = {self.sigmanm:.1f} nm")
50
+
51
+ def _initializePlot(self):
52
+ fig, graph = plt.subplots(figsize=(self.plotW,self.plotH))
53
+ plt.subplots_adjust(wspace=0)
54
+ plt.xticks(fontsize=self.fontSize_axisText,fontweight='bold')
55
+ plt.yticks(fontsize=self.fontSize_axisText,fontweight='bold')
56
+ return fig, graph
57
+
58
+ def _calc_epsiG(self,lambdaX,lambdai,fi):
59
+ """
60
+ calculates a Gaussian band shape around a vertical transition
61
+
62
+ input:
63
+ - lambdaX = wavelength variable, in nm
64
+ - lambdai = vertical excitation wavelength for i_th state, in nm
65
+ - fi = oscillator strength for state i (dimensionless)
66
+
67
+ output :
68
+ molar absorption coefficient, in L mol-1 cm-1
69
+
70
+ """
71
+ import scipy.constants as sc
72
+ import numpy as np
73
+ c = sc.c*1e2 #cm-1
74
+ NA = sc.N_A #mol-1
75
+ me = sc.m_e*1000 #g
76
+ e = sc.e*sc.c*10 #elementary charge in esu
77
+ pf = np.sqrt(np.pi)*e**2*NA/(1000*np.log(10)*c**2*me)
78
+ nubarX = 1e7 / lambdaX # nm to cm-1
79
+ nubari = 1e7 / lambdai
80
+ sigmabar = 1e7 / self.sigmanm
81
+ epsi = pf * (fi / sigmabar) * np.exp(-((nubarX - nubari)/sigmabar)**2)
82
+ return epsi
83
+
84
+ def _Absorbance(self,eps,opl,cc):
85
+ """
86
+ Calculates the Absorbance with the Beer-Lambert law
87
+
88
+ input:
89
+ - eps = molar absorption coefficient, in L mol-1 cm-1
90
+ - opl = optical path length, in cm
91
+ - cc = concentration of the attenuating species, in mol.L-1
92
+
93
+ output :
94
+ Absorbance, A (dimensionless)
95
+
96
+ """
97
+ return eps*opl*cc
98
+
99
+ def _sumStatesWithGf(self,wavel,wavelTAB,feTAB):
100
+ import numpy as np
101
+ sumInt = np.zeros(len(wavel))
102
+ for l in wavel:
103
+ for i in range(len(wavelTAB)):
104
+ sumInt[np.argwhere(l==wavel)[0][0]] += self._calc_epsiG(l,wavelTAB[i],feTAB[i])
105
+ return sumInt
106
+
107
+ def _FindPeaks(self,sumInt,height,prom=1):
108
+ """
109
+ Finds local maxima within the spectrum based on height and prominence.
110
+
111
+ Prominence is crucial when switching between linear and logarithmic scales:
112
+ - In Linear mode: A large prominence (e.g., 1 to 1000) filters out noise.
113
+ - In Log mode: Data is compressed into a range of ~0 to 5. A large
114
+ prominence will 'hide' real peaks. A smaller value (0.01 to 0.1)
115
+ is required to detect shoulders and overlapping bands.
116
+
117
+ Input:
118
+ - sumInt: Array of intensities (Epsilon or Absorbance).
119
+ - height: Minimum height a peak must reach to be considered.
120
+ - prom: Required vertical distance between the peak and its lowest contour line.
121
+
122
+ Returns:
123
+ - PeakIndex: Indices of the detected peaks in the wavelength array.
124
+ - PeakHeight: The intensity values at these peak positions.
125
+
126
+ """
127
+ from scipy.signal import find_peaks
128
+ peaks = find_peaks(sumInt, height = height, threshold = None, distance = 1, prominence=prom)
129
+ PeakIndex = peaks[0]
130
+ # Check if 'peak_heights' exists in the properties dictionary
131
+ if 'peak_heights' in peaks[1]:
132
+ PeakHeight = peaks[1]['peak_heights']
133
+ else:
134
+ # If height=None, we extract values manually from the input data
135
+ PeakHeight = sumInt[PeakIndex]
136
+ return PeakIndex,PeakHeight
137
+
138
+ def _FindShoulders(self, data, tP):
139
+ """
140
+ ###not working###
141
+ Detects shoulders using the second derivative.
142
+ A shoulder appears as a peak in the negative second derivative.
143
+
144
+ Note on scales:
145
+ - If ylog is True: data should be log10(sumInt) and tP should be log10(tP).
146
+ The second derivative on log data is much more sensitive to subtle
147
+ inflection points in weak transitions (like n -> pi*).
148
+ - If ylog is False: data is linear sumInt and tP is linear.
149
+
150
+ Returns:
151
+ - shoulder_idx (ndarray): Array of indices where shoulders were found.
152
+ - shoulder_heights (ndarray): The intensity values at these positions
153
+ extracted from the input data.
154
+
155
+ """
156
+ import numpy as np
157
+ # Calculate the second derivative (rate of change of the slope)
158
+ d2 = np.gradient(np.gradient(data))
159
+
160
+ # We search for peaks in the opposite of the second derivative (-d2).
161
+ # A local maximum in -d2 corresponds to a point of maximum curvature
162
+ # (inflection), which identifies a shoulder.
163
+ # We use a very low prominence threshold to capture subtle inflections.
164
+ shoulder_idx, _ = self._FindPeaks(-d2, height=None, prom=0.0001)
165
+ shoulder_heights = data[shoulder_idx]
166
+ print(shoulder_idx, shoulder_heights )
167
+
168
+ return shoulder_idx, shoulder_heights
169
+
170
+ def _pickPeak(self,wavel,peaksIndex,peaksH,color,\
171
+ shift=500,height=500,posAnnotation=200, ylog=False):
172
+ """
173
+ Annotates peaks with a small vertical tick and the wavelength value.
174
+ Adjusts offsets based on whether the plot is in log10 scale or linear.
175
+ In log mode, peaksH must already be log10 values.
176
+
177
+ """
178
+
179
+ s=shift
180
+ h=height
181
+ a=posAnnotation
182
+
183
+
184
+ for i in range(len(peaksIndex)):
185
+ x = wavel[peaksIndex[i]]
186
+ y = peaksH[i]
187
+ if ylog:
188
+ # In log scale, we use multipliers to keep the same visual distance
189
+ # 1.1 means "10% above the peak"
190
+ # Adjust these factors based on your preference
191
+ y_s = y * 1.1
192
+ y_h = y * 1.3
193
+ y_a = y * 1.5
194
+ self.graph.vlines(x, y_s, y_h, colors=color, linestyles='solid')
195
+ self.graph.annotate(f"{x:.1f}",xy=(x,y),xytext=(x,y_a),rotation=90,size=self.fontsize_peaks,ha='center',va='bottom', color=color)
196
+ else:
197
+ # Classic linear offsets
198
+ self.graph.vlines(x, y+s, y+s+h, colors=color, linestyles='solid')
199
+ self.graph.annotate(f"{x:.1f}",xy=(x,y),xytext=(x,y+s+h+a),rotation=90,size=self.fontsize_peaks,ha='center',va='bottom',color=color)
200
+ return
201
+
202
+ def _setup_axes(self, lambdamin, lambdamax, ymax, ylabel="Absorbance"):
203
+ self.graph.set_xlabel('wavelength / nm', size=self.fontSize_axisLabels, fontweight='bold', color='#2f6b91')
204
+ self.graph.set_ylabel(ylabel, size=self.fontSize_axisLabels, fontweight='bold', color='#2f6b91')
205
+ self.graph.set_xlim(lambdamin, lambdamax)
206
+ self.graph.set_ylim(0, ymax)
207
+ self.graph.tick_params(axis='both', labelsize=self.fontSize_axisText,labelcolor='black')
208
+ for tick in self.graph.xaxis.get_majorticklabels(): tick.set_fontweight('bold') #it is both powerful
209
+ # (you can specify the type of a specific tick) and annoying
210
+ for tick in self.graph.yaxis.get_majorticklabels(): tick.set_fontweight('bold')
211
+
212
+ def plotTDDFTSpectrum(self,wavel,sumInt,wavelTAB,feTAB,tP,ylog,labelSpectrum,colorS='#0000ff',colorT='#0000cf'):
213
+
214
+ """
215
+ Called by plotEps_lambda_TDDFT. Plots a single simulated UV-Vis spectrum, i.e. after
216
+ gaussian broadening, together with the TDDFT vertical transitions (i.e. plotted as lines)
217
+
218
+ Args:
219
+ wavel: array of gaussian-broadened wavelengths, in nm
220
+ sumInt: corresponding molar absorptiopn coefficients, in L. mol-1 cm-1
221
+ wavelTAB: wavelength of TDDFT, e.g. discretized, transitions
222
+ ylog: log plot of epsilon
223
+ tP: threshold for finding the peaks
224
+ feTAB: TDDFT oscillator strength for each transition of wavelTAB
225
+ labelSpectrum: title for the spectrum
226
+
227
+ """
228
+
229
+ # # --- DEBUG START ---
230
+ # if ylog:
231
+ # print(f"\n--- DEBUG LOG MODE ---")
232
+ # print(f"Max sumInt (linear): {np.max(sumInt):.2f}")
233
+ # print(f"Max sumInt (log10): {np.log10(max(np.max(sumInt), 1e-5)):.2f}")
234
+ # # --- DEBUG END ---
235
+ if ylog:
236
+ # Apply safety floor to the entire array
237
+ self.graph.set_yscale('log')
238
+ ymin_val = 1.0 # Epsilon = 1
239
+ else:
240
+ self.graph.set_yscale('linear')
241
+ ymin_val = 0
242
+
243
+ # vertical lines
244
+ for i in range(len(wavelTAB)):
245
+ val_eps = self._calc_epsiG(wavelTAB[i],wavelTAB[i],feTAB[i])
246
+ self.graph.vlines(x=wavelTAB[i], ymin=ymin_val, ymax=max(val_eps, ymin_val), colors=colorT)
247
+
248
+ self.graph.plot(wavel,sumInt,linewidth=3,linestyle='-',color=colorS,label=labelSpectrum)
249
+
250
+ self.graph.legend(fontsize=self.fontSize_legends)
251
+ if ylog:
252
+ # Use log-transformed data and log-transformed threshold
253
+ # Clipping tP to 1e-5 ensures we don't take log of 0 or negative
254
+ tPlog = np.log10(max(tP, 1e-5))
255
+ # prom=0.05 allows detection of peaks that are close in log-magnitude
256
+ peaks, peaksH_log = self._FindPeaks(np.log10(np.clip(sumInt, 1e-5, None)), tPlog, prom=0.05)
257
+ peaksH = 10**peaksH_log
258
+ # shoulders, shouldersH_log = self._FindShoulders(np.log10(np.clip(sumInt, 1e-5, None)), tPlog)
259
+ # all_idx = np.concatenate((peaks, shoulders))
260
+ # allH_log = np.concatenate((peaksH_log, shouldersH_log))
261
+ # allH = 10**allH_log
262
+ else:
263
+ peaks, peaksH = self._FindPeaks(sumInt,tP)
264
+ # shoulders, shouldersH = self._FindShoulders(wavel, sumInt, tP)
265
+ # all_idx = np.concatenate((peaks, shoulders))
266
+ # allH = np.concatenate((peaksH, shouldersH))
267
+ self._pickPeak(wavel,peaks,peaksH,colorS,500,500,200,ylog)
268
+
269
+
270
+ def plotEps_lambda_TDDFT(self,datFile,lambdamin=200,lambdamax=800,\
271
+ epsMax=None, titles=None, tP = 10, \
272
+ ylog=False,\
273
+ filename=None):
274
+ """
275
+ Plots a TDDFT VUV simulated spectrum (vertical transitions and transitions summed with gaussian functions)
276
+ between lambdamin and lambdamax
277
+
278
+ The sum of states is done in the range
279
+ [lambdamin-50, lambdamax+50] nm.
280
+
281
+ Args:
282
+ datFile: list of pathway/names to "XXX_ExcStab.dat" files generated by 'GParser Gaussian.log -S'
283
+ lambdamin, lambdamax: plot range
284
+ epsMax: y axis graph limit
285
+ titles: list of titles (1 per spectrum plot)
286
+ tP: threshold for finding the peaks (default = 10 L. mol-1 cm-1)
287
+ ylog: y logarithmic axis (default: False).
288
+ save: saves in a png file (300 dpi) if True (default = False)
289
+ filename: saves figure in a 300 dpi png file if not None (default), with filename=full pathway
290
+
291
+ """
292
+ import matplotlib.ticker as ticker
293
+
294
+ if self.fig is not None:
295
+ graph = self.graph
296
+ fig = self.fig
297
+ lambdamin = self.lambdamin
298
+ lambdamax = self.lambdamax
299
+ epsMax = self.epsMax
300
+ else:
301
+ fig, graph = self._initializePlot()
302
+
303
+ graph.set_prop_cycle(None)
304
+
305
+ if self.fig is None:
306
+ self.fig = fig
307
+ self.graph = graph
308
+ self.lambdamin = lambdamin
309
+ self.lambdamax = lambdamax
310
+ self.epsMax = epsMax
311
+
312
+ graph.set_xlabel('wavelength / nm',size=self.fontSize_axisLabels,fontweight='bold',color='#2f6b91')
313
+
314
+ graph.set_xlim(lambdamin,lambdamax)
315
+
316
+ graph.xaxis.set_major_locator(ticker.MultipleLocator(50)) # sets a tick for every integer multiple of the base (here 250) within the view interval
317
+
318
+ istate,state,wavel,fe,SSq = np.genfromtxt(datFile,skip_header=1,dtype="<U20,<U20,float,float,<U20",unpack=True)
319
+ wavel = np.array(wavel)
320
+ fe = np.array(fe)
321
+ if wavel.size == 1:
322
+ wavel = np.array([wavel])
323
+ fe = np.array([fe])
324
+ wvl = np.arange(lambdamin-50,lambdamax+50,1)
325
+ sumInt = self._sumStatesWithGf(wvl,wavel,fe)
326
+ self.plotTDDFTSpectrum(wvl,sumInt,wavel,fe,tP,ylog,titles,self.colorS,self.colorVT)
327
+ if ylog:
328
+ graph.set_ylabel('log(molar absorption coefficient / L mol$^{-1}$ cm$^{-1})$',size=self.fontSize_axisLabels,fontweight='bold',color='#2f6b91')
329
+ graph.set_ylim(1, epsMax * 5 if epsMax else None)
330
+ else:
331
+ graph.set_yscale('linear')
332
+ graph.set_ylabel('molar absorption coefficient / L mol$^{-1}$ cm$^{-1}$',size=self.fontSize_axisLabels,fontweight='bold',color='#2f6b91')
333
+ graph.set_ylim(0, epsMax if epsMax else np.max(sumInt)*1.18)
334
+ if filename is not None: self.fig.savefig(filename, dpi=300, bbox_inches='tight')
335
+ plt.show()
336
+
337
+ peaksI, peaksH = self._FindPeaks(sumInt,tP)
338
+ print(f"{bg.LIGHTREDB}{titles}{bg.OFF}")
339
+ for i in range(len(peaksI)):
340
+ print(f"peak {i:3}. {wvl[peaksI[i]]:4} nm. epsilon_max = {peaksH[i]:.1f} L mol-1 cm-1")
341
+ if ylog:
342
+ print()
343
+ # prom=0.05 allows detection of peaks that are close in log-magnitude
344
+ peaksI, peaksH = self._FindPeaks(np.log10(np.clip(sumInt, 1e-5, None)), np.log10(max(tP, 1e-5)), prom=0.05)
345
+ for i in range(len(peaksI)):
346
+ print(f"peak {i:3}. {wvl[peaksI[i]]:4} nm. log10(epsilon_max) = {peaksH[i]:.1f}")
347
+
348
+ def plotAbs_lambda_TDDFT(self, datFiles=None, C0=1e-5, lambdamin=200, lambdamax=800, Amax=2.0,\
349
+ titles=None, linestyles=[], annotateP=[], tP = 0.1,\
350
+ resetColors=False,\
351
+ filename=None):
352
+ """
353
+ Plots a simulated TDDFT VUV absorbance spectrum (transitions summed with gaussian functions)
354
+ between lambdamin and lambdamax (sum of states done in the range [lambdamin-50, lambdamlax+50] nm)
355
+
356
+ Args:
357
+ datFiles: list of pathway/name to files generated by 'GParser Gaussian.log -S'
358
+ C0: list of concentrations needed to calculate A = epsilon x l x c (in mol.L-1)
359
+ lambdamin, lambdamax: plot range (x axis)
360
+ Amax: y axis graph limit
361
+ titles: list of titles (1 per spectrum plot)
362
+ linestyles: list of line styles(default = "-", i.e. a continuous line)
363
+ annotateP: list of Boolean (annotate lambda max True or False. Default = True)
364
+ tP: threshold for finding the peaks (default = 0.1)
365
+ resetColors (bool): If True, resets the matplotlib color cycle
366
+ to the first color. This allows different series
367
+ (e.g., gas phase vs. solvent) to share the same
368
+ color coding for each molecule across multiple calls. Default: False
369
+ save: saves in a png file (300 dpi) if True (default = False)
370
+ filename: saves figure in a 300 dpi png file if not None (default), with filename=full pathway
371
+
372
+ """
373
+
374
+ if self.fig is None:
375
+ fig, graph = self._initializePlot()
376
+ self.fig = fig
377
+ self.graph = graph
378
+ self.lambdamin = lambdamin
379
+ self.lambdamax = lambdamax
380
+ self.Amax = Amax
381
+ else:
382
+ graph = self.graph
383
+ fig = self.fig
384
+ lambdamin = self.lambdamin
385
+ lambdamax = self.lambdamax
386
+ Amax = self.Amax
387
+ if resetColors: graph.set_prop_cycle(None)
388
+
389
+ if linestyles == []: linestyles = len(datFiles)*['-']
390
+ if annotateP == []: annotateP = len(datFiles)*[True]
391
+
392
+ self._setup_axes(lambdamin, lambdamax, self.Amax, ylabel="Absorbance")
393
+
394
+ wvl = np.arange(lambdamin-50,lambdamax+50,1)
395
+ for f in range(len(datFiles)):
396
+ istate,state,wavel,fe,SSq = np.genfromtxt(datFiles[f],skip_header=1,dtype="<U20,<U20,float,float,<U20",unpack=True)
397
+ sumInt = self._sumStatesWithGf(wvl,wavel,fe)
398
+ Abs = self._Absorbance(sumInt,1,C0[f])
399
+ plot=self.graph.plot(wvl,Abs,linewidth=3,linestyle=linestyles[f],label=f"{titles[f]}. TDDFT ($C_0$={C0[f]} mol/L)")
400
+ peaksI, peaksH = self._FindPeaks(Abs,tP,0.01)
401
+ if (annotateP[f]): self._pickPeak(wvl,peaksI,peaksH,plot[0].get_color(),0.01,0.04,0.02)
402
+ print(f"{bg.LIGHTREDB}TDDFT. {titles[f]}{bg.OFF}")
403
+ for i in range(len(peaksI)):
404
+ print(f"peak {i:3}. {wvl[peaksI[i]]:4} nm. A = {peaksH[i]:.2f}")
405
+
406
+ self.graph.legend(fontsize=self.fontSize_legends)
407
+
408
+ if filename is not None: self.fig.savefig(filename, dpi=300, bbox_inches='tight')
409
+
410
+ return
411
+
412
+ def plotAbs_lambda_exp(self, csvFiles, C0, lambdamin=200, lambdamax=800,\
413
+ Amax=2.0, titles=None, linestyles=[], annotateP=[], tP = 0.1,\
414
+ filename=None):
415
+ """
416
+ Plots an experimental VUV absorbance spectrum read from a csv file between lambdamin and lambdamax
417
+
418
+ Args:
419
+ - superpose: False = plots a new graph, otherwise the plot is superposed to a previously created one
420
+ (probably with plotAbs_lambda_TDDFT())
421
+ - csvfiles: list of pathway/name to experimental csvFiles (see examples for the format)
422
+ - C0: list of experimental concentrations, i.e. for each sample
423
+ - lambdamin, lambdamax: plot range (x axis)
424
+ - Amax: graph limit (y axis)
425
+ - titles: list of titles (1 per spectrum plot)
426
+ - linestyles: list of line styles(default = "--", i.e. a dashed line)
427
+ - annotateP: list of Boolean (annotate lambda max True or False. Default = True)
428
+ - tP: threshold for finding the peaks (default = 0.1)
429
+ - save: saves in a png file (300 dpi) if True (default = False)
430
+ - filename: saves figure in a 300 dpi png file if not None (default), with filename=full pathway
431
+
432
+ """
433
+ if linestyles == []: linestyles = len(csvFiles)*['--']
434
+ if annotateP == []: annotateP = len(csvFiles)*[True]
435
+
436
+ if self.fig is not None:
437
+ graph = self.graph
438
+ fig = self.fig
439
+ lambdamin = self.lambdamin
440
+ lambdamax = self.lambdamax
441
+ Amax = self.Amax
442
+ else:
443
+ fig, graph = self._initializePlot()
444
+
445
+ graph.set_prop_cycle(None)
446
+
447
+ if self.fig is None:
448
+ self.graph = graph
449
+ self.fig = fig
450
+ self.lambdamin = lambdamin
451
+ self.lambdamax = lambdamax
452
+ self.Amax = Amax
453
+
454
+ self._setup_axes(lambdamin, lambdamax, self.Amax, ylabel="Absorbance")
455
+
456
+ for f in range(len(csvFiles)):
457
+ wavel,Abs = np.genfromtxt(csvFiles[f],skip_header=1,unpack=True,delimiter=";")
458
+ wavel *= 1e9
459
+ plot=graph.plot(wavel,Abs,linewidth=3,linestyle=linestyles[f],label=f"{titles[f]}. exp ($C_0$={C0[f]} mol/L)")
460
+ peaksI, peaksH = self._FindPeaks(Abs,tP,0.01)
461
+ if (annotateP[f]): self._pickPeak(wavel,peaksI,peaksH,plot[0].get_color(),0.01,0.04,0.02)
462
+ print(f"{bg.LIGHTREDB}exp. {titles[f]}{bg.OFF}")
463
+ for i in range(len(peaksI)):
464
+ print(f"peak {i:3}. {wavel[peaksI[i]]:4} nm. A = {peaksH[i]:.2f}")
465
+
466
+ graph.legend(fontsize=self.fontSize_legends)
467
+
468
+ if filename is not None: self.fig.savefig(filename, dpi=300, bbox_inches='tight')
469
+
470
+ return
471
+