• 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

74.24
/pymatgen/analysis/magnetism/heisenberg.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3

4
"""
1✔
5
This module implements a simple algorithm for extracting nearest neighbor
6
exchange parameters by mapping low energy magnetic orderings to a Heisenberg
7
model.
8
"""
9

10
from __future__ import annotations
1✔
11

12
import copy
1✔
13
import logging
1✔
14
import sys
1✔
15
from ast import literal_eval
1✔
16

17
import numpy as np
1✔
18
import pandas as pd
1✔
19
from monty.json import MSONable, jsanitize
1✔
20
from monty.serialization import dumpfn
1✔
21

22
from pymatgen.analysis.graphs import StructureGraph
1✔
23
from pymatgen.analysis.local_env import MinimumDistanceNN
1✔
24
from pymatgen.analysis.magnetism import CollinearMagneticStructureAnalyzer, Ordering
1✔
25
from pymatgen.core.structure import Structure
1✔
26
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
1✔
27

28
__author__ = "ncfrey"
1✔
29
__version__ = "0.1"
1✔
30
__maintainer__ = "Nathan C. Frey"
1✔
31
__email__ = "ncfrey@lbl.gov"
1✔
32
__status__ = "Development"
1✔
33
__date__ = "June 2019"
1✔
34

35

36
class HeisenbergMapper:
1✔
37
    """
38
    Class to compute exchange parameters from low energy magnetic orderings.
39
    """
40

41
    def __init__(self, ordered_structures, energies, cutoff=0.0, tol: float = 0.02):
1✔
42
        """
43
        Exchange parameters are computed by mapping to a classical Heisenberg
44
        model. Strategy is the scheme for generating neighbors. Currently only
45
        MinimumDistanceNN is implemented.
46
        n+1 unique orderings are required to compute n exchange
47
        parameters.
48

49
        First run a MagneticOrderingsWF to obtain low energy collinear magnetic
50
        orderings and find the magnetic ground state. Then enumerate magnetic
51
        states with the ground state as the input structure, find the subset
52
        of supercells that map to the ground state, and do static calculations
53
        for these orderings.
54

55
        Args:
56
            ordered_structures (list): Structure objects with magmoms.
57
            energies (list): Total energies of each relaxed magnetic structure.
58
            cutoff (float): Cutoff in Angstrom for nearest neighbor search.
59
                Defaults to 0 (only NN, no NNN, etc.)
60
            tol (float): Tolerance (in Angstrom) on nearest neighbor distances
61
                being equal.
62

63
        Parameters:
64
            strategy (object): Class from pymatgen.analysis.local_env for constructing graphs.
65
            sgraphs (list): StructureGraph objects.
66
            unique_site_ids (dict): Maps each site to its unique numerical identifier.
67
            wyckoff_ids (dict): Maps unique numerical identifier to wyckoff position.
68
            nn_interactions (dict): {i: j} pairs of NN interactions between unique sites.
69
            dists (dict): NN, NNN, and NNNN interaction distances
70
            ex_mat (DataFrame): Invertible Heisenberg Hamiltonian for each graph.
71
            ex_params (dict): Exchange parameter values (meV/atom)
72
        """
73
        # Save original copies of inputs
74
        self.ordered_structures_ = ordered_structures
1✔
75
        self.energies_ = energies
1✔
76

77
        # Sanitize inputs and optionally order them by energy / magnetic moments
78
        hs = HeisenbergScreener(ordered_structures, energies, screen=False)
1✔
79
        ordered_structures = hs.screened_structures
1✔
80
        energies = hs.screened_energies
1✔
81

82
        self.ordered_structures = ordered_structures
1✔
83
        self.energies = energies
1✔
84
        self.cutoff = cutoff
1✔
85
        self.tol = tol
1✔
86

87
        # Get graph representations
88
        self.sgraphs = self._get_graphs(cutoff, ordered_structures)
1✔
89

90
        # Get unique site ids and wyckoff symbols
91
        self.unique_site_ids, self.wyckoff_ids = self._get_unique_sites(ordered_structures[0])
1✔
92

93
        # These attributes are set by internal methods
94
        self.nn_interactions = None
1✔
95
        self.dists = None
1✔
96
        self.ex_mat = None
1✔
97
        self.ex_params = None
1✔
98

99
        # Check how many commensurate graphs we found
100
        if len(self.sgraphs) < 2:
1✔
101
            print("We need at least 2 unique orderings.")
×
102
            sys.exit(1)
×
103
        else:  # Set attributes
104
            self._get_nn_dict()
1✔
105
            self._get_exchange_df()
1✔
106

107
    @staticmethod
1✔
108
    def _get_graphs(cutoff, ordered_structures):
1✔
109
        """
110
        Generate graph representations of magnetic structures with nearest
111
        neighbor bonds. Right now this only works for MinimumDistanceNN.
112

113
        Args:
114
            cutoff (float): Cutoff in Angstrom for nearest neighbor search.
115
            ordered_structures (list): Structure objects.
116

117
        Returns:
118
            sgraphs (list): StructureGraph objects.
119
        """
120
        # Strategy for finding neighbors
121
        if cutoff:
1✔
122
            strategy = MinimumDistanceNN(cutoff=cutoff, get_all_sites=True)
1✔
123
        else:
124
            strategy = MinimumDistanceNN()  # only NN
×
125

126
        # Generate structure graphs
127
        sgraphs = [StructureGraph.with_local_env_strategy(s, strategy=strategy) for s in ordered_structures]
1✔
128

