• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pyiron / pyiron_atomistics / 10736148419

06 Sep 2024 09:40AM UTC coverage: 70.934%. First build
10736148419

Pull #1555

github

web-flow
Merge 8fa6b31f7 into 208d0516f
Pull Request #1555: Merge main

74 of 85 new or added lines in 9 files covered. (87.06%)

10689 of 15069 relevant lines covered (70.93%)

0.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.62
/pyiron_atomistics/atomistics/structure/periodic_table.py
1
# coding: utf-8
2
# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
3
# Distributed under the terms of "New BSD License", see the LICENSE file.
4

5
from __future__ import print_function, unicode_literals
1✔
6

7
import io
1✔
8
import pkgutil
1✔
9
from functools import lru_cache
1✔
10

11
import numpy as np
1✔
12
import pandas
1✔
13

14
__author__ = "Joerg Neugebauer, Sudarsan Surendralal, Martin Boeckmann"
1✔
15
__copyright__ = (
1✔
16
    "Copyright 2021, Max-Planck-Institut für Eisenforschung GmbH - "
17
    "Computational Materials Design (CM) Department"
18
)
19
__version__ = "1.0"
1✔
20
__maintainer__ = "Sudarsan Surendralal"
1✔
21
__email__ = "surendralal@mpie.de"
1✔
22
__status__ = "production"
1✔
23
__date__ = "Sep 1, 2017"
1✔
24

25
pandas.options.mode.chained_assignment = None
1✔
26

27

28
MENDELEEV_PROPERTY_LIST = [
1✔
29
    "abundance_crust",
30
    "abundance_sea",
31
    "annotation",
32
    "atomic_number",
33
    "atomic_radius",
34
    "atomic_radius_rahm",
35
    "atomic_volume",
36
    "atomic_weight",
37
    "atomic_weight_uncertainty",
38
    "block",
39
    "boiling_point",
40
    "c6",
41
    "c6_gb",
42
    "cas",
43
    "covalent_radius",
44
    "covalent_radius_bragg",
45
    "covalent_radius_cordero",
46
    "covalent_radius_pyykko",
47
    "covalent_radius_pyykko_double",
48
    "covalent_radius_pyykko_triple",
49
    "cpk_color",
50
    "density",
51
    "description",
52
    "dipole_polarizability",
53
    "dipole_polarizability_unc",
54
    "discoverers",
55
    "discovery_location",
56
    "discovery_year",
57
    "ec",
58
    "econf",
59
    "electron_affinity",
60
    "electronegativity",
61
    "electronegativity_allen",
62
    "electronegativity_allred_rochow",
63
    "electronegativity_cottrell_sutton",
64
    "electronegativity_ghosh",
65
    "electronegativity_gordy",
66
    "electronegativity_li_xue",
67
    "electronegativity_martynov_batsanov",
68
    "electronegativity_mulliken",
69
    "electronegativity_nagle",
70
    "electronegativity_pauling",
71
    "electronegativity_sanderson",
72
    "electronegativity_scales",
73
    "electrons",
74
    "electrophilicity",
75
    "en_allen",
76
    "en_ghosh",
77
    "en_pauling",
78
    "evaporation_heat",
79
    "fusion_heat",
80
    "gas_basicity",
81
    "geochemical_class",
82
    "glawe_number",
83
    "goldschmidt_class",
84
    "group",
85
    "group_id",
86
    "hardness",
87
    "heat_of_formation",
88
    "inchi",
89
    "init_on_load",
90
    "ionenergies",
91
    "ionic_radii",
92
    "is_monoisotopic",
93
    "is_radioactive",
94
    "isotopes",
95
    "jmol_color",
96
    "lattice_constant",
97
    "lattice_structure",
98
    "mass",
99
    "mass_number",
100
    "mass_str",
101
    "melting_point",
102
    "mendeleev_number",
103
    "metadata",
104
    "metallic_radius",
105
    "metallic_radius_c12",
106
    "molar_heat_capacity",
107
    "molcas_gv_color",
108
    "name",
109
    "name_origin",
110
    "neutrons",
111
    "nist_webbook_url",
112
    "nvalence",
113
    "oxidation_states",
114
    "oxides",
115
    "oxistates",
116
    "period",
117
    "pettifor_number",
118
    "phase_transitions",
119
    "proton_affinity",
120
    "protons",
121
    "registry",
122
    "sconst",
123
    "screening_constants",
124
    "series",
125
    "softness",
126
    "sources",
127
    "specific_heat",
128
    "specific_heat_capacity",
129
    "symbol",
130
    "thermal_conductivity",
131
    "uses",
132
    "vdw_radius",
133
    "vdw_radius_alvarez",
134
    "vdw_radius_batsanov",
135
    "vdw_radius_bondi",
136
    "vdw_radius_dreiding",
137
    "vdw_radius_mm3",
138
    "vdw_radius_rt",
139
    "vdw_radius_truhlar",
140
    "vdw_radius_uff",
141
    "zeff",
142
]
143

