Source code for schrodinger.application.matsci.nano.surfaces_interfaces_mod

"""
Classes and functions to enumerate surfaces and interfaces.

Copyright Schrodinger, LLC. All rights reserved."""

# Contributor: Thomas F. Hughes

import argparse
import itertools

from schrodinger.application.matsci import parserutils
from schrodinger.application.matsci import textlogger
from schrodinger.application.matsci.nano import interface_mod
from schrodinger.application.matsci.nano import slab
from schrodinger.application.matsci.nano import xtal
from schrodinger.application.matsci import jobutils
from schrodinger.utils import cmdline
from schrodinger.utils import fileutils

MSGWIDTH = 100

LATTICE_PARAM_KEYS = [xtal.Crystal.A_KEY, xtal.Crystal.B_KEY, \
    xtal.Crystal.C_KEY, xtal.Crystal.ALPHA_KEY, xtal.Crystal.BETA_KEY, \
    xtal.Crystal.GAMMA_KEY]

BASE_ASU_FLAG = '-ref_xtal_asu'
BASE_OPTIONS_FLAG = '-ref_surface_options'
ADSORPTION_ASU_FLAG = '-ads_xtal_asu'
ADSORPTION_OPTIONS_FLAG = '-ads_surface_options'
INTERFACE_OPTIONS_FLAG = '-interface_options'


[docs]def get_base_names(ref_xtal_asu, ref_base_name_in=None, ads_xtal_asu=None, ads_base_name_in=None): """ Return the reference, adsorption, and final combined base names. :type ref_xtal_asu: str :param ref_xtal_asu: reference xtal input file :type ref_base_name_in: str or None :param ref_base_name_in: reference base name or None if none has been given :type ads_xtal_asu: str or None :param ads_xtal_asu: adsorption xtal input file or None if none has been given :type ads_base_name_in: str or None :param ads_base_name_in: adsorption base name or None if none has been given :rtype: str, str or None, str :return: reference, adsorption, and final combined base names, the adsorption base name will be None if no adsorption xtal asu is provided """ if not ref_base_name_in: ref_base_name_out, ext = fileutils.splitext(ref_xtal_asu) else: ref_base_name_out = ref_base_name_in final_base_name = ref_base_name_out if ads_xtal_asu: if not ads_base_name_in: ads_base_name_out, ext = fileutils.splitext(ads_xtal_asu) else: ads_base_name_out = ads_base_name_in final_base_name += '_' + ads_base_name_out else: ads_base_name_out = None return ref_base_name_out, ads_base_name_out, final_base_name
[docs]class ParserWrapper(object): """ Manages the argparse module to parse user command line arguments. """
[docs] def __init__(self, scriptname, description): """ Create a ParserWrapper instance and process it. :type scriptname: str :param scriptname: name of this script :type description: str :param description: description of this script """ name = '$SCHRODINGER/run ' + scriptname self.parserobj = parserutils.DriverParser( prog=name, description=description, add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter)
[docs] def loadIt(self): """ Load ParserWrapper with options. """ ref_xtal_asu_help = """Specify a Maestro file containing a crystalline ASU from which to create reference surfaces.""" self.parserobj.add_argument(BASE_ASU_FLAG, type=str, required=True, help=ref_xtal_asu_help) ref_surface_options_help = """Specify a file containing input options for building reference surfaces. See the surface builder help ($SCHRODINGER/run surface.py -h) for available options. Instead of the -x_index (x = h, k, or l) options the options -x_index_min and -x_index_max can be used to enumerate hkl triples. By default these ranges are %s, %s, and %s for h, k, and l, respectively. Such triples may also be specified directly using the -hkl_indices option which takes whitespace separated integers and internally groups them by order into triples. Note that the option -input_file is not needed here.""" h_range = [slab.H_INDEX_MIN_DEFAULT, slab.H_INDEX_MAX_DEFAULT] k_range = [slab.K_INDEX_MIN_DEFAULT, slab.K_INDEX_MAX_DEFAULT] l_range = [slab.L_INDEX_MIN_DEFAULT, slab.L_INDEX_MAX_DEFAULT] self.parserobj.add_argument(BASE_OPTIONS_FLAG, type=str, help=ref_surface_options_help % (h_range, k_range, l_range)) ads_xtal_asu_help = """Specify a Maestro file containing a crystalline ASU from which to create epitaxial surfaces. If specified then all possible interfaces (see the %s option) will be enumerated by taking reference/epitaxial surface pairs.""" self.parserobj.add_argument(ADSORPTION_ASU_FLAG, type=str, help=ads_xtal_asu_help % INTERFACE_OPTIONS_FLAG) ads_surface_options_help = """Specify a file containing input options for building epitaxial surfaces. These options are analogous to those used in %s.""" self.parserobj.add_argument(ADSORPTION_OPTIONS_FLAG, type=str, help=ads_surface_options_help % BASE_OPTIONS_FLAG) interface_options_help = """Specify a file containing input options for building interfaces. See the interface builder help ($SCHRODINGER/run interface.py -h) for available options. Note that the options -ref_layer and -ads_layer are not needed here.""" self.parserobj.add_argument(INTERFACE_OPTIONS_FLAG, type=str, help=interface_options_help) ref_base_name_help = """Specify a base name to use in naming reference-related output files. If none is provided then the base name of the file given for %s will be used.""" self.parserobj.add_argument('-ref_base_name', type=str, help=ref_base_name_help % BASE_ASU_FLAG) ads_base_name_help = """Specify a base name to use in naming epitaxial-related output files. If none is provided then the base name of the file given for %s will be used.""" self.parserobj.add_argument('-ads_base_name', type=str, help=ads_base_name_help % ADSORPTION_ASU_FLAG) self.parserobj.add_argument('-verbose', action='store_true', help='Turn on verbose printing.') jc_options = [cmdline.HOST, cmdline.NOJOBID, cmdline.SAVE, \ cmdline.WAIT] cmdline.add_jobcontrol_options(self.parserobj, options=jc_options)
[docs] def parseArgs(self, args): """ Parse the command line arguments. :type args: tuple :param args: command line arguments """ self.options = self.parserobj.parse_args(args)
def _get_flags_from_file(afile): """ Return a list of flags from the given file. :type afile: str :param afile: file name :rtype: list :return: contains flags from the given file """ flags = [] with open(afile) as file_obj: for line in file_obj.read().splitlines(): alist = line.split() flags.append(alist[0]) if len(alist) > 1: try: list(map(int, alist[1:])) except ValueError: flags.append(' '.join(alist[1:])) else: flags.extend(alist[1:]) return flags
[docs]def get_surface_kwargs(options_file): """ Return a dictionary of surface options from the given options file. :type options_file: str or None :param options_file: contains options for the surface build or None if there are none in which case the defaults are used :rtype: dict :return: contains options for the surface build """ if options_file: args = _get_flags_from_file(options_file) else: args = '' parser = slab.ParserWrapper('surface.py', 'Build it.') parser.loadEnumeration() parser.loadOptions() parser.parseArgs(args) return vars(parser.options)
[docs]def get_interface_kwargs(options_file): """ Return a dictionary of interface options from the given options file. :type options_file: str or None :param options_file: contains options for the interface build or None if there are none in which case the defaults are used :rtype: dict :return: contains options for the interface build """ if options_file: args = _get_flags_from_file(options_file) else: args = '' parser = interface_mod.ParserWrapper('interface.py', 'Build it.') parser.loadOptions() parser.parseArgs(args) return vars(parser.options)
[docs]def get_hkl_indices(flattened_hkl_indices=None, h_min=0, h_max=0, k_min=0, k_max=0, l_min=0, l_max=0): """ Collect and return all hkl Miller index triples sorted by increasing l then k then h. :type flattened_hkl_indices: list or None :param flattened_hkl_indices: flattened hkl indices, for example 100, 110, etc. as [1, 0, 0, 1, 1, 0, ...] :type h_min: int :param h_min: minimum h index :type h_max: int :param h_max: maximum h index :type k_min: int :param k_min: minimum k index :type k_max: int :param k_max: maximum k index :type l_min: int :param l_min: minimum l index :type l_max: int :param l_max: maximum l index :raise ValueError: if there is an issue :rtype: list :return: contains sorted tuples of hkl triples """ all_hkl_indices = [] h_range = range(h_min, h_max + 1) k_range = range(k_min, k_max + 1) l_range = range(l_min, l_max + 1) hkl_indices = [ atuple for atuple in itertools.product(h_range, k_range, l_range) ] all_hkl_indices.extend(hkl_indices) if flattened_hkl_indices: size = 3 if len(flattened_hkl_indices) % size: msg = ('The list of flattened hkl indices must be a ' f'multiiple of {size}.') raise ValueError(msg) indices = range(0, len(flattened_hkl_indices), size) hkl_indices = [ tuple(flattened_hkl_indices[idx:idx + size]) for idx in indices ] all_hkl_indices.extend(hkl_indices) all_hkl_indices = set(all_hkl_indices) all_hkl_indices.discard((0, 0, 0)) if not all_hkl_indices: msg = ('No valid hkl Miller index triples could be found.') raise ValueError(msg) all_hkl_indices = sorted(all_hkl_indices, key=lambda x: (x[2], x[1], x[0])) return all_hkl_indices
[docs]def write_to_file(file_name, structs): """ Write the structures to file with the feature's WAM type :param str file_name: The path to the file :param list structs: List of structures to write to file """ jobutils.write_mae_with_wam(structs, file_name, jobutils.WAM_TYPES.MS_SURFACES_INTERFACES)
[docs]class Surfaces(object): """ Manage the enumeration of surfaces. """
[docs] def __init__(self, xtal_asu, surface_kwargs=None, logger=None): """ Create an instance. :type xtal_asu: `schrodinger.structure.Structure` :param xtal_asu: the crystalline ASU from which to create surfaces :type surface_kwargs: None or dict :param surface_kwargs: kwargs for the surface build or None if there are none in which case the defaults will be used :type logger: logging.Logger or None :param logger: output logger or None if there isn't one """ self.xtal_asu = xtal_asu self.surface_kwargs = surface_kwargs if not self.surface_kwargs: self.surface_kwargs = {} self.logger = logger self.hkl_indices = [] self.xtal_cell = None self.surfaces = []
[docs] def getHKLIndices(self): """ Collect and return all hkl Miller index triples for this surface enumeration and sort them according to increasing l then k then h. :rtype: list :return: contains sorted tuples of hkl triples """ flattened_hkl_indices = self.surface_kwargs.pop('hkl_indices') h_min = self.surface_kwargs.pop('h_index_min') h_max = self.surface_kwargs.pop('h_index_max') l_min = self.surface_kwargs.pop('l_index_min') l_max = self.surface_kwargs.pop('l_index_max') k_min = self.surface_kwargs.pop('k_index_min') k_max = self.surface_kwargs.pop('k_index_max') self.hkl_indices = get_hkl_indices( flattened_hkl_indices=flattened_hkl_indices, h_min=h_min, h_max=h_max, k_min=k_min, k_max=k_max, l_min=l_min, l_max=l_max) return self.hkl_indices
[docs] def getSurface(self, cell, hkl, logger=None): """ Build and return a slab.Surface. :type cell: `schrodinger.structure.Structure` :param cell: a cell :type hkl: tuple :param hkl: a triple of Miller indices :type logger: logging.Logger or None :param logger: output logger or None if there isn't one :rtype: slab.Surface :return: the surface object """ h_index, k_index, l_index = hkl surface = slab.Surface(cell, h_index=h_index, k_index=k_index, l_index=l_index, logger=logger, **self.surface_kwargs) surface.runIt() return surface
[docs] def logParams(self): """ Log the parameters. """ if self.logger is None: return num_surfaces = textlogger.get_param_string('Number of surfaces', len(self.hkl_indices), MSGWIDTH) self.logger.info(num_surfaces) self.logger.info('')
[docs] def getXtalCell(self): """ Build and return the crystal cell from which surfaces will be created. :rtype: `schrodinger.structure.Structure` :return: the crystal cell """ self.xtal_cell = xtal.get_cell(self.xtal_asu) self.xtal_cell = xtal.make_p1(self.xtal_cell) return self.xtal_cell
[docs] def getSurfaces(self): """ Build and return the slab.Surface objects for all surfaces. :rtype: list of slab.Surface :return: contains surface objects """ # FIXME - see MATSCI-2277, this can be done faster in parallel for hkl in self.hkl_indices: surface = self.getSurface(self.xtal_cell, hkl, logger=self.logger) self.surfaces.append(surface) return self.surfaces
[docs] def writeSurfaces(self, file_name): """ Write surfaces to a Maestro file with the given file name. :type file_name: str :param file_name: file name of the Maestro file """ if self.surfaces: write_to_file(file_name, [surface.surface for surface in self.surfaces])
[docs] def runIt(self): """ Create the surfaces. """ # FIXME - see MATSCI-2278, the hkl triples should probably be uniquified self.getHKLIndices() self.logParams() self.getXtalCell() self.getSurfaces()
[docs]class Interfaces(object): """ Manage the enumeration of interfaces. """
[docs] def __init__(self, ref_surfaces, ads_surfaces, interface_kwargs=None, logger=None): """ Create an instance. :type ref_surfaces: list of `schrodinger.structure.Structure` :param ref_surfaces: reference surface ASUs from which interfaces will be created :type ads_surfaces: list of `schrodinger.structure.Structure` :param ads_surfaces: adsorption surface ASUs from which interfaces will be created :type interface_kwargs: None or dict :param interface_kwargs: kwargs for the interface builds or None if there are none in which case the defaults will be used :type logger: logging.Logger or None :param logger: output logger or None if there isn't one """ self.ref_surfaces = ref_surfaces self.ads_surfaces = ads_surfaces self.interface_kwargs = interface_kwargs self.logger = logger self.interfaces = []
[docs] def logParams(self): """ Log the parameters. """ if self.logger is None: return num_interfaces = textlogger.get_param_string( 'Number of interfaces', len(self.ref_surfaces) * len(self.ads_surfaces), MSGWIDTH) self.logger.info(num_interfaces) self.logger.info('')
[docs] def getInterface(self, ref_surface, ads_surface): """ Build and return a interface_mod.Interface. :type ref_surface: `schrodinger.structure.Structure` :param ref_surface: reference surface ASU :type ads_surface: `schrodinger.structure.Structure` :param ads_surface: adsorption surface ASU :rtype: interface_mod.Interface :return: the interface object """ interface = interface_mod.Interface(ref_surface, ads_surface, logger=self.logger, **self.interface_kwargs) interface.runIt() return interface
[docs] def getInterfaces(self): """ Build and return the interface_mod.Interface objects for all interfaces. :rtype: list of interface_mod.Interface :return: contains interface objects """ # FIXME - see MATSCI-2277, this can be done faster in parallel for ref_surface in self.ref_surfaces: for ads_surface in self.ads_surfaces: interface = self.getInterface(ref_surface, ads_surface) self.interfaces.append(interface) return self.interfaces
[docs] def writeInterfaces(self, file_name): """ Write interfaces to a Maestro file with the given file name. :type file_name: str :param file_name: file name of the Maestro file """ if self.interfaces: write_to_file( file_name, [interface.interface for interface in self.interfaces])
[docs] def runIt(self): """ Create the interfaces. """ self.logParams() self.getInterfaces()
[docs]class SurfacesInterfaces(object): """ Manage the enumeration of surfaces and interfaces. """
[docs] def __init__(self, ref_xtal_asu, ref_surface_kwargs=None, ads_xtal_asu=None, ads_surface_kwargs=None, interface_kwargs=None, logger=None): """ Create an instance. :type ref_xtal_asu: `schrodinger.structure.Structure` :param ref_xtal_asu: the crystalline ASU from which to create reference surfaces :type ref_surface_kwargs: None or dict :param ref_surface_kwargs: kwargs for the reference surface build or None if there are none in which case the defaults will be used :type ads_xtal_asu: None or `schrodinger.structure.Structure` :param ads_xtal_asu: the crystalline ASU from which to create adsorption surfaces or None if interfaces are not needed :type ads_surface_kwargs: None or dict :param ads_surface_kwargs: kwargs for the adsorption surface build or None if there are none in which case the defaults will be used :type interface_kwargs: None or dict :param interface_kwargs: kwargs for the interface build or None if there are none in which case the defaults will be used :type logger: logging.Logger or None :param logger: output logger or None if there isn't one """ self.ref_xtal_asu = ref_xtal_asu self.ads_xtal_asu = ads_xtal_asu self.ref_surface_kwargs = ref_surface_kwargs if not self.ref_surface_kwargs: self.ref_surface_kwargs = {} self.ads_surface_kwargs = ads_surface_kwargs if not self.ads_surface_kwargs: self.ads_surface_kwargs = {} self.interface_kwargs = interface_kwargs if not self.interface_kwargs: self.interface_kwargs = {} self.logger = logger self.ref_surfaces = None self.ads_surfaces = None self.interfaces = None
[docs] def checkInput(self): """ Check input. :raise ValueError: if any input crystal ASU is missing any lattice parameter structure properties """ asus = [self.ref_xtal_asu] if self.ads_xtal_asu: asus.append(self.ads_xtal_asu) try: list(map(xtal.get_lattice_param_properties, asus)) except KeyError: msg = ('At least one of the input crystal ASUs is ' 'missing some lattice parameter structure ' 'properties, i.e. %s.') raise ValueError(msg % LATTICE_PARAM_KEYS)
[docs] def createSurfaces(self): """ Create the surfaces. :raise ValueError: if something goes wrong with either surface build """ self.ref_surfaces = Surfaces(self.ref_xtal_asu, \ surface_kwargs=self.ref_surface_kwargs, logger=self.logger) if self.logger: header = 'Reference Surfaces' self.logger.info(header) self.logger.info('-' * len(header)) try: self.ref_surfaces.runIt() except ValueError as err: msg = ('Working on reference surfaces.') raise ValueError(' '.join([msg, str(err)])) if self.ads_xtal_asu: self.ads_surfaces = Surfaces(self.ads_xtal_asu, \ surface_kwargs=self.ads_surface_kwargs, logger=self.logger) if self.logger: header = 'Epitaxial Surfaces' self.logger.info(header) self.logger.info('-' * len(header)) try: self.ads_surfaces.runIt() except ValueError as err: msg = ('Working on epitaxial surfaces.') raise ValueError(' '.join([msg, str(err)]))
[docs] def createInterfaces(self): """ Create the interfaces. """ afunc = lambda x: [surface.surface.copy() for surface in x.surfaces] ref_surfaces, ads_surfaces = list( map(afunc, [self.ref_surfaces, self.ads_surfaces])) self.interfaces = Interfaces(ref_surfaces, ads_surfaces, interface_kwargs=self.interface_kwargs, logger=self.logger) if self.logger: header = 'Interfaces' self.logger.info(header) self.logger.info('-' * len(header)) self.interfaces.runIt()
[docs] def runIt(self): """ Create the surfaces and interfaces. """ self.checkInput() self.createSurfaces() if self.ads_xtal_asu: self.createInterfaces()