"""
Classes and functions to create interface models.
Copyright Schrodinger, LLC. All rights reserved."""
# Contributor: Thomas F. Hughes
import argparse
import math
from past.utils import old_div
import numpy
from schrodinger.application.desmond.constants import IS_INFINITE
from schrodinger.application.matsci import parserutils
from schrodinger.application.matsci import textlogger
from schrodinger.application.matsci.nano import slab
from schrodinger.application.matsci.nano import xtal
from schrodinger.structutils import transform
_version = '$Revision 0.0 $'
REFERENCE = 'reference'
ADSORPTION = 'epitaxial'
BOTTOM_LAYER_CHOICES = [REFERENCE, ADSORPTION]
BOTTOM_LAYER_DEFAULT = REFERENCE
STRAIN_A_DEFAULT = 0.5
STRAIN_B_DEFAULT = 0.5
STRAIN_ALPHA_DEFAULT = 5.0
STRAIN_BETA_DEFAULT = 5.0
STRAIN_GAMMA_DEFAULT = 5.0
SEPARATION_DEFAULT = 0.0
MAX_MULT = 10
MAX_EXTENTS_DEFAULT = MAX_MULT
TRANSLATE_A_DEFAULT = 0.0
TRANSLATE_B_DEFAULT = 0.0
INTERFACE = 'interface'
ENTRY_NAME_KEY = 's_m_entry_name'
# indices to individual parameters in the list of six
# lattice parameters
A_IDX, B_IDX, C_IDX, ALPHA_IDX, BETA_IDX, GAMMA_IDX = list(range(6))
CHOICE_OFF = xtal.ParserWrapper.CHOICE_OFF
CHOICE_ON = xtal.ParserWrapper.CHOICE_ON
XTAL_DICT = dict(
    list(zip(['bonding', 'bond_orders', 'translate'], 3 * [CHOICE_OFF])))
PARAM_NAMES = dict(
    list(zip(list(range(6)), ['a', 'b', 'c', 'alpha', 'beta', 'gamma'])))
[docs]def lcm_approx(smaller,
               larger,
               tolerance=0.0,
               max_mult=MAX_MULT,
               threshold=1.0e-10):
    """
    From the two given numbers return two least common multiples
    (LCMs) that are within the specified tolerance of each other.
    If the numbers are integers and the tolerance is zero then this
    function is equivalent to the traditional LCM and the two
    returned LCMs will be identical integers.  Raise a ValueError
    if either integer multiplier is larger than the given maximum.
    :type smaller: float
    :param smaller: the smaller number
    :type larger: float
    :param larger: the larger number
    :type tolerance: float
    :param tolerance: a parameter controlling the extent to which
        two floats may be considered equivalent
    :type max_mult: int
    :param max_mult: the maximum allowable multiplier, supported to
        avoid potentially very long loops given that the inputs are
        floats compared using a specified tolerance
    :type threshold: float
    :param threshold: a parameter controlling numerical precision
    :raise ValueError: if either integer multipler is larger than
        the given maximum
    :rtype: tuple, tuple
    :return: the first tuple contains the two LCMs, the second
        contains the two integer multipliers, i.e. those integers
        which when multiplied by the inputs produce the LCMs
    """
    flip = smaller > larger
    nums = [smaller, larger]
    if flip:
        nums = nums[::-1]
    tmp_smaller, tmp_larger = nums
    var_smaller, var_larger = tmp_smaller, tmp_larger
    mult_smaller, mult_larger = 1, 1
    while True:
        if var_smaller < var_larger - tolerance:
            var_smaller += tmp_smaller
            mult_smaller += 1
        if var_smaller > var_larger + tolerance:
            var_larger += tmp_larger
            mult_larger += 1
        if mult_smaller > max_mult or mult_larger > max_mult:
            raise ValueError
        if abs(var_larger - var_smaller) <= tolerance + threshold:
            break
    avars = (var_smaller, var_larger)
    amults = (mult_smaller, mult_larger)
    if flip:
        avars = avars[::-1]
        amults = amults[::-1]
    return avars, amults 