129
        return sgraphs
1✔
130

131
    @staticmethod
1✔
132
    def _get_unique_sites(structure):
1✔
133
        """
134
        Get dict that maps site indices to unique identifiers.
135

136
        Args:
137
            structure (Structure): ground state Structure object.
138

139
        Returns:
140
            unique_site_ids (dict): maps tuples of equivalent site indices to a
141
                unique int identifier
142
            wyckoff_ids (dict): maps tuples of equivalent site indices to their
143
                wyckoff symbols
144
        """
145
        # Get a nonmagnetic representation of the supercell geometry
146
        s0 = CollinearMagneticStructureAnalyzer(
1✔
147
            structure, make_primitive=False, threshold=0.0
148
        ).get_nonmagnetic_structure(make_primitive=False)
149

150
        # Get unique sites and wyckoff positions
151
        if "wyckoff" in s0.site_properties:
1✔
152
            s0.remove_site_property("wyckoff")
×
153

154
        symm_s0 = SpacegroupAnalyzer(s0).get_symmetrized_structure()
1✔
155
        wyckoff = ["n/a"] * len(symm_s0)
1✔
156
        equivalent_indices = symm_s0.equivalent_indices
1✔
157
        wyckoff_symbols = symm_s0.wyckoff_symbols
1✔
158

159
        # Construct dictionaries that map sites to numerical and wyckoff
160
        # identifiers
161
        unique_site_ids = {}
1✔
162
        wyckoff_ids = {}
1✔
163

164
        i = 0
1✔
165
        for indices, symbol in zip(equivalent_indices, wyckoff_symbols):
1✔
166
            unique_site_ids[tuple(indices)] = i
1✔
167
            wyckoff_ids[i] = symbol
1✔
168
            i += 1
1✔
169
            for index in indices:
1✔
170
                wyckoff[index] = symbol
1✔
171

172
        return unique_site_ids, wyckoff_ids
1✔
173

174
    def _get_nn_dict(self):
1✔
175
        """Get dict of unique nearest neighbor interactions.
176

177
        Returns:
178
            None: (sets self.nn_interactions and self.dists instance variables)
179
        """
180
        tol = self.tol  # tolerance on NN distances
1✔
181
        sgraph = self.sgraphs[0]
1✔
182
        unique_site_ids = self.unique_site_ids
1✔
183

184
        nn_dict = {}
1✔
185
        nnn_dict = {}
1✔
186
        nnnn_dict = {}
1✔
187

188
        all_dists = []
1✔
189

190
        # Loop over unique sites and get neighbor distances up to NNNN
191
        for k in unique_site_ids:  # pylint: disable=C0206
1✔
192
            i = k[0]
1✔
193
            i_key = unique_site_ids[k]
1✔
194
            connected_sites = sgraph.get_connected_sites(i)
1✔
195
            dists = [round(cs[-1], 2) for cs in connected_sites]  # i<->j distances
1✔
196
            dists = sorted(list(set(dists)))  # NN, NNN, NNNN, etc.
1✔
197

198
            dists = dists[:3]  # keep up to NNNN
1✔
199
            all_dists += dists
1✔
200

201
        # Keep only up to NNNN and call dists equal if they are within tol
202
        all_dists = sorted(list(set(all_dists)))
1✔
203
        rm_list = []
1✔
204
        for idx, d in enumerate(all_dists[:-1]):
1✔
205
            if abs(d - all_dists[idx + 1]) < tol:
1✔
206
                rm_list.append(idx + 1)
1✔
207

208
        all_dists = [d for idx, d in enumerate(all_dists) if idx not in rm_list]
1✔
209

210
        if len(all_dists) < 3:  # pad with zeros
1✔
211
            all_dists += [0.0] * (3 - len(all_dists))
×
212

213
        all_dists = all_dists[:3]
1✔
214
        labels = ["nn", "nnn", "nnnn"]
1✔
215
        dists = dict(zip(labels, all_dists))
1✔
216

217
        # Get dictionary keys for interactions
218
        for k in unique_site_ids:  # pylint: disable=C0206
1✔
219
            i = k[0]
1✔
220
            i_key = unique_site_ids[k]
1✔
221
            connected_sites = sgraph.get_connected_sites(i)
1✔
222

223
            # Loop over sites and determine unique NN, NNN, etc. interactions
224
            for cs in connected_sites:
1✔
225
                dist = round(cs[-1], 2)  # i_j distance
1✔
226

227
                j = cs[2]  # j index
1✔
228
                for key, value in unique_site_ids.items():
1✔
229
                    if j in key:
1✔
230
                        j_key = value
1✔
231
                if abs(dist - dists["nn"]) <= tol:
1✔
232
                    nn_dict[i_key] = j_key
1✔
233
                elif abs(dist - dists["nnn"]) <= tol:
1✔
234
                    nnn_dict[i_key] = j_key
1✔
235
                elif abs(dist - dists["nnnn"]) <= tol:
1✔
236
                    nnnn_dict[i_key] = j_key
1✔
237

238
        nn_interactions = {"nn": nn_dict, "nnn": nnn_dict, "nnnn": nnnn_dict}
1✔
239

240
        self.dists = dists
1✔
241
        self.nn_interactions = nn_interactions
1✔
242

243
    def _get_exchange_df(self):
1✔
244
        """
245
        Loop over all sites in a graph and count the number and types of
246
        nearest neighbor interactions, computing +-|S_i . S_j| to construct
247
        a Heisenberg Hamiltonian for each graph.
248

249
        Returns:
250
            None: (sets self.ex_mat instance variable)
251

252
        TODO:
253
            * Deal with large variance in |S| across configs
254
        """
