11from __future__ import annotations
22
3- from typing import TYPE_CHECKING , Any
3+ from typing import TYPE_CHECKING , Any , Literal
44
55import numpy as np
66from pymatgen .io .vasp import VolumetricData
77
88from crystal_toolkit .core .scene import Scene , Surface
99
1010if TYPE_CHECKING :
11- from numpy .typing import ArrayLike
11+ from numpy .typing import ArrayLike , NDArray
12+ from pymatgen .core .structure import Lattice
1213
1314_ANGS2_TO_BOHR3 = 1.88973 ** 3
1415
1516
1617def 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 ,
2021 step_size : int = 4 ,
2122 origin : ArrayLike | None = None ,
2223 ** kwargs : Any ,
2324) -> Scene :
2425 """Get the isosurface from a VolumetricData object.
2526
2627 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
3031 step_size (int, optional): step_size parameter for marching_cubes_lewiner. Defaults to 3.
3132 origin (ArrayLike, optional): The origin of the isosurface. Defaults to None.
3233 **kwargs: Passed to the Surface object.
@@ -36,42 +37,65 @@ def get_isosurface_scene(
3637 """
3738 import skimage .measure
3839
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
5056 # 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
5359 pos = [vert for triangle in vertices [faces ].tolist () for vert in triangle ]
5460 return Scene (
5561 "isosurface" , origin = origin , contents = [Surface (pos , show_edges = False , ** kwargs )]
5662 )
5763
5864
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+ ):
6073 """Get the Scene object which contains a structure and a isosurface components.
6174
6275 Args:
6376 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.
6679 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.
6784 **kwargs: Passed to the Structure.get_scene() function.
6885
6986 Returns:
7087 Scene: object containing the structure and isosurface components
7188 """
7289 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 ,
7599 isolvl = isolvl ,
76100 step_size = step_size ,
77101 origin = struct_scene .origin ,
@@ -81,5 +105,4 @@ def get_volumetric_scene(self, data_key="total", isolvl=0.02, step_size=3, **kwa
81105
82106
83107# todo: re-think origin, shift globally at end (scene.origin)
84- VolumetricData .get_isosurface_scene = get_isosurface_scene
85108VolumetricData .get_scene = get_volumetric_scene
0 commit comments