[docs]def list_to_string(alist, accuracy=3, separator=', '):
    """
    Return a formatted string containing the floats in the
    given list rounded to the given accuracy and separated
    by the given separator.
    :type alist: list
    :param alist: list of floats
    :type accuracy: int
    :param accuracy: used to round the floats
    :type separator: str
    :param separator: used to separate the rounded floats
    :rtype: str
    :return: the formatted string
    """
    return separator.join((str(round(x, accuracy)) for x in alist)) 
[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 = argparse.ArgumentParser(
            prog=name,
            description=description,
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter) 
[docs]    def loadIt(self):
        """
        Load ParserWrapper with all options.
        """
        self.loadRequired()
        self.loadOptions()
        self.loadCommon() 
[docs]    def loadRequired(self):
        """
        Load ParserWrapper with required options.
        """
        ref_layer_help = """Specify a Maestro file containing the ASU for the
            reference layer of the interface model.  The epitaxial layer (see
            -ads_layer) is adsorbed onto this reference layer which has a fixed
            geometry."""
        self.parserobj.add_argument('-ref_layer',
                                    type=str,
                                    required=True,
                                    help=ref_layer_help)
        ads_layer_help = """Specify a Maestro file containing the ASU for the
            epitaxial layer of the interface model.  This epitaxial layer is
            adsorbed onto the reference layer (see -ref_layer) and can have a
            strained geometry according to the -strain_a, -strain_b,
            -strain_alpha, -strain_beta, and -strain_gamma options."""
        self.parserobj.add_argument('-ads_layer',
                                    type=str,
                                    required=True,
                                    help=ads_layer_help) 
[docs]    def loadOptions(self):
        """
        Load ParserWrapper with options.
        """
        bot_layer_help = """Specify which layer, %s or %s, should be the
            bottom layer of the interface model.  The layers are stacked
            along the c (or thickness dimension for slab models) lattice
            vector."""
        self.parserobj.add_argument('-bot_layer',
                                    type=str,
                                    choices=BOTTOM_LAYER_CHOICES,
                                    default=BOTTOM_LAYER_DEFAULT,
                                    help=bot_layer_help %
                                    tuple(BOTTOM_LAYER_CHOICES))
        strain_a_help = """Specify by how much (in units of Angstrom) the
            epitaxial layer is allowed to be strained along its a (or u for
            slab models) lattice vector.  By strain we mean either compression
            or expansion along the a lattice vector."""
        self.parserobj.add_argument('-strain_a',
                                    type=parserutils.type_nonnegative_float,
                                    default=STRAIN_A_DEFAULT,
                                    help=strain_a_help)
        strain_b_help = """Specify by how much (in units of Angstrom) the
            epitaxial layer is allowed to be strained along its b (or v for
            slab models) lattice vector.  By strain we mean either compression
            or expansion along the b lattice vector."""
        self.parserobj.add_argument('-strain_b',
                                    type=parserutils.type_nonnegative_float,
                                    default=STRAIN_B_DEFAULT,
                                    help=strain_b_help)
        strain_alpha_help = """Specify by how much (in units of degrees) the
            epitaxial layer is allowed to be strained along its alpha angle
            (the angle between the b (or v) and c (or w) lattice vectors).  By
            strain we mean either compression or expansion along the alpha
            angle."""
        self.parserobj.add_argument('-strain_alpha',
                                    type=parserutils.type_nonnegative_float,
                                    default=STRAIN_ALPHA_DEFAULT,
                                    help=strain_alpha_help)
        strain_beta_help = """Specify by how much (in units of degrees) the
            epitaxial layer is allowed to be strained along its beta angle
            (the angle between the a (or u) and c (or w) lattice vectors).  By
            strain we mean either compression or expansion along the beta
            angle."""
        self.parserobj.add_argument('-strain_beta',
                                    type=parserutils.type_nonnegative_float,
                                    default=STRAIN_BETA_DEFAULT,
                                    help=strain_beta_help)
        strain_gamma_help = """Specify by how much (in units of degrees) the
            epitaxial layer is allowed to be strained along its gamma angle
            (the angle between the a (or u) and b (or v) lattice vectors).  By
            strain we mean either compression or expansion along the gamma
            angle."""
        self.parserobj.add_argument('-strain_gamma',
                                    type=parserutils.type_nonnegative_float,
                                    default=STRAIN_GAMMA_DEFAULT,
                                    help=strain_gamma_help)
        separation_help = """Specify the separation of the epitaxial and
            reference layers in units of Angstrom.  The layers are separated
            along the c (or thickness dimension for slab models) lattice
            vector.  If the ASUs for the reference or epitaxial layers
            contain vacuum space then it may be necessary to set a negative
            value for this option."""
        self.parserobj.add_argument('-separation',
                                    type=float,
                                    default=SEPARATION_DEFAULT,
                                    help=separation_help)
        max_extents_help = """In order to obtain the most commensurate
            interface model the reference and epitaxial layers are extended
            along the a and b lattice vectors until their extended lengths
            are within the specified strain parameters, i.e. -strain_a and/or
            -strain_b.  Depending on the input parameters the necessary
            extents can be very large which may not be the user's intent.
            This parameter controls the build by exiting if any of the extents
            are larger than this value."""
        self.parserobj.add_argument('-max_extents',
                                    type=int,
                                    default=MAX_EXTENTS_DEFAULT,
                                    help=max_extents_help)
        translate_a_help = """Specify a value in units of Angstrom by which
            the top layer will be translated, relative to the bottom layer,
            along the a lattice vector.  This option can be negative."""
        self.parserobj.add_argument('-translate_a',
                                    type=float,
                                    default=TRANSLATE_A_DEFAULT,
                                    help=translate_a_help)
        translate_b_help = """Specify a value in units of Angstrom by which
            the top layer will be translated, relative to the bottom layer,
            along the b lattice vector.  This option can be negative."""
        self.parserobj.add_argument('-translate_b',
                                    type=float,
                                    default=TRANSLATE_B_DEFAULT,
                                    help=translate_b_help)
        base_name_help = """Specify a base name that is used to name various
            output, for example the output Maestro and log files, the output
            structure title, etc.  By default a concatenation of the unit
            cell formulas of the two layers will be used."""
        self.parserobj.add_argument('-base_name', type=str, help=base_name_help) 
[docs]    def loadCommon(self):
        """
        Load ParserWrapper with common options.
        """
        self.parserobj.add_argument('-verbose',
                                    action='store_true',
                                    help='Turn on verbose printing.')
        self.parserobj.add_argument('-help',
                                    '-h',
                                    action='help',
                                    default=argparse.SUPPRESS,
                                    help='Show this help message and exit.')
        self.parserobj.add_argument(
            '-version',
            '-v',
            action='version',
            default=argparse.SUPPRESS,
            version=_version,
            help="""Show the script's version number and exit.""") 
[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)  
[docs]class Interface(object):
    """
    Manage the building of an interface model.
    """
    MSGWIDTH = 100
[docs]    def __init__(self,
                 ref_layer,
                 ads_layer,
                 bot_layer=BOTTOM_LAYER_DEFAULT,
                 strain_a=STRAIN_A_DEFAULT,
                 strain_b=STRAIN_B_DEFAULT,
                 strain_alpha=STRAIN_ALPHA_DEFAULT,
                 strain_beta=STRAIN_BETA_DEFAULT,
                 strain_gamma=STRAIN_GAMMA_DEFAULT,
                 separation=SEPARATION_DEFAULT,
                 max_extents=MAX_EXTENTS_DEFAULT,
                 translate_a=TRANSLATE_A_DEFAULT,
                 translate_b=TRANSLATE_B_DEFAULT,
                 base_name=None,
                 logger=None):
        """
        Create an instance.
        :type ref_layer: `schrodinger.structure.Structure`
        :param ref_layer: the reference layer used to define the
            interface model
        :type ads_layer: `schrodinger.structure.Structure`
        :param ads_layer: the adsorption layer used to define the
            interface model
        :type bot_layer: str
        :param bot_layer: specifies which layer, reference or adsorption,
            is the bottom layer of the interface model
        :type strain_a: float
        :param strain_a: the amount of strain allowed along the a
            lattice vector of the adsorption layer in units of Angstrom
        :type strain_b: float
        :param strain_b: the amount of strain allowed along the b
            lattice vector of the adsorption layer in units of Angstrom
        :type strain_alpha: float
        :param strain_alpha: the amount of strain allowed along the
            alpha angle of the adsorption layer in units of degrees
        :type strain_beta: float
        :param strain_beta: the amount of strain allowed along the
            beta angle of the adsorption layer in units of degrees
        :type strain_gamma: float
        :param strain_gamma: the amount of strain allowed along the
            gamma angle of the adsorption layer in units of degrees
        :type separation: float
        :param separation: the separation between the reference and
            adsorption layers in units of Angstrom
        :type max_extents: int
        :param max_extents: the maximum allowable extent
        :type translate_a: float
        :param translate_a: the amount by which to translate the top
            layer, relative to the bottom layer, along the a lattice vector
        :type translate_b: float
        :param translate_b: the amount by which to translate the top
            layer, relative to the bottom layer, along the b lattice vector
        :type base_name: str
        :param base_name: a base name used to name output
        :type logger: logging.Logger
        :param logger: output logger
        """
        self.ref_layer = ref_layer
        self.ads_layer = ads_layer
        self.bot_layer = bot_layer
        self.strain_a = strain_a
        self.strain_b = strain_b
        self.strain_alpha = strain_alpha
        self.strain_beta = strain_beta
        self.strain_gamma = strain_gamma
        self.separation = separation
        self.max_extents = max_extents
        self.translate_a = translate_a
        self.translate_b = translate_b
        self.base_name = base_name
        self.logger = logger
        self.ref_lattice_params = None
        self.ads_lattice_params = None
        self.ref_hkl = None
        self.ads_hkl = None
        self.ref_ncella = None
        self.ref_ncellb = None
        self.ads_ncella = None
        self.ads_ncellb = None
        self.strained_ads_lattice_params = None
        self.interface_lattice_params = None
        self.interface = None
        self.ref_is_infinite = None
        self.ads_is_infinite = None 
[docs]    def setIsInfinite(self):
        """
        Set the is_infinite attributes.
        """
        def set_it(cell):
            is_infinite = cell.property.get(IS_INFINITE)
            if is_infinite is not None:
                is_infinite = bool(is_infinite)
            return is_infinite
        self.ref_is_infinite = set_it(self.ref_layer)
        self.ads_is_infinite = set_it(self.ads_layer) 
[docs]    def setBaseName(self):
        """
        Set a base name.
        """
        if self.base_name is None:
            self.base_name = \
                
get_formulas_basename(self.ref_layer, self.ads_layer) 
[docs]    def setLatticeProperties(self):
        """
        Set lattice parameter attributes for both the reference and
        adsorption layers.
        """
        afunc = lambda x: list(xtal.get_lattice_param_properties(x))
        self.ref_lattice_params, self.ads_lattice_params = \
            
list(map(afunc, [self.ref_layer, self.ads_layer])) 
[docs]    def setHKL(self):
        """
        Set hkl for both the reference and adsorption layers.
        """
        self.ref_hkl, self.ads_hkl = \
            
list(map(slab.get_hkl, [self.ref_layer, self.ads_layer])) 
[docs]    def getExtentsAndStrainedLength(self, index, strain):
        """
        For the lattice vector specified with the given index
        return (1) the reference and adsorption layer extents necessary
        to bring the extended lengths to within the specified amount
        of strain and (2) the strained adsorption layer length.
        :type index: int
        :param index: an index used to specify along which direction
            to get the extents, i.e. 0 is a, 1 is b, 2 is c
        :type strain: float
        :param strain: the amount of strain allowed along the given
            lattice vector of the adsorption layer in units of Angstrom
        :raise ValueError: if either extent is larger than the allowable
            maximum
        :rtype: tuple, float
        :return: the tuple contains the reference and adsorption
            layer integer extents, the float is the strained adsorption
            layer length in units of Angstrom
        """
        lens = [
            x[index]
            for x in [self.ref_lattice_params, self.ads_lattice_params]
        ]
        try:
            extended_lens, ncells = lcm_approx(*lens,
                                               tolerance=strain,
                                               max_mult=self.max_extents)
        except ValueError:
            msg = ('At least one extent is greater than the maximum '
                   'allowable extent, i.e. %s.  You can either increase '
                   'the maximum allowable extent or increase the amount '
                   'of allowable strain to prevent this error message.')
            raise ValueError(msg % self.max_extents)
        # positive and negative deltas, i.e. extended_lens[0] -
        # extended_lens[1], indicate expansion and compression
        # strains respectively, the delta is applied to the
        # extended adsorption layer so the final strained adsorption
        # layer length is just extended_lens[0]
        strained_len = extended_lens[0]
        return ncells, strained_len 
[docs]    def getStrainedAngle(self, index, strain):
        """
        Return the strained adsorption layer angle.
        :type index: int
        :param index: the angle index
        :type strain: float
        :param strain: the allowable amount of strain in degrees
        :raise ValueError: if the reference and adsorption layer
            angles are not within the specified amount of strain
        :rtype: float
        :return: strained adsorption layer angle in units of degree
        """
        ref_angle, ads_angle = \
            
self.ref_lattice_params[index], self.ads_lattice_params[index]
        if abs(ref_angle - ads_angle) > strain:
            msg = ('The absolute difference in %s angles between the '
                   'reference and epitaxial layers, %s and %s degrees, '
                   'respectively, is larger than the given acceptable amount '
                   'of strain or %s degrees.  You can increase the amount of '
                   'allowable strain to prevent this error message.')
            raise ValueError(msg %
                             (PARAM_NAMES[index], ref_angle, ads_angle, strain))
        strained_angle = ref_angle
        return strained_angle 
[docs]    def setStrainedAdsorptionLatticeProperties(self, strained_a, strained_b,
                                               strained_alpha, strained_beta,
                                               strained_gamma):
        """
        Set the strained lattice parameter attributes for the adsorption
        layer.
        :type strained_a: float
        :param strained_a: the strained lattice a parameter in units of
            Angstrom
        :type strained_b: float
        :param strained_b: the strained lattice b parameter in units of
            Angstrom
        :type strained_alpha: float
        :param strained_alpha: the strained lattice alpha parameter in
            units of degree
        :type strained_beta: float
        :param strained_beta: the strained lattice beta parameter in
            units of degree
        :type strained_gamma: float
        :param strained_gamma: the strained lattice gamma parameter in
            units of degree
        """
        self.strained_ads_lattice_params = list(self.ads_lattice_params)
        self.strained_ads_lattice_params[A_IDX] = strained_a
        self.strained_ads_lattice_params[B_IDX] = strained_b
        self.strained_ads_lattice_params[ALPHA_IDX] = strained_alpha
        self.strained_ads_lattice_params[BETA_IDX] = strained_beta
        self.strained_ads_lattice_params[GAMMA_IDX] = strained_gamma 
[docs]    def setInterfaceLatticeProperties(self):
        """
        Set the lattice parameter attributes for the interface model.
        """
        a_vec, b_vec, c_vec = xtal.get_lattice_vectors(*self.ref_lattice_params)
        normal = numpy.cross(a_vec, b_vec)
        normal_angle = transform.get_angle_between_vectors(normal, c_vec)
        separation = old_div(self.separation, math.cos(normal_angle))
        self.interface_lattice_params = list(self.ref_lattice_params)
        self.interface_lattice_params[A_IDX] *= self.ref_ncella
        self.interface_lattice_params[B_IDX] *= self.ref_ncellb
        olength = self.interface_lattice_params[C_IDX]
        nlength = olength + self.strained_ads_lattice_params[C_IDX] + separation
        if nlength <= olength:
            if self.logger:
                msg = (
                    f'The separation of {separation} Ang. gives a lattice '
                    f'c-vector length for the interface cell of {nlength} Ang. '
                    f'that is smaller than that of the base layer length of '
                    f'{olength} Ang.  Instead proceeding with a length of '
                    f'{olength} Ang.')
                self.logger.warning(msg)
            nlength = olength
        self.interface_lattice_params[C_IDX] = nlength 
[docs]    def logParams(self):
        """
        Log the parameters.
        """
        if self.logger is None:
            return
        HEADER = 'Interface Parameters'
        base_name = \
            
textlogger.get_param_string('Base name', self.base_name, self.MSGWIDTH)
        ref_lattice_params = \
            
textlogger.get_param_string('Ref. layer lattice params', \
            
list_to_string(self.ref_lattice_params), self.MSGWIDTH)
        ads_lattice_params = \
            
textlogger.get_param_string('Ads. layer lattice params', \
            
list_to_string(self.ads_lattice_params), self.MSGWIDTH)
        ref_hkl = \
            
textlogger.get_param_string('Ref. layer hkl', \
            
self.ref_hkl, self.MSGWIDTH)
        ads_hkl = \
            
textlogger.get_param_string('Ads. layer hkl', \
            
self.ads_hkl, self.MSGWIDTH)
        bot_layer = textlogger.get_param_string('Bottom layer', self.bot_layer,
                                                self.MSGWIDTH)
        strain_a = textlogger.get_param_string('Strain a / Ang.', self.strain_a,
                                               self.MSGWIDTH)
        strain_b = textlogger.get_param_string('Strain b / Ang.', self.strain_b,
                                               self.MSGWIDTH)
        strain_alpha = textlogger.get_param_string('Strain alpha / degrees',
                                                   self.strain_alpha,
                                                   self.MSGWIDTH)
        strain_beta = textlogger.get_param_string('Strain beta / degrees',
                                                  self.strain_beta,
                                                  self.MSGWIDTH)
        strain_gamma = textlogger.get_param_string('Strain gamma / degrees',
                                                   self.strain_gamma,
                                                   self.MSGWIDTH)
        separation = textlogger.get_param_string('Separation / Ang.',
                                                 self.separation, self.MSGWIDTH)
        max_extents = textlogger.get_param_string('Max extents',
                                                  self.max_extents,
                                                  self.MSGWIDTH)
        translate_a = textlogger.get_param_string('Translate a / Ang.',
                                                  self.translate_a,
                                                  self.MSGWIDTH)
        translate_b = textlogger.get_param_string('Translate b / Ang.',
                                                  self.translate_b,
                                                  self.MSGWIDTH)
        ref_ncella = textlogger.get_param_string('Ref. layer a extents',
                                                 self.ref_ncella, self.MSGWIDTH)
        ref_ncellb = textlogger.get_param_string('Ref. layer b extents',
                                                 self.ref_ncellb, self.MSGWIDTH)
        ads_ncella = textlogger.get_param_string('Ads. layer a extents',
                                                 self.ads_ncella, self.MSGWIDTH)
        ads_ncellb = textlogger.get_param_string('Ads. layer b extents',
                                                 self.ads_ncellb, self.MSGWIDTH)
        strained_ads_lattice_params = \
            
textlogger.get_param_string('Strained ads. layer lattice params', \
            
list_to_string(self.strained_ads_lattice_params), self.MSGWIDTH)
        interface_lattice_params = \
            
textlogger.get_param_string('Interface lattice params', \
            
list_to_string(self.interface_lattice_params), self.MSGWIDTH)
        ref_is_infinite = textlogger.get_param_string('Ref. is infinite',
                                                      self.ref_is_infinite,
                                                      self.MSGWIDTH)
        ads_is_infinite = textlogger.get_param_string('Ads. is infinite',
                                                      self.ads_is_infinite,
                                                      self.MSGWIDTH)
        self.logger.info(HEADER)
        self.logger.info('-' * len(HEADER))
        self.logger.info(base_name)
        self.logger.info(ref_lattice_params)
        self.logger.info(ads_lattice_params)
        self.logger.info(ref_hkl)
        self.logger.info(ads_hkl)
        self.logger.info(bot_layer)
        self.logger.info(strain_a)
        self.logger.info(strain_b)
        self.logger.info(strain_alpha)
        self.logger.info(strain_beta)
        self.logger.info(strain_gamma)
        self.logger.info(separation)
        self.logger.info(max_extents)
        self.logger.info(translate_a)
        self.logger.info(translate_b)
        self.logger.info(ref_ncella)
        self.logger.info(ref_ncellb)
        self.logger.info(ads_ncella)
        self.logger.info(ads_ncellb)
        self.logger.info(strained_ads_lattice_params)
        self.logger.info(interface_lattice_params)
        self.logger.info(ref_is_infinite)
        self.logger.info(ads_is_infinite)
        self.logger.info('') 
[docs]    def getOrigin(self, lattice_params):
        """
        Return the origin in units of Angstrom for the given
        lattice parameters.
        :type lattice_params: list
        :param lattice_params: the six lattice parameters
        :rtype: numpy.array
        :return: the origin in units of Angstrom
        """
        origin_a = old_div(self.translate_a, lattice_params[A_IDX])
        origin_b = old_div(self.translate_b, lattice_params[B_IDX])
        origin_c = \
            
old_div(self.interface_lattice_params[C_IDX], lattice_params[C_IDX]) - 1.0
        origin = numpy.array([origin_a, origin_b, origin_c])
        origin = xtal.trans_fract_to_cart(origin, *(lattice_params))
        return origin 
[docs]    def translateTopLayer(self, ref_layer, ads_layer):
        """
        Translate the top layer.
        :type ref_layer: `schrodinger.structure.Structure`
        :param ref_layer: the reference layer
        :type ads_layer: `schrodinger.structure.Structure`
        :param ads_layer: the adsorption layer
        """
        if self.bot_layer == REFERENCE:
            origin = self.getOrigin(self.strained_ads_lattice_params)
            transform.translate_structure(ads_layer, *origin)
        else:
            origin = self.getOrigin(self.ref_lattice_params)
            transform.translate_structure(ref_layer, *origin) 
[docs]    def getInterface(self, ref_layer, ads_layer):
        """
        Build the interface model from the two finalized layers
        and return it.
        :type ref_layer: `schrodinger.structure.Structure`
        :param ref_layer: the final reference layer
        :type ads_layer: `schrodinger.structure.Structure`
        :param ads_layer: the final adsorption layer
        :rtype: `schrodinger.structure.Structure`
        :return: the interface model
        """
        if self.bot_layer == REFERENCE:
            interface = ref_layer.copy()
            interface.extend(ads_layer)
        else:
            interface = ads_layer.copy()
            interface.extend(ref_layer)
        chorus_params = xtal.get_chorus_from_params(
            self.interface_lattice_params)
        xtal.set_pbc_properties(interface, chorus_params)
        interface = xtal.make_p1(interface)
        return interface 
[docs]    def setInterfaceProperties(self):
        """
        Set some interface properties.
        """
        crystal_keys = [
            xtal.Crystal.UNIT_CELL_FORMULA_KEY,
            xtal.Crystal.UNIT_CELL_VOLUME_KEY,
            xtal.Crystal.UNIT_CELL_DENSITY_KEY
        ]
        for key in crystal_keys:
            self.interface.property.pop(key, None)
        self.interface.title = self.base_name
        self.interface.property[ENTRY_NAME_KEY] = self.base_name
        self.interface.property[IS_INFINITE] = \
            
any([self.ref_is_infinite, self.ads_is_infinite])
        self.interface.property[xtal.PBC_POSITION_KEY] = \
            
xtal.ANCHOR_PBC_POSITION % ('0', '0', '0') 
[docs]    def getUpdatedCParams(self, in_params):
        """
        Return the set of lattice parameters with an updated
        c parameter.
        :type in_params: list
        :param in_params: the six lattice parameters
        :rtype: list
        :return: the six lattice parameters with c updated
        """
        out_params = list(in_params)
        out_params[C_IDX] = self.interface_lattice_params[C_IDX]
        return out_params 
[docs]    def buildLayer(self, cell, in_params, extents, is_infinite):
        """
        Build a layer by extending the input slab cell, also
        eliminate any PBC bonds along the c lattice vector.
        :type cell: `schrodinger.structure.Structure`
        :param cell: slab cell to extend
        :type in_params: list
        :param in_params: the six lattice parameters
        :type extents: list
        :param extents: extents for layer
        :type is_infinite: bool
        :param is_infinite: whether the slab cell is infinite
        :rtype: `schrodinger.structure.Structure`
        :return: extended layer
        """
        out_params = self.getUpdatedCParams(in_params)
        # extend the slab ASU to obtain the layer,
        # for infinite systems enable bonding but use existing
        # PBC bonds so that adjacent cells are properly connected,
        # but remove PBC bonds along the c-vector, note that this
        # will present a problem for infinite polymers where the
        # PBC bonds cross the c-vector
        xtal_kwargs = dict(XTAL_DICT)
        if is_infinite:
            xtal_kwargs['bonding'] = CHOICE_ON
            xtal_kwargs['use_existing_pbc_bonds'] = True
            xtal.delete_pbc_bonds(cell, along_vector_idxs=[2])
        layer = xtal.get_cell(cell, lattice_params=out_params, \
            
extents=extents, xtal_kwargs=xtal_kwargs)
        # create a new P1 cell from the now extended super cell,
        # this will synchronize the PDB and chorus properties, and
        # remove the fractional coordinate information
        layer = xtal.make_p1(layer)
        return layer 
[docs]    def buildLayers(self, ref_extents, ads_extents):
        """
        Build the reference and adsorption layers by extending the
        input slab cells, also eliminate any PBC bonds along the c
        lattice vector.
        :type ref_extents: list
        :param ref_extents: extents for reference layer
        :type ads_extents: list
        :param ads_extents: extents for adsorption layer
        :rtype: `schrodinger.structure.Structure`,
            `schrodinger.structure.Structure`
        :return: extended reference and adsorption layers
        """
        ref_layer = self.buildLayer(self.ref_layer.copy(),
                                    self.ref_lattice_params, ref_extents,
                                    self.ref_is_infinite)
        ads_layer = self.buildLayer(self.ads_layer.copy(),
                                    self.ads_lattice_params, ads_extents,
                                    self.ads_is_infinite)
        return ref_layer, ads_layer 
[docs]    def getStrainedAdsorptionLayer(self, ads_layer):
        """
        Return the strained adsorption layer.
        :type ads_layer: `schrodinger.structure.Structure`
        :param ads_layer: the extended unstrained adsorption layer
        :rtype: `schrodinger.structure.Structure`
        :return: the strained adsorption layer
        """
        strained_ads_lattice_params = \
            
self.getUpdatedCParams(self.strained_ads_lattice_params)
        # to build the strained adsorption layer we just
        # need to use the original fractional coordinates
        # but with the strained lattice parameters
        vecs = numpy.array(
            xtal.get_lattice_vectors(
                *xtal.get_lattice_param_properties(ads_layer)))
        fracs = xtal.trans_cart_to_frac_from_vecs(ads_layer.getXYZ(), *vecs)
        chorus_params = xtal.get_chorus_from_params(strained_ads_lattice_params)
        xtal.set_pbc_properties(ads_layer, chorus_params)
        strained_vecs = numpy.array(xtal.get_vectors_from_chorus(ads_layer))
        ads_layer.setXYZ(
            xtal.trans_frac_to_cart_from_vecs(fracs, *strained_vecs))
        return ads_layer 
[docs]    def runIt(self):
        """
        Create the interface model.
        """
        self.setIsInfinite()
        # set a base name
        self.setBaseName()
        # set lattice parameters for both layers
        self.setLatticeProperties()
        # set hkl for both layers
        self.setHKL()
        # get extents for both layers and strained lengths
        (self.ref_ncella, self.ads_ncella), strained_a = \
            
self.getExtentsAndStrainedLength(A_IDX, self.strain_a)
        (self.ref_ncellb, self.ads_ncellb), strained_b = \
            
self.getExtentsAndStrainedLength(B_IDX, self.strain_b)
        ref_extents = [self.ref_ncella, self.ref_ncellb, 1]
        ads_extents = [self.ads_ncella, self.ads_ncellb, 1]
        # get strained angles
        strained_alpha = self.getStrainedAngle(ALPHA_IDX, self.strain_alpha)
        strained_beta = self.getStrainedAngle(BETA_IDX, self.strain_beta)
        strained_gamma = self.getStrainedAngle(GAMMA_IDX, self.strain_gamma)
        # set strained lattice parameters for the adsorption layer
        self.setStrainedAdsorptionLatticeProperties(strained_a, strained_b,
                                                    strained_alpha,
                                                    strained_beta,
                                                    strained_gamma)
        # set lattice parameters for the interface model
        self.setInterfaceLatticeProperties()
        # log interface properties
        self.logParams()
        # build layers
        ref_layer, ads_layer = self.buildLayers(ref_extents, ads_extents)
        # strain adsorption layer
        ads_layer = self.getStrainedAdsorptionLayer(ads_layer)
        # translate the top layer
        self.translateTopLayer(ref_layer, ads_layer)
        # get the interface model
        self.interface = self.getInterface(ref_layer, ads_layer)
        # set interface properties
        self.setInterfaceProperties()