255
        sgraphs = self.sgraphs
1✔
256
        tol = self.tol
1✔
257
        unique_site_ids = self.unique_site_ids
1✔
258
        nn_interactions = self.nn_interactions
1✔
259
        dists = self.dists
1✔
260

261
        # Get |site magmoms| from FM ordering so that S_i and S_j are consistent?
262
        # Large S variations is throwing a loop
263
        # fm_struct = self.get_low_energy_orderings()[0]
264

265
        # Total energy and nonmagnetic energy contribution
266
        columns = ["E", "E0"]
1✔
267

268
        # Get labels of unique NN interactions
269
        for k0, v0 in nn_interactions.items():
1✔
270
            for idx, j in v0.items():  # i and j indices
1✔
271
                c = str(idx) + "-" + str(j) + "-" + str(k0)
1✔
272
                c_rev = str(j) + "-" + str(idx) + "-" + str(k0)
1✔
273
                if c not in columns and c_rev not in columns:
1✔
274
                    columns.append(c)
1✔
275

276
        num_sgraphs = len(sgraphs)
1✔
277

278
        # Keep n interactions (not counting 'E') for n+1 structure graphs
279
        columns = columns[: num_sgraphs + 1]
1✔
280

281
        num_nn_j = len(columns) - 1  # ignore total energy
1✔
282
        j_columns = [name for name in columns if name not in ["E", "E0"]]
1✔
283
        ex_mat_empty = pd.DataFrame(columns=columns)
1✔
284
        ex_mat = ex_mat_empty.copy()
1✔
285

286
        if len(j_columns) < 2:
1✔
287
            self.ex_mat = ex_mat  # Only <J> can be calculated here
×
288
        else:
289
            sgraphs_copy = copy.deepcopy(sgraphs)
1✔
290
            sgraph_index = 0
1✔
291

292
            # Loop over all sites in each graph and compute |S_i . S_j|
293
            # for n+1 unique graphs to compute n exchange params
294
            for _graph in sgraphs:
1✔
295
                sgraph = sgraphs_copy.pop(0)
1✔
296
                ex_row = pd.DataFrame(np.zeros((1, num_nn_j + 1)), index=[sgraph_index], columns=columns)
1✔
297

298
                for idx, _node in enumerate(sgraph.graph.nodes):
1✔
299
                    # s_i_sign = np.sign(sgraph.structure.site_properties['magmom'][i])
300
                    s_i = sgraph.structure.site_properties["magmom"][idx]
1✔
301

302
                    for k, v in unique_site_ids.items():
1✔
303
                        if idx in k:
1✔
304
                            i_index = v
1✔
305

306
                    # Get all connections for ith site and compute |S_i . S_j|
307
                    connections = sgraph.get_connected_sites(idx)
1✔
308
                    # dists = [round(cs[-1], 2) for cs in connections]  # i<->j distances
309
                    # dists = sorted(list(set(dists)))  # NN, NNN, NNNN, etc.
310

311
                    for connection in connections:
1✔
312
                        j_site = connection[2]
1✔
313
                        dist = round(connection[-1], 2)  # i_j distance
1✔
314

315
                        # s_j_sign = np.sign(sgraph.structure.site_properties['magmom'][j_site])
316
                        s_j = sgraph.structure.site_properties["magmom"][j_site]
1✔
317

318
                        for k, v in unique_site_ids.items():
1✔
319
                            if j_site in k:
1✔
320
                                j_index = v
1✔
321

322
                        # Determine order of connection
323
                        if abs(dist - dists["nn"]) <= tol:
1✔
324
                            order = "-nn"
1✔
325
                        elif abs(dist - dists["nnn"]) <= tol:
1✔
326
                            order = "-nnn"
1✔
327
                        elif abs(dist - dists["nnnn"]) <= tol:
1✔
328
                            order = "-nnnn"
1✔
329
                        j_ij = str(i_index) + "-" + str(j_index) + order
1✔
330
                        j_ji = str(j_index) + "-" + str(i_index) + order
1✔
331

332
                        if j_ij in ex_mat.columns:
1✔
333
                            ex_row.at[sgraph_index, j_ij] -= s_i * s_j
1✔
334
                        elif j_ji in ex_mat.columns:
1✔
335
                            ex_row.at[sgraph_index, j_ji] -= s_i * s_j
1✔
336

337
                # Ignore the row if it is a duplicate to avoid singular matrix
338
                if ex_mat.append(ex_row)[j_columns].equals(
1✔
339
                    ex_mat.append(ex_row)[j_columns].drop_duplicates(keep="first")
340
                ):
341
                    e_index = self.ordered_structures.index(sgraph.structure)
1✔
342
                    ex_row.at[sgraph_index, "E"] = self.energies[e_index]
1✔
343
                    sgraph_index += 1
1✔
344
                    ex_mat = ex_mat.append(ex_row)
1✔
345
                    # if sgraph_index == num_nn_j:  # check for zero columns
346
                    #     zeros = [b for b in (ex_mat[j_columns] == 0).all(axis=0)]
347
                    #     if True in zeros:
348
                    #         sgraph_index -= 1  # keep looking
349

350
            ex_mat[j_columns] = ex_mat[j_columns].div(2.0)  # 1/2 factor in Heisenberg Hamiltonian
1✔
351
            ex_mat[["E0"]] = 1  # Nonmagnetic contribution
1✔
352

