Source code for pyddt.structure

#!/usr/bin/python3

"""This class is used for structural modelling from *.in* files."""

import re

import time
from difflib import SequenceMatcher
from pathlib import Path

import nglview as nv
import numpy as np
from ase import Atoms

__author__ = "Rafaela Felix"
__credits__ = {"Rafaela Felix", "Sergio Morelhão"}
__version__ = "1.0"
__maintainer__ = "Rafaela Felix"
__email__ = "rafaelafelixp@usp.br"
__status__ = "Production"


[docs]class Structure: def __init__(self, fname: str): """Given a *.in* file, returns a new ``struc`` object. Args: fname (str): Structure filename. """ self.name = fname self.atoms = np.loadtxt(fname, usecols=0, dtype='<U32', skiprows=1) self.positions = np.loadtxt(fname, usecols=(1, 2, 3), skiprows=1) self.occupancy = np.loadtxt(fname, usecols=4, skiprows=1) self.bfactors = np.loadtxt(fname, usecols=5, skiprows=1) self.lattice = np.loadtxt(fname, max_rows=1)
[docs] def visualizer_in(self) -> nv.widget.NGLWidget: """Visualizes the structure. This method is available exclusively in Jupyter Notebooks. Returns: nv.widget.NGLWidget: Interactive visualization of the conventional unit cell. Attention: To avoid bugs, it's highly recommended to assign this method to a variable, then close the figure after the visualization using ``variable_name.close()``. """ at = np.copy(self.atoms) # just atoms (ions don't contribute to resonance) idx = np.where(np.char.isalpha(self.atoms) == False) if not np.shape(idx): pass else: ion = np.copy(self.atoms[idx]) regex = re.compile('[^a-zA-Z]') for i in range(len(ion)): at[idx[0][i]] = regex.sub('', ion[i]) system = Atoms(positions=self.positions * self.lattice[:3], symbols=at, cell=self.lattice) view = nv.show_ase(system) view.add_representation('point', selection='all', radius='0.6') view.add_label(color='black', scale=0.8, labelType='text', labelText=[str(i) for i in range(len(system))], zOffset=2.0, attachment='middle_center') view.add_unitcell() return view
[docs] def replace_ion(self, old: str, new: str): """Replaces atoms or ions. Args: old (str): Array indices or element symbol to be replaced. new (str): New atom/ion symbol. Notes: * There are many ways to pass indices as arguments: a number, numbers separated by commas, an interval of indices (first and last indices separated by ":" ) and intervals separated by commas. A string is expected. * If the new symbol isn't in the current CromerMann list, the symbol is replaced by the most similar and a warning will be displayed. Usage: * ``replace_ion('Ce', 'Ce3+')`` * ``replace_ion('1', 'O1-')`` * ``replace_ion('1, 2, 3', 'O1-')`` * ``replace_ion('1:3', 'Fe2+')`` * ``replace_ion('1:3, 5:6, 7:8', 'N3-')`` """ crom = Path(__file__).parent / "f0_CromerMann.txt" el = np.loadtxt(crom, dtype='str', usecols=0) rank = np.array([SequenceMatcher(None, new, el[i]).ratio() for i in range(len(el))]) if rank.max() != 1: print(new, 'is not included in Cromermann factors. Replaced by', el[rank.argmax()]) # future log warning new = el[rank.argmax()] if old.count(':') == 0: if len(old.split(',')) > 1: idx = [int(i) for i in old.split(',')] self.atoms[idx] = new else: if old.isnumeric(): self.atoms[int(old)] = new else: idx = np.where(self.atoms == old)[0] self.atoms[idx] = np.char.replace(self.atoms[idx], old, new) else: index = old.split(',') idx = [(int(i.split(':')[0]), int(i.split(':')[1]) + 1) for i in index] for i in range(len(idx)): m, n = idx[i] self.atoms[m:n] = new
[docs] def replace_occupancy(self, index: str, value: str): """Replaces a specific set of occupancy numbers. Args: index (str): Array indices of the occupancy numbers to be replaced. value (str): New values. Notes: There are many ways to pass indices as arguments: a number, numbers separated by commas, an interval of indices (first and last indices separated by ":" ) and intervals separated by commas. A string is expected. Usage: * ``replace_occupancy('1', 0.78)`` * ``replace_occupancy('1, 2, 3', 0.92)`` * ``replace_occupancy('1:3', 0)`` * ``replace_occupancy('1:3, 5:6, 7:8', 0.5)`` """ if index.count(':') == 0: if len(index.split(',')) > 1: idx = [int(i) for i in index.split(',')] self.occupancy[idx] = value else: self.occupancy[int(index)] = value else: ind = index.split(',') idx = [(int(i.split(':')[0]), int(i.split(':')[1]) + 1) for i in ind] for i in range(len(idx)): m, n = idx[i] self.occupancy[m:n] = value
[docs] def replace_bfactor(self, index: str, value: str): """Replaces a specific set of B-factors. Args: index (str): Array indices of B-factors to be replaced. value (str): New values. Notes: There are many ways to pass indices as arguments: only a number, numbers separated by commas, an interval of indices (first and last indices separated by ":" ) and intervals separated by commas. A string is expected. Usage: * ``replace_bfactor('1', 3.14)`` * ``replace_bfactor('1, 2, 3', 0.92)`` * ``replace_bfactor('1:3', 10.015)`` * ``replace_bfactor('1:3, 5:6, 7:8', 0.5)`` """ if index == ':': self.bfactors[:] = value elif index.count(':') == 0: if len(index.split(',')) > 1: idx = [int(i) for i in index.split(',')] self.bfactors[idx] = value else: self.bfactors[int(index)] = value else: ind = index.split(',') idx = [(int(i.split(':')[0]), int(i.split(':')[1]) + 1) for i in ind] for i in range(len(idx)): m, n = idx[i] self.bfactors[m:n] = value
[docs] def append_atom(self, info: list): """Appends a new atom to the structure. Args: info (list): Atom or ion symbol, fractional coordinates (x, y and z), occupancy number and B-factor. Usage: * ``append_atom(['Fe2+', 0.5, 0.5, 0.5, 1, 3.14])`` Notes: * The new atom will be allocated in the last index of the atoms array. * For the current version, append atoms one-by-one. """ crom = Path(__file__).parent / "f0_CromerMann.txt" el = np.loadtxt(crom, dtype='str', usecols=0) rank = np.array([SequenceMatcher(None, info[0], el[i]).ratio() for i in range(len(el))]) if rank.max() != 1: print(info[0], 'is not included in Cromermann factors. Replaced by', el[rank.argmax()]) # log warning info[0] = el[rank.argmax()] self.atoms = np.append(self.atoms, info[0]) self.positions = np.row_stack((self.positions, info[1:4])) self.occupancy = np.append(self.occupancy, info[4]) self.bfactors = np.append(self.bfactors, info[5])
[docs] def delete_atom(self, index: str): """Deletes a set of atoms. Args: index (str): Array indices of the atoms to be deleted. Notes: There are many ways to pass indices as arguments: only a number, numbers separated by commas, an interval of indices (first and last indices separated by ":" ) and intervals separated by commas. A string is expected. Usage: * ``delete_atom('1')`` * ``delete_atom('1, 2, 3')`` * ``delete_atom('1:3')`` * ``delete_atom('1:3, 5:6, 7:8')`` """ if index.count(':') == 0: if len(index.split(',')) > 1: idx = [int(i) for i in index.split(',')] else: idx = int(index) self.atoms = np.delete(self.atoms, idx, axis=0) self.positions = np.delete(self.positions, idx, axis=0) self.occupancy = np.delete(self.occupancy, idx, axis=0) self.bfactors = np.delete(self.bfactors, idx, axis=0) else: ind = index.split(',') idx = [(int(i.split(':')[0]), int(i.split(':')[1]) + 1) for i in ind] for i in range(len(idx)): m, n = idx[i] ind = np.arange(m, n) self.atoms = np.delete(self.atoms, ind, axis=0) self.positions = np.delete(self.positions, ind, axis=0) self.occupancy = np.delete(self.occupancy, ind, axis=0) self.bfactors = np.delete(self.bfactors, ind, axis=0)
[docs] def save_infile(self, fout=''): """Saves the *.in* file. Args: fout (str): Filename. **Default**: `datetime_fname` """ fst_row = f"{self.lattice[0]:10.4f} {self.lattice[1]:10.4f} {self.lattice[2]:10.4f} " \ f"{self.lattice[3]:10.4f} {self.lattice[4]:10.4f} {self.lattice[5]:10.4f}\n" if fout == '': fout = time.strftime("%Y%m-%H_%M_%S") + '_' + self.name with open(fout, "w") as f: f.write(fst_row) for i in range(len(self.atoms)): text = f"{self.atoms[i]:>10s} {self.positions[i][0]:10.4f} " \ f"{self.positions[i][1]:10.4f} {self.positions[i][2]:10.4f} " \ f"{self.occupancy[i]:10.4f} {self.bfactors[i]:10.4f}\n" f.write(text)