"""
Analyzes free volume in a structure
Copyright Schrodinger, LLC. All rights reserved.
"""
import functools
import glob
import json
import math
import os
import sys
import tarfile
from collections import OrderedDict
from collections import defaultdict
from collections import namedtuple
from operator import attrgetter
from past.utils import old_div
import numpy
from schrodinger import structure
from schrodinger import surface
from schrodinger.application.matsci import amorphous
from schrodinger.application.matsci import jobutils
from schrodinger.application.matsci import msprops
from schrodinger.application.matsci import textlogger
from schrodinger.structutils import color
from schrodinger.utils import fileutils
GENERIC_TITLE = 'free_volume'
SPACING = 0.25
PROBE_RADIUS = 1.4
INFO = 'info'
VOIDS = 'voids'
SURFS = 'surfs'
XYZ_TOLERANCE = 0.01
FREE_VOLUME_PROP = 'r_matsci_Free_Volume_A^3'
FREE_VOLUME_PCT_PROP = 'r_matsci_Free_Volume_%'
VOL_FILE_PROP = 's_matsci_Volume_File'
VOID_FILE_PROP = 's_matsci_Void_File'
MAE_FILE_PROP = 's_matsci_FV_Coord_File'
FREEVOL_TRJ_ANALYSIS = 'b_matsci_freevolume_trj_analysis'
COMMENT_BIT = '#'
GRID_RAD_BIT = '_g%.2f_r%.2f'
VOL_FILE_ENDING = '-freevolumes.txt'
VOIDS_FILE_ENDING = '-voids.json'
SURF_FILE_ENDING = '.viz'
SURF_FILE_FOLDER = '_surfs'
TAR_ENDING = '.gz'
SURF_TAR_ENDING = SURF_FILE_FOLDER + TAR_ENDING
MAE_FILE_ENDING = '-fv_coords.maegz'
TRJ_MIDFIX = 'trj'
GridRadius = namedtuple('GridRadius', ['grid', 'radius'])
SURF_COMMENT = 'Radius: %.2f, Spacing: %.2f'
SURF_NAME = 'Vol=%.2f r=%.2f g=%.2f %d'
SURF_TRJ_NAME = 'Vol=%.2f r=%.2f g=%.2f fr=%d %d'
[docs]class FatalCoordinateError(Exception):
    """
    Raised if something is wrong with matching the current coordinates to the
    ones used to compute free volume. Typically and atom number or element
    mismatch
    """ 
[docs]def archive_folder(filename, archive_folder):
    """
    Archive and delete the passed folder
    :param filename: Filename of the archived file
    :type filename: str
    :param archive_folder: The folder to archive
    :type archive_folder: str
    """
    jobutils.archive_job_data(filename, [archive_folder])
    jobutils.add_outfile_to_backend(filename)
    fileutils.force_rmtree(archive_folder, True) 
[docs]def unarchive_surf_data(struct):
    """
    Unarchive the free volume surface folders related to the passed structure
    :param struct: The structure for which freevolume was calculated
    :type struct: `schrodinger.structure.Structure`
    """
    if not struct:
        return
    source_path = jobutils.get_source_path(struct)
    with fileutils.chdir(source_path):
        for filename in glob.glob('*' + SURF_TAR_ENDING):
            # Extract the surf files and delete the archived file
            with tarfile.open(name=filename,
                              mode='r:gz',
                              format=jobutils.TGZ_FORMAT) as tar:
                tar.extractall()
            os.remove(filename) 
[docs]def get_free_volume_structure(struct):
    """
    Get the structure that the free volume calculation was run on
    :type struct: `schrodinger.structure.Structure`
    :param struct: The structure that contains the properties that point to the
        free volume structure
    :rtype: `schrodinger.structure.Structure` or None
    :return: The structure from the free volume calculation or None if it
        couldn't be found (due to missing property or file)
    """
    mae_name = struct.property.get(MAE_FILE_PROP)
    if not mae_name:
        return
    # If mae file is not found, check the path relative to the source path
    if not os.path.exists(mae_name):
        source_path = jobutils.get_source_path(struct)
        mae_name = os.path.join(source_path, mae_name)
    try:
        fv_struct = structure.Structure.read(mae_name)
    except IOError:
        return
    return fv_struct 
