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,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
+