#-*- coding: utf-8 -*-
# Copyright (c) 2015-2022, Exa Analytics Development Team
# Distributed under the terms of the Apache License 2.0
"""
Cube File Support
##########################
Cube files contain an atomic geometry and scalar field values corresponding to
a physical quantity.
"""
import os
import six
import numpy as np
import pandas as pd
from glob import glob
from exatomic.exa import Series, TypedMeta
from exatomic import __version__, Atom, Editor, AtomicField, Frame, Universe
from exatomic.base import z2sym, sym2z
[docs]class Cube(six.with_metaclass(Meta, Editor)):
"""
An editor for handling cube files. Assumes scalar field values are arranged
with the x axis as the outer loop and the z axis as the inner loop.
.. code-block:: python
cube = Cube('my.cube')
cube.atom # Displays the atom dataframe
cube.field # Displays the field dataframe
cube.field.field_values # Displays the list of field values
uni = cube.to_universe() # Converts the cube file editor to a universe
UniverseWidget(uni) # Renders the cube file
Warning:
Be sure your cube is in atomic units.
"""
[docs] def parse_atom(self):
"""
Parse the :class:`~exatomic.atom.Atom` object from the cube file in place.
"""
nat = abs(int(self[2].split()[0]))
names = ['Z', 'Zeff', 'x', 'y', 'z']
df = self.pandas_dataframe(6, nat + 6, names)
df['symbol'] = df['Z'].map(z2sym).astype('category')
df['label'] = range(nat)
df['frame'] = 0
self.atom = Atom(df)
[docs] def parse_field(self):
"""
Parse the scalar field into an :class:`~exatomic.core.field.AtomicField`.
Note:
The :class:`~exatomic.core.field.AtomicField` tracks both the
field parameters (i.e. information about the discretization and shape of
the field's spatial points) as well as the field values (at each of
those points in space). See :meth:`~exatomic.algorithms.orbital_util.make_fps`
for more details.
"""
self.meta = {'comments': self[:2]}
typs = [int, float, float, float]
nat, ox, oy, oz = [typ(i) for typ, i in zip(typs, self[2].split())]
nx, dxi, dxj, dxk = [typ(i) for typ, i in zip(typs, self[3].split())]
ny, dyi, dyj, dyk = [typ(i) for typ, i in zip(typs, self[4].split())]
nz, dzi, dzj, dzk = [typ(i) for typ, i in zip(typs, self[5].split())]
nat, nx, ny, nz = abs(nat), abs(nx), abs(ny), abs(nz)
volstart = nat + 6
if len(self[volstart].split()) < 5:
if not len(self[volstart + 1].split()) < 5:
volstart += 1
ncol = len(self[volstart].split())
data = self.pandas_dataframe(volstart, len(self), ncol).values.ravel()
df = pd.Series({'ox': ox, 'oy': oy, 'oz': oz,
'nx': nx, 'ny': ny, 'nz': nz,
'dxi': dxi, 'dxj': dxj, 'dxk': dxk,
'dyi': dyi, 'dyj': dyj, 'dyk': dyk,
'dzi': dzi, 'dzj': dzj, 'dzk': dzk,
'frame': 0, 'label': self.label,
'field_type': self.field_type}).to_frame().T
for col in ['nx', 'ny', 'nz']:
df[col] = df[col].astype(np.int64)
for col in ['ox', 'oy', 'oz', 'dxi', 'dxj', 'dxk',
'dyi', 'dyj', 'dyk', 'dzi', 'dzj', 'dzk']:
df[col] = df[col].astype(np.float64)
fields = [Series(data[~np.isnan(data)])]
self.field = AtomicField(df, field_values=fields)
[docs] @classmethod
def from_universe(cls, uni, idx, name=None, frame=None):
"""
Make a cube file format Editor from a given field in a
:class:`~exatomic.core.universe.Universe`.
Args:
uni (:class:`~exatomic.core.universe.Universe`): a universe
idx (int): field index in :class:`~exatomic.core.field.AtomicField`
name (str): description for comment line
frame (int): frame index in :class:`~exatomic.core.atom.Atom`
"""
name = '' if name is None else name
frame = uni.atom.nframes - 1 if frame is None else frame
hdr = '{} -- written by exatomic v{}\n\n'
ffmt = ' {:> 12.6f}'
flfmt = ('{:>5}' + ffmt * 3 + '\n').format
if 'Z' not in uni.atom:
uni.atom['Z'] = uni.atom['symbol'].map(sym2z)
if 'Zeff' not in uni.atom:
uni.atom['Zeff'] = uni.atom['Z'].astype(np.float64)
frame = uni.atom[uni.atom['frame'] == frame]
for col in ['nx', 'ny', 'nz']:
uni.field[col] = uni.field[col].astype(np.int64)
field = uni.field.loc[idx]
volum = uni.field.field_values[idx]
orig = len(frame.index), field.ox, field.oy, field.oz
nx, ny, nz = int(field.nx), int(field.ny), int(field.nz)
xdim = nx, field.dxi, field.dxj, field.dxk
ydim = ny, field.dyi, field.dyj, field.dyk
zdim = nz, field.dzi, field.dzj, field.dzk
atargs = {'float_format': '%12.6f',
'header': None, 'index': None,
'columns': ['Z', 'Zeff', 'x', 'y', 'z']}
chnk = ''.join(['{}' * 6 + '\n' for i in range(nz // 6)])
if nz % 6: chnk += '{}' * (nz % 6) + '\n'
return cls(hdr.format(name, __version__)
+ flfmt(*orig) + flfmt(*xdim)
+ flfmt(*ydim) + flfmt(*zdim)
+ uni.atom.to_string(**atargs) + '\n'
+ (chnk * nx * ny).format(*volum.apply(
ffmt.replace('f', 'E').format)))
def __init__(self, *args, **kwargs):
label = kwargs.pop("label", None)
field_type = kwargs.pop("field_type", None)
super(Cube, self).__init__(*args, **kwargs)
self.label = label
self.field_type = field_type
[docs]def uni_from_cubes(adir, verbose=False, ncubes=None, ext='cube'):
"""Put a bunch of cubes into a universe.
.. code-block:: python
uni = uni_from_cubes('/path/to/files/') # Parse all cubes matching 'files/*cube'
uni = uni_from_cubes('files/', ext='cub') # Parse all cubes matching 'files/*cub'
uni = uni_from_cubes('files/', verbose=True) # Print file names when parsing
uni = uni_from_cubes('files/', ncubes=5) # Only parse the first 5 cubes
# sorted lexicographically by file name
Args:
verbose (bool): print file names when reading cubes
ncubes (int): get only the first ncubes
ext (str): file extension of cube files
Returns:
uni (:class:`exatomic.core.universe.Universe`)
"""
if not adir.endswith(os.sep): adir += os.sep
cubes = sorted(glob(adir + '*' + ext))
if ncubes is not None:
cubes = cubes[:ncubes]
if verbose:
for cub in cubes: print(cub)
uni = Universe(atom=Cube(cubes[0]).atom)
flds = [Cube(cub).field for cub in cubes]
uni.add_field(flds)
return uni