[docs]def check_coordinates_changed(struct):
    """
    Check that the given structure matches the structure the free volume
    calculation was run on. In addition to basic number/type of atoms, the
    coordinates must also be the same since the void data has XYZ information
    incorporated in it.
    :type struct: `schrodinger.structure.Structure`
    :param struct: The structure that contains the properties that point to the
        free volume structure
    :rtype: `schrodinger.structure.Structure`, False, or None
    :return: False if the structures match (number of atoms, elements and XYZ),
        `Structure` if the coordinates do not match - in this case the
        returned structure is a structure with the original free volume
        coordinates suitable for passing to fix_free_volume_coordinates, None if
        the free volume structure couldn't be found (due to missing property or
        file)
    :raise `FatalCoordinateError`: If something unfixable does not match between
        the two structures, such as number of atoms or elements
    """
    fv_struct = get_free_volume_structure(struct)
    if not fv_struct:
        return
    if struct.atom_total != fv_struct.atom_total:
        raise FatalCoordinateError('Structures have two different atom totals.')
    for index in range(1, struct.atom_total + 1):
        atom = struct.atom[index]
        fv_atom = fv_struct.atom[index]
        if atom.element != fv_atom.element:
            raise FatalCoordinateError('Elements do not match for atom %d.' %
                                       index)
        if not numpy.allclose(
                atom.xyz, fv_atom.xyz, rtol=0.0, atol=XYZ_TOLERANCE):
            return fv_struct
    return False 
[docs]def fix_free_volume_coordinates(struct, fv_struct=None):
    """
    Change the coordinates of struct to be those from the free volume
    calculation. Note that this assumes check_coordinates_changed has been
    called first (and the results dealt with).
    The structure is modified in-place
    :type struct: structure.Structure
    :param struct: The structure to modify
    :type fv_struct: structure.Structure
    :param struct: The structure with the coordinates to transfer to struct. If
        not given, the structure obtained by get_free_volume_structure will be
        used.
    """
    if not fv_struct:
        fv_struct = get_free_volume_structure(struct)
        if not fv_struct:
            return
    struct.setXYZ(fv_struct.getXYZ()) 
[docs]def get_xlink_type_atoms(struct):
    """
    Get atom indexes for atoms that are either crosslinkable, crosslinked or
    both.
    :type struct: `schrodinger.structure.Structure`
    :param struct: The structure to look for atoms in.
    :rtype: set, set, set
    :return: In order, indexes for atoms that are both crosslinked AND
        crosslinkable, atoms that are crosslinkable, atoms that are crosslinked.
        The sets are mutually exclusive.
    """
    xlinkable_indexes = set()
    xlinked_indexes = set()
    xlink_both_indexes = set()
    # Find all atoms that can be crosslinked
    for bond in struct.bond:
        if bond.property.get(msprops.XLINKABLE_TYPE_PROP) is not None:
            for atom in [bond.atom1, bond.atom2]:
                xlinkable_indexes.add(atom.index)
    # Find all atoms that have been crosslinked
    for bond in struct.bond:
        if bond.property.get(msprops.XLINKED_ON_STEP_PROP) is not None:
            for atom in [bond.atom1, bond.atom2]:
                xlinked_indexes.add(atom.index)
    # Find all atoms that are in both sets
    xlink_both_indexes = xlinkable_indexes.intersection(xlinked_indexes)
    xlinkable_indexes = xlinkable_indexes.difference(xlink_both_indexes)
    xlinked_indexes = xlinked_indexes.difference(xlink_both_indexes)
    return xlink_both_indexes, xlinkable_indexes, xlinked_indexes 
[docs]def get_atom_layers(atom, origin, num_layers, layer_width, axis):
    """
    Get the range of layers that the given atom would intersect.
    :type atom: `schrodinger.structure._StructureAtom`
    :param atom: The atom to find the layers for
    :type origin: float
    :param origin: The offset to add to each layer because the system starts at
        a point other than zero. The offset is along the axis defined by the
        axis parameter.
    :type num_layers: int
    :param num_layers: The number of layers
    :type layer_width: float
    :param layer_width: The width of each layer
    :type axis: int
    :param axis: 0, 1 or 2 to indicate the layers are cross sections of the X, Y
        or Z axis
    :rtype: list
    :return: Each item of the list is a layer the atom intersects based on its
        VdW radius
    """
    relative_val = atom.xyz[axis] - origin
    min_val = relative_val - atom.radius
    max_val = relative_val + atom.radius
    min_layer = int(math.floor(old_div(min_val, layer_width)))
    max_layer = int(math.floor(old_div(max_val, layer_width)))
    layers = []
    for raw_layer in range(min_layer, max_layer + 1):
        # For atoms that lie outside the PBC, raw_layer may be the "image" of
        # the actual layer number - either much larger than the total number of
        # layers or a negative value. Bring it mathematically back into the PBC
        # region via these while loops.
        while raw_layer < 0:
            # Cases that extend to the "left" of the PBC
            raw_layer += num_layers
        while raw_layer >= num_layers:
            # Cases that extend to the "right" of the PBC
            raw_layer -= num_layers
        layers.append(raw_layer)
    return layers 
[docs]def extract_grid_and_radius(word):
    """
    Given a string, extract the floating point grid spacing and probe radius
    values
    :type word: str
    :param word: The string to extract the grid & radius from
    :rtype: `GridRadius`
    :return: The GridRadius namedtuple with the extracted values
    """
    tokens = word.split('_')
    radius = float(tokens[-1].lstrip('r'))
    grid = float(tokens[-2].lstrip('g'))
    return GridRadius(grid=grid, radius=radius) 
[docs]def get_grid_radius_property_values(struct, propbase):
    """
    Get all the grid and radius values for properties on the structure that
    begin with the given base string.
    :type struct: `schrodinger.structure.Structure`
    :param struct: The structure to extract the properties from
    :type propbase: str
    :param propbase: The starting string for properties to search for
    :rtype: dict
    :return: Keys are GridRadius instances, values are the property value for
        that set of grid/radius values.
    """
    values = {}
    for prop, value in struct.property.items():
        if prop.startswith(propbase):
            gridrad = extract_grid_and_radius(prop)
            values[gridrad] = value
    return values 
[docs]def get_file_data(struct, propbase, file_reader, override_path=None):
    """
    Use file_reader to extract the data from the files specified by the given
    structure properties
    :type struct: `schrodinger.structure.Structure`
    :param struct: The structure to get data for
    :type propbase: str
    :param propbase: The starting string for properties to search for
    :type file_reader: callable
    :param file_reader: The function that will read the data from the file
    :type override_path: str or None
    :param override_path: If given, look for files in this directory rather than
        the one specified in the structure property
    :rtype: (`OrderedDict`, list)
    :return: Keys of the dict are `GridRadius` objects, values are the data
        extracted from the file for that set of Grid and Radius values. keys are
        orderd by (radius, grid) sort order.  Each item of the list is a str
        that gives the path of a file that could not be located.
    """
    data = {}
    failed_paths = []
    paths = get_grid_radius_property_values(struct, propbase)
    for gridrad, path in paths.items():
        # If override path is not provided and current path does not exist
        # Set the source path as the override path
        if not override_path and not os.path.exists(path):
            override_path = jobutils.get_source_path(struct)
        if override_path:
            filename = os.path.basename(path)
            path = os.path.join(override_path, filename)
        if not os.path.exists(path):
            failed_paths.append(path)
            continue
        filedata = file_reader(path)
        data[gridrad] = filedata
    keys = list(data)
    keys.sort(key=attrgetter('radius', 'grid'))
    ordered_data = OrderedDict([(x, data[x]) for x in keys])
    return ordered_data, failed_paths 
[docs]def read_volume_file(path):
    """
    Read the void volume data file located at path
    :type path: str
    :param path: The path to the file
    :rtype: list
    :return: The volumes from the file, each item is a float
    """
    with open(path, 'r') as volfile:
        volumes = [float(x.strip()) for x in volfile]
    return volumes 
[docs]def read_trj_volume_file(path):
    """
    Read the trajectory void volume data file located at path
    :type path: str
    :param path: The path to the file
    :rtype: dict
    :return: The volumes from the file where the key is the frame id and
        values are list of volume in that frame
    """
    volumes = defaultdict(list)
    with open(path, 'r') as volfile:
        current_fid = None
        for line in volfile:
            line = line.strip()
            # Check if the line is comment line (frame id) or volume line
            if line.startswith(COMMENT_BIT):
                # User-facing frame indexes start at 1 not 0
                current_fid = int(line.replace(COMMENT_BIT, '')) + 1
            elif line:
                volumes[current_fid].append(float(line))
    return volumes 
[docs]def read_void_file(path):
    """
    Read the void data file located at path
    :type path: str
    :param path: The path to the file
    :rtype: dict
    :return: Keys are INFO and VOIDS. INFO value is a `VoidInfo` object, VOIDS
        value is a list of `Void` objects sorted from largest to smallest.
    """
    with open(path, 'r') as voidsfile:
        raw_data = json.load(voidsfile)
    void_data = {}
    void_data[INFO] = VoidInfo.fromJsonData(raw_data[INFO])
    void_data[VOIDS] = [Void.fromJsonData(x) for x in raw_data[VOIDS]]
    # Load surface for voids where it is pre-calculated
    for void in void_data[VOIDS]:
        if void.surf_file:
            job_folder = os.path.dirname(path)
            surf_file = os.path.join(job_folder, void.surf_file)
            void.surf = surface.Surface.read(surf_file)
        else:
            void.surf = None
    void_data[VOIDS].sort(reverse=True)
    return void_data 
[docs]def read_trj_void_file(path):
    """
    Read the all frames void data files located inside the path
    :type path: str
    :param path: The path to the void files folder
    :rtype: dict
    :return: Dictionary where the key is the frame id and value is the
        void data for the frame. Void data is a dictionary where keys are INFO
        and VOIDS. INFO value is a `VoidInfo` object, VOIDS
        value is a list of `Void` objects sorted from largest to smallest.
    """
    all_void_data = {}
    for _, _, files in os.walk(path):
        for filename in files:
            # Read only voids json files
            if not filename.endswith(VOIDS_FILE_ENDING):
                continue
            # filename will be like struct_name_g1.0_r1.2_trj5-voids.json
            trjname_bit = filename.split('_')[-1]
            fid_str = (trjname_bit.replace(TRJ_MIDFIX,
                                           '').replace(VOIDS_FILE_ENDING, ''))
            # User-facing frame indexes start at 1 not 0
            fid = int(fid_str) + 1
            all_void_data[fid] = read_void_file(os.path.join(path, filename))
    return all_void_data 
[docs]def get_volume_data(struct, path=None, read_trj=False):
    """
    Get all the volume data available for the given structure
    :type struct: `schrodigner.structure.Structure`
    :param struct: The structure to get volume data for
    :type path: str or None
    :param path: If given, look for files in this directory rather than the one
        specified in the structure property
    :type read_trj: bool
    :param read_trj: Read trajectory analysis volume data if True, else will
        read the static structure analysis data.
    :rtype: (See read_volume_file or read_trj_volume_file function, list)
    :return: The volume data collected from struct, and a list of paths to files
        that could not be read
    """
    read_method = read_trj_volume_file if read_trj else read_volume_file
    data, failures = get_file_data(struct,
                                   VOL_FILE_PROP,
                                   read_method,
                                   override_path=path)
    return data, failures 
[docs]def get_voids_data(struct, path=None, read_trj=False):
    """
    Get all the void data available for the given structure
    :type struct: `schrodigner.structure.Structure`
    :param struct: The structure to get volume data for
    :type path: str or None
    :param path: If given, look for files in this directory rather than the one
        specified in the structure property
    :type read_trj: bool
    :param read_trj: Read trajectory analysis void data if True, else will
        read the static structure analysis data.
    :rtype: (See read_void_file or read_trj_void_file function, list)
    :return: The volume data collected from struct, and a list of paths to the
        files that could not be read.
    """
    read_method = read_trj_void_file if read_trj else read_void_file
    data, failures = get_file_data(struct,
                                   VOID_FILE_PROP,
                                   read_method,
                                   override_path=path)
    return data, failures 
[docs]def create_surface_from_void(void, void_info):
    """
    Create void surface
    :type void: `Void`
    :param void: The void to make a surface for
    :type void_info: `VoidInfo`
    :param void_info: The VoidInfo object for this void
    :rtype: `surface.Surface`
    :return: surface created using the void and void info
    """
    # Each non zero grid point is a list of x,y,z,value, where value represents
    # the occupancy of the node in the grid.
    nonzero = [list(x) + [1.0] for x in void.nodes]
    # Create the surface
    raw_surf = surface.create_isosurface_from_grid_data(
        void_info.numxyz,
        void_info.spacing,
        void_info.origin,
        0.5,
        nonzero=nonzero,
        surface_color=color.Color('pink'),
        name=void.surf_name,
        comment=void.surf_comment,
        surface_type='Free volume')
    return raw_surf 
[docs]class JsonObject(object):
    """
    Base class for custom objects that can be serialized and deserialized by
    json
    """
[docs]    def jsonData(self):
        """
        Return a json-serializable object for this instance
        :rtype: dict
        :return: A json-serializable object for this instance
        """
        return self.__dict__ 
[docs]    @classmethod
    def fromJsonData(cls, data):
        """
        Create a new VoidInfo object from json data
        :type data: dict
        :param data: A dict such as returned by the jsonData method
        :rtype: class
        :return: A new class object created using the data in dict
        """
        return cls(**data)  
[docs]class VoidInfo(JsonObject):
    """
    Holds general information about the void calculation
    """
[docs]    def __init__(self, origin, numxyz, spacing, periodic, probe):
        """
        Create a VoidInfo object
        :type origin: iterable
        :param origin: The x, y, z coordinates of the grid origin
        :type numxyz: iterable
        :param numxyz: The number of gridpoints in the x, y and z direction
        :type spacing: iterable
        :param spacing: The spacing of gridpoints in the x, y and z direction
        :type periodic: bool
        :param periodic: Whether the grid is periodic
        :type probe: float
        :param probe: The probe radius used in the void calculation
        """
        self.origin = list(origin)
        self.numxyz = list(numxyz)
        self.periodic = bool(periodic)
        self.spacing = list(spacing)
        self.probe = probe 
[docs]    def getNodeXYZ(self, node):
        """
        Get the xyz coordinates a node represents
        :type node: tuple
        :param node: A graph node (a, b, c) for coordinate a, b, c in the grid
        :rtype: list
        :return: [x, y, z] coordinates of the node
        """
        return [self.origin[a] + self.spacing[a] * node[a] for a in range(3)]  
[docs]@functools.total_ordering
class Void(JsonObject):
    """
    Holds information about a single Void
    """
[docs]    def __init__(self,
                 size=0,
                 volume=0,
                 nodes=None,
                 surf_name=None,
                 surf_comment=None,
                 surf_file=None):
        """
        Create a Void instance
        :type size: int
        :param size: The number of nodes in the void
        :type volume: float
        :param volume: The volume the void represents
        :type nodes: iterable
        :param nodes: The collection of grid nodes that make up the void
        :type surf_name: str
        :param surf_name: Name of the surface of the void
        :type surf_comment: str
        :param surf_comment: Comment for the surface of the void
        :type surf_file: str
        :param surf_file: Name of the surface file associated with the current
            void
        """
        self.size = size
        self.volume = volume
        # We explicitly convert to tuples here because json serializes tuples as
        # lists and sometimes we create this class from json data.
        self.nodes = [tuple(x) for x in nodes]
        self.surf_name = surf_name
        self.surf_comment = surf_comment
        self.surf_file = surf_file 
    def __eq__(self, other):
        """
        """
        return self.size == other.size
    def __lt__(self, other):
        """
        """
        return self.size < other.size
[docs]    def getSurfaceNodes(self, vinfo):
        """
        Find all the surface nodes in the void
        :type vinfo: `VoidInfo`
        :param vinfo: The VoidInfo object used to compute the void
        """
        surface = []
        nodeset = set(self.nodes)
        for node in self.nodes:
            # Check each of six neighbors to see if they exist, If any neighbor
            # does not exist, this is a surface node.
            for neighbor in amorphous.XYZVolumeGraph.gridNeighbors(
                    node, vinfo.numxyz, vinfo.pbc):
                if neighbor not in nodeset:
                    break
            surface.append(node)
        return surface  
[docs]class FreeVolumeException(Exception):
    """
    Raised if something goes wrong with computing the Free Volume
    """ 
[docs]class FreeVolumeDriver(object):
    """ Driver to compute Free Volume """
[docs]    def __init__(self,
                 grid=SPACING,
                 probe=PROBE_RADIUS,
                 generic_title=GENERIC_TITLE,
                 logger=None,
                 verbose=True,
                 only_large_void_surf=True):
        """
        Create a FreeVolumeDriver instance
        :type grid: float
        :param grid: The requested grid spacing
        :type probe: float
        :param probe: The probe radius
        :type generic_title: str
        :param generic_title: The title to use if a structure does not have one
        :type logger: None or `logging.logger`
        :param logger: If None, no logging will be done and FreeVolumeException
            will be raised if an error occurs. If a logger, logging will be
            done and the system will exit on an error.
        :type verbose: bool
        :param verbose: Turns on additional logging
        :type only_large_void_surf: bool
        :param only_large_void_surf: Will calculate surface of all voids if
            True. If case of False it will calculate surface of voids with
            volume greater than 1 A^3.
        """
        self.grid = grid
        self.probe = probe
        self.logger = logger
        self.verbose = verbose
        self.cleaner = jobutils.StringCleaner()
        self.voids = []
        self.struct = None
        self.void_count = 0
        self.smallest_void = sys.maxsize
        self.largest_void = 0
        self.fid = None
        self.only_large_void_surf = only_large_void_surf 
[docs]    def log(self, msg, is_verbose=False, **kwargs):
        """
        Log a message if logging is being used
        :type msg: str
        :param msg: The message to log
        Additional keyword arguments are passed to `textlogger.log_msg`
        """
        if is_verbose and not self.verbose:
            return
        if self.logger:
            if not self.verbose:
                kwargs['pad'] = False
            kwargs['logger'] = self.logger
            textlogger.log_msg(msg, **kwargs) 
[docs]    def logError(self, msg, **kwargs):
        """
        Log an error message if logging is being used and exit, otherwise raise
        an Exception.
        :type msg: str
        :param msg: The message to log
        Additional keyword arguments are passed to `textlogger.log_error`
        :raise FreeVolumeException: if self.logger
        :raise SystemExit: if not self.logger
        """
        if self.logger:
            kwargs['logger'] = self.logger
            textlogger.log_error(msg, **kwargs)
        else:
            raise FreeVolumeException(msg) 
[docs]    def findFreeVolume(self, struct):
        """
        Find the free volume in the structure. Results are stored as properties
        on the structure
        :type struct: `schrodinger.structure.Structure`
        :param struct: The structure to find the free volume in
        :raise FreeVolumeException: if the PBC dimensions are too small or
            memory usage grows too large and no logger exists
        :raise SystemExit: if the PBC dimensions are too small or
            memory usage grows too large and a logger exists
        """
        self.log('Dividing system using grid spacing of %.2f...' % self.grid,
                 pad=True)
        self.graph = amorphous.XYZVolumeGraph(struct, spacing=self.grid)
        spacing = self.graph.xyz_spacings
        self.volume_per_node = spacing[0] * spacing[1] * spacing[2]
        self.log('Grid spacing adjusted to partition space equally.',
                 is_verbose=True)
        self.log(
            'Requested spacing: %.2f, actual spacing: %.2fx, %.2fy, %.2fz' %
            (self.grid, spacing[0], spacing[1], spacing[2]),
            is_verbose=True)
        self.log('Finding points not occupied by atoms...')
        if self.verbose:
            plogger = self.logger
        else:
            plogger = None
        # Check to ensure that the PBC is large enough for the probe
        if self.graph.pbc:
            dcell_distance = self.graph.getDistanceCellDistance(
                struct, self.probe)
            lengths = self.graph.pbc.getBoxLengths()
            minlen = min(lengths)
            if minlen <= dcell_distance:
                allowed_probe = minlen - dcell_distance + self.probe
                if allowed_probe <= 0.0:
                    ending = 'too small for a free volume calculation.'
                else:
                    ending = ('too small for a probe radius of %.3f. Use a '
                              'probe radius smaller than %.4f.' %
                              (self.probe, allowed_probe))
                self.logError('The minimum dimension of the system periodic '
                              'boundary condition is %.3f, which is %s' %
                              (minlen, ending))
        try:
            self.graph.locateVoids(probe=self.probe, logger=plogger)
        except amorphous.VolumeMemoryError as msg:
            self.logError(str(msg))
        self.total_volume = self.volume_per_node * self.graph.total_points
        self.log('Gridded volume of entire system = %.3f A^3' %
                 self.total_volume)
        self.struct = struct
        try:
            self.defineVoids()
        except amorphous.VolumeMemoryError as msg:
            self.logError(str(msg)) 
[docs]    def getVoidVolumes(self):
        """
        Get the volume of each void
        :rtype: list
        :return: A list of void volumes. Each item is a float.
        """
        return [x.volume for x in self.voids] 
[docs]    def getPropName(self, propbase):
        """
        Get the property name with grbit for the class added as a suffix
        to the passed property base name.
        :type propbase: str
        :param propbase: the property basename (prefix) to add the grbit to
        :rtype: str
        :return: property name with grbit added to it
        """
        grbit = GRID_RAD_BIT % (self.grid, self.probe)
        return propbase + grbit 
[docs]    def getFreeVolumeData(self):
        """
        Calculate the absolute free volume and free volume percentage from the
        analyzed graph
        :rtype: (float, float)
        :return: Tuple with two values. First is the absolute free volume and
            second is the free volume percentage.
        """
        free_volume = self.volumeOfGraph()
        pct_volume = 100 * free_volume / self.total_volume
        return free_volume, pct_volume 
[docs]    def defineVoids(self):
        """
        Define all the voids found in the structure
        """
        self.voids = []
        self.void_count = 0
        self.log('Defining voids...')
        for void in self.graph.voids():
            num_nodes = len(void)
            volume = num_nodes * self.volume_per_node
            this_void = Void(num_nodes, volume, void)
            self.voids.append(this_void)
            self.void_count += 1
        if self.void_count:
            self.largest_void = self.voids[0]
            self.smallest_void = self.voids[-1]
        self.log('Found %d voids' % self.void_count, pad=True, is_verbose=True)
        if self.void_count:
            self.log('Largest void: %.2f' % self.largest_void.volume)
            self.log('Smallest void: %.2f' % self.smallest_void.volume,
                     is_verbose=True)
        free_volume, pct_volume = self.getFreeVolumeData()
        self.log('Free volume = %.2f A^3' % free_volume)
        self.log('Free volume percent = %.2f%%' % pct_volume)
        self.struct.property[self.getPropName(FREE_VOLUME_PROP)] = free_volume
        self.struct.property[self.getPropName(
            FREE_VOLUME_PCT_PROP)] = pct_volume 
[docs]    def volumeOfGraph(self):
        """
        Get the current total volume of the graph
        :rtype: float
        :return: The volume of all the remaining nodes in the graph
        """
        return self.volume_per_node * len(self.graph.graph) 
[docs]    def getVoidData(self, voids_filename, archive_data=True):
        """
        Get the void data from the current analysis
        :type voids_filename: str
        :param voids_filename: name of the voids json file
        :type archive_data: bool
        :param archive_data: whether to archive void data. Archiving helps
            transferring of files easier.
        :rtype: dict
        :return: Keys are INFO and VOIDS. INFO value is a `VoidInfo` object,
            VOIDS value is a list of `Void` objects sorted from largest to
            smallest.
        """
        # Get void info
        void_info = VoidInfo(self.graph.shifted_origin,
                             self.graph.num_xyz, self.graph.xyz_spacings,
                             bool(self.graph.pbc), self.probe)
        # Make folder to hold all surface viz files
        basename = voids_filename.replace(VOIDS_FILE_ENDING, '')
        surf_folder = basename + SURF_FILE_FOLDER
        fileutils.force_rmtree(surf_folder, True)
        os.makedirs(surf_folder)
        # Create surfaces for all voids
        self.createSurfaces(self.voids, void_info, surf_folder)
        if archive_data:
            # Zip surface files for faster transfer
            tar_file = surf_folder + TAR_ENDING
            archive_folder(tar_file, surf_folder)
        # Store data
        void_data = {
            INFO: void_info.jsonData(),
            VOIDS: [x.jsonData() for x in self.voids]
        }
        return void_data 
[docs]    def createSurfaces(self, voids, void_info, surf_folder):
        """
        Create a surfaces for voids larger than 1 A^3 and store them inside
        surf folder. Also set the name, comment, and location of the created
        surface file for the void as an attribute in the void.
        :type void: `Void`
        :param void: The void to make a surface for
        :type void_info: `VoidInfo`
        :param void_info: The VoidInfo object for this void
        :type surf_folder: str
        :param surf_folder: Folder to store all the surface files inside
        """
        vol_sa_vals = []
        for index, void in enumerate(voids):
            void_volume = void.volume
            # Set surface name and comment
            if self.fid is None:
                name = SURF_NAME % (void_volume, self.probe, self.grid, index)
            else:
                name = SURF_TRJ_NAME % (void.volume, self.probe, self.grid,
                                        self.fid, index)
            void.surf_comment = SURF_COMMENT % (self.probe, self.grid)
            void.surf_name = name
            # Calculate surface only when volume is larger than 1A
            if void.volume > 1.0 or not self.only_large_void_surf:
                raw_surf = create_surface_from_void(void, void_info)
                filename = f'surface_{index}{SURF_FILE_ENDING}'
                void.surf_file = os.path.join(surf_folder, filename)
                raw_surf.write(void.surf_file)
                vol_sa_vals.append((void.volume, raw_surf.surface_area))
            else:
                void.surf_file = None
        # Write surface areas for voids, this is not used by the viewer
        if vol_sa_vals:
            numpy.savetxt(os.path.join(surf_folder, 'surface_area.csv'),
                          vol_sa_vals,
                          header='Volume SurfaceArea') 
[docs]    def writeFiles(self, basename=None):
        """
        Write all the data to files
        The actual node graph is written to a pkl file
        The void data is written to a json file
        The volumes are written to a simple text file, one volume per line
        The structure is written to a Maestro file
        :type basename: str
        :param basename: Use this instead of the structure title as the base
            file name
        :rtype: str, str, str
        :return: The paths to the files written (volume file, void file, graph
            file)
        """
        names = self._formFileNamesAndProps(basename)
        volume_name, voids_name, mae_name = names
        # Volume file
        with open(volume_name, 'w') as volfile:
            for void in self.voids:
                volfile.write(str(void.volume) + '\n')
        self.log('Void volumes written to %s' % volume_name, is_verbose=True)
        # Void file
        void_data = self.getVoidData(voids_name)
        with open(voids_name, 'w') as voidfile:
            json.dump(void_data, voidfile)
        self.log('Clusters written to %s' % voids_name, is_verbose=True)
        # MAE file
        fileutils.force_remove(mae_name)
        self.struct.write(mae_name)
        self.log('Structure written to %s' % mae_name, is_verbose=True)
        return volume_name, voids_name, mae_name 
[docs]    def getBasename(self):
        """
        Get the basename from the structure
        :rtype: str
        :return: basename from structure title or the generic name cleaned
            and uniquified.
        """
        return self.cleaner.cleanAndUniquify(self.struct.title or GENERIC_TITLE) 
    def _formFileNamesAndProps(self, basename):
        """
        Form the file and structure property names for this instance of the
        driver and then store the paths to these files in the structure
        properties.
        :type basename: str or None
        :param basename: If not None, Use this instead of the structure title as
            the base file name
        :rtype: (str, str, str, str)
        :return: The names of the volume file, void file, graph file and Maestro
            file
        """
        volprop = self.getPropName(VOL_FILE_PROP)
        voidprop = self.getPropName(VOID_FILE_PROP)
        maeprop = MAE_FILE_PROP
        jobutils.set_source_path(self.struct)
        if not basename:
            basename = self.getBasename()
        volfile = self.getPropName(basename) + VOL_FILE_ENDING
        voidfile = self.getPropName(basename) + VOIDS_FILE_ENDING
        maefile = basename + MAE_FILE_ENDING
        self.struct.property[volprop] = volfile
        self.struct.property[voidprop] = voidfile
        self.struct.property[maeprop] = maefile
        return volfile, voidfile, maefile
[docs]    def clearGraph(self):
        """
        Clear out the graph for this driver to preserve memory space
        """
        self.graph = None