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.
- pyphyschemtools/Chem3D.py +831 -0
- pyphyschemtools/ML.py +42 -0
- pyphyschemtools/PeriodicTable.py +289 -0
- pyphyschemtools/__init__.py +43 -0
- pyphyschemtools/aithermo.py +350 -0
- pyphyschemtools/cheminformatics.py +230 -0
- pyphyschemtools/core.py +119 -0
- pyphyschemtools/icons-logos-banner/Logo_pyPhysChem_border.svg +1109 -0
- pyphyschemtools/icons-logos-banner/__init__.py +0 -0
- pyphyschemtools/icons-logos-banner/logo.png +0 -0
- pyphyschemtools/icons-logos-banner/tools4pyPC_banner.png +0 -0
- pyphyschemtools/icons-logos-banner/tools4pyPC_banner.svg +193 -0
- pyphyschemtools/kinetics.py +193 -0
- pyphyschemtools/resources/css/BrainHalfHalf-120x139.base64 +1 -0
- pyphyschemtools/resources/css/BrainHalfHalf-120x139.png +0 -0
- pyphyschemtools/resources/css/BrainHalfHalf.base64 +8231 -0
- pyphyschemtools/resources/css/BrainHalfHalf.png +0 -0
- pyphyschemtools/resources/css/BrainHalfHalf.svg +289 -0
- pyphyschemtools/resources/css/visualID.css +325 -0
- pyphyschemtools/resources/img/Tranformative_3.webp +0 -0
- pyphyschemtools/resources/img/Tranformative_3_banner.png +0 -0
- pyphyschemtools/resources/img/pyPhysChem_1.png +0 -0
- pyphyschemtools/resources/svg/BrainHalfHalf.png +0 -0
- pyphyschemtools/resources/svg/BrainHalfHalf.svg +289 -0
- pyphyschemtools/resources/svg/GitHub-Logo-C.png +0 -0
- pyphyschemtools/resources/svg/GitHub-Logo.png +0 -0
- pyphyschemtools/resources/svg/Logo-Universite-Toulouse-n-2023.png +0 -0
- pyphyschemtools/resources/svg/Logo_pyPhysChem_1-translucentBgd-woName.png +0 -0
- pyphyschemtools/resources/svg/Logo_pyPhysChem_1-translucentBgd.png +0 -0
- pyphyschemtools/resources/svg/Logo_pyPhysChem_1.png +0 -0
- pyphyschemtools/resources/svg/Logo_pyPhysChem_1.svg +622 -0
- pyphyschemtools/resources/svg/Logo_pyPhysChem_5.png +0 -0
- pyphyschemtools/resources/svg/Logo_pyPhysChem_5.svg +48 -0
- pyphyschemtools/resources/svg/Logo_pyPhysChem_border.svg +1109 -0
- pyphyschemtools/resources/svg/Python-logo-notext.svg +265 -0
- pyphyschemtools/resources/svg/Python_logo_and_wordmark.svg.png +0 -0
- pyphyschemtools/resources/svg/UT3_logoQ.jpg +0 -0
- pyphyschemtools/resources/svg/UT3_logoQ.png +0 -0
- pyphyschemtools/resources/svg/Universite-Toulouse-n-2023.svg +141 -0
- pyphyschemtools/resources/svg/X.png +0 -0
- pyphyschemtools/resources/svg/logoAnaconda.png +0 -0
- pyphyschemtools/resources/svg/logoAnaconda.webp +0 -0
- pyphyschemtools/resources/svg/logoCNRS.png +0 -0
- pyphyschemtools/resources/svg/logoDebut.svg +316 -0
- pyphyschemtools/resources/svg/logoEnd.svg +172 -0
- pyphyschemtools/resources/svg/logoFin.svg +172 -0
- pyphyschemtools/resources/svg/logoPPCL.svg +359 -0
- pyphyschemtools/resources/svg/logoPytChem.png +0 -0
- pyphyschemtools/resources/svg/logo_lpcno_300_dpi_notexttransparent.png +0 -0
- pyphyschemtools/resources/svg/logo_pyPhysChem.png +0 -0
- pyphyschemtools/resources/svg/logo_pyPhysChem_0.png +0 -0
- pyphyschemtools/resources/svg/logo_pyPhysChem_0.svg +390 -0
- pyphyschemtools/resources/svg/logopyPhyschem.png +0 -0
- pyphyschemtools/resources/svg/logopyPhyschem_2.webp +0 -0
- pyphyschemtools/resources/svg/logopyPhyschem_3.webp +0 -0
- pyphyschemtools/resources/svg/logopyPhyschem_4.webp +0 -0
- pyphyschemtools/resources/svg/logopyPhyschem_5.png +0 -0
- pyphyschemtools/resources/svg/logopyPhyschem_5.webp +0 -0
- pyphyschemtools/resources/svg/logopyPhyschem_6.webp +0 -0
- pyphyschemtools/resources/svg/logopyPhyschem_7.webp +0 -0
- pyphyschemtools/resources/svg/logos-Anaconda-pyPhysChem.png +0 -0
- pyphyschemtools/resources/svg/logos-Anaconda-pyPhysChem.svg +58 -0
- pyphyschemtools/resources/svg/pyPCBanner.svg +309 -0
- pyphyschemtools/resources/svg/pyPhysChem-GitHubSocialMediaTemplate.png +0 -0
- pyphyschemtools/resources/svg/pyPhysChem-GitHubSocialMediaTemplate.svg +295 -0
- pyphyschemtools/resources/svg/pyPhysChemBanner.png +0 -0
- pyphyschemtools/resources/svg/pyPhysChemBanner.svg +639 -0
- pyphyschemtools/resources/svg/qrcode-pyPhysChem.png +0 -0
- pyphyschemtools/resources/svg/repository-open-graph-template.png +0 -0
- pyphyschemtools/spectra.py +451 -0
- pyphyschemtools/survey.py +1048 -0
- pyphyschemtools/sympyUtilities.py +51 -0
- pyphyschemtools/tools4AS.py +960 -0
- pyphyschemtools/visualID.py +101 -0
- pyphyschemtools/visualID_Eng.py +175 -0
- pyphyschemtools-0.1.0.dist-info/METADATA +38 -0
- pyphyschemtools-0.1.0.dist-info/RECORD +80 -0
- pyphyschemtools-0.1.0.dist-info/WHEEL +5 -0
- pyphyschemtools-0.1.0.dist-info/licenses/LICENSE +674 -0
- 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
|
+
|