pyphyschemtools 0.1.0__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 (80) hide show
  1. pyphyschemtools/Chem3D.py +831 -0
  2. pyphyschemtools/ML.py +42 -0
  3. pyphyschemtools/PeriodicTable.py +289 -0
  4. pyphyschemtools/__init__.py +43 -0
  5. pyphyschemtools/aithermo.py +350 -0
  6. pyphyschemtools/cheminformatics.py +230 -0
  7. pyphyschemtools/core.py +119 -0
  8. pyphyschemtools/icons-logos-banner/Logo_pyPhysChem_border.svg +1109 -0
  9. pyphyschemtools/icons-logos-banner/__init__.py +0 -0
  10. pyphyschemtools/icons-logos-banner/logo.png +0 -0
  11. pyphyschemtools/icons-logos-banner/tools4pyPC_banner.png +0 -0
  12. pyphyschemtools/icons-logos-banner/tools4pyPC_banner.svg +193 -0
  13. pyphyschemtools/kinetics.py +193 -0
  14. pyphyschemtools/resources/css/BrainHalfHalf-120x139.base64 +1 -0
  15. pyphyschemtools/resources/css/BrainHalfHalf-120x139.png +0 -0
  16. pyphyschemtools/resources/css/BrainHalfHalf.base64 +8231 -0
  17. pyphyschemtools/resources/css/BrainHalfHalf.png +0 -0
  18. pyphyschemtools/resources/css/BrainHalfHalf.svg +289 -0
  19. pyphyschemtools/resources/css/visualID.css +325 -0
  20. pyphyschemtools/resources/img/Tranformative_3.webp +0 -0
  21. pyphyschemtools/resources/img/Tranformative_3_banner.png +0 -0
  22. pyphyschemtools/resources/img/pyPhysChem_1.png +0 -0
  23. pyphyschemtools/resources/svg/BrainHalfHalf.png +0 -0
  24. pyphyschemtools/resources/svg/BrainHalfHalf.svg +289 -0
  25. pyphyschemtools/resources/svg/GitHub-Logo-C.png +0 -0
  26. pyphyschemtools/resources/svg/GitHub-Logo.png +0 -0
  27. pyphyschemtools/resources/svg/Logo-Universite-Toulouse-n-2023.png +0 -0
  28. pyphyschemtools/resources/svg/Logo_pyPhysChem_1-translucentBgd-woName.png +0 -0
  29. pyphyschemtools/resources/svg/Logo_pyPhysChem_1-translucentBgd.png +0 -0
  30. pyphyschemtools/resources/svg/Logo_pyPhysChem_1.png +0 -0
  31. pyphyschemtools/resources/svg/Logo_pyPhysChem_1.svg +622 -0
  32. pyphyschemtools/resources/svg/Logo_pyPhysChem_5.png +0 -0
  33. pyphyschemtools/resources/svg/Logo_pyPhysChem_5.svg +48 -0
  34. pyphyschemtools/resources/svg/Logo_pyPhysChem_border.svg +1109 -0
  35. pyphyschemtools/resources/svg/Python-logo-notext.svg +265 -0
  36. pyphyschemtools/resources/svg/Python_logo_and_wordmark.svg.png +0 -0
  37. pyphyschemtools/resources/svg/UT3_logoQ.jpg +0 -0
  38. pyphyschemtools/resources/svg/UT3_logoQ.png +0 -0
  39. pyphyschemtools/resources/svg/Universite-Toulouse-n-2023.svg +141 -0
  40. pyphyschemtools/resources/svg/X.png +0 -0
  41. pyphyschemtools/resources/svg/logoAnaconda.png +0 -0
  42. pyphyschemtools/resources/svg/logoAnaconda.webp +0 -0
  43. pyphyschemtools/resources/svg/logoCNRS.png +0 -0
  44. pyphyschemtools/resources/svg/logoDebut.svg +316 -0
  45. pyphyschemtools/resources/svg/logoEnd.svg +172 -0
  46. pyphyschemtools/resources/svg/logoFin.svg +172 -0
  47. pyphyschemtools/resources/svg/logoPPCL.svg +359 -0
  48. pyphyschemtools/resources/svg/logoPytChem.png +0 -0
  49. pyphyschemtools/resources/svg/logo_lpcno_300_dpi_notexttransparent.png +0 -0
  50. pyphyschemtools/resources/svg/logo_pyPhysChem.png +0 -0
  51. pyphyschemtools/resources/svg/logo_pyPhysChem_0.png +0 -0
  52. pyphyschemtools/resources/svg/logo_pyPhysChem_0.svg +390 -0
  53. pyphyschemtools/resources/svg/logopyPhyschem.png +0 -0
  54. pyphyschemtools/resources/svg/logopyPhyschem_2.webp +0 -0
  55. pyphyschemtools/resources/svg/logopyPhyschem_3.webp +0 -0
  56. pyphyschemtools/resources/svg/logopyPhyschem_4.webp +0 -0
  57. pyphyschemtools/resources/svg/logopyPhyschem_5.png +0 -0
  58. pyphyschemtools/resources/svg/logopyPhyschem_5.webp +0 -0
  59. pyphyschemtools/resources/svg/logopyPhyschem_6.webp +0 -0
  60. pyphyschemtools/resources/svg/logopyPhyschem_7.webp +0 -0
  61. pyphyschemtools/resources/svg/logos-Anaconda-pyPhysChem.png +0 -0
  62. pyphyschemtools/resources/svg/logos-Anaconda-pyPhysChem.svg +58 -0
  63. pyphyschemtools/resources/svg/pyPCBanner.svg +309 -0
  64. pyphyschemtools/resources/svg/pyPhysChem-GitHubSocialMediaTemplate.png +0 -0
  65. pyphyschemtools/resources/svg/pyPhysChem-GitHubSocialMediaTemplate.svg +295 -0
  66. pyphyschemtools/resources/svg/pyPhysChemBanner.png +0 -0
  67. pyphyschemtools/resources/svg/pyPhysChemBanner.svg +639 -0
  68. pyphyschemtools/resources/svg/qrcode-pyPhysChem.png +0 -0
  69. pyphyschemtools/resources/svg/repository-open-graph-template.png +0 -0
  70. pyphyschemtools/spectra.py +451 -0
  71. pyphyschemtools/survey.py +1048 -0
  72. pyphyschemtools/sympyUtilities.py +51 -0
  73. pyphyschemtools/tools4AS.py +960 -0
  74. pyphyschemtools/visualID.py +101 -0
  75. pyphyschemtools/visualID_Eng.py +175 -0
  76. pyphyschemtools-0.1.0.dist-info/METADATA +38 -0
  77. pyphyschemtools-0.1.0.dist-info/RECORD +80 -0
  78. pyphyschemtools-0.1.0.dist-info/WHEEL +5 -0
  79. pyphyschemtools-0.1.0.dist-info/licenses/LICENSE +674 -0
  80. pyphyschemtools-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,350 @@
