1
1
from __future__ import annotations
2
2
3
- from typing import TYPE_CHECKING , Any
3
+ from typing import TYPE_CHECKING , Any , Literal
4
4
5
5
import numpy as np
6
6
from pymatgen .io .vasp import VolumetricData
7
7
8
8
from crystal_toolkit .core .scene import Scene , Surface
9
9
10
10
if TYPE_CHECKING :
11
- from numpy .typing import ArrayLike
11
+ from numpy .typing import ArrayLike , NDArray
12
+ from pymatgen .core .structure import Lattice
12
13
13
14
_ANGS2_TO_BOHR3 = 1.88973 ** 3
14
15
15
16
16
17
def get_isosurface_scene (
17
- self ,
18
- data_key : str = "total" ,
19
- isolvl : float = 0.05 ,
18
+ data : NDArray ,
19
+ lattice : Lattice ,
20
+ isolvl : float | None = None ,
20
21
step_size : int = 4 ,
21
22
origin : ArrayLike | None = None ,
22
23
** kwargs : Any ,
23
24
) -> Scene :
24
25
"""Get the isosurface from a VolumetricData object.
25
26
26
27
Args:
27
- data_key (str, optional ): Use the volumetric data from self.data[data_key]. Defaults to 'total' .
28
- isolvl (float, optional ): The cutoff for the isosurface to using the same units as VESTA so
29
- e/bohr and kept grid size independent
28
+ data (NDArray ): The volumetric data array .
29
+ lattice (Lattice ): The lattice.
30
+ isolvl (float, optional): The cutoff to compute the isosurface
30
31
step_size (int, optional): step_size parameter for marching_cubes_lewiner. Defaults to 3.
31
32
origin (ArrayLike, optional): The origin of the isosurface. Defaults to None.
32
33
**kwargs: Passed to the Surface object.
@@ -36,42 +37,65 @@ def get_isosurface_scene(
36
37
"""
37
38
import skimage .measure
38
39
39
- origin = origin or list (
40
- - self .structure .lattice .get_cartesian_coords ([0.5 , 0.5 , 0.5 ])
41
- )
42
- vol_data = np .copy (self .data [data_key ])
43
- vol = self .structure .volume
44
- vol_data = vol_data / vol / _ANGS2_TO_BOHR3
45
-
46
- padded_data = np .pad (vol_data , (0 , 1 ), "wrap" )
47
- vertices , faces , normals , values = skimage .measure .marching_cubes (
48
- padded_data , level = isolvl , step_size = step_size , method = "lewiner"
49
- )
40
+ origin = origin or list (- lattice .get_cartesian_coords ([0.5 , 0.5 , 0.5 ]))
41
+ if isolvl is None :
42
+ # get the value such that 20% of the weight is enclosed
43
+ isolvl = np .percentile (data , 20 )
44
+
45
+ padded_data = np .pad (data , (0 , 1 ), "wrap" )
46
+ try :
47
+ vertices , faces , normals , values = skimage .measure .marching_cubes (
48
+ padded_data , level = isolvl , step_size = step_size , method = "lewiner"
49
+ )
50
+ except (ValueError , RuntimeError ) as err :
51
+ if "Surface level" in str (err ):
52
+ raise ValueError (
53
+ f"Isosurface level is not within data range. min: { data .min ()} , max: { data .max ()} "
54
+ ) from err
55
+ raise err
50
56
# transform to fractional coordinates
51
- vertices = vertices / (vol_data .shape [0 ], vol_data .shape [1 ], vol_data .shape [2 ])
52
- vertices = np .dot (vertices , self . structure . lattice .matrix ) # transform to Cartesian
57
+ vertices = vertices / (data .shape [0 ], data .shape [1 ], data .shape [2 ])
58
+ vertices = np .dot (vertices , lattice .matrix ) # transform to Cartesian
53
59
pos = [vert for triangle in vertices [faces ].tolist () for vert in triangle ]
54
60
return Scene (
55
61
"isosurface" , origin = origin , contents = [Surface (pos , show_edges = False , ** kwargs )]
56
62
)
57
63
58
64
59
- def get_volumetric_scene (self , data_key = "total" , isolvl = 0.02 , step_size = 3 , ** kwargs ):
65
+ def get_volumetric_scene (
66
+ self ,
67
+ data_key : str = "total" ,
68
+ isolvl : float | None = None ,
69
+ step_size : int = 3 ,
70
+ normalization : Literal ["vol" , "vesta" ] | None = "vol" ,
71
+ ** kwargs ,
72
+ ):
60
73
"""Get the Scene object which contains a structure and a isosurface components.
61
74
62
75
Args:
63
76
data_key (str, optional): Use the volumetric data from self.data[data_key]. Defaults to 'total'.
64
- isolvl (float, optional): The cutoff for the isosurface to using the same units as VESTA so e/bhor
65
- and kept grid size independent
77
+ isolvl (float, optional): The cutoff for the isosurface if none is provided we default to
78
+ a surface that encloses 20% of the weight.
66
79
step_size (int, optional): step_size parameter for marching_cubes_lewiner. Defaults to 3.
80
+ normalization (str, optional): Normalize the volumetric data by the volume of the unit cell.
81
+ Default is 'vol', which divides the data by the volume of the unit cell, this is required
82
+ for all VASP volumetric data formats. If normalization is 'vesta' we also change
83
+ the units from Angstroms to Bohr.
67
84
**kwargs: Passed to the Structure.get_scene() function.
68
85
69
86
Returns:
70
87
Scene: object containing the structure and isosurface components
71
88
"""
72
89
struct_scene = self .structure .get_scene (** kwargs )
73
- iso_scene = self .get_isosurface_scene (
74
- data_key = data_key ,
90
+ vol_data = self .data [data_key ]
91
+ if normalization in ("vol" , "vesta" ):
92
+ vol_data = vol_data / self .structure .volume
93
+ if normalization == "vesta" :
94
+ vol_data = vol_data / _ANGS2_TO_BOHR3
95
+
96
+ iso_scene = get_isosurface_scene (
97
+ data = vol_data ,
98
+ lattice = self .structure .lattice ,
75
99
isolvl = isolvl ,
76
100
step_size = step_size ,
77
101
origin = struct_scene .origin ,
@@ -81,5 +105,4 @@ def get_volumetric_scene(self, data_key="total", isolvl=0.02, step_size=3, **kwa
81
105
82
106
83
107
# todo: re-think origin, shift globally at end (scene.origin)
84
- VolumetricData .get_isosurface_scene = get_isosurface_scene
85
108
VolumetricData .get_scene = get_volumetric_scene
0 commit comments