353
            # Check for singularities and delete columns with all zeros
354
            zeros = list((ex_mat == 0).all(axis=0))
1✔
355
            if True in zeros:
1✔
356
                c = ex_mat.columns[zeros.index(True)]
×
357
                ex_mat = ex_mat.drop(columns=[c], axis=1)
×
358
                # ex_mat = ex_mat.drop(ex_mat.tail(len_zeros).index)
359

360
            # Force ex_mat to be square
361
            ex_mat = ex_mat[: ex_mat.shape[1] - 1]
1✔
362

363
            self.ex_mat = ex_mat
1✔
364

365
    def get_exchange(self):
1✔
366
        """
367
        Take Heisenberg Hamiltonian and corresponding energy for each row and
368
        solve for the exchange parameters.
369

370
        Returns:
371
            ex_params (dict): Exchange parameter values (meV/atom).
372
        """
373
        ex_mat = self.ex_mat
1✔
374
        # Solve the matrix equation for J_ij values
375
        E = ex_mat[["E"]]
1✔
376
        j_names = [j for j in ex_mat.columns if j not in ["E"]]
1✔
377

378
        # Only 1 NN interaction
379
        if len(j_names) < 3:
1✔
380
            # Estimate exchange by J ~ E_AFM - E_FM
381
            j_avg = self.estimate_exchange()
×
382
            ex_params = {"<J>": j_avg}
×
383
            self.ex_params = ex_params
×
384

385
            return ex_params
×
386

387
        # Solve eigenvalue problem for more than 1 NN interaction
388
        H = np.array(ex_mat.loc[:, ex_mat.columns != "E"].values).astype("float64")
1✔
389
        H_inv = np.linalg.inv(H)
1✔
390
        j_ij = np.dot(H_inv, E)
1✔
391

392
        # Convert J_ij to meV
393
        j_ij[1:] *= 1000  # J_ij in meV
1✔
394
        j_ij = j_ij.tolist()
1✔
395
        ex_params = {j_name: j[0] for j_name, j in zip(j_names, j_ij)}
1✔
396

397
        self.ex_params = ex_params
1✔
398

399
        return ex_params
1✔
400

401
    def get_low_energy_orderings(self):
1✔
402
        """
403
        Find lowest energy FM and AFM orderings to compute E_AFM - E_FM.
404

405
        Returns:
406
            fm_struct (Structure): fm structure with 'magmom' site property
407
            afm_struct (Structure): afm structure with 'magmom' site property
408
            fm_e (float): fm energy
409
            afm_e (float): afm energy
410
        """
411
        fm_struct, afm_struct = None, None
1✔
412
        mag_min = np.inf
1✔
413
        mag_max = 0.001
1✔
414
        fm_e_min = 0
1✔
415
        afm_e_min = 0
1✔
416

417
        # epas = [e / len(s) for (e, s) in zip(self.energies, self.ordered_structures)]
418

419
        for s, e in zip(self.ordered_structures, self.energies):
1✔
420
            ordering = CollinearMagneticStructureAnalyzer(s, threshold=0.0, make_primitive=False).ordering
1✔
421
            magmoms = s.site_properties["magmom"]
1✔
422

423
            # Try to find matching orderings first
424
            if ordering == Ordering.FM and e < fm_e_min:
1✔
425
                fm_struct = s
×
426
                mag_max = abs(sum(magmoms))
×
427
                fm_e = e
×
428
                fm_e_min = e
×
429

430
            if ordering == Ordering.AFM and e < afm_e_min:
1✔
431
                afm_struct = s
×
432
                afm_e = e
×
433
                mag_min = abs(sum(magmoms))
×
434
                afm_e_min = e
×
435

436
        # Brute force search for closest thing to FM and AFM
437
        if not fm_struct or not afm_struct:
1✔
438
            for s, e in zip(self.ordered_structures, self.energies):
1✔
439
                magmoms = s.site_properties["magmom"]
1✔
440

441
                if abs(sum(magmoms)) > mag_max:  # FM ground state
1✔
442
                    fm_struct = s
1✔
443
                    fm_e = e
1✔
444
                    mag_max = abs(sum(magmoms))
1✔
445

446
                # AFM ground state
447
                if abs(sum(magmoms)) < mag_min:
1✔
448
                    afm_struct = s
1✔
449
                    afm_e = e
1✔
450
                    mag_min = abs(sum(magmoms))
1✔
451
                    afm_e_min = e
1✔
452
                elif abs(sum(magmoms)) == 0 and mag_min == 0:
1✔
453
                    if e < afm_e_min:
×
454
                        afm_struct = s
×
455
                        afm_e = e
×
456
                        afm_e_min = e
×
457

458
        # Convert to magnetic structures with 'magmom' site property
459
        fm_struct = CollinearMagneticStructureAnalyzer(
1✔
460
            fm_struct, make_primitive=False, threshold=0.0
461
        ).get_structure_with_only_magnetic_atoms(make_primitive=False)
462

463
        afm_struct = CollinearMagneticStructureAnalyzer(
1✔
464
            afm_struct, make_primitive=False, threshold=0.0
465
        ).get_structure_with_only_magnetic_atoms(make_primitive=False)
466

467
        return fm_struct, afm_struct, fm_e, afm_e
1✔
468

469
    def estimate_exchange(self, fm_struct=None, afm_struct=None, fm_e=None, afm_e=None):
