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

materialsproject / pymatgen / 4075885785

pending completion
4075885785

push

github

Shyue Ping Ong
Merge branch 'master' of github.com:materialsproject/pymatgen

96 of 96 new or added lines in 27 files covered. (100.0%)

81013 of 102710 relevant lines covered (78.88%)

0.79 hits per line

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

93.48
/pymatgen/electronic_structure/core.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3

4
"""
1✔
5
This module provides core classes needed by all define electronic structure,
6
such as the Spin, Orbital, etc.
7
"""
8

9
from __future__ import annotations
1✔
10

11
from enum import Enum, unique
1✔
12

13
import numpy as np
1✔
14
from monty.json import MSONable
1✔
15

16

17
@unique
1✔
18
class Spin(Enum):
1✔
19
    """
20
    Enum type for Spin. Only up and down.
21
    Usage: Spin.up, Spin.down.
22
    """
23

24
    up, down = (1, -1)
1✔
25

26
    def __int__(self):
1✔
27
        return self.value
1✔
28

29
    def __float__(self):
1✔
30
        return float(self.value)
×
31

32
    def __str__(self):
1✔
33
        return str(self.value)
1✔
34

35

36
@unique
1✔
37
class OrbitalType(Enum):
1✔
38
    """
39
    Enum type for orbital type. Indices are basically the azimuthal quantum
40
    number, l.
41
    """
42

43
    s = 0
1✔
44
    p = 1
1✔
45
    d = 2
1✔
46
    f = 3
1✔
47

48
    def __str__(self):
1✔
49
        return str(self.name)
1✔
50

51

52
@unique
1✔
53
class Orbital(Enum):
1✔
54
    """
55
    Enum type for specific orbitals. The indices are basically the order in
56
    which the orbitals are reported in VASP and has no special meaning.
57
    """
58

59
    s = 0
1✔
60
    py = 1
1✔
61
    pz = 2
1✔
62
    px = 3
1✔
63
    dxy = 4
1✔
64
    dyz = 5
1✔
65
    dz2 = 6
1✔
66
    dxz = 7
1✔
67
    dx2 = 8
1✔
68
    f_3 = 9
1✔
69
    f_2 = 10
1✔
70
    f_1 = 11
1✔
71
    f0 = 12
1✔
72
    f1 = 13
1✔
73
    f2 = 14
1✔
74
    f3 = 15
1✔
75

76
    def __int__(self):
1✔
77
        return self.value
×
78

79
    def __str__(self):
1✔
80
        return str(self.name)
1✔
81

82
    @property
1✔
83
    def orbital_type(self):
1✔
84
        """
85
        Returns OrbitalType of an orbital.
86
        """
87
        # pylint: disable=E1136
88
        return OrbitalType[self.name[0]]
1✔
89

90

91
class Magmom(MSONable):
1✔
92
    """
93
    New class in active development. Use with caution, feedback is
94
    appreciated.
95

96
    Class to handle magnetic moments. Defines the magnetic moment of a
97
    site or species relative to a spin quantization axis. Designed for
98
    use in electronic structure calculations.
99

100
    * For the general case, Magmom can be specified by a vector,
101
      e.g. m = Magmom([1.0, 1.0, 2.0]), and subscripts will work as
102
      expected, e.g. m[0] gives 1.0
103

104
    * For collinear calculations, Magmom can assumed to be scalar-like,
105
      e.g. m = Magmom(5.0) will work as expected, e.g. float(m) gives 5.0
106

107
    Both of these cases should be safe and shouldn't give any surprises,
108
    but more advanced functionality is available if required.
109

110
    There also exist useful static methods for lists of magmoms:
111

112
    * Magmom.are_collinear(magmoms) - if true, a collinear electronic
113
      structure calculation can be safely initialized, with float(Magmom)
114
      giving the expected scalar magnetic moment value
115

116
    * Magmom.get_consistent_set_and_saxis(magmoms) - for non-collinear
117
      electronic structure calculations, a global, consistent spin axis
118
      has to be used. This method returns a list of Magmoms which all
119
      share a common spin axis, along with the global spin axis.
120

121
    All methods that take lists of magmoms will accept magmoms either as
122
    Magmom objects or as scalars/lists and will automatically convert to
123
    a Magmom representation internally.
124

125
    The following methods are also particularly useful in the context of
126
    VASP calculations:
127

128
    * Magmom.get_xyz_magmom_with_001_saxis()
129
    * Magmom.get_00t_magmom_with_xyz_saxis()
130

131
    See VASP documentation for more information:
132

133
    https://cms.mpi.univie.ac.at/wiki/index.php/SAXIS
134
    """
