elecboltz 0.1.0__tar.gz
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.
- elecboltz-0.1.0/LICENSE +21 -0
- elecboltz-0.1.0/PKG-INFO +10 -0
- elecboltz-0.1.0/README.md +29 -0
- elecboltz-0.1.0/elecboltz/__init__.py +3 -0
- elecboltz-0.1.0/elecboltz/bandstructure.py +399 -0
- elecboltz-0.1.0/elecboltz/conductivity.py +449 -0
- elecboltz-0.1.0/elecboltz/integrate.py +110 -0
- elecboltz-0.1.0/elecboltz/params.py +174 -0
- elecboltz-0.1.0/elecboltz.egg-info/PKG-INFO +10 -0
- elecboltz-0.1.0/elecboltz.egg-info/SOURCES.txt +14 -0
- elecboltz-0.1.0/elecboltz.egg-info/dependency_links.txt +1 -0
- elecboltz-0.1.0/elecboltz.egg-info/requires.txt +4 -0
- elecboltz-0.1.0/elecboltz.egg-info/top_level.txt +1 -0
- elecboltz-0.1.0/pyproject.toml +17 -0
- elecboltz-0.1.0/setup.cfg +4 -0
- elecboltz-0.1.0/tests/test_geometrical_calculations.py +95 -0
elecboltz-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Saleh Shamloo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
elecboltz-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: elecboltz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Boltzmann transport for conductivity of materials using an FEM
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Dist: numpy>=1.26.4
|
|
7
|
+
Requires-Dist: scipy>=1.12.0
|
|
8
|
+
Requires-Dist: sympy>=1.10.1
|
|
9
|
+
Requires-Dist: scikit-image>=0.22.0
|
|
10
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# FEM for Boltzmann Transport Conductivity
|
|
2
|
+
Conductivity calculated using Boltzmann Transport theory, using a Finite Element Method (FEM).
|
|
3
|
+
In addition to arbitrary Fermi surfaces, arbitrary scattering kernels are also supported.
|
|
4
|
+
|
|
5
|
+
Developed and maintained by the [Grissonnanche group](https://gaelgrissonnanche.com).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
```
|
|
9
|
+
pip install elecboltz
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
Here is a minimal example for a simple free electron system.
|
|
14
|
+
```python
|
|
15
|
+
import elecboltz
|
|
16
|
+
|
|
17
|
+
band = elecboltz.BandStructure(
|
|
18
|
+
dispersion="kx**2 + ky**2", chemical_potential=1.0,
|
|
19
|
+
unit_cell=(2*np.pi, 2*np.pi, 2*np.pi), periodic=2)
|
|
20
|
+
band.discretize()
|
|
21
|
+
|
|
22
|
+
cond = elecboltz.Conductivity(
|
|
23
|
+
band=band, scattering=1.0, field=(1.0, 2.0, 3.0))
|
|
24
|
+
sigma = cond.solve()
|
|
25
|
+
print(sigma)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Documentation
|
|
29
|
+
The documentation is available at [elecboltz.readthedocs.io](https://elecboltz.readthedocs.io).
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import sympy
|
|
3
|
+
import itertools
|
|
4
|
+
from skimage.measure import marching_cubes
|
|
5
|
+
# units
|
|
6
|
+
from scipy.constants import hbar, eV, angstrom
|
|
7
|
+
# type hinting
|
|
8
|
+
from collections.abc import Collection
|
|
9
|
+
from .integrate import adaptive_octree_integrate
|
|
10
|
+
# conversion from energy gradient units to m/s for velocity
|
|
11
|
+
velocity_units = 1e-3 * eV * angstrom / hbar
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BandStructure:
|
|
15
|
+
"""
|
|
16
|
+
Contains bandstructure information for a given material.
|
|
17
|
+
|
|
18
|
+
In addition to the dispersion relation and general parameters, this
|
|
19
|
+
class also contains methods for discretizing the Fermi surface and
|
|
20
|
+
calculating electronic properties.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
dispersion : str
|
|
25
|
+
The dispersion relation. Expresses the dispersion relation
|
|
26
|
+
in terms of symbols in `wavevector_names` and additional
|
|
27
|
+
parameters in `band_params`. It must be parsable and
|
|
28
|
+
differentiable by `sympy`. Energy units are milli eV.
|
|
29
|
+
chemical_potential : float
|
|
30
|
+
The chemical potential in milli eV.
|
|
31
|
+
unit_cell : Collection[float]
|
|
32
|
+
The dimensions of the unit cell in angstrom.
|
|
33
|
+
band_params : dict, optional
|
|
34
|
+
The parameters of the dispersion relation. Energy units are
|
|
35
|
+
milli eV and distance units are angstrom.
|
|
36
|
+
domain_size : Collection[float]
|
|
37
|
+
The ratio of the reciprocal space domain sidelengths to simple
|
|
38
|
+
cubic unit cell dimensions in reciprocal space. The product
|
|
39
|
+
of the numbers in this collection must be equal to the number
|
|
40
|
+
of atoms in the conventional unit cell specified by `unit_cell`.
|
|
41
|
+
periodic : bool or int or Collection[bool] or Collection[int]
|
|
42
|
+
If bool, whether periodic boundary conditions are applied to all
|
|
43
|
+
axes or not. If a single int, specifies which single axis is
|
|
44
|
+
periodic. If a collection, specifies which axes are periodic.
|
|
45
|
+
If the collection is of integers, the integers specify the
|
|
46
|
+
periodic axes, e.g. [0, 2] means periodic in x and z axes.
|
|
47
|
+
If the collection is of booleans, it specifies whether each axis
|
|
48
|
+
is periodic or not, e.g. [True, False, True] means periodic in
|
|
49
|
+
x and z axes, but not in y axis.
|
|
50
|
+
axis_names : str or Collection[str], optional
|
|
51
|
+
The names of the unit cell axes. Must be parsable by
|
|
52
|
+
`sympy.symbols`.
|
|
53
|
+
wavevector_names : str or Collection[str], optional
|
|
54
|
+
The names of the wavevector components. Must be parsable by
|
|
55
|
+
`sympy.symbols`.
|
|
56
|
+
resolution : int or Collection[int], optional
|
|
57
|
+
Controls the resolution of the grids used for discretizing the
|
|
58
|
+
Fermi surface. If a collection of integers is provided, each
|
|
59
|
+
element corresponds to the resolution along the respective
|
|
60
|
+
axis. If a single integer is provided, it is used for all axes.
|
|
61
|
+
ncorrect : int, optional
|
|
62
|
+
The number of correction steps for improving the accuracy of
|
|
63
|
+
the discretization of the Fermi surface.
|
|
64
|
+
sort_axis : int, optional
|
|
65
|
+
The axis along which to sort the points after triangulation.
|
|
66
|
+
If None, do not sort the points.
|
|
67
|
+
|
|
68
|
+
Attributes
|
|
69
|
+
----------
|
|
70
|
+
dispersion : str
|
|
71
|
+
The dispersion relation. Updating this will automatically
|
|
72
|
+
update `energy_func` and `velocity_func`.
|
|
73
|
+
chemical_potential : float
|
|
74
|
+
The chemical potential in milli eV.
|
|
75
|
+
unit_cell : Collection[float]
|
|
76
|
+
The dimensions of the unit cell in angstrom.
|
|
77
|
+
domain_size : Collection[float]
|
|
78
|
+
The ratio of the reciprocal space domain sidelengths to simple
|
|
79
|
+
cubic unit cell dimensions in reciprocal space. The product
|
|
80
|
+
of the numbers in this collection must be equal to the number
|
|
81
|
+
of atoms in the conventional unit cell specified by `unit_cell`.
|
|
82
|
+
periodic : Collection[int]
|
|
83
|
+
The periodic axes.
|
|
84
|
+
band_params : dict
|
|
85
|
+
The parameters of the dispersion relation. Updating this
|
|
86
|
+
will automatically update `energy_func` and `velocity_func`.
|
|
87
|
+
energy_func : function
|
|
88
|
+
The energy function for the dispersion relation. Takes
|
|
89
|
+
kx, ky, and kz in angstrome^-1 as arguments and returns
|
|
90
|
+
the energy in milli eV.
|
|
91
|
+
velocity_func : function
|
|
92
|
+
The velocity function for the dispersion relation. Takes
|
|
93
|
+
kx, ky, and kz in angstrome^-1 as arguments and returns
|
|
94
|
+
the velocity vector as a list [vx, vy, vz] in units of m/s.
|
|
95
|
+
kpoints : (N, 3) numpy.ndarray
|
|
96
|
+
The discretized k-points on the Fermi surface. Each row
|
|
97
|
+
corresponds to a k-point in the form [kx, ky, kz].
|
|
98
|
+
kfaces : (F, 3) numpy.ndarray
|
|
99
|
+
The faces of the triangulated surface in k-space. Each row
|
|
100
|
+
corresponds to a face in the form [i, j, k], where i, j,
|
|
101
|
+
and k are the indices of the vertices of the face in the
|
|
102
|
+
`kpoints` array.
|
|
103
|
+
kpoints_periodic : (N, 3) numpy.ndarray of float
|
|
104
|
+
The kpoints on the Fermi surface with the duplicate boundary
|
|
105
|
+
points removed. Is only different from `kpoints` if `periodic`
|
|
106
|
+
is True.
|
|
107
|
+
kfaces_periodic : (F, 3) numpy.ndarray of int
|
|
108
|
+
Same as `kfaces`, but points to the unique points in
|
|
109
|
+
`kpoints_periodic`. is only different from `kfaces` if
|
|
110
|
+
`periodic` is True.
|
|
111
|
+
resolution : int or Collection[int]
|
|
112
|
+
The resolution of the grids used for approximating the Fermi
|
|
113
|
+
surface geometry with the marching cubes algorithm.
|
|
114
|
+
ncorrect : int
|
|
115
|
+
The number of Newton--Raphson steps applied to correct the
|
|
116
|
+
triangulated surface after the marching cubes algorithm.
|
|
117
|
+
sort_axis : int, optional
|
|
118
|
+
The axis along which to sort the points after triangulation.
|
|
119
|
+
axis_names : str or Collection[str]
|
|
120
|
+
The names of the unit cell axes.
|
|
121
|
+
wavevector_names : str or Collection[str]
|
|
122
|
+
The names of the wavevector components.
|
|
123
|
+
"""
|
|
124
|
+
def __init__(
|
|
125
|
+
self, dispersion: str, chemical_potential: float,
|
|
126
|
+
unit_cell: Collection[float], band_params: dict = {},
|
|
127
|
+
domain_size: Collection[float] = [1.0, 1.0, 1.0],
|
|
128
|
+
periodic: bool | Collection[int|bool] = True,
|
|
129
|
+
axis_names: Collection[str] | str = ['a', 'b', 'c'],
|
|
130
|
+
wavevector_names: Collection[str] | str = ['kx', 'ky', 'kz'],
|
|
131
|
+
sort_axis: int = None, resolution: int | Collection[int] = 21,
|
|
132
|
+
ncorrect: int = 2, **kwargs):
|
|
133
|
+
# avoid triggering the __setattr__ method for the first time
|
|
134
|
+
super().__setattr__('dispersion', dispersion)
|
|
135
|
+
super().__setattr__('band_params', band_params)
|
|
136
|
+
self.chemical_potential = chemical_potential
|
|
137
|
+
self.unit_cell = unit_cell
|
|
138
|
+
self.domain_size = domain_size
|
|
139
|
+
self.periodic = periodic
|
|
140
|
+
self.axis_names = axis_names
|
|
141
|
+
self.wavevector_names = wavevector_names
|
|
142
|
+
self.resolution = resolution
|
|
143
|
+
self.ncorrect = ncorrect
|
|
144
|
+
self._parse_dispersion()
|
|
145
|
+
self.kpoints = None
|
|
146
|
+
self.kfaces = None
|
|
147
|
+
self.kpoints_periodic = None
|
|
148
|
+
self.kfaces_periodic = None
|
|
149
|
+
self.sort_axis = sort_axis
|
|
150
|
+
|
|
151
|
+
def __setattr__(self, name, value):
|
|
152
|
+
if name == 'dispersion' or name == 'band_params':
|
|
153
|
+
self._parse_dispersion()
|
|
154
|
+
if name == 'resolution':
|
|
155
|
+
if isinstance(value, Collection):
|
|
156
|
+
value = np.array(value)
|
|
157
|
+
else:
|
|
158
|
+
value = np.array([value, value, value])
|
|
159
|
+
if name in {'unit_cell', 'domain_size'}:
|
|
160
|
+
value = np.array(value, dtype=float)
|
|
161
|
+
if name == 'periodic':
|
|
162
|
+
if isinstance(value, bool):
|
|
163
|
+
if value:
|
|
164
|
+
value = [0, 1, 2]
|
|
165
|
+
else:
|
|
166
|
+
value = []
|
|
167
|
+
if isinstance(value, int):
|
|
168
|
+
value = [value]
|
|
169
|
+
elif isinstance(value, Collection) and all(
|
|
170
|
+
isinstance(i, bool) for i in value):
|
|
171
|
+
value = [i for i, v in enumerate(value) if v]
|
|
172
|
+
super().__setattr__(name, value)
|
|
173
|
+
|
|
174
|
+
def discretize(self):
|
|
175
|
+
"""
|
|
176
|
+
Discretize the Fermi surface.
|
|
177
|
+
|
|
178
|
+
First, the surface is triangulated using the marching cubes
|
|
179
|
+
algorithm with `resolution` controlling the resolution of the
|
|
180
|
+
grid. Next, to improve the accuracy of the isosurface,
|
|
181
|
+
`ncorrect` steps of the Newton--Raphson root-finding method are
|
|
182
|
+
applied to the output of marching cubes. Finally, after the
|
|
183
|
+
surface construction, periodic boundary conditions are applied
|
|
184
|
+
to "stitch" the open ends of the surface together.
|
|
185
|
+
"""
|
|
186
|
+
self._gvec = self.domain_size * np.pi / self.unit_cell
|
|
187
|
+
self.kpoints, self.kfaces, _, _ = marching_cubes(
|
|
188
|
+
self.energy_func(*np.mgrid[
|
|
189
|
+
-self._gvec[0]:self._gvec[0]:1j*self.resolution[0],
|
|
190
|
+
-self._gvec[1]:self._gvec[1]:1j*self.resolution[1],
|
|
191
|
+
-self._gvec[2]:self._gvec[2]:1j*self.resolution[2]]),
|
|
192
|
+
level=self.chemical_potential)
|
|
193
|
+
self.kpoints *= (2*self._gvec / (self.resolution-1))[None, :]
|
|
194
|
+
self.kpoints -= self._gvec[None, :]
|
|
195
|
+
for _ in range(self.ncorrect):
|
|
196
|
+
self.kpoints = self._apply_newton_correction(self.kpoints)
|
|
197
|
+
if self.sort_axis:
|
|
198
|
+
self._sort_and_reindex(self.sort_axis)
|
|
199
|
+
self._stitch_periodic_boundaries()
|
|
200
|
+
|
|
201
|
+
def calculate_filling_fraction(self, depth: int = 7) -> float:
|
|
202
|
+
"""
|
|
203
|
+
Calculate the filling fraction n of the material.
|
|
204
|
+
|
|
205
|
+
The filling fraction is calculated by integrating the volume
|
|
206
|
+
in the reciprocal space having energy bellow the Fermi level
|
|
207
|
+
(calculated by an adaptive octree integration method), then
|
|
208
|
+
dividing by the volume of the unit cell in the reciprocal space.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
depth : int, optional
|
|
213
|
+
The depth of the adaptive octree integration. Higher values
|
|
214
|
+
result in more accurate integration, but take exponentially
|
|
215
|
+
longer to compute.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
float
|
|
220
|
+
The filling fraction n of the material.
|
|
221
|
+
"""
|
|
222
|
+
self._gvec = self.domain_size * np.pi / self.unit_cell
|
|
223
|
+
# the extra factor of 2 is the spin degeneracy
|
|
224
|
+
return 2 * adaptive_octree_integrate(
|
|
225
|
+
lambda kx, ky, kz: (self.energy_func(kx, ky, kz)
|
|
226
|
+
< self.chemical_potential),
|
|
227
|
+
(-self._gvec[0], self._gvec[0], -self._gvec[1], self._gvec[1],
|
|
228
|
+
-self._gvec[2], self._gvec[2]), depth
|
|
229
|
+
) / 8 / np.prod(self._gvec)
|
|
230
|
+
|
|
231
|
+
def calculate_electron_density(self, depth: int = 7) -> float:
|
|
232
|
+
"""
|
|
233
|
+
Calculate the electron density n_e of the material.
|
|
234
|
+
|
|
235
|
+
Note that the surface needs to be discretized before calling
|
|
236
|
+
this method. First, the filling fraction is calculated (see
|
|
237
|
+
`calculate_filling_fraction`). The electron density is obtained
|
|
238
|
+
by dividing the filling fraction by the volume of the unit cell
|
|
239
|
+
in real space.
|
|
240
|
+
|
|
241
|
+
Parameters
|
|
242
|
+
----------
|
|
243
|
+
depth : int, optional
|
|
244
|
+
The depth of the adaptive octree integration in
|
|
245
|
+
`calculate_filling_fraction`.
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
float
|
|
250
|
+
The electron density n_e of the material in SI units.
|
|
251
|
+
"""
|
|
252
|
+
filling_fraction = self.calculate_filling_fraction(depth)
|
|
253
|
+
# the volume of the unit cell in real space is scaled by
|
|
254
|
+
# the inverse scaling of the unit cell in reciprocal space
|
|
255
|
+
unit_cell_volume = (np.prod(self.unit_cell) * angstrom**3
|
|
256
|
+
/ np.prod(self.domain_size))
|
|
257
|
+
return filling_fraction / unit_cell_volume
|
|
258
|
+
|
|
259
|
+
def calculate_mass(self):
|
|
260
|
+
"""
|
|
261
|
+
Calculate the effective mass of the charge carries.
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
float
|
|
266
|
+
The effective mass divided by the rest mass of
|
|
267
|
+
the electron, m_e.
|
|
268
|
+
"""
|
|
269
|
+
# Placeholder for actual calculation
|
|
270
|
+
return 0.0
|
|
271
|
+
|
|
272
|
+
def _parse_dispersion(self):
|
|
273
|
+
"""
|
|
274
|
+
Parse the dispersion relation and extract the necessary
|
|
275
|
+
information for further calculations.
|
|
276
|
+
"""
|
|
277
|
+
ksymbols = sympy.symbols(self.wavevector_names)
|
|
278
|
+
all_symbols = (ksymbols + sympy.symbols(self.axis_names)
|
|
279
|
+
+ sympy.symbols(list(self.band_params.keys())))
|
|
280
|
+
# symbolic expressions
|
|
281
|
+
self._energy_sympy = sympy.sympify(self.dispersion)
|
|
282
|
+
self._velocities_sympy = [
|
|
283
|
+
sympy.diff(self._energy_sympy, k) * velocity_units
|
|
284
|
+
for k in sympy.symbols(self.wavevector_names)]
|
|
285
|
+
velocity_magnitude_sympy = sympy.sqrt(
|
|
286
|
+
sum(v**2 for v in self._velocities_sympy))
|
|
287
|
+
vhats_sympy = [
|
|
288
|
+
v / velocity_magnitude_sympy if velocity_magnitude_sympy != 0
|
|
289
|
+
else 0 for v in self._velocities_sympy]
|
|
290
|
+
self._curvature_sympy = -0.5 * sum(
|
|
291
|
+
sympy.diff(vhat, k) for vhat, k in zip(vhats_sympy, ksymbols))
|
|
292
|
+
# replace zero velocities with zero arrays
|
|
293
|
+
for i, v in enumerate(self._velocities_sympy):
|
|
294
|
+
if v == 0:
|
|
295
|
+
self._velocities_sympy[i] = f"numpy.zeros_like({ksymbols[i]})"
|
|
296
|
+
# convert expressions into python functions
|
|
297
|
+
self._energy_func_full = sympy.lambdify(
|
|
298
|
+
all_symbols, self._energy_sympy)
|
|
299
|
+
self._velocity_funcs_full = [
|
|
300
|
+
sympy.lambdify(all_symbols, vexpr, 'numpy')
|
|
301
|
+
for vexpr in self._velocities_sympy]
|
|
302
|
+
self._curvature_func_full = sympy.lambdify(
|
|
303
|
+
all_symbols, self._curvature_sympy, 'numpy')
|
|
304
|
+
# reduce the arguments to only kx, ky, kz
|
|
305
|
+
self.energy_func = lambda kx, ky, kz: self._energy_func_full(
|
|
306
|
+
kx, ky, kz, *self.unit_cell, **self.band_params)
|
|
307
|
+
self.velocity_func = lambda kx, ky, kz: [
|
|
308
|
+
vfunc(kx, ky, kz, *self.unit_cell, **self.band_params)
|
|
309
|
+
for vfunc in self._velocity_funcs_full]
|
|
310
|
+
self.curvature_func = lambda kx, ky, kz: self._curvature_func_full(
|
|
311
|
+
kx, ky, kz, *self.unit_cell, **self.band_params)
|
|
312
|
+
|
|
313
|
+
def _sort_and_reindex(self, sort_axis):
|
|
314
|
+
new_order, self.kfaces = self._generate_reindex(sort_axis)
|
|
315
|
+
self.kpoints = self.kpoints[new_order]
|
|
316
|
+
|
|
317
|
+
def _generate_reindex(self, sort_axis):
|
|
318
|
+
new_order = np.argsort(self.kpoints[:, sort_axis])
|
|
319
|
+
old_to_new_map = np.empty(len(new_order), dtype=int)
|
|
320
|
+
old_to_new_map[new_order] = np.arange(len(new_order))
|
|
321
|
+
return new_order, old_to_new_map[self.kfaces]
|
|
322
|
+
|
|
323
|
+
def _stitch_periodic_boundaries(self):
|
|
324
|
+
"""
|
|
325
|
+
Find duplicate points on the periodic boundaries, then make the
|
|
326
|
+
periodic mesh arrays.
|
|
327
|
+
"""
|
|
328
|
+
duplicates = dict()
|
|
329
|
+
threshold = np.min(self._gvec / self.resolution) / 10
|
|
330
|
+
for axis in self.periodic:
|
|
331
|
+
low_border = np.argwhere(
|
|
332
|
+
self.kpoints[:, axis] + self._gvec[axis] < threshold).ravel()
|
|
333
|
+
high_border = np.argwhere(
|
|
334
|
+
self.kpoints[:, axis] - self._gvec[axis] > -threshold).ravel()
|
|
335
|
+
if len(low_border) == 0 or len(high_border) == 0:
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
min_dist = min(
|
|
339
|
+
self._get_min_border_distance(low_border),
|
|
340
|
+
self._get_min_border_distance(high_border))
|
|
341
|
+
|
|
342
|
+
k1 = self.kpoints[low_border][None, :]
|
|
343
|
+
k2 = self.kpoints[high_border][:, None]
|
|
344
|
+
kdiff = k2 - k1
|
|
345
|
+
kdiff[:, :, axis] += self._gvec[axis]
|
|
346
|
+
kdiff[:, :, axis] %= 2 * self._gvec[axis]
|
|
347
|
+
kdiff[:, :, axis] -= self._gvec[axis]
|
|
348
|
+
kdiff = np.linalg.norm(kdiff, axis=-1)
|
|
349
|
+
|
|
350
|
+
min_pair = np.argmin(kdiff, axis=1)
|
|
351
|
+
is_duplicate = kdiff[np.arange(len(high_border)), min_pair
|
|
352
|
+
] < min_dist / 2
|
|
353
|
+
duplicates.update(dict(zip(
|
|
354
|
+
high_border[is_duplicate],
|
|
355
|
+
low_border[min_pair[is_duplicate]])))
|
|
356
|
+
self._build_periodic_mesh(duplicates)
|
|
357
|
+
|
|
358
|
+
def _get_min_border_distance(self, border):
|
|
359
|
+
"""
|
|
360
|
+
Find minimum intra-layer distance to set the threshold
|
|
361
|
+
for duplicate point detection.
|
|
362
|
+
"""
|
|
363
|
+
is_triangle_point_in_border = np.isin(self.kfaces, border)
|
|
364
|
+
border_triangles = self.kfaces[np.any(
|
|
365
|
+
is_triangle_point_in_border, axis=1)]
|
|
366
|
+
points = self.kpoints[border_triangles]
|
|
367
|
+
is_triangle_point_in_border = is_triangle_point_in_border[
|
|
368
|
+
np.any(is_triangle_point_in_border, axis=1)]
|
|
369
|
+
is_pair_intra_layer = np.logical_xor(
|
|
370
|
+
is_triangle_point_in_border,
|
|
371
|
+
np.roll(is_triangle_point_in_border, 1, axis=-1))
|
|
372
|
+
return np.min(np.linalg.norm(
|
|
373
|
+
(points - np.roll(points, 1, axis=1))[is_pair_intra_layer],
|
|
374
|
+
axis=-1))
|
|
375
|
+
|
|
376
|
+
def _build_periodic_mesh(self, duplicates):
|
|
377
|
+
"""
|
|
378
|
+
Build the periodic kpoints and kfaces arrays by removing
|
|
379
|
+
duplicate points and reindexing.
|
|
380
|
+
"""
|
|
381
|
+
unique_mask = np.full(len(self.kpoints), True)
|
|
382
|
+
unique_mask[list(duplicates.keys())] = False
|
|
383
|
+
self.kpoints_periodic = self.kpoints[unique_mask]
|
|
384
|
+
reindex_map = np.cumsum(unique_mask) - 1
|
|
385
|
+
self.kfaces_periodic = np.empty_like(self.kfaces)
|
|
386
|
+
for i, face in enumerate(self.kfaces):
|
|
387
|
+
for j, point in enumerate(face):
|
|
388
|
+
if point in duplicates:
|
|
389
|
+
reindex_map[point] = reindex_map[
|
|
390
|
+
duplicates[point]]
|
|
391
|
+
self.kfaces_periodic[i, j] = reindex_map[point]
|
|
392
|
+
|
|
393
|
+
def _apply_newton_correction(self, points):
|
|
394
|
+
residuals = self.energy_func(
|
|
395
|
+
points[:, 0], points[:, 1], points[:, 2]) - self.chemical_potential
|
|
396
|
+
gradients = np.column_stack(self.velocity_func(
|
|
397
|
+
points[:, 0], points[:, 1], points[:, 2])) / velocity_units
|
|
398
|
+
gradient_norms = np.linalg.norm(gradients, axis=-1)
|
|
399
|
+
return points - (residuals/gradient_norms**2)[:, None]*gradients
|