1✔
470
        """
471
        Estimate <J> for a structure based on low energy FM and AFM orderings.
472

473
        Args:
474
            fm_struct (Structure): fm structure with 'magmom' site property
475
            afm_struct (Structure): afm structure with 'magmom' site property
476
            fm_e (float): fm energy/atom
477
            afm_e (float): afm energy/atom
478

479
        Returns:
480
            j_avg (float): Average exchange parameter (meV/atom)
481
        """
482
        # Get low energy orderings if not supplied
483
        if any(arg is None for arg in [fm_struct, afm_struct, fm_e, afm_e]):
1✔
484
            fm_struct, afm_struct, fm_e, afm_e = self.get_low_energy_orderings()
1✔
485

486
        magmoms = fm_struct.site_properties["magmom"]
1✔
487

488
        # Normalize energies by number of magnetic ions
489
        # fm_e = fm_e / len(magmoms)
490
        # afm_e = afm_e / len(afm_magmoms)
491

492
        m_avg = np.mean([np.sqrt(m**2) for m in magmoms])
1✔
493

494
        # If m_avg for FM config is < 1 we won't get sensibile results.
495
        if m_avg < 1:
1✔
496
            iamthedanger = """
1✔
497
                Local magnetic moments are small (< 1 muB / atom). The
498
                exchange parameters may be wrong, but <J> and the mean
499
                field critical temperature estimate may be OK.
500
                """
501
            logging.warning(iamthedanger)
1✔
502

503
        delta_e = afm_e - fm_e  # J > 0 -> FM
1✔
504
        j_avg = delta_e / (m_avg**2)  # eV / magnetic ion
1✔
505
        j_avg *= 1000  # meV / ion
1✔
506

507
        return j_avg
1✔
508

509
    def get_mft_temperature(self, j_avg):
1✔
510
        """
511
        Crude mean field estimate of critical temperature based on <J> for
512
        one sublattice, or solving the coupled equations for a multisublattice
513
        material.
514

515
        Args:
516
            j_avg (float): j_avg (float): Average exchange parameter (meV/atom)
517

518
        Returns:
519
            mft_t (float): Critical temperature (K)
520
        """
521
        num_sublattices = len(self.unique_site_ids)
1✔
522
        k_boltzmann = 0.0861733  # meV/K
1✔
523

524
        # Only 1 magnetic sublattice
525
        if num_sublattices == 1:
1✔
526
            mft_t = 2 * abs(j_avg) / 3 / k_boltzmann
×
527

528
        else:  # multiple magnetic sublattices
529
            omega = np.zeros((num_sublattices, num_sublattices))
1✔
530
            ex_params = self.ex_params
1✔
531
            ex_params = {k: v for (k, v) in ex_params.items() if k != "E0"}  # ignore E0
1✔
532
            for k in ex_params:
1✔
533
                # split into i, j unique site identifiers
534
                sites = k.split("-")
1✔
535
                sites = [int(num) for num in sites[:2]]  # cut 'nn' identifier
1✔
536
                i, j = sites[0], sites[1]
1✔
537
                omega[i, j] += ex_params[k]
1✔
538
                omega[j, i] += ex_params[k]
1✔
539

540
            omega = omega * 2 / 3 / k_boltzmann
1✔
541
            eigenvals, eigenvecs = np.linalg.eig(omega)
1✔
542
            mft_t = max(eigenvals)
1✔
543

544
        if mft_t > 1500:  # Not sensible!
1✔
545
            stayoutofmyterritory = """
×
546
                This mean field estimate is too high! Probably
547
                the true low energy orderings were not given as inputs.
548
                """
549
            logging.warning(stayoutofmyterritory)
×
550

551
        return mft_t
1✔
552

553
    def get_interaction_graph(self, filename=None):
1✔
554
        """
555
        Get a StructureGraph with edges and weights that correspond to exchange
556
        interactions and J_ij values, respectively.
557

558
        Args:
559
            filename (str): if not None, save interaction graph to filename.
560
        Returns:
561
            igraph (StructureGraph): Exchange interaction graph.
562
        """
563
        structure = self.ordered_structures[0]
1✔
564
        sgraph = self.sgraphs[0]
1✔
565

566
        igraph = StructureGraph.with_empty_graph(
1✔
567
            structure, edge_weight_name="exchange_constant", edge_weight_units="meV"
568
        )
569

570
        if "<J>" in self.ex_params:  # Only <J> is available
1✔
571
            warning_msg = """
×
572
                Only <J> is available. The interaction graph will not tell
573
                you much.
574
                """
575
            logging.warning(warning_msg)
×
576

577
        # J_ij exchange interaction matrix
578
        for i, _node in enumerate(sgraph.graph.nodes):
1✔
579
            connections = sgraph.get_connected_sites(i)
1✔
580
            for c in connections:
1✔
581
                jimage = c[1]  # relative integer coordinates of atom j
1✔
582
                j = c[2]  # index of neighbor
1✔
583
                dist = c[-1]  # i <-> j distance
1✔
584

585
                j_exc = self._get_j_exc(i, j, dist)
1✔
586

587
                igraph.add_edge(i, j, to_jimage=jimage, weight=j_exc, warn_duplicates=False)
1✔
588

589
        # Save to a json file if desired
590
        if filename:
1✔
591
            if filename.endswith(".json"):
×
592
                dumpfn(igraph, filename)
×
593
            else:
594
                filename += ".json"
×
595
                dumpfn(igraph, filename)
×
596

597
        return igraph
1✔
598

599
    def _get_j_exc(self, i, j, dist):