135

136
    def __init__(self, moment, saxis=(0, 0, 1)):
1✔
137
        """
138
        :param moment: magnetic moment, supplied as float or list/np.ndarray
139
        :param saxis: spin axis, supplied as list/np.ndarray, parameter will
140
            be converted to unit vector (default is [0, 0, 1])
141
        :return: Magmom object
142
        """
143
        # to init from another Magmom instance
144
        if isinstance(moment, Magmom):
1✔
145
            saxis = moment.saxis
1✔
146
            moment = moment.moment
1✔
147

148
        moment = np.array(moment, dtype="d")
1✔
149
        if moment.ndim == 0:
1✔
150
            moment = moment * [0, 0, 1]
1✔
151

152
        self.moment = moment
1✔
153

154
        saxis = np.array(saxis, dtype="d")
1✔
155

156
        self.saxis = saxis / np.linalg.norm(saxis)
1✔
157

158
    @classmethod
1✔
159
    def from_global_moment_and_saxis(cls, global_moment, saxis):
1✔
160
        """
161
        Convenience method to initialize Magmom from a given global
162
        magnetic moment, i.e. magnetic moment with saxis=(0,0,1), and
163
        provided saxis.
164

165
        Method is useful if you do not know the components of your
166
        magnetic moment in frame of your desired saxis.
167

168
        :param global_moment:
169
        :param saxis: desired saxis
170
        :return:
171
        """
172
        magmom = Magmom(global_moment)
1✔
173
        return cls(magmom.get_moment(saxis=saxis), saxis=saxis)
1✔
174

175
    @classmethod
1✔
176
    def _get_transformation_matrix(cls, saxis):
1✔
177
        saxis = saxis / np.linalg.norm(saxis)
1✔
178

179
        alpha = np.arctan2(saxis[1], saxis[0])
1✔
180
        beta = np.arctan2(np.sqrt(saxis[0] ** 2 + saxis[1] ** 2), saxis[2])
1✔
181

182
        cos_a = np.cos(alpha)
1✔
183
        cos_b = np.cos(beta)
1✔
184
        sin_a = np.sin(alpha)
1✔
185
        sin_b = np.sin(beta)
1✔
186

187
        m = [
1✔
188
            [cos_b * cos_a, -sin_a, sin_b * cos_a],
189
            [cos_b * sin_a, cos_a, sin_b * sin_a],
190
            [-sin_b, 0, cos_b],
191
        ]
192

193
        return m
1✔
194

195
    @classmethod
1✔
196
    def _get_transformation_matrix_inv(cls, saxis):
1✔
197
        saxis = saxis / np.linalg.norm(saxis)
1✔
198

199
        alpha = np.arctan2(saxis[1], saxis[0])
1✔
200
        beta = np.arctan2(np.sqrt(saxis[0] ** 2 + saxis[1] ** 2), saxis[2])
1✔
201

202
        cos_a = np.cos(alpha)
1✔
203
        cos_b = np.cos(beta)
1✔
204
        sin_a = np.sin(alpha)
1✔
205
        sin_b = np.sin(beta)
1✔
206

