Coverage for gpaw/borncharges.py: 94%
109 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-19 00:19 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-19 00:19 +0000
1import numpy as np
2from gpaw.mpi import world
3from gpaw.berryphase import polarization_phase, ionic_phase
4from ase.parallel import paropen, parprint
5from ase.io.jsonio import write_json, read_json
6from pathlib import Path
9def born_charges_wf(atoms, calc, delta=0.01, cleanup=False,
10 ionic_only=False, out='born_charges.json'):
12 # generate displacement dictionary
13 disps_av = _all_disp(atoms, delta)
15 # carry out polarization phase calculation
16 # for each displacement
17 phases_c = {}
18 for dlabel in disps_av:
19 ia, iv, sign, delta = disps_av[dlabel]
20 atoms_d = displace_atom(atoms, ia, iv, sign, delta)
21 check_distance_to_non_pbc_boundary(atoms_d)
23 if not ionic_only:
25 # proper polarization phase calculation
26 gpw_wfs = Path(dlabel + '.gpw')
27 berryname = Path(dlabel + '_berry-phases.json')
28 if not berryname.is_file():
29 if not gpw_wfs.is_file():
31 # run calculations
32 atoms_d.calc = calc
33 assert is_symmetry_off(atoms_d.calc), 'Set symmetry off'
34 atoms_d.get_potential_energy()
36 # write wavefunctions
37 atoms_d.calc.write(gpw_wfs, 'all')
39 # dict with entries phase_c, electronic_phase_c
40 # atomic_phase_c, dipole_moment_c
41 phase_c = polarization_phase(gpw_wfs, comm=world)
43 # only master rank should write
44 with paropen(berryname, 'w') as fd:
45 write_json(fd, phase_c)
47 else:
48 # all ranks can read
49 with open(berryname, 'r') as fd:
50 phase_c = read_json(fd)
52 if cleanup:
53 if berryname.is_file():
54 # remove gpw file
55 if world.rank == 0:
56 gpw_wfs.unlink()
57 else:
58 # only atomic contribution considered
59 # for unexpensive testing only
60 phase_c = ionic_phase(atoms_d)
62 phases_c[dlabel] = phase_c['phase_c']
64 results = born_charges(atoms, disps_av, phases_c, check=(not ionic_only))
65 with paropen(out, 'w') as fd:
66 write_json(fd, results)
68 return results
71def is_symmetry_off(calc):
72 params = calc.parameters
73 if calc.old:
74 if 'symmetry' in params:
75 return params['symmetry'] == 'off'
76 else:
77 return False
78 else:
79 # new:
80 return (not params.symmetry.point_group and
81 not params.symmetry.time_reversal)
84def born_charges(atoms, disps_av, phases_c, check=True):
86 natoms = len(atoms)
87 cell_cv = atoms.get_cell()
88 vol = abs(np.linalg.det(cell_cv))
89 sym_a = atoms.get_chemical_symbols()
91 ndisp = len(disps_av)
92 parprint('Not using symmetry: ndisp:', ndisp)
94 # obtain phi(dr) map
95 phi_ascv = np.zeros((natoms, 2, 3, 3), float)
96 for dlabel in disps_av:
97 ia, iv, sign, delta = disps_av[dlabel]
98 isign = [None, 1, 0][sign]
99 phi_ascv[ia, isign, :, iv] = phases_c[dlabel]
101 # calculate dphi / dr
102 # exploit +- displacement
103 dphi_acv = phi_ascv[:, 1] - phi_ascv[:, 0]
104 # mod 2 pi
105 mod_acv = np.round(dphi_acv / (2 * np.pi)) * 2 * np.pi
106 dphi_acv -= mod_acv
107 # transform to cartesian
108 dphi_avv = np.array([np.dot(dphi_cv.T, cell_cv).T for dphi_cv in dphi_acv])
109 dphi_dr_avv = dphi_avv / (2.0 * delta)
111 # calculate polarization change and born charges
112 dP_dr_avv = dphi_dr_avv / (2 * np.pi * vol)
113 Z_avv = dP_dr_avv * vol
115 if check:
116 # check acoustic sum rule: sum_a Z_aij = 0 for all i,j
117 asr_vv = np.sum(Z_avv, axis=0)
118 asr_dev = np.abs(asr_vv).max() / natoms
119 assert asr_dev < 1e-1, f'Acoustic sum rule violated: {asr_vv}'
121 # correct to match acoustic sum rule
122 Z_avv -= asr_vv[None, :, :] / natoms
124 results = {'Z_avv': Z_avv, 'sym_a': sym_a}
126 return results
129def _cartesian_label(ia, iv, sign):
130 """Generate name from (ia, iv, sign).
131 ia ... atomic_index
132 iv ... cartesian_index
133 sign ... +-
134 """
136 sym_v = 'xyz'[iv]
137 sym_s = ' +-'[sign]
138 return f'{ia}{sym_v}{sym_s}'
141def _all_avs(atoms):
142 """Generate ia, iv, sign for all displacements."""
143 for ia in range(len(atoms)):
144 for iv in range(3):
145 for sign in [-1, 1]:
146 yield (ia, iv, sign)
149def _all_disp(atoms, delta):
150 all_disp = {}
151 for dd, avs in enumerate(_all_avs(atoms)):
152 dd = int(dd)
153 lavs = _cartesian_label(*avs)
154 label = f'disp_{dd:03d}_' + lavs
155 all_disp[label] = (*avs, delta)
156 return all_disp
159def displace_atom(atoms, ia, iv, sign, delta):
160 new_atoms = atoms.copy()
161 pos_av = new_atoms.get_positions()
162 pos_av[ia, iv] += sign * delta
163 new_atoms.set_positions(pos_av)
164 return new_atoms
167def check_distance_to_non_pbc_boundary(atoms, eps=1):
168 dist_a = distance_to_non_pbc_boundary(atoms)
169 if dist_a is not None and np.any(dist_a < eps):
170 raise AtomsTooCloseToBoundary(
171 'The atoms are too close to a non-pbc boundary '
172 'which creates problems when using a dipole correction. '
173 f'Please center the atoms in the unit-cell. Distances: {dist_a}.'
174 )
177def distance_to_non_pbc_boundary(atoms):
178 pbc_c = atoms.get_pbc()
179 if pbc_c.all():
180 return None
181 cell_cv = atoms.get_cell()
182 pos_ac = atoms.get_scaled_positions()
183 pos_ac -= np.round(pos_ac)
184 posnonpbc_av = np.dot(pos_ac[:, ~pbc_c], cell_cv[~pbc_c])
185 dist_to_cell_edge_a = np.linalg.norm(posnonpbc_av, axis=1)
186 return dist_to_cell_edge_a
189class AtomsTooCloseToBoundary(Exception):
190 pass