1✔
600
        """
601
        Convenience method for looking up exchange parameter between two sites.
602

603
        Args:
604
            i (int): index of ith site
605
            j (int): index of jth site
606
            dist (float): distance (Angstrom) between sites
607
                (10E-2 precision)
608

609
        Returns:
610
            j_exc (float): Exchange parameter in meV
611
        """
612
        # Get unique site identifiers
613
        for k, v in self.unique_site_ids.items():
1✔
614
            if i in k:
1✔
615
                i_index = v
1✔
616
            if j in k:
1✔
617
                j_index = v
1✔
618

619
        order = ""
1✔
620

621
        # Determine order of interaction
622
        if abs(dist - self.dists["nn"]) <= self.tol:
1✔
623
            order = "-nn"
1✔
624
        elif abs(dist - self.dists["nnn"]) <= self.tol:
1✔
625
            order = "-nnn"
1✔
626
        elif abs(dist - self.dists["nnnn"]) <= self.tol:
1✔
627
            order = "-nnnn"
1✔
628

629
        j_ij = str(i_index) + "-" + str(j_index) + order
1✔
630
        j_ji = str(j_index) + "-" + str(i_index) + order
1✔
631

632
        if j_ij in self.ex_params:
1✔
633
            j_exc = self.ex_params[j_ij]
1✔
634
        elif j_ji in self.ex_params:
1✔
635
            j_exc = self.ex_params[j_ji]
1✔
636
        else:
637
            j_exc = 0
1✔
638

639
        # Check if only averaged NN <J> values are available
640
        if "<J>" in self.ex_params and order == "-nn":
1✔
641
            j_exc = self.ex_params["<J>"]
×
642

643
        return j_exc
1✔
644

645
    def get_heisenberg_model(self):
1✔
646
        """Save results of mapping to a HeisenbergModel object.
647

648
        Returns:
649
            hmodel (HeisenbergModel): MSONable object.
650
        """
651
        # Original formula unit with nonmagnetic ions
652
        hm_formula = str(self.ordered_structures_[0].composition.reduced_formula)
1✔
653

654
        hm_structures = self.ordered_structures
1✔
655
        hm_energies = self.energies
1✔
656
        hm_cutoff = self.cutoff
1✔
657
        hm_tol = self.tol
1✔
658
        hm_sgraphs = self.sgraphs
1✔
659
        hm_usi = self.unique_site_ids
1✔
660
        hm_wids = self.wyckoff_ids
1✔
661
        hm_nni = self.nn_interactions
1✔
662
        hm_d = self.dists
1✔
663

664
        # Exchange matrix DataFrame in json format
665
        hm_em = self.ex_mat.to_json()
1✔
666
        hm_ep = self.get_exchange()
1✔
667
        hm_javg = self.estimate_exchange()
1✔
668
        hm_igraph = self.get_interaction_graph()
1✔
669

670
        hmodel = HeisenbergModel(
1✔
671
            hm_formula,
672
            hm_structures,
673
            hm_energies,
674
            hm_cutoff,
675
            hm_tol,
676
            hm_sgraphs,
677
            hm_usi,
678
            hm_wids,
679
            hm_nni,
680
            hm_d,
681
            hm_em,
682
            hm_ep,
683
            hm_javg,
684
            hm_igraph,
685
        )
686

687
        return hmodel
1✔
688

689

690
class HeisenbergScreener:
1✔
691
    """
692
    Class to clean and screen magnetic orderings.
693
    """
694

695
    def __init__(self, structures, energies, screen=False):
1✔
696
        """
697
        This class pre-processes magnetic orderings and energies for
698
        HeisenbergMapper. It prioritizes low-energy orderings with large and
699
        localized magnetic moments.
700

701
        Args:
702
            structures (list): Structure objects with magnetic moments.
703
            energies (list): Energies/atom of magnetic orderings.
704
            screen (bool): Try to screen out high energy and low-spin configurations.
705

706
        Attributes:
707
            screened_structures (list): Sorted structures.
708
            screened_energies (list): Sorted energies.
709
        """
710
        # Cleanup
711
        structures, energies = self._do_cleanup(structures, energies)
1✔
712

713
        n_structures = len(structures)
1✔
714

715
        # If there are more than 2 structures, we want to perform a
716
        # screening to prioritize well-behaved orderings
717
        if screen and n_structures > 2:
1✔
718
            structures, energies = self._do_screen(structures, energies)
×
719

720
        self.screened_structures = structures
1✔
721
        self.screened_energies = energies
1✔
722

723
    @staticmethod
1✔
724
    def _do_cleanup(structures, energies):
1✔
725
        """Sanitize input structures and energies.
726

727
        Takes magnetic structures and performs the following operations
728
        - Erases nonmagnetic ions and gives all ions ['magmom'] site prop
729
        - Converts total energies -> energy / magnetic ion
730
        - Checks for duplicate/degenerate orderings
731
        - Sorts by energy
732

733
        Args:
734
            structures (list): Structure objects with magmoms.
735
            energies (list): Corresponding energies.
736

737
        Returns:
738
            ordered_structures (list): Sanitized structures.
739
            ordered_energies (list): Sorted energies.
740
        """
741
        # Get only magnetic ions & give all structures site_properties['magmom']
742
        # zero threshold so that magnetic ions with small moments
743
        # are preserved
744
        ordered_structures = [
1✔
745
            CollinearMagneticStructureAnalyzer(
746
                s, make_primitive=False, threshold=0.0
747
            ).get_structure_with_only_magnetic_atoms(make_primitive=False)
748
            for s in structures
749
        ]
750