207
        m = [
1✔
208
            [cos_b * cos_a, cos_b * sin_a, -sin_b],
209
            [-sin_a, cos_a, 0],
210
            [sin_b * cos_a, sin_b * sin_a, cos_b],
211
        ]
212

213
        return m
1✔
214

215
    def get_moment(self, saxis=(0, 0, 1)):
1✔
216
        """
217
        Get magnetic moment relative to a given spin quantization axis.
218
        If no axis is provided, moment will be given relative to the
219
        Magmom's internal spin quantization axis, i.e. equivalent to
220
        Magmom.moment
221

222
        :param saxis: (list/numpy array) spin quantization axis
223
        :return: np.ndarray of length 3
224
        """
225
        # transform back to moment with spin axis [0, 0, 1]
226
        m_inv = self._get_transformation_matrix_inv(self.saxis)
1✔
227
        moment = np.matmul(self.moment, m_inv)
1✔
228

229
        # transform to new saxis
230
        m = self._get_transformation_matrix(saxis)
1✔
231
        moment = np.matmul(moment, m)
1✔
232

233
        # round small values to zero
234
        moment[np.abs(moment) < 1e-8] = 0
1✔
235

236
        return moment
1✔
237

238
    @property
1✔
239
    def global_moment(self):
1✔
240
        """
241
        Get the magnetic moment defined in an arbitrary global reference frame.
242

243
        :return: np.ndarray of length 3
244
        """
245
        return self.get_moment()
1✔
246

247
    @property
1✔
248
    def projection(self):
1✔
249
        """
250
        Projects moment along spin quantisation axis. Useful for obtaining
251
        collinear approximation for slightly non-collinear magmoms.
252

253
        :return: float
254
        """
255
        return np.dot(self.moment, self.saxis)
×
256

257
    def get_xyz_magmom_with_001_saxis(self):
1✔
258
        """
259
        Returns a Magmom in the default setting of saxis = [0, 0, 1] and
260
        the magnetic moment rotated as required.
261

262
        :return: Magmom
263
        """
264
        return Magmom(self.get_moment())
1✔
265

266
    def get_00t_magmom_with_xyz_saxis(self):
1✔
267
        """
268
        For internal implementation reasons, in non-collinear calculations
269
        VASP prefers:
270

271
        MAGMOM = 0 0 total_magnetic_moment
272
        SAXIS = x y z
273

274
        to an equivalent:
275

276
        MAGMOM = x y z
277
        SAXIS = 0 0 1
278

279
        This method returns a Magmom object with magnetic moment [0, 0, t],
280
        where t is the total magnetic moment, and saxis rotated as required.
281

282
        A consistent direction of saxis is applied such that t might be positive
283
        or negative depending on the direction of the initial moment. This is useful
284
        in the case of collinear structures, rather than constraining assuming
285
        t is always positive.
286

287
        :return: Magmom
288
        """
289
        # reference direction gives sign of moment
290
        # entirely arbitrary, there will always be a pathological case
291
        # where a consistent sign is not possible if the magnetic moments
292
        # are aligned along the reference direction, but in practice this
293
        # is unlikely to happen
294
        ref_direction = np.array([1.01, 1.02, 1.03])
1✔
295
        t = abs(self)
1✔
296
        if t != 0:
1✔
297
            new_saxis = self.moment / np.linalg.norm(self.moment)
1✔
298
            if np.dot(ref_direction, new_saxis) < 0:
1✔
299
                t = -t
1✔
300
                new_saxis = -new_saxis
1✔
301
            return Magmom([0, 0, t], saxis=new_saxis)
1✔
302
        return Magmom(self)
1✔
303

304
    @staticmethod
1✔
305
    def have_consistent_saxis(magmoms):
