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
pyphyschemtools/ML.py ADDED
@@ -0,0 +1,42 @@
1
+ ############################################################
2
+ # Machine Learning
3
+ ############################################################
4
+ from .visualID_Eng import fg, bg, hl
5
+ from .core import centerTitle, centertxt
6
+
7
+ def y2c(mc2i,y):
8
+ import tensorflow as tf
9
+ from tensorflow import keras
10
+ #from keras.utils import np_utils
11
+ from keras.utils import to_categorical
12
+ y_array = y.copy()
13
+ y_array = y_array.to_numpy() # transformation au format numpy
14
+ # transformation des valeurs de y1 & y2 en entiers
15
+ for x in range(len(y_array)):
16
+ #print(x, y_array[x], mapc2i[y_array[x]])
17
+ y_array[x] = mc2i[y_array[x]]
18
+ yohe = to_categorical(y_array)
19
+ del y_array
20
+ return yohe
21
+
22
+ def categorizeY_2ohe(Ctot, y1, y2):
23
+ """
24
+ one-hot-encodes a pandas column of categorical data
25
+ input:
26
+ - Ctot is the reference pandas column, necessary to find all unique categories in this column
27
+ - y1 and y2 are the actual pandas column that will be categorized. y1 and y2 are supposed to be the ytest and ytrain subsets of Ctot
28
+ output:
29
+ - y1ohe and y2ohe are the numpy arrays returned by this routine
30
+ """
31
+
32
+ uv = Ctot.unique()
33
+ print(f"Catégories uniques : {uv}")
34
+ mapc2i = {}
35
+ for x in range(len(uv)):
36
+ mapc2i[uv[x]] = x
37
+ print(f"Correspondance entre chaque catégorie unique et un entier : {mapc2i}")
38
+ y1ohe = y2c(mapc2i,y1)
39
+ y2ohe = y2c(mapc2i,y2)
40
+ print(f"Structure (shape) des tableaux renvoyés par categorize1C_2ohe. y1 : {y1ohe.shape}, y2 : {y2ohe.shape}")
41
+ del mapc2i, uv
42
+ return y1ohe, y2ohe
@@ -0,0 +1,289 @@
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
+ input : elements est un dataframe pandas préalablement créé par la fonction create_vis_dataframe() de mendeleev.vis
51
+ output : elements avec deux nouvelles colonnes name_seriesFr et nom, qui contient dorénavant les noms des éléments en français
52
+ + correction des données name_series et series_id pour les éléments Zn, Cd, Hg, Cn
53
+ + de nouvelles colonnes qui contiennent l'énergie de première ionisation et les isotopes naturels
54
+ '''
55
+ def series_eng2fr(s):
56
+ '''Correspondance entre nom des séries (familles) en anglais et en français'''
57
+ s = TableauPeriodique.trad[s]
58
+ return s
59
+
60
+ def name_eng2fr():
61
+ self.elements["nom"] = TableauPeriodique.nomsFr
62
+ return
63
+
64
+ def ajouter_donnees():
65
+ import numpy as np
66
+ from mendeleev.fetch import fetch_table, fetch_ionization_energies
67
+ import pandas as pd
68
+ # dfElts = fetch_table("elements")
69
+ # display(dfElts)
70
+ dfEi1 = fetch_ionization_energies(degree = 1)
71
+ # display(dfEi1)
72
+ b = pd.DataFrame({'atomic_number':[x for x in range(1, 119)]})
73
+ dfEi1tot = pd.merge(left=dfEi1, right=b, on='atomic_number', how='outer').sort_values(by='atomic_number')
74
+ self.elements["Ei1"] = dfEi1tot["IE1"]
75
+
76
+ # les éléments du groupe 12 ne sont pas des métaux de transition
77
+ self.elements.loc[29,"name_series"] = 'Metals'
78
+ self.elements.loc[47,"name_series"] = 'Metals'
79
+ self.elements.loc[79,"name_series"] = 'Metals'
80
+ self.elements.loc[111,"name_series"] = 'Metals'
81
+ self.elements.loc[29,"series_id"] = 11
82
+ self.elements.loc[47,"series_id"] = 11
83
+ self.elements.loc[79,"series_id"] = 11
84
+ self.elements.loc[111,"series_id"] = 11
85
+ self.elements.loc[29,"color"] = "#bbd3a5"
86
+ self.elements.loc[47,"color"] = "#bbd3a5"
87
+ self.elements.loc[79,"color"] = "#bbd3a5"
88
+ self.elements.loc[111,"color"] = "#bbd3a5"
89
+ # english > français. Ajout d'une nouvelle colonne
90
+ self.elements["name_seriesFr"] = self.elements["name_series"].apply(series_eng2fr)
91
+ # english > français. Noms des éléments en français changés dans la colonne name
92
+ name_eng2fr()
93
+ ajouter_donnees()
94
+ return
95
+
96
+ def prop(self,elt_id):
97
+ from mendeleev import element
98
+
99
+ elt = element(elt_id)
100
+ print(f"Nom de l'élement = {TableauPeriodique.nomsFr[elt.atomic_number-1]} ({elt.symbol}, Z = {elt.atomic_number})")
101
+ print(f"Nom en anglais = {elt.name}")
102
+ print(f"Origine du nom = {elt.name_origin}")
103
+ print()
104
+ print(f"CEF = {elt.ec} = {elt.econf}")
105
+ print(f"Nombre d'électrons célibataires = {elt.ec.unpaired_electrons()}")
106
+ print(f"Groupe {elt.group_id}, Période {elt.period}, bloc {elt.block}")
107
+ print(f"Famille = {self.elements.loc[elt.atomic_number-1,'name_seriesFr']}")
108
+ print()
109
+ print(f"Masse molaire = {elt.atomic_weight} g/mol")
110
+ isotopes = ""
111
+ X = elt.symbol
112
+ for i in elt.isotopes:
113
+ if i.abundance is not None:
114
+ isotopes = isotopes + str(i.mass_number)+ "^" + X + f"({i.abundance}%) / "
115
+ print("Isotopes naturels = ",isotopes[:-2])
116
+ print()
117
+ if elt.electronegativity(scale='pauling') is None:
118
+ print(f"Électronégativité de Pauling = Non définie")
119
+ else:
120
+ print(f"Électronégativité de Pauling = {elt.electronegativity(scale='pauling')}")
121
+ print(f"Énergie de 1ère ionisation = {elt.ionenergies[1]:.2f} eV")
122
+ if elt.electron_affinity is None:
123
+ print(f"Affinité électronique = Non définie")
124
+ else:
125
+ print(f"Afinité électronique = {elt.electron_affinity:.2f} eV")
126
+ print(f"Rayon atomique = {elt.atomic_radius:.1f} pm")
127
+ print()
128
+ print("▶ Description : ",elt.description)
129
+ print("▶ Sources : ",elt.sources)
130
+ print("▶ Utilisation : ",elt.uses)
131
+ print("---------------------------------------------------------------------------------------")
132
+ print()
133
+
134
+ def afficher(self):
135
+ from bokeh.plotting import show, output_notebook
136
+ from mendeleev.vis import periodic_table_bokeh
137
+
138
+ # Toute cette partie du code est une copie du module bokeh de mendeleev.vis
139
+ # La fonction periodic_table_bokeh étant faiblement configurable avec des args/kwargs,
140
+ # elle est adaptée ici pour un affichage personnalisé
141
+
142
+ from collections import OrderedDict
143
+
144
+ import pandas as pd
145
+ from pandas.api.types import is_float_dtype
146
+
147
+ from bokeh.plotting import figure
148
+ from bokeh.models import HoverTool, ColumnDataSource, FixedTicker
149
+
150
+ from mendeleev.vis.utils import colormap_column
151
+
152
+
153
+ def periodic_table_bokeh(
154
+ elements: pd.DataFrame,
155
+ attribute: str = "atomic_weight",
156
+ cmap: str = "RdBu_r",
157
+ colorby: str = "color",
158
+ decimals: int = 3,
159
+ height: int = 800,
160
+ missing: str = "#ffffff",
161
+ title: str = "Periodic Table",
162
+ wide_layout: bool = False,
163
+ width: int = 1200,
164
+ ):
165
+ """
166
+ 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
167
+
168
+ Args:
169
+ elements : Pandas DataFrame with the elements data. Needs to have `x` and `y`
170
+ columns with coordianates for each tile.
171
+ attribute : Name of the attribute to be displayed
172
+ cmap : Colormap to use, see matplotlib colormaps
173
+ colorby : Name of the column containig the colors
174
+ decimals : Number of decimals to be displayed in the bottom row of each cell
175
+ height : Height of the figure in pixels
176
+ missing : Hex code of the color to be used for the missing values
177
+ title : Title to appear above the periodic table
178
+ wide_layout: wide layout variant of the periodic table
179
+ width : Width of the figure in pixels
180
+ """
181
+
182
+ if any(col not in elements.columns for col in ["x", "y"]):
183
+ raise ValueError(
184
+ "Coordinate columns named 'x' and 'y' are required "
185
+ "in 'elements' DataFrame. Consider using "
186
+ "'mendeleev.vis.utils.create_vis_dataframe' and try again."
187
+ )
188
+
189
+ # additional columns for positioning of the text
190
+
191
+ elements.loc[:, "y_anumber"] = elements["y"] - 0.3
192
+ elements.loc[:, "y_name"] = elements["y"] + 0.2
193
+
194
+ if attribute:
195
+ elements.loc[elements[attribute].notnull(), "y_prop"] = (
196
+ elements.loc[elements[attribute].notnull(), "y"] + 0.35
197
+ )
198
+ else:
199
+ elements.loc[:, "y_prop"] = elements["y"] + 0.35
200
+
201
+ ac = "display_attribute"
202
+ if is_float_dtype(elements[attribute]):
203
+ elements[ac] = elements[attribute].round(decimals=decimals)
204
+ else:
205
+ elements[ac] = elements[attribute]
206
+
207
+ if colorby == "attribute":
208
+ colored = colormap_column(elements, attribute, cmap=cmap, missing=missing)
209
+ elements.loc[:, "attribute_color"] = colored
210
+ colorby = "attribute_color"
211
+
212
+ # bokeh configuration
213
+
214
+ source = ColumnDataSource(data=elements)
215
+
216
+ TOOLS = "hover,save,reset"
217
+
218
+ fig = figure(
219
+ title=title,
220
+ tools=TOOLS,
221
+ x_axis_location="above",
222
+ x_range=(elements.x.min() - 0.5, elements.x.max() + 0.5),
223
+ y_range=(elements.y.max() + 0.5, elements.y.min() - 0.5),
224
+ width=width,
225
+ height=height,
226
+ toolbar_location="above",
227
+ toolbar_sticky=False,
228
+ )
229
+
230
+ fig.rect("x", "y", 0.9, 0.9, source=source, color=colorby, fill_alpha=0.6)
231
+
232
+ # adjust the ticks and axis bounds
233
+ fig.yaxis.bounds = (1, 7)
234
+ fig.axis[1].ticker.num_minor_ticks = 0
235
+ if wide_layout:
236
+ # Turn off tick labels
237
+ fig.axis[0].major_label_text_font_size = "0pt"
238
+ # Turn off tick marks
239
+ fig.axis[0].major_tick_line_color = None # turn off major ticks
240
+ fig.axis[0].ticker.num_minor_ticks = 0 # turn off minor ticks
241
+ else:
242
+ fig.axis[0].ticker = FixedTicker(ticks=list(range(1, 19)))
243
+
244
+ text_props = {
245
+ "source": source,
246
+ "angle": 0,
247
+ "color": "black",
248
+ "text_align": "center",
249
+ "text_baseline": "middle",
250
+ }
251
+
252
+ fig.text(
253
+ x="x",
254
+ y="y",
255
+ text="symbol",
256
+ text_font_style="bold",
257
+ text_font_size="15pt",
258
+ **text_props,
259
+ )
260
+ fig.text(
261
+ x="x", y="y_anumber", text="atomic_number", text_font_size="9pt", **text_props
262
+ )
263
+ fig.text(x="x", y="y_name", text="name", text_font_size="6pt", **text_props)
264
+ fig.text(x="x", y="y_prop", text=ac, text_font_size="7pt", **text_props)
265
+
266
+ fig.grid.grid_line_color = None
267
+
268
+ hover = fig.select(dict(type=HoverTool))
269
+ hover.tooltips = OrderedDict(
270
+ [
271
+ ("nom", "@nom"),
272
+ ("name", "@name"),
273
+ ("famille", "@name_seriesFr"),
274
+ ("numéro atomique", "@atomic_number"),
275
+ ("masse molaire", "@atomic_weight"),
276
+ ("rayon atomique", "@atomic_radius"),
277
+ ("énergie de première ionisation", "@Ei1"),
278
+ ("affinité électronique", "@electron_affinity"),
279
+ ("EN Pauling", "@en_pauling"),
280
+ ("CEF", "@electronic_configuration"),
281
+ ]
282
+ )
283
+
284
+ return fig
285
+
286
+ output_notebook()
287
+
288
+ fig = periodic_table_bokeh(self.elements, colorby="color")
289
+ show(fig)
@@ -0,0 +1,43 @@
1
+ # tools4pyPhysChem/__init__.py
2
+ __version__ = "0.1.0"
3
+ __last_update__ = "2026-02-01"
4
+
5
+ import importlib
6
+ import importlib.util
7
+
8
+ # 1. FAST IMPORTS
9
+ from .visualID_Eng import fg, hl, bg, color, init, apply_css_style, chrono_start, chrono_stop, end
10
+ from .core import centerTitle, centertxt, crop_images
11
+
12
+ # On définit explicitement ce qui est déjà importé pour que __getattr__ ne s'en mêle pas
13
+ _EXPLICIT_EXPORTS = {
14
+ "fg", "hl", "bg", "color", "init", "apply_css_style",
15
+ "chrono_start", "chrono_stop", "end", "centerTitle",
16
+ "centertxt", "crop_images"
17
+ }
18
+
19
+ # 2. AUTOMATIC LAZY LOADING
20
+ def __getattr__(name):
21
+ # Si l'attribut est dans les imports explicites, on ne devrait pas être ici,
22
+ # mais au cas où, on le gère.
23
+ if name in _EXPLICIT_EXPORTS:
24
+ # On le récupère dans le namespace local
25
+ return globals()[name]
26
+
27
+ modules_to_search = [
28
+ ".ML", ".PeriodicTable", ".Chem3D",
29
+ ".aithermo", ".cheminformatics", ".kinetics", # Virgule corrigée ici
30
+ ".spectra", ".survey",
31
+ ".sympyUtilities", ".tools4AS"
32
+ ]
33
+
34
+ for mod_name in modules_to_search:
35
+ try:
36
+ # On tente l'import relatif
37
+ module = importlib.import_module(mod_name, __package__)
38
+ if hasattr(module, name):
39
+ return getattr(module, name)
40
+ except (ImportError, AttributeError):
41
+ continue
42
+
43
+ raise AttributeError(f"module {__name__} has no attribute {name}")