fsvisual 0.0.1__py3-none-any.whl
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.
- fsvisual/__init__.py +0 -0
- fsvisual/brillouin_zone.py +35 -0
- fsvisual/cli.py +68 -0
- fsvisual/fermisurface.py +251 -0
- fsvisual/input.py +52 -0
- fsvisual/mesh.py +134 -0
- fsvisual/visualisation.py +126 -0
- fsvisual-0.0.1.dist-info/METADATA +211 -0
- fsvisual-0.0.1.dist-info/RECORD +13 -0
- fsvisual-0.0.1.dist-info/WHEEL +5 -0
- fsvisual-0.0.1.dist-info/entry_points.txt +2 -0
- fsvisual-0.0.1.dist-info/licenses/LICENSE +674 -0
- fsvisual-0.0.1.dist-info/top_level.txt +1 -0
fsvisual/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from pymatgen.core import Lattice
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def first_bz(rez_lattice):
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
calculates the edge points of a Brillouin zone for any given reciprocal lattice.
|
|
8
|
+
|
|
9
|
+
:param rez_lattice: coordinates for the reciprocal lattice vectors (list of three 3 Dimensional coordinates)
|
|
10
|
+
:return: 1. Brillouin zone as a list containing x, y, z coordinates on separate lists for each
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
new_lattice = Lattice(rez_lattice)
|
|
14
|
+
brillouin_zone = new_lattice.get_wigner_seitz_cell()
|
|
15
|
+
|
|
16
|
+
# Prepare data for Plotly
|
|
17
|
+
x, y, z = [], [], []
|
|
18
|
+
for facet in brillouin_zone:
|
|
19
|
+
# brillouin zone consists of many facets -> facets consists of vertices (edge points -> koordinates)
|
|
20
|
+
# Ensure each facet is closed by adding the first vertex at the end
|
|
21
|
+
facet.append(facet[0])
|
|
22
|
+
for vertex in facet:
|
|
23
|
+
x.append(vertex[0])
|
|
24
|
+
y.append(vertex[1])
|
|
25
|
+
z.append(vertex[2])
|
|
26
|
+
# Add None to create a break in the plot lines between facets
|
|
27
|
+
x.append(None)
|
|
28
|
+
y.append(None)
|
|
29
|
+
z.append(None)
|
|
30
|
+
brillouin_zone_xyz = [x, y, z]
|
|
31
|
+
return brillouin_zone_xyz, brillouin_zone
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
fsvisual/cli.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from .fermisurface import FermiSurface
|
|
2
|
+
import os
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
parser = argparse.ArgumentParser()
|
|
7
|
+
parser.add_argument( "bxsf_files_directory", help="directory (folder) where the .bxsf files (Fermi"
|
|
8
|
+
"surface files) are stored")
|
|
9
|
+
|
|
10
|
+
# optional arguments
|
|
11
|
+
|
|
12
|
+
parser.add_argument("-sf","--save_fermisurfaces", help="directory where visualized Fermi surfaces "
|
|
13
|
+
"are stored")
|
|
14
|
+
|
|
15
|
+
parser.add_argument("-s","--subdivision_surface", help="divides every triangle of the Fermi surface mesh"
|
|
16
|
+
" into two triangles; executes as many times as the input says",
|
|
17
|
+
default=0, type=int)
|
|
18
|
+
|
|
19
|
+
parser.add_argument("-dp","--downsampling_surface_percentage", help="lowers the resolution of the "
|
|
20
|
+
"Fermi surface mesh (number of faces) to a given percentage "
|
|
21
|
+
"(from original face count)",default=100, type=int)
|
|
22
|
+
|
|
23
|
+
parser.add_argument("-df","--downsampling_surface_face", help="lowers the resolution of the "
|
|
24
|
+
"Fermi surface mesh (number of faces) to a given face number",
|
|
25
|
+
default=None, type=int)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
parser.add_argument("-c","--create_SVG", help="boolean whether to create SVG files ",
|
|
29
|
+
action="store_true")
|
|
30
|
+
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
|
|
33
|
+
# you can either parse a whole directory of .bxsf files or just the path to a single .bxsf file
|
|
34
|
+
file_list = []
|
|
35
|
+
if os.path.isfile(args.bxsf_files_directory):
|
|
36
|
+
file_list.append(os.path.basename(args.bxsf_files_directory))
|
|
37
|
+
path = os.path.abspath(os.path.dirname(args.bxsf_files_directory))
|
|
38
|
+
if args.save_fermisurfaces is not None:
|
|
39
|
+
save_path = args.save_fermisurfaces
|
|
40
|
+
else:
|
|
41
|
+
save_path = path
|
|
42
|
+
else:
|
|
43
|
+
file_list.extend(file_ for file_ in os.listdir(args.bxsf_files_directory) if file_.endswith('.bxsf'))
|
|
44
|
+
path = args.bxsf_files_directory
|
|
45
|
+
if args.save_fermisurfaces is not None:
|
|
46
|
+
save_path = args.save_fermisurfaces
|
|
47
|
+
else:
|
|
48
|
+
save_path = args.bxsf_files_directory
|
|
49
|
+
|
|
50
|
+
for filename in file_list:
|
|
51
|
+
filepath = os.path.join(path, filename)
|
|
52
|
+
# check if path leads to file
|
|
53
|
+
if not os.path.isfile(filepath):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
new_fermisurface = FermiSurface()
|
|
57
|
+
|
|
58
|
+
if args.subdivision_surface != 0 and args.downsampling_surface != 100:
|
|
59
|
+
raise ValueError("subdivision_surface and downsampling_surface are contrary functions")
|
|
60
|
+
|
|
61
|
+
new_fermisurface.build_surface_with_bxsf_files(filepath, args.subdivision_surface,
|
|
62
|
+
args.downsampling_surface_percentage,
|
|
63
|
+
args.downsampling_surface_face)
|
|
64
|
+
|
|
65
|
+
new_fermisurface.visualization(filepath, save_path, svg=args.create_SVG)
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
main()
|
fsvisual/fermisurface.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from .input import read_energy_numbers
|
|
2
|
+
from .brillouin_zone import first_bz
|
|
3
|
+
from .visualisation import build_plotly_figure, write_figure_to_file
|
|
4
|
+
from .mesh import create_cartesian_mesh, face_center_BZ
|
|
5
|
+
from skimage import measure
|
|
6
|
+
import numpy as np
|
|
7
|
+
import trimesh
|
|
8
|
+
import pymeshlab
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FermiSurface:
|
|
12
|
+
"""
|
|
13
|
+
Class for computing three-dimensional, interactive Fermi surfaces from .bxsf files, a filetype established by the
|
|
14
|
+
visualization software XCrsSDen. A Fermi surface is an object in k-space, that separates the occupied from the
|
|
15
|
+
unoccupied states (at the Fermi energy). Fermi surfaces are often shown within the first brillouin zone.
|
|
16
|
+
|
|
17
|
+
usage: If the Fermi surface data is present as a .bxsf file, which is widely adopted as an output for Fermi
|
|
18
|
+
surface calculations e.g. by Wannier90 or exciting, it is sufficient to just create an object of the FermiSurface
|
|
19
|
+
class and call the build_surface_with_bxsf_files method for building a Fermi surface. Afterwards the visualization
|
|
20
|
+
method can be called to create 3D interactive plots of the Fermi surface.
|
|
21
|
+
|
|
22
|
+
For information concerning the .bxsf file format, and also if your fermi surface data is stored in a different format,
|
|
23
|
+
please consult the documentation of FSvisual.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.energy_values = None
|
|
28
|
+
self.fermi_energy = None
|
|
29
|
+
self.rez_base_vect = None
|
|
30
|
+
self.grid_size = None
|
|
31
|
+
self.brillouin_zone = None
|
|
32
|
+
self.surface = None
|
|
33
|
+
self.fermi_surface_list = None
|
|
34
|
+
self.band_index = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def cartesian_mesh(self):
|
|
39
|
+
return create_cartesian_mesh(self.grid_size)
|
|
40
|
+
|
|
41
|
+
def set_energy_values(self, energy_values):
|
|
42
|
+
self.energy_values = energy_values
|
|
43
|
+
|
|
44
|
+
def set_fermi_energy(self, fermi_energy):
|
|
45
|
+
self.fermi_energy = fermi_energy
|
|
46
|
+
|
|
47
|
+
def set_rez_base_vect(self, rez_base_vect):
|
|
48
|
+
self.rez_base_vect = rez_base_vect
|
|
49
|
+
|
|
50
|
+
def set_k_grid_by_size(self, grid_size):
|
|
51
|
+
self.grid_size = grid_size
|
|
52
|
+
|
|
53
|
+
def compute_brillouin_zone(self):
|
|
54
|
+
self.brillouin_zone = first_bz(self.rez_base_vect)
|
|
55
|
+
|
|
56
|
+
def marching_cubes(self, energyColumn):
|
|
57
|
+
grid_size = self.grid_size
|
|
58
|
+
new_basevect_grid_size = np.array([grid_size[0] * 2 - 1, grid_size[1] * 2 - 1, grid_size[2] * 2 - 1])
|
|
59
|
+
|
|
60
|
+
# creates an array with energies taken by the corresponding indices of new_cart_mesh_helper -> created array is
|
|
61
|
+
# as big as the indexing array
|
|
62
|
+
new_cart_mesh_helper = self.energy_values[energyColumn][self.cartesian_mesh.astype(int)]
|
|
63
|
+
new_cart_mesh_helper = np.array(new_cart_mesh_helper).reshape(
|
|
64
|
+
(new_basevect_grid_size[0], new_basevect_grid_size[1],
|
|
65
|
+
new_basevect_grid_size[2]))
|
|
66
|
+
|
|
67
|
+
# Apply the Marching Cubes algorithm
|
|
68
|
+
vertices, faces, normals, values = measure.marching_cubes(new_cart_mesh_helper, level=self.fermi_energy)
|
|
69
|
+
|
|
70
|
+
# coordinate transformation
|
|
71
|
+
new_basevect_mesh = np.dot(vertices, np.array(self.rez_base_vect))
|
|
72
|
+
ms = pymeshlab.MeshSet()
|
|
73
|
+
ms.add_mesh(pymeshlab.Mesh(new_basevect_mesh, faces))
|
|
74
|
+
new_mesh = ms.current_mesh()
|
|
75
|
+
|
|
76
|
+
self.surface = trimesh.Trimesh(vertices=np.asarray(new_mesh.vertex_matrix()),
|
|
77
|
+
faces=np.asarray(new_mesh.face_matrix()), process=False)
|
|
78
|
+
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def scale_surface(self, scale_factor):
|
|
82
|
+
"""
|
|
83
|
+
Scales the Fermi surface according to the given `scale_factor`.
|
|
84
|
+
:param scale_factor: factor to scale the Fermi surface by
|
|
85
|
+
:return: self
|
|
86
|
+
"""
|
|
87
|
+
if self.surface is None:
|
|
88
|
+
raise ValueError("surface is not yet defined")
|
|
89
|
+
# Create a scaling matrix
|
|
90
|
+
scaling_matrix = np.eye(4)
|
|
91
|
+
scaling_matrix[:3, :3] *= scale_factor
|
|
92
|
+
|
|
93
|
+
# Apply the scaling transformation to the mesh
|
|
94
|
+
self.surface.apply_transform(scaling_matrix)
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def center_surface(self):
|
|
98
|
+
"""
|
|
99
|
+
centers the Fermi surface to the origin
|
|
100
|
+
:return: centered Fermi surface as Trimesh object
|
|
101
|
+
"""
|
|
102
|
+
if self.surface is None:
|
|
103
|
+
raise ValueError("surface is not yet defined")
|
|
104
|
+
|
|
105
|
+
self.surface.apply_translation([-self.surface.centroid[i] for i in range(3)])
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def slice_surface(self):
|
|
109
|
+
"""
|
|
110
|
+
Slices parts of the surface that extend beyond the brillouin zone
|
|
111
|
+
Note: Fermi surface needs to be centered and scaled according to the brillouin zone
|
|
112
|
+
:return: Sliced Fermi surface
|
|
113
|
+
"""
|
|
114
|
+
if self.brillouin_zone is None:
|
|
115
|
+
raise ValueError("Brillouin Zone is not yet defined")
|
|
116
|
+
if self.surface is None:
|
|
117
|
+
raise ValueError("surface is not yet defined")
|
|
118
|
+
|
|
119
|
+
facet_centers = face_center_BZ(self.brillouin_zone[1])
|
|
120
|
+
|
|
121
|
+
# cutting off the surface area outside the 1. BZ
|
|
122
|
+
for i in range(len(self.brillouin_zone[1])):
|
|
123
|
+
facets_normal = np.array(facet_centers[i]) + 1 / 2 * np.array(facet_centers[i])
|
|
124
|
+
|
|
125
|
+
self.surface = self.surface.slice_plane(plane_origin=self.brillouin_zone[1][i][0],
|
|
126
|
+
plane_normal=facets_normal * (-1))
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
def subdivide_surface(self, iterations):
|
|
130
|
+
"""
|
|
131
|
+
divides each triangle of the parsed triangle mesh in to two triangles and therefore
|
|
132
|
+
providing a higher resolution
|
|
133
|
+
:param iterations: how many times this algorithm is applied
|
|
134
|
+
:return: the higher resolution triangle mesh
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
if self.surface is None:
|
|
138
|
+
raise ValueError("surface is not yet defined")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
vertices = self.surface.vertices
|
|
142
|
+
faces = self.surface.faces
|
|
143
|
+
|
|
144
|
+
ms = pymeshlab.MeshSet()
|
|
145
|
+
|
|
146
|
+
# Add it to the MeshSet
|
|
147
|
+
ms.add_mesh(pymeshlab.Mesh(vertex_matrix=vertices, face_matrix=faces))
|
|
148
|
+
ms.meshing_surface_subdivision_loop(iterations=iterations, threshold=pymeshlab.PercentageValue(0))
|
|
149
|
+
|
|
150
|
+
smoothed_mesh = ms.current_mesh()
|
|
151
|
+
self.surface = trimesh.Trimesh(vertices=np.asarray(smoothed_mesh.vertex_matrix()),
|
|
152
|
+
faces=np.asarray(smoothed_mesh.face_matrix()), process=False)
|
|
153
|
+
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
def downsample_surface(self, face_percentage, face_numbers):
|
|
157
|
+
"""
|
|
158
|
+
lowers the resolution of the Fermi surface mesh (number of faces) to a given percentage
|
|
159
|
+
(from original face count)
|
|
160
|
+
:param face_percentage: targeted face percentage
|
|
161
|
+
:return: self
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
if self.surface is None:
|
|
165
|
+
raise ValueError("surface is not yet defined")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if face_percentage == 100 and face_numbers is None:
|
|
169
|
+
return self
|
|
170
|
+
vertices = self.surface.vertices
|
|
171
|
+
faces = self.surface.faces
|
|
172
|
+
|
|
173
|
+
ms = pymeshlab.MeshSet()
|
|
174
|
+
# Add it to the MeshSet
|
|
175
|
+
ms.add_mesh(pymeshlab.Mesh(vertex_matrix=vertices, face_matrix=faces))
|
|
176
|
+
|
|
177
|
+
if face_numbers is not None and face_percentage != 100:
|
|
178
|
+
raise ValueError("You can only either provide a face_percentage or face_numbers, not both")
|
|
179
|
+
elif face_numbers is not None:
|
|
180
|
+
numFaces = face_numbers
|
|
181
|
+
else:
|
|
182
|
+
facenum = len(faces) * face_percentage / 100
|
|
183
|
+
numFaces = int(facenum)
|
|
184
|
+
|
|
185
|
+
ms.meshing_decimation_quadric_edge_collapse(targetfacenum=numFaces)
|
|
186
|
+
|
|
187
|
+
smoothed_mesh = ms.current_mesh()
|
|
188
|
+
self.surface = trimesh.Trimesh(vertices=np.asarray(smoothed_mesh.vertex_matrix()),
|
|
189
|
+
faces=np.asarray(smoothed_mesh.face_matrix()), process=False)
|
|
190
|
+
|
|
191
|
+
return self
|
|
192
|
+
|
|
193
|
+
def build_surface_with_bxsf_files(self, filepath, subdivide_iterations=0, down_sampling_percentage=100,
|
|
194
|
+
downsampling_surface_face=None):
|
|
195
|
+
"""
|
|
196
|
+
whole fermi surface construction process for bxsf files, including reading out the input data, building the
|
|
197
|
+
first brillouin zone and applying the marching cubes' algorithm.
|
|
198
|
+
:param filepath: path to bxsf file.
|
|
199
|
+
:param subdivide_iterations: number of iterations to subdivide the Fermi surface by subdivide_surface
|
|
200
|
+
:param down_sampling_percentage: percentage to which the downsampling_surface method reduces the number of faces
|
|
201
|
+
:param downsampling_surface_face: number of faces to which the downsampling_surface method reduces the mesh
|
|
202
|
+
:return: self
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
data = read_energy_numbers(filepath)
|
|
206
|
+
self.set_energy_values(data[0])
|
|
207
|
+
self.set_fermi_energy(data[1])
|
|
208
|
+
self.set_rez_base_vect(data[2])
|
|
209
|
+
self.set_k_grid_by_size(data[3])
|
|
210
|
+
|
|
211
|
+
self.compute_brillouin_zone()
|
|
212
|
+
|
|
213
|
+
grid_size = self.grid_size
|
|
214
|
+
new_basevect_grid_size = np.array([grid_size[0] * 2 - 1, grid_size[1] * 2 - 1, grid_size[2] * 2 - 1])
|
|
215
|
+
|
|
216
|
+
self.band_index = []
|
|
217
|
+
self.fermi_surface_list = []
|
|
218
|
+
for index, columnName in enumerate(self.energy_values.columns):
|
|
219
|
+
|
|
220
|
+
# Apply the Marching Cubes algorithm
|
|
221
|
+
try:
|
|
222
|
+
self.marching_cubes(columnName)
|
|
223
|
+
except ValueError:
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
self.subdivide_surface(subdivide_iterations)
|
|
228
|
+
self.downsample_surface(face_percentage=down_sampling_percentage,face_numbers=downsampling_surface_face)
|
|
229
|
+
|
|
230
|
+
# translation and shrinkage
|
|
231
|
+
self.scale_surface(2 / new_basevect_grid_size)
|
|
232
|
+
|
|
233
|
+
# translation
|
|
234
|
+
self.center_surface()
|
|
235
|
+
|
|
236
|
+
self.slice_surface()
|
|
237
|
+
|
|
238
|
+
self.fermi_surface_list.append(self.surface)
|
|
239
|
+
self.band_index.append(index + 1) # for the plot
|
|
240
|
+
return self
|
|
241
|
+
|
|
242
|
+
def visualization(self, filepath, save_fermisurf_path, svg=False):
|
|
243
|
+
"""
|
|
244
|
+
Visualizes the Fermi surface as a 3D interactive plot saved as an html file. Also allows for creating
|
|
245
|
+
an SVG Image of the Fermi surface along the html file.
|
|
246
|
+
:param filepath: path to bxsf file.
|
|
247
|
+
:param save_fermisurf_path: directory where the created files and imaged will be stored
|
|
248
|
+
:param svg: boolean whether to create the SVG image
|
|
249
|
+
"""
|
|
250
|
+
figure = build_plotly_figure(self.fermi_surface_list, self.brillouin_zone, self.band_index)
|
|
251
|
+
write_figure_to_file(figure, filepath, save_fermisurf_path, create_SVG=svg)
|
fsvisual/input.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# Define a function to process the text file
|
|
5
|
+
def read_energy_numbers(filepath):
|
|
6
|
+
"""
|
|
7
|
+
function that extracts all necessary data out of the FERMISURF.bxsf file
|
|
8
|
+
:param filepath: path of the FERMISURF.bxsf file
|
|
9
|
+
:return: energy DataFrame, fermi energy, reciprocal base vectors, grid size
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
with open(filepath, 'r') as file:
|
|
13
|
+
lines = file.readlines()
|
|
14
|
+
|
|
15
|
+
# Filter out the lines containing energy numbers
|
|
16
|
+
|
|
17
|
+
values_in_series = []
|
|
18
|
+
my_dict = {}
|
|
19
|
+
fermi_energy = 0
|
|
20
|
+
rez_base_vect = []
|
|
21
|
+
grid_size = []
|
|
22
|
+
j = 0
|
|
23
|
+
for i, line in enumerate(lines):
|
|
24
|
+
if i == 3: # fermi_energy
|
|
25
|
+
fermi_energy = float(line[19:31])
|
|
26
|
+
if i == 9: # grid_size
|
|
27
|
+
grid = line.split(" ")
|
|
28
|
+
grid = [int(num) for num in grid if num != "" and num != "\n"]
|
|
29
|
+
grid_size.append(grid)
|
|
30
|
+
|
|
31
|
+
if 10 < i < 14: # base vectors
|
|
32
|
+
vect = [line.split(" ")]
|
|
33
|
+
vect[0] = [float(num) for num in vect[0] if num != "" and num != "\n"]
|
|
34
|
+
rez_base_vect.extend(vect)
|
|
35
|
+
if i >= 13: # algorythm that collects all the energies and stores them into separate bands
|
|
36
|
+
if line.startswith(" BAND"):
|
|
37
|
+
if j == 0:
|
|
38
|
+
pass
|
|
39
|
+
else:
|
|
40
|
+
my_dict[f"Band {j}"] = values_in_series
|
|
41
|
+
values_in_series = []
|
|
42
|
+
j += 1
|
|
43
|
+
try:
|
|
44
|
+
energy = float(line.strip())
|
|
45
|
+
values_in_series.append(energy)
|
|
46
|
+
except ValueError:
|
|
47
|
+
pass # Ignore lines that cannot be converted to float
|
|
48
|
+
|
|
49
|
+
i += 1
|
|
50
|
+
my_dict[f"Band {j}"] = values_in_series
|
|
51
|
+
df = pd.DataFrame(my_dict)
|
|
52
|
+
return df, fermi_energy, rez_base_vect, grid_size[0]
|
fsvisual/mesh.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def create_cartesian_mesh(grid_size):
|
|
5
|
+
"""
|
|
6
|
+
creates a mesh within the reciprocal unit cell for any reciprocal lattice
|
|
7
|
+
:param grid_size: number of datapoints the grid should have (list of 3 number for each lattice vector)
|
|
8
|
+
:return: standard mesh
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
grid_size = [int(grid_size_n) for grid_size_n in grid_size]
|
|
12
|
+
Imin = [0] * 3
|
|
13
|
+
Imax = [0] * 3
|
|
14
|
+
|
|
15
|
+
# loop that creates the mesh
|
|
16
|
+
|
|
17
|
+
band_grid = np.arange((grid_size[0]) * (grid_size[1]) * (grid_size[2]))
|
|
18
|
+
band_grid = band_grid.reshape((grid_size[0], grid_size[1], grid_size[2]))
|
|
19
|
+
|
|
20
|
+
grid_size_minus_one = [grid - 1 for grid in grid_size]
|
|
21
|
+
|
|
22
|
+
for k in range(3):
|
|
23
|
+
Imin[k] = -int(grid_size_minus_one[k])
|
|
24
|
+
Imax[k] = int(grid_size_minus_one[k])
|
|
25
|
+
|
|
26
|
+
# creating the mols
|
|
27
|
+
mesh_axis_one = np.arange(Imin[0], Imax[0] + 1)
|
|
28
|
+
mesh_axis_two = np.arange(Imin[1], Imax[1] + 1)
|
|
29
|
+
mesh_axis_three = np.arange(Imin[2], Imax[2] + 1)
|
|
30
|
+
|
|
31
|
+
mesh_axis_one = np.where(mesh_axis_one >= 0, mesh_axis_one, mesh_axis_one + grid_size_minus_one[0])
|
|
32
|
+
mesh_axis_two = np.where(mesh_axis_two >= 0, mesh_axis_two, mesh_axis_two + grid_size_minus_one[1])
|
|
33
|
+
mesh_axis_three = np.where(mesh_axis_three >= 0, mesh_axis_three, mesh_axis_three + grid_size_minus_one[2])
|
|
34
|
+
|
|
35
|
+
index_mesh = np.ix_(mesh_axis_one, mesh_axis_two, mesh_axis_three)
|
|
36
|
+
|
|
37
|
+
cartesian_mesh = band_grid[index_mesh].flatten()
|
|
38
|
+
|
|
39
|
+
return cartesian_mesh
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def triangulate_faces(facets):
|
|
43
|
+
"""
|
|
44
|
+
splits every facet of the 1. BZ into triangles for Trimesh to read -> function is used by brillouin_intersect_mesh()
|
|
45
|
+
:param facets: list of all facets with vertices counted from 0 to last vertex
|
|
46
|
+
:return:
|
|
47
|
+
"""
|
|
48
|
+
triangles = []
|
|
49
|
+
for facet in facets:
|
|
50
|
+
if len(facet) == 3:
|
|
51
|
+
triangles.append(facet)
|
|
52
|
+
elif len(facet) == 4:
|
|
53
|
+
triangles.append([facet[0], facet[1], facet[2]])
|
|
54
|
+
triangles.append([facet[0], facet[2], facet[3]])
|
|
55
|
+
else:
|
|
56
|
+
# For facets with more than 4 vertices, use a fan triangulation
|
|
57
|
+
for i in range(1, len(facet) - 1):
|
|
58
|
+
triangles.append([facet[0], facet[i], facet[i + 1]])
|
|
59
|
+
return triangles
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def abs_vect(vector):
|
|
63
|
+
"""
|
|
64
|
+
:param vector: takes 3D vector as a list or array
|
|
65
|
+
:return: absolute value of that vector
|
|
66
|
+
"""
|
|
67
|
+
return np.sqrt(vector[0] ** 2 + vector[1] ** 2 + vector[2] ** 2)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def triangle_area(triangle):
|
|
71
|
+
"""
|
|
72
|
+
calculates the area of a triangle via vertices
|
|
73
|
+
:param triangle: list of 3 3D vertices
|
|
74
|
+
:return: area of a triangle in FE
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
# from coordinates in space
|
|
78
|
+
|
|
79
|
+
P1 = np.array(triangle[0])
|
|
80
|
+
P2 = np.array(triangle[1])
|
|
81
|
+
P3 = np.array(triangle[2])
|
|
82
|
+
|
|
83
|
+
area = 1 / 2 * np.sqrt(abs_vect(P2 - P1) ** 2 * abs_vect(P3 - P1) ** 2 - np.dot((P2 - P1), (P3 - P1)) ** 2)
|
|
84
|
+
return area
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def triangle_center(triangle):
|
|
88
|
+
"""
|
|
89
|
+
calculates the center of every triangle
|
|
90
|
+
:param triangle: list of 3 vertices
|
|
91
|
+
:return: center point of a triangle
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
xS = 1 / 3 * (triangle[0][0] + triangle[1][0] + triangle[2][0])
|
|
95
|
+
yS = 1 / 3 * (triangle[0][1] + triangle[1][1] + triangle[2][1])
|
|
96
|
+
zS = 1 / 3 * (triangle[0][2] + triangle[1][2] + triangle[2][2])
|
|
97
|
+
|
|
98
|
+
return [xS, yS, zS]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def face_center_BZ(brillouin_zone_facets):
|
|
102
|
+
"""
|
|
103
|
+
function to calculate the center of every facet
|
|
104
|
+
:param brillouin_zone_facets: list with all facets (filled with points, not indices)
|
|
105
|
+
:return: list of 3D center points for every facet
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
# triangulate the facets:
|
|
109
|
+
triangle_list = triangulate_faces(brillouin_zone_facets)
|
|
110
|
+
|
|
111
|
+
face_centers = []
|
|
112
|
+
j = 0
|
|
113
|
+
for facet in brillouin_zone_facets: # goes through every facet and calculates the center of it
|
|
114
|
+
triangle_areas = []
|
|
115
|
+
triangle_centers = []
|
|
116
|
+
for triangle in triangle_list[j:j + len(facet) - 2]: # goes through every triangle of each facet
|
|
117
|
+
# triangle area:
|
|
118
|
+
if triangle_area(triangle) != 0:
|
|
119
|
+
triangle_areas.append(triangle_area(triangle)) # triangle area
|
|
120
|
+
triangle_centers.append(triangle_center(triangle)) # triangle center
|
|
121
|
+
j += len(facet) - 2
|
|
122
|
+
|
|
123
|
+
x_coord = np.array([point[0] for point in triangle_centers])
|
|
124
|
+
y_coord = np.array([point[1] for point in triangle_centers])
|
|
125
|
+
z_coord = np.array([point[2] for point in triangle_centers])
|
|
126
|
+
|
|
127
|
+
# calculation of the facet center (geometrischer Schwerpunkt)
|
|
128
|
+
xS = np.sum(x_coord * np.array(triangle_areas)) / np.sum(triangle_areas)
|
|
129
|
+
yS = np.sum(y_coord * np.array(triangle_areas)) / np.sum(triangle_areas)
|
|
130
|
+
zS = np.sum(z_coord * np.array(triangle_areas)) / np.sum(triangle_areas)
|
|
131
|
+
|
|
132
|
+
face_centers.append([xS, yS, zS])
|
|
133
|
+
|
|
134
|
+
return face_centers
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import plotly.graph_objects as go
|
|
3
|
+
import plotly.io as pio
|
|
4
|
+
import os
|
|
5
|
+
import kaleido
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_plotly_figure(fermi_surface_list, brillouin_zone_object, band_index):
|
|
9
|
+
"""
|
|
10
|
+
to build the Fermi surface as a 3D interactive plotly figure
|
|
11
|
+
:param fermi_surface_list: list of all surface parts of the Fermi surface
|
|
12
|
+
:param brillouin_zone_object: the brillouin zone object created with the compute_brillouin_zone from FSvisual
|
|
13
|
+
:param band_index: list of band indices corresponding to each surface part
|
|
14
|
+
:return: the plotly figure
|
|
15
|
+
"""
|
|
16
|
+
mesh_fermi_surfaces = []
|
|
17
|
+
for index, fermi_surface in enumerate(fermi_surface_list):
|
|
18
|
+
x_mesh, y_mesh, z_mesh = fermi_surface.vertices[:, 0], fermi_surface.vertices[:, 1], fermi_surface.vertices[:,
|
|
19
|
+
2]
|
|
20
|
+
|
|
21
|
+
# Extract I, J, K indices of faces
|
|
22
|
+
i, j, k = fermi_surface.faces[:, 0], fermi_surface.faces[:, 1], fermi_surface.faces[:, 2]
|
|
23
|
+
|
|
24
|
+
mesh_fermi_surfaces.append(go.Mesh3d(
|
|
25
|
+
x=np.array(x_mesh),
|
|
26
|
+
y=np.array(y_mesh),
|
|
27
|
+
z=np.array(z_mesh),
|
|
28
|
+
i=np.array(i),
|
|
29
|
+
j=np.array(j),
|
|
30
|
+
k=np.array(k),
|
|
31
|
+
name=f"Band {band_index[index]}",
|
|
32
|
+
opacity=1,
|
|
33
|
+
showlegend=True
|
|
34
|
+
))
|
|
35
|
+
|
|
36
|
+
x = brillouin_zone_object[0][0]
|
|
37
|
+
y = brillouin_zone_object[0][1]
|
|
38
|
+
z = brillouin_zone_object[0][2]
|
|
39
|
+
|
|
40
|
+
# find highest and lowest point of BZ for axis scaling:
|
|
41
|
+
|
|
42
|
+
# filter non values and find maximum on each axis
|
|
43
|
+
max_x = np.max([np.abs(num) for num in x if num is not None])
|
|
44
|
+
max_y = np.max([np.abs(num) for num in y if num is not None])
|
|
45
|
+
max_z = np.max([np.abs(num) for num in z if num is not None])
|
|
46
|
+
|
|
47
|
+
# calculate global maximum
|
|
48
|
+
max_value_axis = np.max([max_x, max_y, max_z])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if max_value_axis > 0:
|
|
52
|
+
max_value_axis += 1/10*max_value_axis
|
|
53
|
+
else:
|
|
54
|
+
max_value_axis -= 1/10*max_value_axis
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Create a 3D scatter plot
|
|
58
|
+
scatter_BZ = go.Scatter3d(
|
|
59
|
+
x=x,
|
|
60
|
+
y=y,
|
|
61
|
+
z=z,
|
|
62
|
+
mode='lines',
|
|
63
|
+
name="1. BZ",
|
|
64
|
+
line=dict(color='black', width=6)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# contains all
|
|
68
|
+
fig_data = [scatter_BZ]
|
|
69
|
+
fig_data.extend(mesh_fermi_surfaces)
|
|
70
|
+
|
|
71
|
+
fig = go.Figure(data=fig_data)
|
|
72
|
+
|
|
73
|
+
fig.update_layout(
|
|
74
|
+
|
|
75
|
+
scene=dict(
|
|
76
|
+
xaxis=dict(
|
|
77
|
+
range=[-max_value_axis, max_value_axis], # Fester Bereich für x-Achse
|
|
78
|
+
visible=False
|
|
79
|
+
),
|
|
80
|
+
yaxis=dict(
|
|
81
|
+
range=[-max_value_axis, max_value_axis], # Fester Bereich für y-Achse
|
|
82
|
+
visible=False
|
|
83
|
+
),
|
|
84
|
+
zaxis=dict(
|
|
85
|
+
range=[-max_value_axis, max_value_axis], # Fester Bereich für z-Achse
|
|
86
|
+
visible=False
|
|
87
|
+
),
|
|
88
|
+
annotations=[], # Remove any annotations if present
|
|
89
|
+
aspectmode='cube',
|
|
90
|
+
camera=dict(
|
|
91
|
+
projection=dict(
|
|
92
|
+
type='orthographic'
|
|
93
|
+
# to change the perspective (so that lines don't distort over distance)
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
return fig
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def write_figure_to_file(fig, filepath, save_figure_directory, create_SVG=True,
|
|
102
|
+
scene_camera_SVG=None):
|
|
103
|
+
"""
|
|
104
|
+
Function to take a plotly figure and write it to a file with potentially also creating an SVG file.
|
|
105
|
+
:param fig: plotly figure
|
|
106
|
+
:param filepath: path to .bxsf file/files
|
|
107
|
+
:param save_figure_directory: directory where to save the figure
|
|
108
|
+
:param create_SVG: boolean whether to create SVG file
|
|
109
|
+
:param scene_camera_SVG: alter the cameras position and angle with dictionary (scene_camera parameter in plotly)
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
filename = os.path.basename(filepath)
|
|
113
|
+
filename = filename.split(".")[0]
|
|
114
|
+
pio.write_html(fig, file=f'{save_figure_directory}/{filename}.html', auto_open=False,
|
|
115
|
+
config={'displayModeBar': False})
|
|
116
|
+
|
|
117
|
+
if create_SVG:
|
|
118
|
+
kaleido.get_chrome_sync()
|
|
119
|
+
if scene_camera_SVG is None:
|
|
120
|
+
scene_camera_SVG = dict(eye=dict(x=1, y=1, z=1))
|
|
121
|
+
fig.update_layout(
|
|
122
|
+
showlegend=False,
|
|
123
|
+
scene_camera=scene_camera_SVG, # change camera scene
|
|
124
|
+
margin=dict(l=0, r=0, b=0, t=0) # set the space on the edges to 0 (so that the plot fills out the image)
|
|
125
|
+
)
|
|
126
|
+
fig.write_image(f'{save_figure_directory}/{filename}.svg', format="svg", width=500, height=800)
|