1✔
306
        """
307
        This method checks that all Magmom objects in a list have a
308
        consistent spin quantization axis. To write MAGMOM tags to a
309
        VASP INCAR, a global SAXIS value for all magmoms has to be used.
310
        If saxis are inconsistent, can create consistent set with:
311
        Magmom.get_consistent_set(magmoms)
312

313
        :param magmoms: list of magmoms (Magmoms, scalars or vectors)
314
        :return: bool
315
        """
316
        magmoms = [Magmom(magmom) for magmom in magmoms]
1✔
317
        ref_saxis = magmoms[0].saxis
1✔
318
        match_ref = [magmom.saxis == ref_saxis for magmom in magmoms]
1✔
319
        if np.all(match_ref):
1✔
320
            return True
1✔
321
        return False
1✔
322

323
    @staticmethod
1✔
324
    def get_consistent_set_and_saxis(magmoms, saxis=None):
1✔
325
        """
326
        Method to ensure a list of magmoms use the same spin axis.
327
        Returns a tuple of a list of Magmoms and their global spin axis.
328

329
        :param magmoms: list of magmoms (Magmoms, scalars or vectors)
330
        :param saxis: can provide a specific global spin axis
331
        :return: (list of Magmoms, global spin axis) tuple
332
        """
333
        magmoms = [Magmom(magmom) for magmom in magmoms]
1✔
334
        if saxis is None:
1✔
335
            saxis = Magmom.get_suggested_saxis(magmoms)
1✔
336
        else:
337
            saxis = saxis / np.linalg.norm(saxis)
×
338
        magmoms = [magmom.get_moment(saxis=saxis) for magmom in magmoms]
1✔
339
        return magmoms, saxis
1✔
340

341
    @staticmethod
1✔
342
    def get_suggested_saxis(magmoms):
1✔
343
        """
344
        This method returns a suggested spin axis for a set of magmoms,
345
        taking the largest magnetic moment as the reference. For calculations
346
        with collinear spins, this would give a sensible saxis for a ncl
347
        calculation.
348

349
        :param magmoms: list of magmoms (Magmoms, scalars or vectors)
350
        :return: np.ndarray of length 3
351
        """
352
        # heuristic, will pick largest magmom as reference
353
        # useful for creating collinear approximations of
354
        # e.g. slightly canted magnetic structures
355
        # for fully collinear structures, will return expected
356
        # result
357

358
        magmoms = [Magmom(magmom) for magmom in magmoms]
1✔
359
        # filter only non-zero magmoms
360
        magmoms = [magmom for magmom in magmoms if abs(magmom)]
1✔
361
        magmoms.sort(reverse=True)
1✔
362
        if len(magmoms) > 0:
1✔
363
            return magmoms[0].get_00t_magmom_with_xyz_saxis().saxis
1✔
364
        return np.array([0, 0, 1], dtype="d")
×
365

366
    @staticmethod
1✔
367
    def are_collinear(magmoms) -> bool:
1✔
368
        """
369
        Method checks to see if a set of magnetic moments are collinear
370
        with each other.
371
        :param magmoms: list of magmoms (Magmoms, scalars or vectors)
372
        :return: bool
373
        """
374
        magmoms = [Magmom(magmom) for magmom in magmoms]
1✔
375
        if not Magmom.have_consistent_saxis(magmoms):
1✔
376
            magmoms = Magmom.get_consistent_set_and_saxis(magmoms)[0]
×
377

378
        # convert to numpy array for convenience
379
        magmoms = np.array([list(magmom) for magmom in magmoms])
1✔
380
        magmoms = magmoms[np.any(magmoms, axis=1)]  # remove zero magmoms
1✔
381
        if len(magmoms) == 0:
1✔
382
            return True
1✔
383

384
        # use first moment as reference to compare against
385
        ref_magmom = magmoms[0]
1✔
386
        # magnitude of cross products != 0 if non-collinear with reference
387
        num_ncl = np.count_nonzero(np.linalg.norm(np.cross(ref_magmom, magmoms), axis=1))
1✔
388
        if num_ncl > 0:
1✔
389
            return False
1✔
390
        return True