144

145
@lru_cache(maxsize=118)
1✔
146
def element(*args):
1✔
147
    import mendeleev
1✔
148

149
    return mendeleev.element(*args)
1✔
150

151

152
class ChemicalElement(object):
1✔
153
    """
154
    An Object which contains the element specific parameters
155
    """
156

157
    def __init__(self, sub):
1✔
158
        """
159
        Constructor: assign PSE dictionary to object
160
        """
161
        self._dataset = None
1✔
162
        self.sub = sub
1✔
163
        self._element_str = None
1✔
164
        stringtypes = str
1✔
165
        if isinstance(self.sub, stringtypes):
1✔
166
            self._element_str = self.sub
×
167
        elif "Parent" in self.sub.index and isinstance(self.sub.Parent, stringtypes):
1✔
168
            self._element_str = self.sub.Parent
1✔
169
        elif len(self.sub) > 0:
1✔
170
            self._element_str = self.sub.Abbreviation
1✔
171

172
        self._mendeleev_translation_dict = {
1✔
173
            "AtomicNumber": "atomic_number",
174
            "AtomicRadius": "covalent_radius_cordero",
175
            "AtomicMass": "mass",
176
            "Color": "cpk_color",
177
            "CovalentRadius": "covalent_radius",
178
            "CrystalStructure": "lattice_structure",
179
            "Density": "density",
180
            "DiscoveryYear": "discovery_year",
181
            "ElectronAffinity": "electron_affinity",
182
            "Electronegativity": "electronegativity",
183
            "Group": "group_id",
184
            "Name": "name",
185
            "Period": "period",
186
            "StandardName": "name",
187
            "VanDerWaalsRadius": "vdw_radius",
188
            "MeltingPoint": "melting_point",
189
        }
190
        self.el = None
1✔
191

192
    def __getattr__(self, item):
1✔
193
        if item in ["__array_struct__", "__array_interface__", "__array__"]:
1✔
194
            raise AttributeError
1✔
195
        return self[item]
1✔
196

197
    def __getitem__(self, item):
1✔
198
        if item in self._mendeleev_translation_dict.keys():
1✔
199
            item = self._mendeleev_translation_dict[item]
1✔
200
        if item in MENDELEEV_PROPERTY_LIST:
1✔
201
            return getattr(element(self._element_str), item)
1✔
202
        if item in self.sub.index:
1✔
203
            return self.sub[item]
1✔
204

205
    def __setstate__(self, state):
1✔
206
        self.__dict__.update(state)
1✔
207

208
    def __getstate__(self):
1✔
209
        # Only necessary to support pickling in python <3.11
210
        # https://docs.python.org/release/3.11.2/library/pickle.html#object.__getstate__
211
        return self.__dict__
1✔
212

213
    def __eq__(self, other):
1✔
214
        if self is other:
1✔
215
            return True
1✔
216
        elif isinstance(other, self.__class__):
1✔
217
            conditions = list()
1✔
218
            conditions.append(self.sub.to_dict() == other.sub.to_dict())
1✔
219
            return all(conditions)
1✔
220
        elif isinstance(other, (np.ndarray, list)):
1✔
221
            conditions = list()
×
222
            for sp in other:
×
223
                conditions.append(self.sub.to_dict() == sp.sub.to_dict())
×
224
            return any(conditions)
×
225

226
    def __ne__(self, other):
1✔
227
        return not self.__eq__(other)
1✔
228

229
    def __gt__(self, other):
1✔
230
        if self != other:
1✔
231
            if self["AtomicNumber"] != other["AtomicNumber"]:
1✔
232
                return self["AtomicNumber"] > other["AtomicNumber"]
1✔
233
            else:
234
                return self["Abbreviation"] > other["Abbreviation"]
1✔
235
        else:
236
            return False
1✔
237

238
    def __ge__(self, other):
1✔
239
        if self != other:
1✔
240
            return self > other
×
241
        else:
242
            return True
1✔
243

244
    def __hash__(self):
1✔
245
        return hash(repr(self))
1✔
246

247
    @property
1✔
248
    def tags(self):
1✔
249
        if "tags" not in self.sub.keys() or self.sub["tags"] is None:
1✔
250
            return dict()
1✔
251
        return self.sub["tags"]
1✔
252

253
    def __dir__(self):
1✔
254
        return list(self.sub.index) + super(ChemicalElement, self).__dir__()
×
255

256
    def __str__(self):
1✔
257
        return str([self._dataset, self.sub])
×
258

259
    def add_tags(self, tag_dic):
1✔
260
        """
261
        Add tags to an existing element inside its specific panda series without overwriting the old tags
262

263
        Args:
264
            tag_dic (dict): dictionary containing e.g. key = "spin" value = "up",
265
                            more than one tag can be added at once
266

267
        """
268
        (self.sub["tags"]).update(tag_dic)
1✔
269

270
    def to_dict(self):
1✔
271
        hdf_el = {}
1✔
272
        # TODO: save all parameters that are different from the parent (e.g. modified mass)
273
        if self.Parent is not None:
1✔
274
            self._dataset = {"Parameter": ["Parent"], "Value": [self.Parent]}
1✔
275
            hdf_el["elementData"] = self._dataset
1✔
276
        # "Dictionary of element tag static"
277
        hdf_el.update({"tagData/" + key: self.tags[key] for key in self.tags.keys()})
1✔
278
        return hdf_el
1✔
279

280
    def from_dict(self, obj_dict):
1✔
281
        pse = PeriodicTable()
1✔
282
        elname = self.sub.name
1✔
283
        if "elementData" in obj_dict.keys():
1✔
284
            element_data = obj_dict["elementData"]
1✔
285
            for key, val in zip(element_data["Parameter"], element_data["Value"]):
1✔
286
                if key in "Parent":
1✔
287
                    self.sub = pse.dataframe.loc[val]
1✔
288
                    self.sub["Parent"] = val
1✔
289
                    self._element_str = val
1✔
290
                else:
291
                    self.sub["Parent"] = None
×
292
                    self._element_str = elname
×
293
                self.sub.name = elname
1✔
294
        if "tagData" in obj_dict.keys():
1✔
295
            self.sub["tags"] = obj_dict["tagData"]
1✔
296

297
    def to_hdf(self, hdf):
1✔
298
        """
299
        saves the element with his parameters into his hdf5 job file
300
        Args:
301
            hdf (Hdfio): Hdfio object which will be used
302
        """
303
        chemical_element_dict_to_hdf(
×
304
            data_dict=self.to_dict(), hdf=hdf, group_name=self.Abbreviation
305
        )
306

307
    def from_hdf(self, hdf):
1✔
308
        """
309
        loads an element with his parameters from the hdf5 job file and store it into its specific pandas series
310
        Args:
311
            hdf (Hdfio): Hdfio object which will be used to read a hdf5 file
312
        """
313
        elname = self.sub.name
×
314
        with hdf.open(elname) as hdf_el:
×
NEW
315
            self.from_dict(obj_dict=hdf_el.read_dict_from_hdf(recursive=True))
×
316

317

318
class PeriodicTable:
1✔
319
    """
320
    An Object which stores an elementary table which can be modified for the current session
321
    """
322

323
    def __init__(self, file_name=None):  # PSE_dat_file = None):
1✔
324
        """
325

326
        Args:
327
            file_name (str): Possibility to choose an source hdf5 file
328
        """
329
        self.dataframe = self._get_periodic_table_df(file_name)
1✔
330
        if "Abbreviation" not in self.dataframe.columns.values:
1✔
331
            self.dataframe["Abbreviation"] = None
×
332
        if not all(self.dataframe["Abbreviation"].values):
1✔
333
            for item in self.dataframe.index.values:
×
334
                if self.dataframe["Abbreviation"][item] is None:
×
335
                    self.dataframe["Abbreviation"][item] = item
×
336
        self._parent_element = None
1✔
337
        self.el = None
1✔
338

339
    def __getattr__(self, item):
1✔
340
        return self[item]
1✔
341

342
    def __getitem__(self, item):
1✔
343
        if item in self.dataframe.columns.values:
1✔
344
            return self.dataframe[item]
1✔
345
        if item in self.dataframe.index.values:
1✔
346
            return self.dataframe.loc[item]
1✔
347

348
    def __setstate__(self, state):
1✔
349
        """
350
        Used by (cloud)pickle; force the state update to avoid recursion pickling Atoms
351
        """
352
        self.__dict__.update(state)
1✔
353

354
    def __getstate__(self):
1✔
355
        # Only necessary to support pickling in python <3.11
356
        # https://docs.python.org/release/3.11.2/library/pickle.html#object.__getstate__
357
        return self.__dict__
1✔
358

359
    def from_dict(self, obj_dict):
1✔
360
        for el, el_dict in obj_dict.items():
1✔
361
            sub = pandas.Series(dtype=object)
1✔
362
            new_element = ChemicalElement(sub)
1✔
363
            new_element.sub.name = el
1✔
364
            new_element.from_dict(obj_dict=el_dict)
1✔
365
            new_element.sub["Abbreviation"] = el
1✔
366

367
            if "sub_tags" in new_element.tags:
1✔
368
                if not new_element.tags["sub_tags"]:
×
369
                    del new_element.tags["sub_tags"]
×
370

371
            if new_element.Parent is None:
1✔
372
                if not (el in self.dataframe.index.values):
×
373
                    raise AssertionError()
×
374
                if len(new_element.sub["tags"]) > 0:
×
375
                    raise ValueError("Element cannot get tag-assignment twice")
×
376
                if "tags" not in self.dataframe.keys():
×
377
                    self.dataframe["tags"] = None
×
378
                self.dataframe["tags"][el] = new_element.tags
×
379
            else:
380
                self.dataframe = pandas.concat(
1✔
381
                    [self.dataframe, new_element.sub.to_frame().T]
382
                )
383
                self.dataframe["tags"] = self.dataframe["tags"].apply(
1✔
384
                    lambda x: None if pandas.isnull(x) else x
385
                )
386
                self.dataframe["Parent"] = self.dataframe["Parent"].apply(
1✔
387
                    lambda x: None if pandas.isnull(x) else x
388
                )
389

390
    def from_hdf(self, hdf):
1✔
391
        """
392
        loads an element with his parameters from the hdf5 job file by creating an Object of the ChemicalElement type.
393
        The new element will be stored in the current periodic table.
394
        Changes in the tags will also be modified inside the periodic table.
395

396
        Args:
397
            hdf (Hdfio): Hdfio object which will be used to read the data from a hdf5 file
398

399
        Returns:
400

401
        """
NEW
402
        self.from_dict(obj_dict=hdf.read_dict_from_hdf(recursive=True))
×
403

404
    def element(self, arg, **qwargs):
1✔
405
        """
406
        The method searches through the periodic table. If the table contains the element,
407
        it will return an Object of the type ChemicalElement containing all parameters from the periodic table.
408
        The option **qwargs allows a direct modification of the tag-values during the creation process
409
        Args:
410
            arg (str, ChemicalElement): sort of element
411
            **qwargs: e.g. a dictionary of tags
412

413
        Returns element (ChemicalElement): a element with all its properties (Abbreviation, AtomicMass, Weight, ...)
414

415
        """
416
        stringtypes = str
1✔
417
        if isinstance(arg, stringtypes):
1✔
418
            if arg in self.dataframe.index.values:
1✔
419
                self.el = arg
1✔
420
            else:
421
                raise KeyError(arg)
1✔
422
        elif isinstance(arg, int):
1✔
423
            if arg in list(self.dataframe["AtomicNumber"]):
1✔
424
                index = list(self.dataframe["AtomicNumber"]).index(arg)
1✔
425
                self.el = self.dataframe.iloc[index].name
1✔
426
        else:
427
            raise ValueError("type not defined: " + str(type(arg)))
×
428
        if len(qwargs.values()) > 0:
1✔
429
            if "tags" not in self.dataframe.columns.values:
1✔
430
                self.dataframe["tags"] = None
1✔
431
            self.dataframe["tags"][self.el] = qwargs
1✔
432
        element = self.dataframe.loc[self.el]
1✔
433
        # element['CovalentRadius'] /= 100
434
        return ChemicalElement(element)
1✔
435

436
    def is_element(self, symbol):
1✔
437
        """
438
        Compares the Symbol with the Abbreviations of elements inside the periodic table
439
        Args:
440
            symbol (str): name of element, str
441

442
        Returns boolean: true for the same element, false otherwise
443

444
        """
445
        return symbol in self.dataframe["Abbreviation"]
1✔
446

447
    def atomic_number_to_abbreviation(self, atom_no):
1✔
448
        """
449

450
        Args:
451
            atom_no:
452

453
        Returns:
454

455
        """
456
        if not isinstance(atom_no, int):
1✔
457
            raise ValueError("type not defined: " + str(type(atom_no)))
×
458

459
        return self.Abbreviation[
1✔
460
            np.nonzero(self.AtomicNumber.to_numpy() == atom_no)[0][0]
461
        ]
462

463
    def add_element(
1✔
464
        self, parent_element, new_element, use_parent_potential=False, **qwargs
465
    ):
466
        """
467
        Add "additional" chemical elements to the Periodic Table. These can be used to distinguish between the various
468
        potentials which may exist for a given species or to introduce artificial elements such as pseudohydrogen. For
469
        this case set use_parent_potential = False and add in the directory containing the potential files a new file
470
        which is derived from the name new element.
471

472
        This function may be also used to provide additional information for the identical chemical element, e.g., to
473
        define a Fe_up and Fe_down to perform the correct symmetry search as well as initialization.
474

475
        Args:
476
            parent_element (str): name of parent element
477
            new_element (str): name of new element
478
            use_parent_potential: True: use the potential from the parent species
479
            **qwargs: define tags and their values, e.g. spin = "up", relax = [True, True, True]
480

481
        Returns: new element (ChemicalElement)
482

483
        """
484

485
        pandas.options.mode.chained_assignment = None
1✔
486
        parent_element_data_series = self.dataframe.loc[parent_element]
1✔
487
        parent_element_data_series["Abbreviation"] = new_element
1✔
488
        parent_element_data_series["Parent"] = parent_element
1✔
489
        parent_element_data_series.name = new_element
1✔
490
        if new_element not in self.dataframe.T.columns:
1✔
491
            self.dataframe = pandas.concat(
1✔
492
                [self.dataframe, parent_element_data_series.to_frame().T],
493
            )
494
        else:
495
            self.dataframe.loc[new_element] = parent_element_data_series
1✔
496
        if use_parent_potential:
1✔
497
            self._parent_element = parent_element
×
498
        return self.element(new_element, **qwargs)
1✔
499

500
    @staticmethod
1✔
501
    @lru_cache(maxsize=1)
1✔
502
    def _get_periodic_table_df(file_name):
1✔
503
        """
504

505
        Args:
506
            file_name:
507

508
        Returns:
509

510
        """
511
        if not file_name:
1✔
512
            return pandas.read_csv(
1✔
513
                io.BytesIO(
514
                    pkgutil.get_data("pyiron_atomistics", "data/periodic_table.csv")
515
                ),
516
                index_col=0,
517
            )
518
        else:
519
            if file_name.endswith(".h5"):
×
520
                return pandas.read_hdf(file_name, mode="r")
×
521
            elif file_name.endswith(".csv"):
×
522
                return pandas.read_csv(file_name, index_col=0)
×
523
            raise TypeError(
×
524
                "PeriodicTable file format not recognised: "
525
                + file_name
526
                + " supported file formats are csv, h5."
527
            )
528

529

530
def chemical_element_dict_to_hdf(data_dict, hdf, group_name):
1✔
531
    with hdf.open(group_name) as hdf_el:
×
532
        if "elementData" in data_dict.keys():
×
533
            hdf_el["elementData"] = data_dict["elementData"]
×
534
        with hdf_el.open("tagData") as hdf_tag:
×
535
            if "tagData" in data_dict.keys():
×
536
                for k, v in data_dict["tagData"].items():
×
537
                    hdf_tag[k] = v
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc