import numpy as np
import requests
import os
from EPWpy.structure.position_atoms import *
from EPWpy.utilities.printing import *
from EPWpy.utilities.display_struct import *
from EPWpy.error_handling import error_handler
from EPWpy.structure.QE_position_atoms import *
from EPWpy.default import default_dicts as dc
mayavi_found = False
try:
from mayavi import mlab
from mayavi.modules.iso_surface import IsoSurface
from tvtk.api import tvtk
mayavi_found = True
except ImportError:
error_handler.error_mayavi()
try:
from mp_api.client import MPRester
except ImportError:
error_handler.error_mp()
[docs]
class Lattice:
"""
A class representing a crystal lattice and its associated data.
This class provides a basic interface for storing and managing lattice-related
information. It can serve as the foundational class for structural manipulation,
symmetry analysis, or visualization routines in materials simulations.
Parameters
----------
data : dict or object
Input data structure containing lattice information. Typically includes
lattice vectors, atomic positions, and element types, but may be any
user-defined format compatible with downstream methods.
Attributes
----------
data : dict or object
The input lattice data, stored for use by other methods.
home : str
The working directory path at the time of object initialization.
Examples
--------
>>> lattice_data = {
... 'lattice_vectors': [[1.0, 0.0, 0.0],
... [0.0, 1.0, 0.0],
... [0.0, 0.0, 1.0]],
... 'atomic_positions': [[0.0, 0.0, 0.0]],
... 'elements': ['Si']
... }
>>> lat = Lattice(lattice_data)
>>> lat.home
'/home/user/project'
"""
def __init__(self, data):
self.data = data
self.home = os.getcwd()
[docs]
def get_atom(self):
"""
Gets atomic attributes from a poscar structure file
"""
if (self.structure != None):
atom_pos,atom_pos2,lattice_vec,mat,materials,natoms,a = main_extract([0.0,0.0],self.structure,T=1)
else:
atom_pos,atom_pos2,lattice_vec,mat,materials,natoms,a = self.get_QE_structure()
return(atom_pos, atom_pos2, lattice_vec, mat, materials, natoms, a)
[docs]
def atom_cart(self):
"""
Gets atomic positions in cartesian co-ordinate
"""
atom_pos,atom_pos2,lattice_vec,mat,materials,natoms,a = main_extract([0.0,0.0],self.structure,T=1)
return(atom_pos2)
[docs]
def get_xyz(self, supercell=None):
if supercell is None:
supercell = []
"""
Writes a xyz file
"""
atom_pos,atom_pos2,lattice_vec,mat,materials,natoms,a = self.get_atom()
#atom_pos2 = self.atom_cart()
if(len(supercell) !=0):
atom_pos2,natoms,mat = get_supercell(atom_pos2,natoms,mat,lattice_vec,a,supercell)
gen_xyz(atom_pos2,natoms,mat)
[docs]
def get_supercell(self, supercell=None, pl_vec=None):
"""
returns the co-ordinates for supercell
"""
if supercell is None:
supercell = [1,1,1]
if pl_vec is None:
pl_vec = []
atom_pos,atom_pos2,lattice_vec,mat,materials,natoms,a = self.get_atom()
#atom_pos2 = self.atom_cart()
if(len(supercell) != 0):
if (len(pl_vec) == 0):
atom_pos2,natoms,mat = get_supercell(atom_pos2,natoms,mat,lattice_vec,a,supercell)
return(atom_pos2,natoms,mat)
else:
atom_pos2,natoms,mat,pl_vec = get_supercell_pl(atom_pos2,natoms,mat,lattice_vec,a,supercell,pl_vec)
return(atom_pos2,natoms,mat,pl_vec)
[docs]
def get_QE_structure(self):
"""
Gets structure from a QE input
"""
atom_pos = self.pw_atomic_positions['atomic_pos']
atom_pos_type = self.pw_atomic_positions['atomic_position_type']
mat = self.pw_atomic_positions['atoms']
materials = self.pw_atomic_species['atomic_species']
natoms = self.pw_atomic_positions['num']
ibrav = self.pw_system['ibrav']
cell_parameters = self.pw_cell_parameters['lattice_vector']
cell_type = self.pw_cell_parameters['cell_type']
try:
celldm ={'1':self.pw_system['celldm(1)'],
'2':self.pw_system['celldm(2)'],
'3':self.pw_system['celldm(3)']}
except KeyError:
celldm ={'1':1.0,
'2':self.pw_system['celldm(2)'],
'3':self.pw_system['celldm(3)']}
atom_pos2,lattice_vec = atom_pos_lat_crystal({'ibrav': ibrav,
'cell_parameters': cell_parameters,
'cell_type': cell_type,
'atom_pos': atom_pos,
'atom_pos_type':atom_pos_type,
'celldm': celldm})
return(atom_pos,atom_pos2,lattice_vec,mat,materials,natoms,1.0)
[docs]
def display_lattice_legacy(self, supercell=None, view=None):
if supercell is None:
supercell = []
if view is None:
view = {}
"""
Display lattice
"""
self.get_xyz(supercell)
return(display_molecule(view))#self.atom_cart())
[docs]
def display_lattice(self, supercell=None, view=None, bond_length=3.5, type_view='mayavi'):
"""
Visualize the atomic lattice of the system.
This method displays the atomic structure of the simulated material
using either **Mayavi** (for interactive 3D visualization) or
**Matplotlib** (for static plotting). It supports supercell expansion
and customizable view parameters.
Parameters
----------
supercell : list or None, optional
Supercell expansion factors along each lattice direction, e.g.,
``[2, 2, 1]`` for a 2×2×1 supercell.
If ``None``, uses the primitive cell.
view : dict or None, optional
Dictionary specifying visualization options such as rotation,
zoom, or color scheme. Example:
``{'azimuth': 45, 'elevation': 30, 'wrap': False}``
If ``None``, defaults are used.
bond_length : float, optional
Maximum interatomic distance (in Å) for drawing bonds (default: ``3.5``).
type_view : str, optional
Visualization backend:
- ``'mayavi'`` → interactive 3D visualization (default)
- ``'matplotlib'`` → static 3D plotting for environments without Mayavi
Returns
-------
object
Visualization object returned by the selected backend:
- ``Mayavi`` scene if available and selected.
- ``Matplotlib`` figure otherwise.
Notes
-----
- Requires Mayavi to be installed for full 3D interactive visualization.
- If Mayavi is not available, falls back automatically to Matplotlib rendering.
- The visualization includes lattice vectors and atomic species.
Example
-------
>>> epw = EPWProperties(file='epw2.out', system='MoS2')
>>> epw.display_lattice(supercell=[2,2,1], type_view='matplotlib')
"""
if supercell is None:
supercell = []
if view is None:
view = {}
"""
Display lattice
"""
if ((mayavi_found) & (type_view == 'mayavi')):
atom_pos2, natoms, mat = self.get_supercell(supercell)
Data= {'positions':atom_pos2,'mat':mat}
return(display_crystal(Data, view = view, bond_length=bond_length))#self.atom_cart())
else:
atom_pos2, natoms, mat = self.get_supercell(supercell)
_,_,lattice_vec,_,materials,natoms,a = self.get_atom()
Data= {'positions':atom_pos2,
'mat':mat,
'natoms':natoms,
'atomic_species':materials,
'lattice_vec':lattice_vec}
return(display_crystal_matplotlib(Data, view = view, bond_length = bond_length))
[docs]
def display_phonons(self, pl_vec, supercell=None, view=None, bond_length=3.5, type_view='mayavi'):
"""
Display the phonon displacement modes of the crystal structure.
This method visualizes phonon modes (atomic vibrations) using the
provided phonon displacement vectors (`pl_vec`). The visualization
can be generated using either Mayavi or Matplotlib, depending on
availability and the `type_view` argument.
Parameters
----------
pl_vec : array-like
Phonon displacement vectors for each atom in the structure.
Typically obtained from a phonon calculation or interpolation step.
Must have shape (N_atoms, 3).
supercell : list[int], optional
Supercell expansion factors in each lattice direction [nx, ny, nz].
If not provided, the primitive cell is used.
view : dict, optional
Visualization parameters, such as camera orientation or zoom level.
Default is an empty dictionary.
bond_length : float, optional
Maximum bond distance (in Ångströms) used to determine which atoms
are connected by bonds in the visualization. Default is 3.5 Å.
type_view : str, optional
Visualization backend to use. Options are:
- `'mayavi'`: use Mayavi for 3D interactive rendering (default)
- `'matplotlib'`: use Matplotlib for static rendering
Returns
-------
object
A visualization object or figure handle created by the selected
visualization backend.
Notes
-----
This method constructs the full crystal structure (including optional
supercell expansion) and overlays atomic displacement vectors
representing phonon modes. The resulting data is passed to an external
visualization routine (`display_crystal_phonon`).
Examples
--------
>>> pl_vec = np.random.randn(4, 3) # Example displacement vectors
>>> model.display_phonons(pl_vec, supercell=[2, 2, 1], type_view='matplotlib')
"""
if supercell is None:
supercell = []
if view is None:
view = {}
atom_pos2, natoms, mat, pl_vec = self.get_supercell(supercell, pl_vec)
_,_,lattice_vec,_,materials,natoms,a = self.get_atom()
Data= {'positions':atom_pos2,
'mat':mat,
'natoms':natoms,
'atomic_species':materials,
'lattice_vec':lattice_vec,
'forces':pl_vec}
return(display_crystal_phonon(Data, view = view, bond_length = bond_length))
[docs]
def get_Data(self, pl_vec, supercell=None, view=None, bond_length=3.5, type_view='mayavi'):
"""
Display phonon modes
"""
if supercell is None:
supercell = []
if view is None:
view = {}
atom_pos2, natoms, mat, pl_vec = self.get_supercell(supercell, pl_vec)
_,_,lattice_vec,_,materials,natoms,a = self.get_atom()
Data= {'positions':atom_pos2,
'mat':mat,
'natoms':natoms,
'atomic_species':materials,
'lattice_vec':lattice_vec,
'forces':pl_vec}
return(Data)
[docs]
def get_poscar(self):
"""
Gets the structure file from materials project
"""
try:
with MPRester("DZYdCD0qMIVqUZQRlojThyabVVnfANXR") as mpr:
docs = mpr.materials.summary.search(material_ids=[f'{self.matid}'], fields=["structure"])
structure = docs[0].structure
# -- Shortcut for a single Materials Project ID:
structure = mpr.get_structure_by_material_id(f'{self.matid}')
#conventional_cell = structure.to_conventional()
#conventional_cell.to(fmt="POSCAR", filename=f'POSCAR_{self.matid}')
prim_cell = structure.to_cell('primitive',symprec=0.1,angle_tolerance=2)
prim_cell.to(fmt="POSCAR", filename=f'POSCAR_{self.matid}')
except NameError:
print(f"Cannot retrieve the structure with mp-api ID = {self.matid}")
print("Install EPWpy with mp-api or specify the structure using celldm")
print("EPWpy will be terminated")
raise SystemExit(1)
[docs]
def get_pseudo(self,name='pseudo'):
"""
Automatically download pseudopotential from pseudodojo
"""
try:
os.mkdir('pseudo')
except OSError:
pass
#print('pseudo folder found')
cwd = self.home
pseudos=[]
resp = []
for atoms in self.atomic_species:
response = self.get_response(atoms)
with open(f'./pseudo/{atoms}_r.{self.pseudo_end}', 'w') as f:
for line in response.text:
f.write(line)
pseudos.append(f'{atoms}_r.{self.pseudo_end}')
resp.append(response.ok)
return(pseudos, str(cwd)+'/pseudo/', resp)
[docs]
def get_ecutwfc(self):
"""
Gets standard ecutwfc
"""
cutoff_array = []
for atoms in self.pw_atomic_species['atomic_species']:
atom_cutoff = dc.ecutwfc_standard[atoms]
cutoff_array.append(int(atom_cutoff))
ecutwfc = max(cutoff_array)
return ecutwfc
[docs]
def get_mass(self):
"""
get mass
"""
mass_array = []
for atoms in self.pw_atomic_species['atomic_species']:
atom_mass = dc.atomic_mass[atoms]
mass_array.append(float(atom_mass))
return mass_array
[docs]
def get_response(self, atoms):
pseudo_type = self.pseudo_typ
end = self.pseudo_end
"""
Get the response from pseudodojo website
"""
for orbital in self.pseudo_orbitals:
url = f'https://raw.githubusercontent.com/PseudoDojo/ONCVPSP-{pseudo_type}/master/{atoms}/{atoms}{orbital}.{end}'#/As/As-d_r.upf
print(url)
response = requests.get(url)
if (response.ok == True):
found = f'ONCVPSP-{pseudo_type}/{atoms}{orbital}.{end}'
break
if(response.ok == False):
print(f'pseudo not found for {atoms} in pseudodojo, please download manually')
print(f'Add tags pseudo_dir:<location of pseudo> and pseudo:[\'each species\'] in EPWpy object')
else:
print(f'pseudo found at pseudodojo : {found}')
return(response)
[docs]
def gen_supercell(self, supercell=None):
"""
returns the co-ordinates for supercell
"""
if supercell is None:
supercell = [1,1,1]
atom_pos,atom_pos2,lattice_vec,mat,materials,natoms,a = self.get_atom()
#atom_pos2 = self.atom_cart()
if(len(supercell) != 0):
atom_pos2, natoms, mat = get_supercell(atom_pos2,natoms,mat,lattice_vec,a,supercell)
lattice_vec_sup = lattice_vec @ np.diag(supercell)
atom_pos_new = np.zeros((np.shape(atom_pos2)), dtype=float)
# Cartesian to Direct
atom_pos_new[:,:] = np.dot(atom_pos2[:,:],np.linalg.inv(lattice_vec_sup))
materials_n, natoms_n = np.unique(mat, return_counts=True)
# Generate POSCAR
gen_POSCAR(lattice_vec_sup,atom_pos_new,natoms_n,materials_n)
# Return everything
return({'atom_pos':atom_pos_new,'lattice_vec':lattice_vec_sup,
'natoms':natoms_n, 'materials':materials_n})
return (None)
@property
def structure_params(self):
"""
Returns structure parameters as a dictionary
Units are Angstrom for variables in Cartesian
"""
atom_pos,atom_pos2,lattice_vec,mat,materials,natoms,a = self.get_atom()
atomic_weight = []
atomic_mass = []
for atm in materials:
atomic_weight.append(dc.atomic_number[atm])
atomic_mass.append(dc.atomic_mass[atm])
return({'atomic_positions_crystal':atom_pos,
'atomic_positions_cartesian':atom_pos2,
'lattice_vector':lattice_vec,
'atomic_species':materials,
'atoms':mat,
'natoms':natoms,
'atomic_weight':atomic_weight,
'atomic_mass':atomic_mass,
'lattice_constant':a})
@property
def structure_params_summary(self):
"""
Print structure as a formatted string
"""
data = self.structure_params
lines = []
lines.append("=== Structure Information ===")
lines.append(f"Lattice constant: {data['lattice_constant']}")
lines.append(f"Number of atoms: {data['natoms']}")
lines.append(f"Atomic species : {', '.join(data['atomic_species'])}")
lines.append(f"Atomic weights : {data['atomic_weight']}")
lines.append(f"Atomic masses : {data['atomic_mass']}\n")
lines.append("--- Lattice Vectors (Å) ---")
for row in data['lattice_vector']:
lines.append(" {:+10.6f} {:+10.6f} {:+10.6f}".format(*row))
lines.append("\n--- Atomic Positions (crystal) ---")
for atom, pos in zip(data['atoms'], data['atomic_positions_crystal']):
lines.append(f" {atom:2s} : {pos[0]:8.3f} {pos[1]:8.3f} {pos[2]:8.3f}")
lines.append("\n--- Atomic Positions (cartesian, Å) ---")
for atom, pos in zip(data['atoms'], data['atomic_positions_cartesian']):
lines.append(f" {atom:2s} : {pos[0]:10.6f} {pos[1]:10.6f} {pos[2]:10.6f}")
formatted_output = "\n".join(lines)
return formatted_output
[docs]
def get_supercell(atom_pos2,natoms,mat,lattice_vec,a,supercell):
"""
Builds supercell of a certain size
"""
Nx = supercell[0]
Ny = supercell[1]
Nz = supercell[2]
matn = []
natoms = []
atom_posn = np.zeros((len(atom_pos2[:,0])*Nx*Ny*Nz,3), dtype = float)
p=0
for t in range(len(atom_pos2[:,0])):
for i in range(Nx):
for j in range(Ny):
for k in range(Nz):
atom_posn[p,:] = atom_pos2[t,:]+a*(i*lattice_vec[0,:]+j*lattice_vec[1,:]+k*lattice_vec[2,:])
matn.append(mat[t])
natoms.append(1)
p +=1
return(atom_posn,natoms,matn)
[docs]
def get_supercell_pl(atom_pos2,natoms,mat,lattice_vec,a,supercell,pl_vec):
"""
Builds supercell of a certain size for the polarization vector
"""
Nx = supercell[0]
Ny = supercell[1]
Nz = supercell[2]
matn = []
natoms = []
atom_posn = np.zeros((len(atom_pos2[:,0])*Nx*Ny*Nz,3), dtype = float)
pl_vecn = np.zeros((len(atom_pos2[:,0])*Nx*Ny*Nz,3), dtype = float)
p=0
for t in range(len(atom_pos2[:,0])):
for i in range(Nx):
for j in range(Ny):
for k in range(Nz):
atom_posn[p,:] = atom_pos2[t,:]+a*(i*lattice_vec[0,:]+j*lattice_vec[1,:]+k*lattice_vec[2,:])
pl_vecn[p,:] = pl_vec[t,:]
matn.append(mat[t])
natoms.append(1)
p +=1
return(atom_posn,natoms,matn,pl_vecn)