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.
- pyphyschemtools/.__init__.py.swp +0 -0
- pyphyschemtools/.ipynb_checkpoints/Chem3D-checkpoint.py +835 -0
- pyphyschemtools/.ipynb_checkpoints/PeriodicTable-checkpoint.py +294 -0
- pyphyschemtools/.ipynb_checkpoints/aithermo-checkpoint.py +349 -0
- pyphyschemtools/.ipynb_checkpoints/core-checkpoint.py +120 -0
- pyphyschemtools/.ipynb_checkpoints/spectra-checkpoint.py +471 -0
- pyphyschemtools/.ipynb_checkpoints/survey-checkpoint.py +1048 -0
- pyphyschemtools/.ipynb_checkpoints/sympyUtilities-checkpoint.py +51 -0
- pyphyschemtools/.ipynb_checkpoints/tools4AS-checkpoint.py +964 -0
- pyphyschemtools/Chem3D.py +12 -8
- pyphyschemtools/ML.py +6 -4
- pyphyschemtools/PeriodicTable.py +9 -4
- pyphyschemtools/__init__.py +3 -3
- pyphyschemtools/aithermo.py +5 -6
- pyphyschemtools/core.py +7 -6
- pyphyschemtools/spectra.py +78 -58
- pyphyschemtools/survey.py +0 -449
- pyphyschemtools/sympyUtilities.py +9 -9
- pyphyschemtools/tools4AS.py +12 -8
- {pyphyschemtools-0.1.0.dist-info → pyphyschemtools-0.1.1.dist-info}/METADATA +2 -2
- {pyphyschemtools-0.1.0.dist-info → pyphyschemtools-0.1.1.dist-info}/RECORD +29 -20
- /pyphyschemtools/{icons-logos-banner → icons_logos_banner}/Logo_pyPhysChem_border.svg +0 -0
- /pyphyschemtools/{icons-logos-banner → icons_logos_banner}/__init__.py +0 -0
- /pyphyschemtools/{icons-logos-banner → icons_logos_banner}/logo.png +0 -0
- /pyphyschemtools/{icons-logos-banner → icons_logos_banner}/tools4pyPC_banner.png +0 -0
- /pyphyschemtools/{icons-logos-banner → icons_logos_banner}/tools4pyPC_banner.svg +0 -0
- {pyphyschemtools-0.1.0.dist-info → pyphyschemtools-0.1.1.dist-info}/WHEEL +0 -0
- {pyphyschemtools-0.1.0.dist-info → pyphyschemtools-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {pyphyschemtools-0.1.0.dist-info → pyphyschemtools-0.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
############################################################
|
|
2
|
+
# Periodic Table
|
|
3
|
+
############################################################
|
|
4
|
+
from .visualID_Eng import fg, bg, hl
|
|
5
|
+
from .core import centerTitle, centertxt
|
|
6
|
+
|
|
7
|
+
import mendeleev
|
|
8
|
+
|
|
9
|
+
class TableauPeriodique:
|
|
10
|
+
nomsFr=['Hydrogène','Hélium','Lithium','Béryllium','Bore','Carbone','Azote','Oxygène',
|
|
11
|
+
'Fluor','Néon','Sodium','Magnésium','Aluminium','Silicium','Phosphore','Soufre',
|
|
12
|
+
'Chlore','Argon','Potassium','Calcium','Scandium','Titane','Vanadium','Chrome',
|
|
13
|
+
'Manganèse','Fer','Cobalt','Nickel','Cuivre','Zinc','Gallium','Germanium',
|
|
14
|
+
'Arsenic','Sélénium','Brome','Krypton','Rubidium','Strontium','Yttrium',
|
|
15
|
+
'Zirconium','Niobium','Molybdène','Technétium','Ruthénium','Rhodium',
|
|
16
|
+
'Palladium','Argent','Cadmium','Indium',
|
|
17
|
+
'Étain','Antimoine','Tellure','Iode','Xénon','Césium','Baryum','Lanthane','Cérium',
|
|
18
|
+
'Praséodyme','Néodyme','Prométhium','Samarium','Europium','Gadolinium','Terbium',
|
|
19
|
+
'Dysprosium','Holmium','Erbium','Thulium','Ytterbium','Lutetium','Hafnium','Tantale',
|
|
20
|
+
'Tungstène','Rhénium','Osmium','Iridium','Platine','Or','Mercure','Thallium','Plomb',
|
|
21
|
+
'Bismuth','Polonium','Astate','Radon','Francium','Radium','Actinium','Thorium','Protactinium',
|
|
22
|
+
'Uranium','Neptunium','Plutonium','Americium','Curium','Berkelium','Californium','Einsteinium',
|
|
23
|
+
'Fermium','Mendelevium','Nobelium','Lawrencium','Rutherfordium','Dubnium','Seaborgium','Bohrium',
|
|
24
|
+
'Hassium','Meitnerium','Darmstadtium','Roentgenium','Copernicium','Nihonium','Flerovium',
|
|
25
|
+
'Moscovium','Livermorium','Tennesse','Oganesson',
|
|
26
|
+
]
|
|
27
|
+
trad = {'Nonmetals':'Non métal',
|
|
28
|
+
'Noble gases':'Gaz noble',
|
|
29
|
+
'Alkali metals':'Métal alcalin',
|
|
30
|
+
'Alkaline earth metals':'Métal alcalino-terreux',
|
|
31
|
+
'Metalloids':'Métalloïde',
|
|
32
|
+
'Halogens':'Halogène',
|
|
33
|
+
'Poor metals':'Métal pauvre',
|
|
34
|
+
'Transition metals':'Métal de transition',
|
|
35
|
+
'Lanthanides':'Lanthanide',
|
|
36
|
+
'Actinides':'Actinide',
|
|
37
|
+
'Metals':'Métal',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
from mendeleev.vis import create_vis_dataframe
|
|
42
|
+
self.elements = create_vis_dataframe()
|
|
43
|
+
self.patch_elements()
|
|
44
|
+
|
|
45
|
+
def patch_elements(self):
|
|
46
|
+
'''
|
|
47
|
+
Ce patch, appliqué à self.elements, créé par l'appel à create_vis_dataframe(), va servir à :
|
|
48
|
+
- ajouter des informations en français : les noms des éléments et des séries (familles) auxquelles ils appartiennent
|
|
49
|
+
- retirer les éléments du groupe 12 de la famille des métaux de transition, qui est le choix CONTESTABLE par défaut de la bibliothèque mendeleev
|
|
50
|
+
|
|
51
|
+
input :
|
|
52
|
+
elements est un dataframe pandas préalablement créé par la fonction create_vis_dataframe() de mendeleev.vis
|
|
53
|
+
|
|
54
|
+
output :
|
|
55
|
+
elements avec deux nouvelles colonnes name_seriesFr et nom, qui contient dorénavant les noms des éléments en français
|
|
56
|
+
+ correction des données name_series et series_id pour les éléments Zn, Cd, Hg, Cn
|
|
57
|
+
+ de nouvelles colonnes qui contiennent l'énergie de première ionisation et les isotopes naturels
|
|
58
|
+
|
|
59
|
+
'''
|
|
60
|
+
def series_eng2fr(s):
|
|
61
|
+
'''Correspondance entre nom des séries (familles) en anglais et en français'''
|
|
62
|
+
s = TableauPeriodique.trad[s]
|
|
63
|
+
return s
|
|
64
|
+
|
|
65
|
+
def name_eng2fr():
|
|
66
|
+
self.elements["nom"] = TableauPeriodique.nomsFr
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
def ajouter_donnees():
|
|
70
|
+
import numpy as np
|
|
71
|
+
from mendeleev.fetch import fetch_table, fetch_ionization_energies
|
|
72
|
+
import pandas as pd
|
|
73
|
+
# dfElts = fetch_table("elements")
|
|
74
|
+
# display(dfElts)
|
|
75
|
+
dfEi1 = fetch_ionization_energies(degree = 1)
|
|
76
|
+
# display(dfEi1)
|
|
77
|
+
b = pd.DataFrame({'atomic_number':[x for x in range(1, 119)]})
|
|
78
|
+
dfEi1tot = pd.merge(left=dfEi1, right=b, on='atomic_number', how='outer').sort_values(by='atomic_number')
|
|
79
|
+
self.elements["Ei1"] = dfEi1tot["IE1"]
|
|
80
|
+
|
|
81
|
+
# les éléments du groupe 12 ne sont pas des métaux de transition
|
|
82
|
+
self.elements.loc[29,"name_series"] = 'Metals'
|
|
83
|
+
self.elements.loc[47,"name_series"] = 'Metals'
|
|
84
|
+
self.elements.loc[79,"name_series"] = 'Metals'
|
|
85
|
+
self.elements.loc[111,"name_series"] = 'Metals'
|
|
86
|
+
self.elements.loc[29,"series_id"] = 11
|
|
87
|
+
self.elements.loc[47,"series_id"] = 11
|
|
88
|
+
self.elements.loc[79,"series_id"] = 11
|
|
89
|
+
self.elements.loc[111,"series_id"] = 11
|
|
90
|
+
self.elements.loc[29,"color"] = "#bbd3a5"
|
|
91
|
+
self.elements.loc[47,"color"] = "#bbd3a5"
|
|
92
|
+
self.elements.loc[79,"color"] = "#bbd3a5"
|
|
93
|
+
self.elements.loc[111,"color"] = "#bbd3a5"
|
|
94
|
+
# english > français. Ajout d'une nouvelle colonne
|
|
95
|
+
self.elements["name_seriesFr"] = self.elements["name_series"].apply(series_eng2fr)
|
|
96
|
+
# english > français. Noms des éléments en français changés dans la colonne name
|
|
97
|
+
name_eng2fr()
|
|
98
|
+
ajouter_donnees()
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
def prop(self,elt_id):
|
|
102
|
+
from mendeleev import element
|
|
103
|
+
|
|
104
|
+
elt = element(elt_id)
|
|
105
|
+
print(f"Nom de l'élement = {TableauPeriodique.nomsFr[elt.atomic_number-1]} ({elt.symbol}, Z = {elt.atomic_number})")
|
|
106
|
+
print(f"Nom en anglais = {elt.name}")
|
|
107
|
+
print(f"Origine du nom = {elt.name_origin}")
|
|
108
|
+
print()
|
|
109
|
+
print(f"CEF = {elt.ec} = {elt.econf}")
|
|
110
|
+
print(f"Nombre d'électrons célibataires = {elt.ec.unpaired_electrons()}")
|
|
111
|
+
print(f"Groupe {elt.group_id}, Période {elt.period}, bloc {elt.block}")
|
|
112
|
+
print(f"Famille = {self.elements.loc[elt.atomic_number-1,'name_seriesFr']}")
|
|
113
|
+
print()
|
|
114
|
+
print(f"Masse molaire = {elt.atomic_weight} g/mol")
|
|
115
|
+
isotopes = ""
|
|
116
|
+
X = elt.symbol
|
|
117
|
+
for i in elt.isotopes:
|
|
118
|
+
if i.abundance is not None:
|
|
119
|
+
isotopes = isotopes + str(i.mass_number)+ "^" + X + f"({i.abundance}%) / "
|
|
120
|
+
print("Isotopes naturels = ",isotopes[:-2])
|
|
121
|
+
print()
|
|
122
|
+
if elt.electronegativity(scale='pauling') is None:
|
|
123
|
+
print(f"Électronégativité de Pauling = Non définie")
|
|
124
|
+
else:
|
|
125
|
+
print(f"Électronégativité de Pauling = {elt.electronegativity(scale='pauling')}")
|
|
126
|
+
print(f"Énergie de 1ère ionisation = {elt.ionenergies[1]:.2f} eV")
|
|
127
|
+
if elt.electron_affinity is None:
|
|
128
|
+
print(f"Affinité électronique = Non définie")
|
|
129
|
+
else:
|
|
130
|
+
print(f"Afinité électronique = {elt.electron_affinity:.2f} eV")
|
|
131
|
+
print(f"Rayon atomique = {elt.atomic_radius:.1f} pm")
|
|
132
|
+
print()
|
|
133
|
+
print("▶ Description : ",elt.description)
|
|
134
|
+
print("▶ Sources : ",elt.sources)
|
|
135
|
+
print("▶ Utilisation : ",elt.uses)
|
|
136
|
+
print("---------------------------------------------------------------------------------------")
|
|
137
|
+
print()
|
|
138
|
+
|
|
139
|
+
def afficher(self):
|
|
140
|
+
from bokeh.plotting import show, output_notebook
|
|
141
|
+
from mendeleev.vis import periodic_table_bokeh
|
|
142
|
+
|
|
143
|
+
# Toute cette partie du code est une copie du module bokeh de mendeleev.vis
|
|
144
|
+
# La fonction periodic_table_bokeh étant faiblement configurable avec des args/kwargs,
|
|
145
|
+
# elle est adaptée ici pour un affichage personnalisé
|
|
146
|
+
|
|
147
|
+
from collections import OrderedDict
|
|
148
|
+
|
|
149
|
+
import pandas as pd
|
|
150
|
+
from pandas.api.types import is_float_dtype
|
|
151
|
+
|
|
152
|
+
from bokeh.plotting import figure
|
|
153
|
+
from bokeh.models import HoverTool, ColumnDataSource, FixedTicker
|
|
154
|
+
|
|
155
|
+
from mendeleev.vis.utils import colormap_column
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def periodic_table_bokeh(
|
|
159
|
+
elements: pd.DataFrame,
|
|
160
|
+
attribute: str = "atomic_weight",
|
|
161
|
+
cmap: str = "RdBu_r",
|
|
162
|
+
colorby: str = "color",
|
|
163
|
+
decimals: int = 3,
|
|
164
|
+
height: int = 800,
|
|
165
|
+
missing: str = "#ffffff",
|
|
166
|
+
title: str = "Periodic Table",
|
|
167
|
+
wide_layout: bool = False,
|
|
168
|
+
width: int = 1200,
|
|
169
|
+
):
|
|
170
|
+
"""
|
|
171
|
+
Use Bokeh backend to plot the periodic table. Adaptation by Romuald Poteau (romuald.poteau@univ-tlse3.fr) of the orignal periodic_table_bokeh() function of the mendeleev library
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
elements : Pandas DataFrame with the elements data. Needs to have `x` and `y`
|
|
175
|
+
columns with coordianates for each tile.
|
|
176
|
+
attribute : Name of the attribute to be displayed
|
|
177
|
+
cmap : Colormap to use, see matplotlib colormaps
|
|
178
|
+
colorby : Name of the column containig the colors
|
|
179
|
+
decimals : Number of decimals to be displayed in the bottom row of each cell
|
|
180
|
+
height : Height of the figure in pixels
|
|
181
|
+
missing : Hex code of the color to be used for the missing values
|
|
182
|
+
title : Title to appear above the periodic table
|
|
183
|
+
wide_layout: wide layout variant of the periodic table
|
|
184
|
+
width : Width of the figure in pixels
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
if any(col not in elements.columns for col in ["x", "y"]):
|
|
188
|
+
raise ValueError(
|
|
189
|
+
"Coordinate columns named 'x' and 'y' are required "
|
|
190
|
+
"in 'elements' DataFrame. Consider using "
|
|
191
|
+
"'mendeleev.vis.utils.create_vis_dataframe' and try again."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# additional columns for positioning of the text
|
|
195
|
+
|
|
196
|
+
elements.loc[:, "y_anumber"] = elements["y"] - 0.3
|
|
197
|
+
elements.loc[:, "y_name"] = elements["y"] + 0.2
|
|
198
|
+
|
|
199
|
+
if attribute:
|
|
200
|
+
elements.loc[elements[attribute].notnull(), "y_prop"] = (
|
|
201
|
+
elements.loc[elements[attribute].notnull(), "y"] + 0.35
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
elements.loc[:, "y_prop"] = elements["y"] + 0.35
|
|
205
|
+
|
|
206
|
+
ac = "display_attribute"
|
|
207
|
+
if is_float_dtype(elements[attribute]):
|
|
208
|
+
elements[ac] = elements[attribute].round(decimals=decimals)
|
|
209
|
+
else:
|
|
210
|
+
elements[ac] = elements[attribute]
|
|
211
|
+
|
|
212
|
+
if colorby == "attribute":
|
|
213
|
+
colored = colormap_column(elements, attribute, cmap=cmap, missing=missing)
|
|
214
|
+
elements.loc[:, "attribute_color"] = colored
|
|
215
|
+
colorby = "attribute_color"
|
|
216
|
+
|
|
217
|
+
# bokeh configuration
|
|
218
|
+
|
|
219
|
+
source = ColumnDataSource(data=elements)
|
|
220
|
+
|
|
221
|
+
TOOLS = "hover,save,reset"
|
|
222
|
+
|
|
223
|
+
fig = figure(
|
|
224
|
+
title=title,
|
|
225
|
+
tools=TOOLS,
|
|
226
|
+
x_axis_location="above",
|
|
227
|
+
x_range=(elements.x.min() - 0.5, elements.x.max() + 0.5),
|
|
228
|
+
y_range=(elements.y.max() + 0.5, elements.y.min() - 0.5),
|
|
229
|
+
width=width,
|
|
230
|
+
height=height,
|
|
231
|
+
toolbar_location="above",
|
|
232
|
+
toolbar_sticky=False,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
fig.rect("x", "y", 0.9, 0.9, source=source, color=colorby, fill_alpha=0.6)
|
|
236
|
+
|
|
237
|
+
# adjust the ticks and axis bounds
|
|
238
|
+
fig.yaxis.bounds = (1, 7)
|
|
239
|
+
fig.axis[1].ticker.num_minor_ticks = 0
|
|
240
|
+
if wide_layout:
|
|
241
|
+
# Turn off tick labels
|
|
242
|
+
fig.axis[0].major_label_text_font_size = "0pt"
|
|
243
|
+
# Turn off tick marks
|
|
244
|
+
fig.axis[0].major_tick_line_color = None # turn off major ticks
|
|
245
|
+
fig.axis[0].ticker.num_minor_ticks = 0 # turn off minor ticks
|
|
246
|
+
else:
|
|
247
|
+
fig.axis[0].ticker = FixedTicker(ticks=list(range(1, 19)))
|
|
248
|
+
|
|
249
|
+
text_props = {
|
|
250
|
+
"source": source,
|
|
251
|
+
"angle": 0,
|
|
252
|
+
"color": "black",
|
|
253
|
+
"text_align": "center",
|
|
254
|
+
"text_baseline": "middle",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fig.text(
|
|
258
|
+
x="x",
|
|
259
|
+
y="y",
|
|
260
|
+
text="symbol",
|
|
261
|
+
text_font_style="bold",
|
|
262
|
+
text_font_size="15pt",
|
|
263
|
+
**text_props,
|
|
264
|
+
)
|
|
265
|
+
fig.text(
|
|
266
|
+
x="x", y="y_anumber", text="atomic_number", text_font_size="9pt", **text_props
|
|
267
|
+
)
|
|
268
|
+
fig.text(x="x", y="y_name", text="name", text_font_size="6pt", **text_props)
|
|
269
|
+
fig.text(x="x", y="y_prop", text=ac, text_font_size="7pt", **text_props)
|
|
270
|
+
|
|
271
|
+
fig.grid.grid_line_color = None
|
|
272
|
+
|
|
273
|
+
hover = fig.select(dict(type=HoverTool))
|
|
274
|
+
hover.tooltips = OrderedDict(
|
|
275
|
+
[
|
|
276
|
+
("nom", "@nom"),
|
|
277
|
+
("name", "@name"),
|
|
278
|
+
("famille", "@name_seriesFr"),
|
|
279
|
+
("numéro atomique", "@atomic_number"),
|
|
280
|
+
("masse molaire", "@atomic_weight"),
|
|
281
|
+
("rayon atomique", "@atomic_radius"),
|
|
282
|
+
("énergie de première ionisation", "@Ei1"),
|
|
283
|
+
("affinité électronique", "@electron_affinity"),
|
|
284
|
+
("EN Pauling", "@en_pauling"),
|
|
285
|
+
("CEF", "@electronic_configuration"),
|
|
286
|
+
]
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return fig
|
|
290
|
+
|
|
291
|
+
output_notebook()
|
|
292
|
+
|
|
293
|
+
fig = periodic_table_bokeh(self.elements, colorby="color")
|
|
294
|
+
show(fig)
|
|
@@ -0,0 +1,349 @@
|
|
|
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 (prefixed with ``vib_``). If False, looks for standard thermodynamic data.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
tuple: A triplet containing:
|
|
60
|
+
- file_paths (list of str): Absolute or relative paths to the .dat files.
|
|
61
|
+
- names (list of str): Internal identifiers for each surface phase.
|
|
62
|
+
- legends (list of str): LaTeX-formatted or plain text labels for graphical legends.
|
|
63
|
+
|
|
64
|
+
Notes:
|
|
65
|
+
- The lists are returned in reverse order to ensure correct layering
|
|
66
|
+
during 3D plotting.
|
|
67
|
+
- Relies on 'ListOfStableSurfaces.dat' existing in the folder_path.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
self._check_folder()
|
|
71
|
+
from .core import centertxt
|
|
72
|
+
import glob
|
|
73
|
+
pattern = "TPcoveragevib_*.dat" if vib else "TPcoverage_*.dat"
|
|
74
|
+
file_paths = glob.glob(str(self.folder_path / pattern))
|
|
75
|
+
listOfMinCov = self.folder_path / "ListOfStableSurfaces.dat"
|
|
76
|
+
print(f"List of Stable surfaces is in: {listOfMinCov}")
|
|
77
|
+
# if vib:
|
|
78
|
+
# file_paths = glob.glob(os.path.join(self.folder_path, "TPcoveragevib_*.dat"))
|
|
79
|
+
# else:
|
|
80
|
+
# file_paths = glob.glob(os.path.join(self.folder_path, "TPcoverage_*.dat"))
|
|
81
|
+
# print(vib,file_paths)
|
|
82
|
+
# listOfMinCov = os.path.join(self.folder_path, "ListOfStableSurfaces.dat")
|
|
83
|
+
# print("list of Stable surfaces is in: ",listOfMinCov)
|
|
84
|
+
try:
|
|
85
|
+
with open(listOfMinCov, "r") as f:
|
|
86
|
+
lines = [line.rstrip('\n').split() for line in f]
|
|
87
|
+
|
|
88
|
+
file_paths = []
|
|
89
|
+
names = []
|
|
90
|
+
legends = []
|
|
91
|
+
for l in lines:
|
|
92
|
+
# file_paths = file_paths + glob.glob(os.path.join(self.folder_path, l[0]))
|
|
93
|
+
file_paths = file_paths + glob.glob(str(self.folder_path / l[0]))
|
|
94
|
+
names = names + [l[1]]
|
|
95
|
+
# legends = legends + [l[2]]
|
|
96
|
+
legends.append(fr"{l[2]}") # The 'fr' ensures it is a Raw Formatted string
|
|
97
|
+
names = names[::-1]
|
|
98
|
+
legends = legends[::-1]
|
|
99
|
+
file_paths = file_paths[::-1]
|
|
100
|
+
centertxt(f"List of stable surface compositions. Vibrations = {vib}",size=14,weight="bold")
|
|
101
|
+
if not vib:
|
|
102
|
+
file_paths = [f.replace('vib_', '_') for f in file_paths]
|
|
103
|
+
for i,f in enumerate(file_paths):
|
|
104
|
+
print(f"{f} {names[i]} {legends[i]}")
|
|
105
|
+
|
|
106
|
+
except FileNotFoundError:
|
|
107
|
+
print(f"ListOfStableSurfaces.dat file has not been found in the {self.folder_path} folder. Exiting...")
|
|
108
|
+
sys.exit()
|
|
109
|
+
return file_paths,names,legends
|
|
110
|
+
|
|
111
|
+
def plot_surface(self, saveFig=None, vib=True, texLegend=False, xLegend=0.5, yLegend=0.4):
|
|
112
|
+
"""
|
|
113
|
+
Generate an interactive 3D thermodynamic stability map using Plotly.
|
|
114
|
+
|
|
115
|
+
This method visualizes multiple Gibbs free energy surfaces as a function of
|
|
116
|
+
Temperature (X) and Pressure (Y). It automatically handles log-scale
|
|
117
|
+
transformations for the pressure axis and projects reference experimental
|
|
118
|
+
conditions and phase boundaries onto the plot.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
saveFig (str, optional): The filename (without extension) to export
|
|
122
|
+
the resulting plot as a PNG image. Defaults to None (no save).
|
|
123
|
+
vib (bool): Whether to use vibration-corrected data. Defaults to True.
|
|
124
|
+
texLegend (bool): If True, uses LaTeX legends extracted from the
|
|
125
|
+
configuration file. Defaults to False.
|
|
126
|
+
xLegend (float): Horizontal position of the legend box (0 to 1).
|
|
127
|
+
Defaults to 0.5.
|
|
128
|
+
yLegend (float): Vertical position of the legend box (0 to 1).
|
|
129
|
+
Defaults to 0.4.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
plotly.graph_objects.FigureWidget: An interactive widget containing
|
|
133
|
+
the 3D surfaces, experimental markers, and reference lines.
|
|
134
|
+
|
|
135
|
+
Workflow:
|
|
136
|
+
1. Scans data files and parses Temperature/Pressure/Energy grids.
|
|
137
|
+
2. Traces individual 3D surfaces with mapped color scales.
|
|
138
|
+
3. Calculates and plots intersection boundaries between surface phases.
|
|
139
|
+
4. Overlays experimental markers (e.g., specific T/P conditions).
|
|
140
|
+
5. Optionally exports and crops the resulting image using Pillow.
|
|
141
|
+
"""
|
|
142
|
+
self._check_folder()
|
|
143
|
+
import os
|
|
144
|
+
# Define tick values explicitly for log scale (powers of 10)
|
|
145
|
+
color_scales = self.color_scales
|
|
146
|
+
logmin = -20
|
|
147
|
+
logmax = 5
|
|
148
|
+
log_tick_vals = np.logspace(logmin, logmax, num=1+(logmax-logmin)//5) # Example range from 10^20 to 10^5
|
|
149
|
+
log_tick_labels = [f"10<sup>{int(np.log10(tick))}</sup>" for tick in log_tick_vals] # Format labels as 10^n
|
|
150
|
+
import plotly.graph_objects as go
|
|
151
|
+
|
|
152
|
+
stableSurfaces, nameOfStableSurfaces, legendOfStableSurfaces = self.ListOfStableSurfaceCompositions(vib)
|
|
153
|
+
|
|
154
|
+
# FIX: Track all Z values to find a true global minimum for the floor
|
|
155
|
+
all_z_mins = []
|
|
156
|
+
|
|
157
|
+
fig = go.Figure()
|
|
158
|
+
|
|
159
|
+
for i, file_path in enumerate(stableSurfaces):
|
|
160
|
+
with open(file_path, "r") as f:
|
|
161
|
+
lines = f.readlines()
|
|
162
|
+
|
|
163
|
+
series = []
|
|
164
|
+
temp = []
|
|
165
|
+
for line in lines:
|
|
166
|
+
if line.strip():
|
|
167
|
+
temp.append(list(map(float, line.split())))
|
|
168
|
+
else:
|
|
169
|
+
if temp:
|
|
170
|
+
series.append(np.array(temp))
|
|
171
|
+
temp = []
|
|
172
|
+
if temp:
|
|
173
|
+
series.append(np.array(temp))
|
|
174
|
+
|
|
175
|
+
data = np.array(series)
|
|
176
|
+
|
|
177
|
+
X = data[:, :, 0]
|
|
178
|
+
Y = data[:, :, 1]
|
|
179
|
+
Z = data[:, :, 2]
|
|
180
|
+
all_z_mins.append(np.min(Z))
|
|
181
|
+
|
|
182
|
+
fig.add_trace(go.Surface(
|
|
183
|
+
x=X,
|
|
184
|
+
y=Y,
|
|
185
|
+
z=Z,
|
|
186
|
+
colorscale=color_scales[i % len(color_scales)],
|
|
187
|
+
showscale=False,
|
|
188
|
+
name = nameOfStableSurfaces[i]))
|
|
189
|
+
|
|
190
|
+
if legendOfStableSurfaces[i] != "None" and texLegend:
|
|
191
|
+
name=f"{legendOfStableSurfaces[i]}"
|
|
192
|
+
else:
|
|
193
|
+
name=f"{nameOfStableSurfaces[i]}"
|
|
194
|
+
fig.add_trace(go.Scatter3d(
|
|
195
|
+
x=[None], y=[None], z=[None], # Invisible point
|
|
196
|
+
mode="markers",
|
|
197
|
+
name=f"{name}",
|
|
198
|
+
marker=dict(color=color_scales[i % len(color_scales)][-1][1], size=10),
|
|
199
|
+
showlegend=True))
|
|
200
|
+
|
|
201
|
+
# FIX: Calculate zmin globally
|
|
202
|
+
zmin = np.min(all_z_mins) - 50
|
|
203
|
+
|
|
204
|
+
fig.add_trace(go.Scatter3d(
|
|
205
|
+
x=[55+273.15,90+273.15], y=[np.log10(2),np.log10(4)], z=[zmin-10,zmin-10], # Invisible point
|
|
206
|
+
mode="markers",
|
|
207
|
+
marker=dict(color='red', size=10, symbol='cross'),
|
|
208
|
+
name='exp. Conditions (55°C, 2 bar & 90°C, 4 bar)',
|
|
209
|
+
showlegend=True))
|
|
210
|
+
|
|
211
|
+
fig.add_trace(go.Scatter3d(
|
|
212
|
+
x=[0, 1000], y=[np.log10(1),np.log10(1)], z=[zmin+600]*2,
|
|
213
|
+
mode="lines",
|
|
214
|
+
line=dict(color="blue", width=3),
|
|
215
|
+
name="1 bar",
|
|
216
|
+
showlegend=False
|
|
217
|
+
))
|
|
218
|
+
|
|
219
|
+
fig.add_trace(go.Scatter3d(
|
|
220
|
+
x=[298, 298], y=[-20,5], z=[zmin+600]*2,
|
|
221
|
+
mode="lines",
|
|
222
|
+
line=dict(color="black", width=3),
|
|
223
|
+
name="298 K",
|
|
224
|
+
showlegend=False
|
|
225
|
+
))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
fig.update_layout(
|
|
229
|
+
width=1200, # Increase figure width (default is ~700)
|
|
230
|
+
height=1200,
|
|
231
|
+
paper_bgcolor='rgba(0,0,0,0)', # White background outside the plot
|
|
232
|
+
plot_bgcolor='rgba(0,0,0,0)', # White background inside the 3D plot
|
|
233
|
+
|
|
234
|
+
margin = dict(l=0,r=0,t=0,b=0),
|
|
235
|
+
|
|
236
|
+
scene=dict(
|
|
237
|
+
aspectmode="manual", # Allows custom aspect ratio
|
|
238
|
+
aspectratio=dict(x=1.15, y=1.15, z=1), # Adjust scaling
|
|
239
|
+
xaxis=dict(
|
|
240
|
+
title=dict(
|
|
241
|
+
text="Temperature / K",
|
|
242
|
+
font=dict(size=16, family="Arial", color="blue", weight='bold'),
|
|
243
|
+
),
|
|
244
|
+
autorange="reversed", # This inverts the x-axis direction
|
|
245
|
+
showgrid=True,
|
|
246
|
+
zeroline=True,
|
|
247
|
+
tickfont=dict(color="black", size=15,weight="bold"),
|
|
248
|
+
tickangle=0,
|
|
249
|
+
ticklen=10,
|
|
250
|
+
tickwidth=2,
|
|
251
|
+
ticks="outside",
|
|
252
|
+
showbackground=False, # Enable background to create a frame
|
|
253
|
+
backgroundcolor="grey" # Black frame
|
|
254
|
+
),
|
|
255
|
+
yaxis=dict(
|
|
256
|
+
title=dict(
|
|
257
|
+
text="Pressure / bar",
|
|
258
|
+
font=dict(size=16, family="Arial", color="blue", weight='bold'),
|
|
259
|
+
),
|
|
260
|
+
tickangle=0, # Rotate Y-axis ticks
|
|
261
|
+
showgrid=True,
|
|
262
|
+
zeroline=True,
|
|
263
|
+
type='log',
|
|
264
|
+
tickvals=log_tick_vals.tolist(), # Set tick positions
|
|
265
|
+
ticktext=log_tick_labels, # Display ticks as 10^(-n)
|
|
266
|
+
tickfont=dict(color="black", size=15,weight="bold"),
|
|
267
|
+
ticklen=10,
|
|
268
|
+
tickwidth=2,
|
|
269
|
+
ticks="outside",
|
|
270
|
+
showbackground=False, # Enable background to create a frame
|
|
271
|
+
backgroundcolor="grey" # Black frame
|
|
272
|
+
),
|
|
273
|
+
zaxis=dict(
|
|
274
|
+
title="",
|
|
275
|
+
showgrid=False,
|
|
276
|
+
zeroline=False,
|
|
277
|
+
showticklabels=False,
|
|
278
|
+
showbackground=False, # Enable background to create a frame
|
|
279
|
+
backgroundcolor="grey" # Black frame
|
|
280
|
+
),
|
|
281
|
+
camera=dict(
|
|
282
|
+
eye=dict(x=1e-5, y=-1e-2, z=-1000),
|
|
283
|
+
# eye=dict(x=1e-5, y=-1e-2, z=-1000),
|
|
284
|
+
up=dict(x=0, y=1, z=0),
|
|
285
|
+
projection=dict(type="orthographic")
|
|
286
|
+
),
|
|
287
|
+
),
|
|
288
|
+
legend=dict(
|
|
289
|
+
# y=0,
|
|
290
|
+
# x=0.2,
|
|
291
|
+
x = xLegend, y = yLegend,
|
|
292
|
+
font=dict(size=13, color="black"),
|
|
293
|
+
bgcolor="rgba(255, 255, 255, 1)", # Light transparent background
|
|
294
|
+
bordercolor="grey",
|
|
295
|
+
borderwidth=1,
|
|
296
|
+
itemsizing='constant'
|
|
297
|
+
),
|
|
298
|
+
showlegend=True
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if saveFig is not None:
|
|
302
|
+
from .core import crop_images
|
|
303
|
+
# pngFile = os.path.join(folder_path, savedFig+".png")
|
|
304
|
+
# import plotly.io as pio
|
|
305
|
+
# fig.write_image(pngFile, format="png", width=1200, height=1200, scale=3)
|
|
306
|
+
pngFile = self.folder_path / f"{saveFig}.png"
|
|
307
|
+
fig.write_image(pngFile, format="png", width=1200, height=1200, scale=3)
|
|
308
|
+
# Automatic crop after saving
|
|
309
|
+
crop_images(pngFile)
|
|
310
|
+
|
|
311
|
+
fig_widget = go.FigureWidget(fig)
|
|
312
|
+
fig_widget.show()
|
|
313
|
+
return fig_widget
|
|
314
|
+
|
|
315
|
+
def plot_palette(self, angle=0, save_png=None):
|
|
316
|
+
"""
|
|
317
|
+
Visualize the 1D color palette used for surface identification.
|
|
318
|
+
|
|
319
|
+
This method generates a horizontal bar of colors corresponding to the
|
|
320
|
+
different surface phases defined in the instance. Each color is labeled
|
|
321
|
+
with its numerical index, allowing for quick cross-referencing between
|
|
322
|
+
the palette and the 3D surface plot.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
angle (int, optional): Rotation angle of the x-axis tick labels (indices).
|
|
326
|
+
Defaults to 0.
|
|
327
|
+
save_png (str, optional): Filename (including .png extension) to save
|
|
328
|
+
the palette image to the working directory. Defaults to None
|
|
329
|
+
(display only).
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
None: Displays the plot using matplotlib.pyplot.show().
|
|
333
|
+
|
|
334
|
+
Notes:
|
|
335
|
+
- Requires 'seaborn' for the palplot generation.
|
|
336
|
+
- If 'save_png' is provided, the image is saved with a resolution of
|
|
337
|
+
300 DPI and a transparent background.
|
|
338
|
+
"""
|
|
339
|
+
names = [str(i) for i in range(len(self.palette))]
|
|
340
|
+
sns.palplot(sns.color_palette(self.palette))
|
|
341
|
+
ax = plt.gca()
|
|
342
|
+
ax.set_xticks(np.arange(len(names)))
|
|
343
|
+
ax.set_xticklabels(names, weight='bold', size=10, rotation=angle)
|
|
344
|
+
|
|
345
|
+
if save_png:
|
|
346
|
+
plt.tight_layout()
|
|
347
|
+
plt.savefig(self.folder_path / save_png, dpi=300, transparent=True)
|
|
348
|
+
plt.show()
|
|
349
|
+
|