751
        # Convert to energies / magnetic ion
752
        energies = [e / len(s) for (e, s) in zip(energies, ordered_structures)]
1✔
753

754
        # Check for duplicate / degenerate states (sometimes different initial
755
        # configs relax to the same state)
756
        remove_list = []
1✔
757
        for i, e in enumerate(energies):
1✔
758
            e_tol = 6  # 10^-6 eV/atom tol on energies
1✔
759
            e = round(e, e_tol)
1✔
760
            if i not in remove_list:
1✔
761
                for i_check, e_check in enumerate(energies):
1✔
762
                    e_check = round(e_check, e_tol)
1✔
763
                    if i != i_check and i_check not in remove_list and e == e_check:
1✔
764
                        remove_list.append(i_check)
1✔
765

766
        # Also discard structures with small |magmoms| < 0.1 uB
767
        # xx - get rid of these or just bury them in the list?
768
        # for i, s in enumerate(ordered_structures):
769
        #     magmoms = s.site_properties['magmom']
770
        #     if i not in remove_list:
771
        #         if any(abs(m) < 0.1 for m in magmoms):
772
        #             remove_list.append(i)
773

774
        # Remove duplicates
775
        if len(remove_list) > 0:
1✔
776
            ordered_structures = [s for i, s in enumerate(ordered_structures) if i not in remove_list]
1✔
777
            energies = [e for i, e in enumerate(energies) if i not in remove_list]
1✔
778

779
        # Sort by energy if not already sorted
780
        ordered_structures = [s for _, s in sorted(zip(energies, ordered_structures), reverse=False)]
1✔
781
        ordered_energies = sorted(energies, reverse=False)
1✔
782

783
        return ordered_structures, ordered_energies
1✔
784

785
    @staticmethod
1✔
786
    def _do_screen(structures, energies):
1✔
787
        """Screen and sort magnetic orderings based on some criteria.
788

789
        Prioritize low energy orderings and large, localized magmoms. do_clean should be run first to sanitize inputs.
790

791
        Args:
792
            structures (list): At least three structure objects.
793
            energies (list): Energies.
794

795
        Returns:
796
            screened_structures (list): Sorted structures.
797
            screened_energies (list): Sorted energies.
798
        """
799
        magmoms = [s.site_properties["magmom"] for s in structures]
×
800
        n_below_1ub = [len([m for m in ms if abs(m) < 1]) for ms in magmoms]
×
801

802
        df = pd.DataFrame(
×
803
            {
804
                "structure": structures,
805
                "energy": energies,
806
                "magmoms": magmoms,
807
                "n_below_1ub": n_below_1ub,
808
            }
809
        )
810

811
        # keep the ground and first excited state fixed to capture the
812
        # low-energy spectrum
813
        index = list(df.index)[2:]
×
814
        df_high_energy = df.iloc[2:]
×
815

816
        # Prioritize structures with fewer magmoms < 1 uB
817
        df_high_energy = df_high_energy.sort_values(by="n_below_1ub")
×
818

819
        index = [0, 1] + list(df_high_energy.index)
×
820

821
        # sort
822
        df = df.reindex(index)
×
823
        screened_structures = list(df["structure"].values)
×
824
        screened_energies = list(df["energy"].values)
×
825

826
        return screened_structures, screened_energies
×
827

828

829
class HeisenbergModel(MSONable):
1✔
830
    """
831
    Store a Heisenberg model fit to low-energy magnetic orderings.
832
    Intended to be generated by HeisenbergMapper.get_heisenberg_model().
833
    """
834

835
    def __init__(
1✔
836
        self,
837
        formula=None,
838
        structures=None,
839
        energies=None,
840
        cutoff=None,
841
        tol=None,
842
        sgraphs=None,
843
        unique_site_ids=None,
844
        wyckoff_ids=None,
845
        nn_interactions=None,
846
        dists=None,
847
        ex_mat=None,
848
        ex_params=None,
849
        javg=None,
850
        igraph=None,
851
    ):
852
        """
853
        Args:
854
            formula (str): Reduced formula of compound.
855
            structures (list): Structure objects with magmoms.
856
            energies (list): Energies of each relaxed magnetic structure.
857
            cutoff (float): Cutoff in Angstrom for nearest neighbor search.
858
            tol (float): Tolerance (in Angstrom) on nearest neighbor distances being equal.
859
            sgraphs (list): StructureGraph objects.
860
            unique_site_ids (dict): Maps each site to its unique numerical
861
                identifier.
862
            wyckoff_ids (dict): Maps unique numerical identifier to wyckoff
863
                position.
864
            nn_interactions (dict): {i: j} pairs of NN interactions
865
                between unique sites.
866
            dists (dict): NN, NNN, and NNNN interaction distances
867
            ex_mat (DataFrame): Invertible Heisenberg Hamiltonian for each
868
                graph.
869
            ex_params (dict): Exchange parameter values (meV/atom).
870
            javg (float): <J> exchange param (meV/atom).
871
            igraph (StructureGraph): Exchange interaction graph.
872
        """
873
        self.formula = formula
1✔
874
        self.structures = structures
1✔
875
        self.energies = energies
1✔
876
        self.cutoff = cutoff
1✔
877
        self.tol = tol
1✔
878
        self.sgraphs = sgraphs
1✔
879
        self.unique_site_ids = unique_site_ids
1✔
880
        self.wyckoff_ids = wyckoff_ids
1✔
881
        self.nn_interactions = nn_interactions
1✔
882
        self.dists = dists
1✔
883
        self.ex_mat = ex_mat
1✔
884
        self.ex_params = ex_params
1✔
885
        self.javg = javg
1✔
886
        self.igraph = igraph
1✔
887

888
    def as_dict(self):
1✔
889
        """
890
        Because some dicts have tuple keys, some sanitization is required for json compatibility.
891
        """
892
        d = {}
×
893
        d["@module"] = type(self).__module__
×
894
        d["@class"] = type(self).__name__
×
895
        d["@version"] = __version__
×
896
        d["formula"] = self.formula
×
897
        d["structures"] = [s.as_dict() for s in self.structures]
×
898
        d["energies"] = self.energies
×
899
        d["cutoff"] = self.cutoff
×
900
        d["tol"] = self.tol
×
901
        d["sgraphs"] = [sgraph.as_dict() for sgraph in self.sgraphs]
×
902
        d["dists"] = self.dists
×
903
        d["ex_params"] = self.ex_params
×
904
        d["javg"] = self.javg
×
905
        d["igraph"] = self.igraph.as_dict()
×
906

907
        # Sanitize tuple & int keys
908
        d["ex_mat"] = jsanitize(self.ex_mat)
×
909
        d["nn_interactions"] = jsanitize(self.nn_interactions)
×
910
        d["unique_site_ids"] = jsanitize(self.unique_site_ids)
×
911
        d["wyckoff_ids"] = jsanitize(self.wyckoff_ids)
×
912

913
        return d
×
914

915
    @classmethod
1✔
916
    def from_dict(cls, d):
1✔
917
        """Create a HeisenbergModel from a dict."""
918

919
        # Reconstitute the site ids
920
        usids = {}
×
921
        wids = {}
×
922
        nnis = {}
×
923

924
        for k, v in d["nn_interactions"].items():
×
925
            nn_dict = {}
×
926
            for k1, v1 in v.items():
×
927
                key = literal_eval(k1)
×
928
                nn_dict[key] = v1
×
929
            nnis[k] = nn_dict
×
930

931
        for k, v in d["unique_site_ids"].items():
×
932
            key = literal_eval(k)
×
933
            if isinstance(key, int):
×
934
                usids[tuple([key])] = v
×
935
            elif isinstance(key, tuple):
×
936
                usids[key] = v
×
937

938
        for k, v in d["wyckoff_ids"].items():
×
939
            key = literal_eval(k)
×
940
            wids[key] = v
×
941

942
        # Reconstitute the structure and graph objects
943
        structures = []
×
944
        sgraphs = []
×
945
        for v in d["structures"]:
×
946
            structures.append(Structure.from_dict(v))
×
947
        for v in d["sgraphs"]:
×
948
            sgraphs.append(StructureGraph.from_dict(v))
×
949

950
        # Interaction graph
951
        igraph = StructureGraph.from_dict(d["igraph"])
×
952

953
        # Reconstitute the exchange matrix DataFrame
954
        try:
×
955
            ex_mat = eval(d["ex_mat"])
×
956
            ex_mat = pd.DataFrame.from_dict(ex_mat)
×
957
        except SyntaxError:  # if ex_mat is empty
×
958
            ex_mat = pd.DataFrame(columns=["E", "E0"])
×
959

960
        hmodel = HeisenbergModel(
×
961
            formula=d["formula"],
962
            structures=structures,
963
            energies=d["energies"],
964
            cutoff=d["cutoff"],
965
            tol=d["tol"],
966
            sgraphs=sgraphs,
967
            unique_site_ids=usids,
968
            wyckoff_ids=wids,
969
            nn_interactions=nnis,
970
            dists=d["dists"],
971
            ex_mat=ex_mat,
972
            ex_params=d["ex_params"],
973
            javg=d["javg"],
974
            igraph=igraph,
975
        )
976

977
        return hmodel
×
978

979
    def _get_j_exc(self, i, j, dist):
1✔
980
        """
981
        Convenience method for looking up exchange parameter between two sites.
982

983
        Args:
984
            i (int): index of ith site
985
            j (int): index of jth site
986
            dist (float): distance (Angstrom) between sites +- tol
987

988
        Returns:
989
            j_exc (float): Exchange parameter in meV
990
        """
991
        # Get unique site identifiers
992
        for k in self.unique_site_ids:
×
993
            if i in k:
×
994
                i_index = self.unique_site_ids[k]
×
995
            if j in k:
×
996
                j_index = self.unique_site_ids[k]
×
997

998
        order = ""
×
999

1000
        # Determine order of interaction
1001
        if abs(dist - self.dists["nn"]) <= self.tol:
×
1002
            order = "-nn"
×
1003
        elif abs(dist - self.dists["nnn"]) <= self.tol:
×
1004
            order = "-nnn"
×
1005
        elif abs(dist - self.dists["nnnn"]) <= self.tol:
×
1006
            order = "-nnnn"
×
1007

1008
        j_ij = str(i_index) + "-" + str(j_index) + order
×
1009
        j_ji = str(j_index) + "-" + str(i_index) + order
×
1010

1011
        if j_ij in self.ex_params:
×
1012
            j_exc = self.ex_params[j_ij]
×
1013
        elif j_ji in self.ex_params:
×
1014
            j_exc = self.ex_params[j_ji]
×
1015
        else:
1016
            j_exc = 0
×
1017

1018
        # Check if only averaged NN <J> values are available
1019
        if "<J>" in self.ex_params and order == "-nn":
×
1020
            j_exc = self.ex_params["<J>"]
×
1021

1022
        return j_exc
×
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