1✔
391

392
    @classmethod
1✔
393
    def from_moment_relative_to_crystal_axes(cls, moment, lattice):
1✔
394
        """
395
        Obtaining a Magmom object from a magnetic moment provided
396
        relative to crystal axes.
397

398
        Used for obtaining moments from magCIF file.
399
        :param moment: list of floats specifying vector magmom
400
        :param lattice: Lattice
401
        :return: Magmom
402
        """
403
        # get matrix representing unit lattice vectors
404
        unit_m = lattice.matrix / np.linalg.norm(lattice.matrix, axis=1)[:, None]
1✔
405
        moment = np.matmul(list(moment), unit_m)
1✔
406
        # round small values to zero
407
        moment[np.abs(moment) < 1e-8] = 0
1✔
408
        return cls(moment)
1✔
409

410
    def get_moment_relative_to_crystal_axes(self, lattice):
1✔
411
        """
412
        If scalar magmoms, moments will be given arbitrarily along z.
413
        Used for writing moments to magCIF file.
414

415
        :param lattice: Lattice
416
        :return: vector as list of floats
417
        """
418
        # get matrix representing unit lattice vectors
419
        unit_m = lattice.matrix / np.linalg.norm(lattice.matrix, axis=1)[:, None]
1✔
420
        # note np.matmul() requires numpy version >= 1.10
421
        moment = np.matmul(self.global_moment, np.linalg.inv(unit_m))
1✔
422
        # round small values to zero
423
        moment[np.abs(moment) < 1e-8] = 0
1✔
424
        return moment
1✔
425

426
    def __getitem__(self, key):
1✔
427
        return self.moment[key]
1✔
428

429
    def __iter__(self):
1✔
430
        return iter(self.moment)
1✔
431

432
    def __abs__(self):
1✔
433
        return np.linalg.norm(self.moment)
1✔
434

435
    def __eq__(self, other: object) -> bool:
1✔
436
        """
437
        Equal if 'global' magnetic moments are the same, saxis can differ.
438
        """
439
        try:
1✔
440
            other_magmom = Magmom(other)
1✔
441
        except (TypeError, ValueError):
×
442
            return NotImplemented
×
443

444
        return np.allclose(self.global_moment, other_magmom.global_moment)
1✔
445

446
    def __lt__(self, other):
1✔
447
        return abs(self) < abs(other)
1✔
448

449
    def __neg__(self):
1✔
450
        return Magmom(-self.moment, saxis=self.saxis)
1✔
451

452
    def __hash__(self):
1✔
453
        return (tuple(self.moment) + tuple(self.saxis)).__hash__()
×
454

455
    def __float__(self):
1✔
456
        """
457
        Returns magnitude of magnetic moment with a sign with respect to
458
        an arbitrary direction.
459

460
        Should give unsurprising output if Magmom is treated like a
461
        scalar or if a set of Magmoms describes a collinear structure.
462

463
        Implemented this way rather than simpler abs(self) so that
464
        moments will have a consistent sign in case of e.g.
465
        antiferromagnetic collinear structures without additional
466
        user intervention.
467

468
        However, should be used with caution for non-collinear
469
        structures and might give nonsensical results except in the case
470
        of only slightly non-collinear structures (e.g. small canting).
471

472
        This approach is also used to obtain "diff" VolumetricDensity
473
        in pymatgen.io.vasp.outputs.VolumetricDensity when processing
474
        Chgcars from SOC calculations.
475
        """
476
        return float(self.get_00t_magmom_with_xyz_saxis()[2])
1✔
477

478
    def __str__(self):
1✔
479
        return str(float(self))
1✔
480

481
    def __repr__(self):
1✔
482
        if np.allclose(self.saxis, (0, 0, 1)):
×
483
            return f"Magnetic moment {self.moment}"
×
484
        return f"Magnetic moment {self.moment} (spin axis = {self.saxis})"
×
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