1
+ import os
2
+ import sys
3
+ import glob
4
+ import numpy as np
5
+ from pathlib import Path
6
+ from PIL import Image, ImageOps
7
+ import plotly.graph_objects as go
8
+ import plotly.io as pio
9
+ import seaborn as sns
10
+ from matplotlib import pyplot as plt
11
+
12
+ class aiThermo:
13
+ """
14
+ A class to handle thermodynamic surface stability analysis and visualization
15
+ within the tools4pyPhysChem framework.
16
+ """
17
+
18
+ def __init__(self, folder_path=None, color_scales=None):
19
+ """
20
+ Initialize the aiThermo object.
21
+
22
+ Args:
23
+ folder_path (str or Path): Path to the working directory.
24
+ color_scales (list, optional): List of plotly-compatible color scales.
25
+ """
26
+ self.folder_path = Path(folder_path) if folder_path else None
27
+ self.color_scales = color_scales or [
28
+ [[0, "#dadada"], [1, "#dadada"]], [[0, "#99daaf"], [1, "#99daaf"]],
29
+ [[0, "#f1aeaf"], [1, "#f1aeaf"]], [[0, "#81bbda"], [1, "#81bbda"]],
30
+ [[0, "#da9ac9"], [1, "#da9ac9"]], [[0, "#79dad7"], [1, "#79dad7"]],
31
+ [[0, "#da9f6e"], [1, "#da9f6e"]], [[0, "#b5a8da"], [1, "#b5a8da"]],
32
+ [[0, "#edf1c6"], [1, "#edf1c6"]], [[0, "#c4ffe3"], [1, "#c4ffe3"]],
33
+ [[0, "#61b3ff"], [1, "#61b3ff"]]
34
+ ]
35
+ self.palette = [c[0][1] for c in self.color_scales]
36
+ def _check_folder(self):
37
+ """Internal check to ensure folder_path is set before file operations."""
38
+ if self.folder_path is None:
39
+ raise ValueError(
40
+ "❌ Error: folder_path is not defined for this instance. "
41
+ "Please provide a path when initializing: aiThermo(folder_path='...')"
42
+ )
43
+ if not self.folder_path.exists():
44
+ raise FileNotFoundError(f"❌ Error: The directory {self.folder_path} does not exist.")
45
+
46
+ def ListOfStableSurfaceCompositions(self, vib):
47
+ """
48
+ Identify and list the relevant thermodynamic data files for the current analysis.
49
+
50
+ This method scans the working directory for data files matching specific
51
+ naming conventions (TPcoverage or TPcoveragevib). It cross-references
52
+ these with a local configuration file 'ListOfStableSurfaces.dat' to
53
+ extract surface names and legend labels.
54
+
55
+ Args:
56
+ vib (bool): If True, filters for files including vibrational corrections
57
+ (prefixed with 'vib_'). If False, looks for standard thermodynamic data.
58
+
59
+ Returns:
60
+ tuple: A triplet containing:
61
+ - file_paths (list of str): Absolute or relative paths to the .dat files.
62
+ - names (list of str): Internal identifiers for each surface phase.
63
+ - legends (list of str): LaTeX-formatted or plain text labels for
64
+ graphical legends.
65
+
66
+ Notes:
67
+ - The lists are returned in reverse order to ensure correct layering
68
+ during 3D plotting.
69
+ - Relies on 'ListOfStableSurfaces.dat' existing in the folder_path.
70
+ """
71
+ self._check_folder()
72
+ from .core import centertxt
73
+ import glob
74
+ pattern = "TPcoveragevib_*.dat" if vib else "TPcoverage_*.dat"
75
+ file_paths = glob.glob(str(self.folder_path / pattern))
76
+ listOfMinCov = self.folder_path / "ListOfStableSurfaces.dat"
77
+ print(f"List of Stable surfaces is in: {listOfMinCov}")
78
+ # if vib:
79
+ # file_paths = glob.glob(os.path.join(self.folder_path, "TPcoveragevib_*.dat"))
80
+ # else:
81
+ # file_paths = glob.glob(os.path.join(self.folder_path, "TPcoverage_*.dat"))
82
+ # print(vib,file_paths)
83
+ # listOfMinCov = os.path.join(self.folder_path, "ListOfStableSurfaces.dat")
84
+ # print("list of Stable surfaces is in: ",listOfMinCov)
85
+ try:
86
+ with open(listOfMinCov, "r") as f:
87
+ lines = [line.rstrip('\n').split() for line in f]
88
+
89
+ file_paths = []
90
+ names = []
91
+ legends = []
92
+ for l in lines:
93
+ # file_paths = file_paths + glob.glob(os.path.join(self.folder_path, l[0]))
94
+ file_paths = file_paths + glob.glob(str(self.folder_path / l[0]))
95
+ names = names + [l[1]]
96
+ # legends = legends + [l[2]]
97
+ legends.append(fr"{l[2]}") # The 'fr' ensures it is a Raw Formatted string
98
+ names = names[::-1]
99
+ legends = legends[::-1]
100
+ file_paths = file_paths[::-1]
101
+ centertxt(f"List of stable surface compositions. Vibrations = {vib}",size=14,weight="bold")
102
+ if not vib:
103
+ file_paths = [f.replace('vib_', '_') for f in file_paths]
104
+ for i,f in enumerate(file_paths):
105
+ print(f"{f} {names[i]} {legends[i]}")
106
+
107
+ except FileNotFoundError:
108
+ print(f"ListOfStableSurfaces.dat file has not been found in the {self.folder_path} folder. Exiting...")
109
+ sys.exit()
110
+ return file_paths,names,legends
111
+
112
+ def plot_surface(self, saveFig=None, vib=True, texLegend=False, xLegend=0.5, yLegend=0.4):
113
+ """
114
+ Generate an interactive 3D thermodynamic stability map using Plotly.
115
+
116
+ This method visualizes multiple Gibbs free energy surfaces as a function of
117
+ Temperature (X) and Pressure (Y). It automatically handles log-scale
118
+ transformations for the pressure axis and projects reference experimental
119
+ conditions and phase boundaries onto the plot.
120
+
121
+ Args:
122
+ saveFig (str, optional): The filename (without extension) to export
123
+ the resulting plot as a PNG image. Defaults to None (no save).
124
+ vib (bool): Whether to use vibration-corrected data. Defaults to True.
125
+ texLegend (bool): If True, uses LaTeX legends extracted from the
126
+ configuration file. Defaults to False.
127
+ xLegend (float): Horizontal position of the legend box (0 to 1).
128
+ Defaults to 0.5.
129
+ yLegend (float): Vertical position of the legend box (0 to 1).
130
+ Defaults to 0.4.
131
+
132
+ Returns:
133
+ plotly.graph_objects.FigureWidget: An interactive widget containing
134
+ the 3D surfaces, experimental markers, and reference lines.
135
+
136
+ Workflow:
137
+ 1. Scans data files and parses Temperature/Pressure/Energy grids.
138
+ 2. Traces individual 3D surfaces with mapped color scales.
139
+ 3. Calculates and plots intersection boundaries between surface phases.
140
+ 4. Overlays experimental markers (e.g., specific T/P conditions).
141
+ 5. Optionally exports and crops the resulting image using Pillow.
142
+ """
143
+ self._check_folder()
144
+ import os
145
+ # Define tick values explicitly for log scale (powers of 10)
146
+ color_scales = self.color_scales
147
+ logmin = -20
148
+ logmax = 5
149
+ log_tick_vals = np.logspace(logmin, logmax, num=1+(logmax-logmin)//5) # Example range from 10^20 to 10^5
150
+ log_tick_labels = [f"10<sup>{int(np.log10(tick))}</sup>" for tick in log_tick_vals] # Format labels as 10^n
151
+ import plotly.graph_objects as go
152
+
153
+ stableSurfaces, nameOfStableSurfaces, legendOfStableSurfaces = self.ListOfStableSurfaceCompositions(vib)
154
+
155
+ # FIX: Track all Z values to find a true global minimum for the floor
156
+ all_z_mins = []
157
+
158
+ fig = go.Figure()
159
+
160
+ for i, file_path in enumerate(stableSurfaces):
161
+ with open(file_path, "r") as f:
162
+ lines = f.readlines()
163
+
164
+ series = []
165
+ temp = []
166
+ for line in lines:
167
+ if line.strip():
168
+ temp.append(list(map(float, line.split())))
169
+ else:
170
+ if temp:
171
+ series.append(np.array(temp))
172
+ temp = []
173
+ if temp:
174
+ series.append(np.array(temp))
175
+
176
+ data = np.array(series)
177
+
178
+ X = data[:, :, 0]
179
+ Y = data[:, :, 1]
180
+ Z = data[:, :, 2]
181
+ all_z_mins.append(np.min(Z))
182
+
183
+ fig.add_trace(go.Surface(
184
+ x=X,
185
+ y=Y,
186
+ z=Z,
187
+ colorscale=color_scales[i % len(color_scales)],
188
+ showscale=False,
189
+ name = nameOfStableSurfaces[i]))
190
+
191
+ if legendOfStableSurfaces[i] != "None" and texLegend:
192
+ name=f"{legendOfStableSurfaces[i]}"
193
+ else:
194
+ name=f"{nameOfStableSurfaces[i]}"
195
+ fig.add_trace(go.Scatter3d(
196
+ x=[None], y=[None], z=[None], # Invisible point
197
+ mode="markers",
198
+ name=f"{name}",
199
+ marker=dict(color=color_scales[i % len(color_scales)][-1][1], size=10),
200
+ showlegend=True))
201
+
202
+ # FIX: Calculate zmin globally
203
+ zmin = np.min(all_z_mins) - 50
204
+
205
+ fig.add_trace(go.Scatter3d(
206
+ x=[55+273.15,90+273.15], y=[np.log10(2),np.log10(4)], z=[zmin-10,zmin-10], # Invisible point
207
+ mode="markers",
208
+ marker=dict(color='red', size=10, symbol='cross'),
209
+ name='exp. Conditions (55°C, 2 bar & 90°C, 4 bar)',
210
+ showlegend=True))
211
+
212
+ fig.add_trace(go.Scatter3d(
213
+ x=[0, 1000], y=[np.log10(1),np.log10(1)], z=[zmin+600]*2,
214
+ mode="lines",
215
+ line=dict(color="blue", width=3),
216
+ name="1 bar",
217
+ showlegend=False
218
+ ))
219
+
220
+ fig.add_trace(go.Scatter3d(
221
+ x=[298, 298], y=[-20,5], z=[zmin+600]*2,
222
+ mode="lines",
223
+ line=dict(color="black", width=3),
224
+ name="298 K",
225
+ showlegend=False
226
+ ))
227
+
228
+
229
+ fig.update_layout(
230
+ width=1200, # Increase figure width (default is ~700)
231
+ height=1200,
232
+ paper_bgcolor='rgba(0,0,0,0)', # White background outside the plot
233
+ plot_bgcolor='rgba(0,0,0,0)', # White background inside the 3D plot
234
+
235
+ margin = dict(l=0,r=0,t=0,b=0),
236
+
237
+ scene=dict(
238
+ aspectmode="manual", # Allows custom aspect ratio
239
+ aspectratio=dict(x=1.15, y=1.15, z=1), # Adjust scaling
240
+ xaxis=dict(
241
+ title=dict(
242
+ text="Temperature / K",
243
+ font=dict(size=16, family="Arial", color="blue", weight='bold'),
244
+ ),
245
+ autorange="reversed", # This inverts the x-axis direction
246
+ showgrid=True,
247
+ zeroline=True,
248
+ tickfont=dict(color="black", size=15,weight="bold"),
249
+ tickangle=0,
250
+ ticklen=10,
251
+ tickwidth=2,
252
+ ticks="outside",
253
+ showbackground=False, # Enable background to create a frame
254
+ backgroundcolor="grey" # Black frame
255
+ ),
256
+ yaxis=dict(
257
+ title=dict(
258
+ text="Pressure / bar",
259
+ font=dict(size=16, family="Arial", color="blue", weight='bold'),
260
+ ),
261
+ tickangle=0, # Rotate Y-axis ticks
262
+ showgrid=True,
263
+ zeroline=True,
264
+ type='log',
265
+ tickvals=log_tick_vals.tolist(), # Set tick positions
266
+ ticktext=log_tick_labels, # Display ticks as 10^(-n)
267
+ tickfont=dict(color="black", size=15,weight="bold"),
268
+ ticklen=10,
269
+ tickwidth=2,
270
+ ticks="outside",
271
+ showbackground=False, # Enable background to create a frame
272
+ backgroundcolor="grey" # Black frame
273
+ ),
274
+ zaxis=dict(
275
+ title="",
276
+ showgrid=False,
277
+ zeroline=False,
278
+ showticklabels=False,
279
+ showbackground=False, # Enable background to create a frame
280
+ backgroundcolor="grey" # Black frame
281
+ ),
282
+ camera=dict(
283
+ eye=dict(x=1e-5, y=-1e-2, z=-1000),
284
+ # eye=dict(x=1e-5, y=-1e-2, z=-1000),
285
+ up=dict(x=0, y=1, z=0),
286
+ projection=dict(type="orthographic")
287
+ ),
288
+ ),
289
+ legend=dict(
290
+ # y=0,
291
+ # x=0.2,
292
+ x = xLegend, y = yLegend,
293
+ font=dict(size=13, color="black"),
294
+ bgcolor="rgba(255, 255, 255, 1)", # Light transparent background
295
+ bordercolor="grey",
296
+ borderwidth=1,
297
+ itemsizing='constant'
298
+ ),
299
+ showlegend=True
300
+ )
301
+
302
+ if saveFig is not None:
303
+ from .core import crop_images
304
+ # pngFile = os.path.join(folder_path, savedFig+".png")
305
+ # import plotly.io as pio
306
+ # fig.write_image(pngFile, format="png", width=1200, height=1200, scale=3)
307
+ pngFile = self.folder_path / f"{saveFig}.png"
308
+ fig.write_image(pngFile, format="png", width=1200, height=1200, scale=3)
309
+ # Automatic crop after saving
310
+ crop_images(pngFile)
311
+
312
+ fig_widget = go.FigureWidget(fig)
313
+ fig_widget.show()
314
+ return fig_widget
315
+
316
+ def plot_palette(self, angle=0, save_png=None):
317
+ """
318
+ Visualize the 1D color palette used for surface identification.
319
+
320
+ This method generates a horizontal bar of colors corresponding to the
321
+ different surface phases defined in the instance. Each color is labeled
322
+ with its numerical index, allowing for quick cross-referencing between
323
+ the palette and the 3D surface plot.
324
+
325
+ Args:
326
+ angle (int, optional): Rotation angle of the x-axis tick labels (indices).
327
+ Defaults to 0.
328
+ save_png (str, optional): Filename (including .png extension) to save
329
+ the palette image to the working directory. Defaults to None
330
+ (display only).
331
+
332
+ Returns:
333
+ None: Displays the plot using matplotlib.pyplot.show().
334
+
335
+ Notes:
336
+ - Requires 'seaborn' for the palplot generation.
337
+ - If 'save_png' is provided, the image is saved with a resolution of
338
+ 300 DPI and a transparent background.
339
+ """
340
+ names = [str(i) for i in range(len(self.palette))]
341
+ sns.palplot(sns.color_palette(self.palette))
342
+ ax = plt.gca()
343
+ ax.set_xticks(np.arange(len(names)))
344
+ ax.set_xticklabels(names, weight='bold', size=10, rotation=angle)
345
+
346
+ if save_png:
347
+ plt.tight_layout()
348
+ plt.savefig(self.folder_path / save_png, dpi=300, transparent=True)
349
+ plt.show()
350
+
@@ -0,0 +1,230 @@
1
+ ############################################################
2
+ # easy_rdkit
3
+ ############################################################
4
+ from .visualID_Eng import fg, bg, hl
5
+ from .core import centerTitle, centertxt
6
+
7
+ import rdkit
8
+ from rdkit import Chem
9
+ from rdkit.Chem import AllChem, GetPeriodicTable, Draw, rdCoordGen
10
+ import pandas as pd
11
+ from rdkit.Chem.Draw import rdMolDraw2D
12
+ from IPython.display import SVG
13
+ from PIL import Image
14
+
15
+ class easy_rdkit():
16
+ """
17
+ A helper class to analyze and visualize molecules using RDKit.
18
+ Provides tools for Lewis structure analysis and advanced 2D drawing.
19
+ """
20
+
21
+ def __init__(self,smiles, canonical=True):
22
+ """
23
+ Initialize the molecule object from a SMILES string.
24
+
25
+ Args:
26
+ smiles (str): The SMILES representation of the molecule.
27
+ canonical (bool): If True, converts the SMILES to its canonical form
28
+ to ensure consistent atom numbering and uniqueness.
29
+ """
30
+ from rdkit import Chem
31
+
32
+ mol = Chem.MolFromSmiles(smiles)
33
+ if mol is None:
34
+ raise ValueError(f"Invalid SMILES string: {smiles}")
35
+
36
+ if canonical:
37
+ # Generate canonical isomeric SMILES
38
+ self.smiles = Chem.MolToSmiles(mol, isomericSmiles=True, canonical=True)
39
+ # Re-load the molecule from the canonical SMILES to sync atom indices
40
+ self.mol = Chem.MolFromSmiles(self.smiles)
41
+ else:
42
+ self.mol=mol
43
+ self.smiles = smiles
44
+
45
+ def analyze_lewis(self):
46
+ """
47
+ Performs a Lewis structure analysis for each atom in the molecule.
48
+ Calculates valence electrons, lone pairs, formal charges, and octet rule compliance.
49
+
50
+ Returns:
51
+ pd.DataFrame: A table containing detailed Lewis electronic data per atom.
52
+ """
53
+ if self.mol is None:
54
+ raise ValueError(f"Molécule invalide pour {self.smiles} (SMILES incorrect ?)")
55
+
56
+ pt = GetPeriodicTable()
57
+ rows = []
58
+
59
+ for atom in self.mol.GetAtoms():
60
+ Z = atom.GetAtomicNum()
61
+ valence_e = pt.GetNOuterElecs(Z)
62
+ bonding_e = atom.GetTotalValence()
63
+ formal_charge = atom.GetFormalCharge()
64
+ num_bonds = int(sum(bond.GetBondTypeAsDouble() for bond in atom.GetBonds()))
65
+ # hybridization = atom.GetHybridization()
66
+ nonbonding = valence_e - bonding_e - formal_charge
67
+
68
+ lone_pairs = max(0, nonbonding // 2)
69
+
70
+ if Z==1 or Z==2: # règle du duet
71
+ target = 2
72
+ else: # règle de l’octet
73
+ target = 8
74
+
75
+ missing_e = max(0, target/2 - (bonding_e + 2*lone_pairs))
76
+ vacancies = int(missing_e)
77
+ total_e = 2*(lone_pairs + bonding_e)
78
+
79
+ if total_e > 8:
80
+ octet_msg = "❌ hypercoordiné"
81
+ elif total_e < 8 and Z > 2:
82
+ octet_msg = "❌ électron-déficient"
83
+ elif total_e == 8:
84
+ octet_msg = "✅ octet"
85
+ elif total_e == 2 and (Z == 1 or Z == 2):
86
+ octet_msg = "✅ duet"
87
+ else:
88
+ octet_msg = "🤔"
89
+ rows.append({
90
+ "index atome": atom.GetIdx(),
91
+ "symbole": atom.GetSymbol(),
92
+ "e- valence": valence_e,
93
+ "e- liants": bonding_e,
94
+ "charge formelle": formal_charge,
95
+ "doublets non-liants (DNL)": lone_pairs,
96
+ "lacunes ([])": vacancies,
97
+ "nombre de liaisons": num_bonds,
98
+ "e- total (octet ?)": total_e,
99
+ "O/H/D ?": octet_msg
100
+ })
101
+ return pd.DataFrame(rows)
102
+
103
+ def show_mol(self,
104
+ size: tuple=(400,400),
105
+ show_Lewis: bool=False,
106
+ plot_conjugation: bool=False,
107
+ plot_aromatic: bool=False,
108
+ show_n: bool=False,
109
+ show_hybrid: bool=False,
110
+ show_H: bool=False,
111
+ rep3D: bool=False,
112
+ macrocycle: bool=False,
113
+ highlightAtoms: list=[],
114
+ legend: str=''
115
+ ):
116
+ """
117
+ Renders the molecule in 2D SVG format with optional property overlays.
118
+
119
+ Args:
120
+ size (tuple): Drawing dimensions in pixels.
121
+ show_Lewis (bool): Annotates atoms with Lone Pairs and Vacancies.
122
+ plot_conjugation (bool): Highlights conjugated bonds in blue.
123
+ plot_aromatic (bool): Highlights aromatic rings in red.
124
+ show_n (bool): Displays atom indices.
125
+ show_hybrid (bool): Displays atom hybridization (sp3, sp2, etc.).
126
+ show_H (bool): Adds explicit Hydrogens to the drawing.
127
+ rep3D (bool): Computes a 3D-like conformation before drawing.
128
+ macrocycle (bool): Uses CoordGen for better rendering of large rings (e.g., Cyclodextrins).
129
+ highlightAtoms (list): List of indices to highlight.
130
+ legend (str): Title or legend text for the drawing.
131
+ """
132
+
133
+ def safe_add_hs():
134
+ try:
135
+ return Chem.AddHs(self.mol)
136
+ except Exception as e:
137
+ print(f"[Warning] Impossible d'ajouter les H pour {self.smiles} ({e}), on garde la version brute.")
138
+ return mol
139
+
140
+ if show_H and not show_Lewis:
141
+ mol = Chem.AddHs(self.mol)
142
+ else:
143
+ mol = self.mol
144
+ if show_Lewis:
145
+ mol = safe_add_hs()
146
+ self.mol = mol
147
+ df = self.analyze_lewis()
148
+ lewis_info = {row["index atome"]: (row["doublets non-liants (DNL)"], row["lacunes ([])"])
149
+ for _, row in df.iterrows()}
150
+ else:
151
+ df = None
152
+
153
+ if rep3D:
154
+ mol = Chem.AddHs(self.mol)
155
+ self.mol = mol
156
+ AllChem.EmbedMolecule(mol)
157
+
158
+ if macrocycle:
159
+ rdCoordGen.AddCoords(self.mol)
160
+
161
+ d2d = rdMolDraw2D.MolDraw2DSVG(size[0],size[1])
162
+
163
+ atoms = list(mol.GetAtoms())
164
+
165
+ if plot_conjugation:
166
+ from collections import defaultdict
167
+ Chem.SetConjugation(mol)
168
+ colors = [(0.0, 0.0, 1.0, 0.4)]
169
+ athighlights = defaultdict(list)
170
+ arads = {}
171
+ bndhighlights = defaultdict(list)
172
+ for bond in mol.GetBonds():
173
+ aid1 = bond.GetBeginAtomIdx()
174
+ aid2 = bond.GetEndAtomIdx()
175
+
176
+ if bond.GetIsConjugated():
177
+ bid = mol.GetBondBetweenAtoms(aid1,aid2).GetIdx()
178
+ bndhighlights[bid].append(colors[0])
179
+
180
+ if plot_aromatic:
181
+ from collections import defaultdict
182
+ colors = [(1.0, 0.0, 0.0, 0.4)]
183
+ athighlights = defaultdict(list)
184
+ arads = {}
185
+ for a in atoms:
186
+ if a.GetIsAromatic():
187
+ aid = a.GetIdx()
188
+ athighlights[aid].append(colors[0])
189
+ arads[aid] = 0.3
190
+
191
+ bndhighlights = defaultdict(list)
192
+ for bond in mol.GetBonds():
193
+ aid1 = bond.GetBeginAtomIdx()
194
+ aid2 = bond.GetEndAtomIdx()
195
+
196
+ if bond.GetIsAromatic():
197
+ bid = mol.GetBondBetweenAtoms(aid1,aid2).GetIdx()
198
+ bndhighlights[bid].append(colors[0])
199
+
200
+ if show_hybrid or show_Lewis:
201
+ for i,atom in enumerate(atoms):
202
+ # print(i,atom.GetDegree(),atom.GetImplicitValence())
203
+ note_parts = []
204
+ if show_hybrid and(atom.GetValence(rdkit.Chem.rdchem.ValenceType.IMPLICIT) > 0 or atom.GetDegree() > 1):
205
+ note_parts.append(str(atom.GetHybridization()))
206
+ if show_Lewis and i in lewis_info:
207
+ lp, vac = lewis_info[i]
208
+ if lp > 0:
209
+ note_parts.append(f" {lp}DNL")
210
+ if vac > 0:
211
+ note_parts.append(f" {vac}[]")
212
+ if note_parts:
213
+ mol.GetAtomWithIdx(i).SetProp('atomNote',"".join(note_parts))
214
+ # print(f"Atom {i+1:3}: {atom.GetAtomicNum():3} {atom.GetSymbol():>2} {atom.GetHybridization()}")
215
+ if show_Lewis:
216
+ display(df)
217
+
218
+ if show_n:
219
+ d2d.drawOptions().addAtomIndices=show_n
220
+
221
+ if plot_aromatic or plot_conjugation:
222
+ d2d.DrawMoleculeWithHighlights(mol,legend,dict(athighlights),dict(bndhighlights),arads,{})
223
+ else:
224
+ d2d.DrawMolecule(mol,legend=legend, highlightAtoms=highlightAtoms)
225
+
226
+ d2d.FinishDrawing()
227
+ display(SVG(d2d.GetDrawingText()))
228
+
229
+ return
230
+