"""
Classes and functions for building single- and multi-walled nanotubes.
Copyright Schrodinger, LLC. All rights reserved."""
# Contributor: Thomas F. Hughes
import math
import numpy
from schrodinger.application.matsci import textlogger as tlog
from schrodinger.application.matsci.coarsegrain import set_as_coarse_grain
from schrodinger.application.matsci.nano import check
from schrodinger.application.matsci.nano import constants
from schrodinger.application.matsci.nano import sheet
from schrodinger.application.matsci.nano import slab
from schrodinger.application.matsci.nano import util
from schrodinger.application.matsci.nano import xtal
from schrodinger.infra import mm
from schrodinger.infra.mmerr import ErrorHandler
from schrodinger.structutils import build
from schrodinger.structutils import transform
_version = '$Revision 0.0 $'
TWOPI = 2 * math.pi
[docs]class Rectangle(object):
    """
    Manage the properties of a rectangle.
    """
    INSIDETHRESH = 0.00001
[docs]    def __init__(self, origin, bottom, left, end):
        """
        Create an instance.
        :type origin: numpy.array
        :param origin: lower left point
        :type bottom: numpy.array
        :param bottom: lower right point
        :type left: numpy.array
        :param left: upper left point
        :type end: numpy.array
        :param end: upper right point
        """
        self.origin = origin
        self.bottom = bottom
        self.left = left
        self.end = end 
[docs]    def linear_equation(self, ixy, fxy, x):
        """
        Return y = m*x + b for m and b from the line formed by
        initial point ixy and final point fxy.
        :type ixy: numpy.array
        :param ixy: initial point on line
        :type fxy: numpy.array
        :param fxy: final point on line
        :type x: float
        :param x: domain argument
        :rtype: float
        :return: y, range value
        """
        ix, iy = ixy
        fx, fy = fxy
        denom = fx - ix
        numer = fy - iy
        slope = numer / denom
        numer = fx * iy - ix * fy
        interc = numer / denom
        y = slope * x + interc
        return y 
[docs]    def insideRectangle(self, xy, logger=None):
        """
        Return boolean specifying if the provided plane coordinates
        lie within the boundary.
        :type xy: numpy.array
        :param xy: plane coordinates
        :type logger: logging.getLogger
        :param logger: output logger
        :rtype: bool, bool
        :return: insidex, insidey, inside the x-boundary or not, same for
            y-boundary
        """
        x, y = xy
        # given (x, y) find points on the lines defined by the boundaries of
        # the rectangle, handle the case of potentially vertical left and right
        # sides by reversing input vectors and passing x = f(y) rather than
        # y = f(x)
        xleft = self.linear_equation(self.origin[::-1], self.left[::-1], y)
        xright = self.linear_equation(self.bottom[::-1], self.end[::-1], y)
        ybottom = self.linear_equation(self.origin, self.bottom, x)
        ytop = self.linear_equation(self.left, self.end, x)
        # provide a little buffer area for float comparison
        xleft += -1 * self.INSIDETHRESH
        xright += self.INSIDETHRESH
        ybottom += -1 * self.INSIDETHRESH
        ytop += self.INSIDETHRESH
        # is x inside the boundary, same for y
        insidex = False
        insidey = False
        if xleft <= x <= xright:
            insidex = True
        if ybottom <= y <= ytop:
            insidey = True
        return insidex, insidey  
[docs]class NanoSheet(object):
    """
    Create a sheet.HoneycombLattice that is large enough
    so that the nanotube sheet can be cut out from it.
    """
    ANGLEMEDIUM = sheet.HoneycombUnitCell.ANGLEMEDIUM
[docs]    def __init__(self, nanotube_sheet_obj):
        """
        Create an instance.
        :type nanotube_sheet_obj: NanoTubeSheet
        :param nanotube_sheet_obj: contains parameters of the
            nanotube sheet
        """
        self.element1 = nanotube_sheet_obj.element1
        self.element2 = nanotube_sheet_obj.element2
        self.bondlength = nanotube_sheet_obj.bondlength
        self.ncell1 = None
        self.edgetype1 = constants.Constants.ARMCHAIR
        self.ncell2 = None
        self.edgetype2 = constants.Constants.ARMCHAIR
        self.termfrag = constants.Constants.TERMFRAGS[0]
        self.min_term_frags = constants.Constants.MIN_TERM_FRAGS
        self.is_coarse_grain = nanotube_sheet_obj.is_coarse_grain
        self.nanotube_sheet_obj = nanotube_sheet_obj
        self.nanosheet_obj = sheet.HoneycombLattice(
            self.element1, self.element2, self.bondlength, self.ncell1,
            self.edgetype1, self.ncell2, self.edgetype2, self.termfrag,
            self.min_term_frags, self.is_coarse_grain) 
[docs]    def defineVectors(self):
        """
        Define HoneycombLattice and NanoTubeSheet lattice, etc. vectors.
        :rtype: numpy.array, numpy.array
        :return: lattvec1, lattvec2, the HoneycombLattice lattice vectors
        """
        # get HoneycombLattice lattice vectors
        bondlength = self.nanosheet_obj.bondlength
        lattvec1, lattvec2 = sheet.get_lattice_vectors(bondlength)
        # Make another handle for ease of typing/readability
        tube_sheet = self.nanotube_sheet_obj
        # get NanoTubeSheet lattice vectors
        tube_sheet.lattvec1, tube_sheet.lattvec2 = \
            
tube_sheet.redefineLatticeVecs(lattvec1, lattvec2)
        # get NanoTubeSheet basis vectors, i.e. chiral and translation vectors
        tube_sheet.chiral, tube_sheet.translat = \
            
get_tube_vectors(ncells=tube_sheet.ncells,
                             nindex=tube_sheet.nindex,
                             mindex=tube_sheet.mindex,
                             lattvec1=tube_sheet.lattvec1,
                             lattvec2=tube_sheet.lattvec2)
        return lattvec1, lattvec2 
[docs]    def getGrowParams(self, lattvec1, lattvec2):
        """
        Get HoneycombLattice grow parameters.
        :type lattvec1: numpy.array
        :param lattvec1: lattice vector 1
        :type lattvec2: numpy.array
        :param lattvec2: lattice vector 2
        :rtype: float, numpy.array, float, numpy.array
        :return: grow1len, grow1unit, grow2len, grow2unit,
            the lengths and unit vectors of the grow vectors
        """
        # get grow vectors
        grow = sheet.Grow(self.edgetype1, self.edgetype2, self.ncell1,
                          self.ncell2)
        growvec1, growvec2 = grow.getGrowVectors(lattvec1, lattvec2)
        # save length
        grow1len = numpy.linalg.norm(growvec1)
        grow2len = numpy.linalg.norm(growvec2)
        # rotate grow vectors so that the first one falls on the x-axis
        angle_to_xaxis = transform.get_angle_between_vectors(
            growvec1, numpy.array(transform.X_AXIS))
        growvec1 = util.get_rotated_vector(growvec1, angle_to_xaxis)
        growvec2 = util.get_rotated_vector(growvec2, angle_to_xaxis)
        # get unit vectors
        grow1unit = transform.get_normalized_vector(growvec1)
        grow2unit = transform.get_normalized_vector(growvec2)
        grow1unit = numpy.delete(grow1unit, 2)
        grow2unit = numpy.delete(grow2unit, 2)
        return grow1len, grow1unit, grow2len, grow2unit 
[docs]    def changeBasis(self, grow1unit, grow2unit):
        """
        Change the basis of the NanoTubeSheet to that of the NanoSheet.
        :type grow1unit: numpy.array
        :param grow1unit: unit vector of first grow vector
        :type grow2unit: numpy.array
        :param grow2unit: unit vector of second grow vector
        :rtype: float, float
        :return: coef1, coef2, coefficients of the end vector in the grow
            basis
        """
        # build projection matrix
        dot11 = numpy.dot(grow1unit, grow1unit)
        dot12 = numpy.dot(grow1unit, grow2unit)
        dot21 = numpy.dot(grow2unit, grow1unit)
        dot22 = numpy.dot(grow2unit, grow2unit)
        projmatrix = numpy.matrix([[dot11, dot12], [dot21, dot22]])
        # the end vector spans the area of the NanoTubeSheet, project
        # that vector onto the grow vectors to get the required dimensions
        # of the NanoSheet
        end = self.nanotube_sheet_obj.chiral + self.nanotube_sheet_obj.translat
        dot1end = numpy.dot(grow1unit, end)
        dot2end = numpy.dot(grow2unit, end)
        vec = numpy.matrix([dot1end, dot2end])
        # solve matrix equation
        coefs = projmatrix.I * vec.T
        coef1, coef2 = coefs.A1
        return coef1, coef2 
[docs]    def defineDimensions(self, coef1, grow1len, coef2, grow2len):
        """
        Define the dimensions of the NanoSheet.
        :type coef1: float
        :param coef1: coefficient of end vector on first grow vector
        :type grow1len: float
        :param grow1len: length of first grow vector
        :type coef2: float
        :param coef2: coefficient of end vector on second grow vector
        :type grow2len: float
        :param grow2len: length of second grow vector
        """
        # do the math to find the values of ncell1 and ncell2 needed to
        # build a big enough NanoSheet to contain the requested NanoTubeSheet
        ncell1 = coef1 / grow1len
        ncell1 = math.ceil(ncell1)
        self.nanosheet_obj.ncell1 = int(ncell1) + 1
        ncell2 = coef2 / grow2len
        ncell2 = math.ceil(ncell2)
        self.nanosheet_obj.ncell2 = int(ncell2) + 1 
[docs]    def rotateNanoSheet(self):
        """
        Rotate the nanosheet so that lattice edge 1 is along the x-axis.
        """
        axis = transform.Z_AXIS
        rotmatrix = transform.get_rotation_matrix(axis, self.ANGLEMEDIUM)
        transform.transform_structure(self.nanosheet_obj.structure, rotmatrix) 
[docs]    def getNanoSheet(self, logger=None):
        """
        Get the sheet.HoneycombLattice from which the
        nanotube sheet will be cut.
        :type logger: logging.getLogger
        :param logger: output logger
        """
        # define vectors
        lattvec1, lattvec2 = self.defineVectors()
        # get grow parameters
        grow1len, grow1unit, grow2len, grow2unit = self.getGrowParams(
            lattvec1, lattvec2)
        # change basis of NanoTubeSheet to NanoSheet
        coef1, coef2 = self.changeBasis(grow1unit, grow2unit)
        # define NanoSheet dimensions
        self.defineDimensions(coef1, grow1len, coef2, grow2len)
        # build NanoSheet
        self.nanosheet_obj.buildLattice()
        # remove PBCs
        remove_pbc(self.nanosheet_obj.structure)
        # rotate NanoSheet
        self.rotateNanoSheet()  
[docs]class NanoTubeSheet(object):
    """
    Preprocess a nanosheet into a nanotube sheet which will be rolled
    up into a nanotube.
    """
    ZEROVEC = numpy.zeros(2)
[docs]    def __init__(self, element1, element2, bondlength, nindex, mindex, ncells,
                 is_coarse_grain):
        """
        Create an instance.
        :type element1: str
        :param element1: elemental symbol of the first atom
        :type element2: str
        :param element2: elemental symbol of the second atom
        :type bondlength: float
        :param bondlength: bond length between the first and
            second atoms in Angstrom
        :type nindex: int
        :param nindex: first chiral index
        :type mindex: int
        :param mindex: second chiral index
        :type ncells: int
        :param ncells: number of unit cells
        :type is_coarse_grain: bool
        :param is_coarse_grain: Whether a coarse grain structure is being
            created
        """
        self.element1 = element1
        self.element2 = element2
        self.bondlength = bondlength
        self.is_coarse_grain = is_coarse_grain
        self.nindex = nindex
        self.mindex = mindex
        self.ncells = ncells
        self.lattvec1 = None
        self.lattvec2 = None
        self.chiral = None
        self.translat = None
        self.termatoms = None
        self.matchleft = None
        self.matchright = None
        self.nanosheet_st = None
        self.nanotube_sheet_st = None
        self.nanosheet_obj = None 
[docs]    @staticmethod
    def redefineLatticeVecs(lattvec1, lattvec2):
        """
        Redefine lattice vectors according to Dresselhaus.
        :type lattvec1: numpy.array
        :param lattvec1: first lattice vector
        :type lattvec2: numpy.array
        :param lattvec2: second lattice vector
        :rtype: numpy.array, numpy.array
        :return: nlattvec1, nlattvec2, first and second lattice
            vectors redefined
        """
        # reverse lattice vectors
        nlattvec1 = numpy.delete(lattvec2, 2)
        nlattvec2 = numpy.delete(lattvec1, 2)
        return nlattvec1, nlattvec2 
[docs]    def renumberAtomLists(self, renumbermap):
        """
        Apply the given renumbering map to the terminating and matching atom
        lists.
        :type renumbermap: dict
        :param renumbermap: maps old indicies into new indicies
        """
        self.matchleft = [renumbermap[index] for index in self.matchleft \
            
if renumbermap[index] is not None]
        self.matchright = [renumbermap[index] for index in self.matchright \
            
if renumbermap[index] is not None]
        self.termatoms = [renumbermap[index] for index in self.termatoms \
            
if renumbermap[index] is not None] 
[docs]    def cutOutNanoSheet(self, logger=None):
        """
        Cut out the nanotube sheet from the nanosheet.
        :type logger: logging.getLogger
        :param logger: output logger
        """
        def process_atom(atom, unit, rect, logger=None):
            """
            Determine which boundary constraints are satisfied for the
            target atom.
            """
            atomxy = numpy.array(atom.xyz[:-1])
            projlen = abs(numpy.dot(atomxy, unit))
            insidex, insidey = rect.insideRectangle(atomxy)
            inside = insidex and insidey
            return projlen, inside, insidex, insidey
        # define rectangle
        origin = self.ZEROVEC
        end = self.chiral + self.translat
        rect = Rectangle(origin, self.chiral, self.translat, end)
        # get a few parameters
        chiralunit = transform.get_normalized_vector(self.chiral)
        half_chiral_len = numpy.linalg.norm(self.chiral) / 2
        # copy nanosheet structure to nanotubesheet structure
        self.nanotube_sheet_st = self.nanosheet_st.copy()
        # initialize a few iterables to hold atomic indicies of (1) atoms
        # outside of the rectangle to be deleted, (2) atoms to be terminated,
        # i.e. at the ends of the nanotube, and (3) atoms from the left and
        # right sides of the nanotube which are to be matched up when the
        # nanotube is rolled up.  The relative left and right match index
        # ordering is correct as is because of the way in which atom indexing
        # is done row-wise in sheet.HoneycombLattice.
        outsideatoms = set()
        termatoms = set()
        matchleft, matchright = set(), set()
        for bond in self.nanotube_sheet_st.bond:
            # get some descriptors about atoms in target bond
            atom1, atom2 = bond.atom1, bond.atom2
            len1, in1, inx1, iny1 = process_atom(atom1, chiralunit, rect)
            len2, in2, inx2, iny2 = process_atom(atom2, chiralunit, rect)
            # handle both outside
            if not in1 and not in2:
                outsideatoms.add(atom1.index)
                outsideatoms.add(atom2.index)
            # handle bond across boundary, classify
            # the inside atom as terminating or matching
            # left or right
            elif in1 and not in2:
                outsideatoms.add(atom2.index)
                if inx2:
                    termatoms.add(atom1.index)
                else:
                    if len1 < half_chiral_len:
                        matchleft.add(atom1.index)
                    else:
                        matchright.add(atom1.index)
            # handle bond across boundary, classify
            # the inside atom as terminating or matching
            # left or right
            elif not in1 and in2:
                outsideatoms.add(atom1.index)
                if inx1:
                    termatoms.add(atom2.index)
                else:
                    if len2 < half_chiral_len:
                        matchleft.add(atom2.index)
                    else:
                        matchright.add(atom2.index)
        # define class attrs, be sure to sort those for which order matters
        self.termatoms = sorted(termatoms)
        self.matchleft = sorted(matchleft)
        self.matchright = sorted(matchright)
        # delete outside atoms and renumber terminating and matching atoms
        renumbermap = self.nanotube_sheet_st.deleteAtoms(
            list(outsideatoms), True)
        self.renumberAtomLists(renumbermap) 
[docs]    def delDanglingTermAtoms(self):
        """
        Remove dangling atoms from the top and bottom of the nanotube sheet.
        """
        # dangling terminating atoms that are also match atoms are needed so
        # skip them
        matches = set(self.matchleft + self.matchright)
        termatoms = [index for index in self.termatoms if index not in matches]
        dangling_atoms = []
        new_term_atoms = []
        for index in termatoms:
            atom = self.nanotube_sheet_st.atom[index]
            if atom.bond_total == 1:
                dangling_atoms.append(atom.index)
                new_term_atoms.append(next(atom.bonded_atoms).index)
        # delete the dangling atoms
        renumbermap = self.nanotube_sheet_st.deleteAtoms(dangling_atoms, True)
        # extend the list of terminating atoms with those atoms that were bonded
        # to the dangling atoms, no sorting necessary
        self.termatoms.extend(new_term_atoms)
        # apply renumbering map
        self.renumberAtomLists(renumbermap) 
[docs]    def delZigZagMatchAtoms(self, logger=None):
        """
        Remove overlapping match atoms for the zigzag case.
        :type logger: logging.getLogger
        :param logger: output logger
        """
        new_match_left = []
        for index in self.matchleft:
            atom1 = self.nanotube_sheet_st.atom[index]
            for atom2 in atom1.bonded_atoms:
                if atom2.index not in self.matchleft:
                    new_match_left.append(atom2.index)
        # delete the overlapping atoms
        renumbermap = self.nanotube_sheet_st.deleteAtoms(self.matchleft, True)
        # make the list of left matching atoms equal to those atoms that
        # were bonded to the overlapping atoms, no sorting needed here
        self.matchleft = list(new_match_left)
        # apply renumbering map
        self.renumberAtomLists(renumbermap) 
[docs]    def delChiralMatchAtoms(self, logger=None):
        """
        Remove overlapping match atoms for the chiral case.
        :type logger: logging.getLogger
        :param logger: output logger
        """
        def check_atom(atom, index, match, overlapping, new):
            """
            Check if this atom is an overlapping atom.
            """
            if atom.bond_total == 1:
                before = self.nanotube_sheet_st.atom[match[index - 1]]
                current = self.nanotube_sheet_st.atom[match[index]]
                after = self.nanotube_sheet_st.atom[match[index + 1]]
                if before.bond_total != 1 and current.bond_total != 1 and \
                    
after.bond_total != 1:
                    overlapping.append(atom.index)
                    new.append(next(atom.bonded_atoms).index)
        # start loop one in and end loop one out because index-1, index, and index+1
        # are used
        overlapping_atoms = []
        new_match_left, new_match_right = [], []
        for index, pair in enumerate(
                zip(self.matchleft[1:-1], self.matchright[1:-1]), 1):
            left, right = pair
            leftatom = self.nanotube_sheet_st.atom[left]
            rightatom = self.nanotube_sheet_st.atom[right]
            check_atom(leftatom, index, self.matchright, overlapping_atoms,
                       new_match_left)
            check_atom(rightatom, index, self.matchleft, overlapping_atoms,
                       new_match_right)
        # delete the overlapping atoms
        renumbermap = self.nanotube_sheet_st.deleteAtoms(
            overlapping_atoms, True)
        # extend the lists of left and right matching atoms with those atoms that were
        # bonded to the overlapping atoms, sort the lists to maintain congruence with
        # each other
        self.matchleft.extend(new_match_left)
        self.matchleft.sort()
        self.matchright.extend(new_match_right)
        self.matchright.sort()
        # apply renumbering map
        self.renumberAtomLists(renumbermap) 
[docs]    def buildNanoTubeSheet(self, termfrag, use_finite_bos=True, logger=None):
        """
        Build the nanotube sheet.
        :type termfrag: str
        :param termfrag: terminate the lattice with a given fragment
        :type use_finite_bos: bool
        :param use_finite_bos: use a bond order protocol meant for finite
            molecules
        :type logger: logging.getLogger
        :param logger: output logger
        """
        # get the nanosheet
        #
        # pass this instance thus far to NanoSheet so that the NanoSheet
        # class can use its properties and define some new properties for it
        # as well, save the nanosheet instance for later
        asheet = NanoSheet(self)
        asheet.getNanoSheet(logger)
        self.nanosheet_obj = asheet.nanosheet_obj
        self.nanosheet_st = asheet.nanosheet_obj.structure
        # cut out the nanotube sheet from the nanosheet
        self.cutOutNanoSheet(logger)
        # clean up the ends of the nanotube sheet
        if use_finite_bos:
            self.delDanglingTermAtoms()
        # clean up the sides of the nanotube sheet for zigzag and chiral cases
        if self.mindex == 0:
            self.delZigZagMatchAtoms()
        elif self.nindex != self.mindex:
            self.delChiralMatchAtoms()  
[docs]class NanoTube(object):
    """
    Create a nanotube by rolling up a nanotube sheet.
    """
    TITLEKEY = 's_m_title'
    ENTRYKEY = 's_m_entry_name'
    TITLENAME = 'nanotube'
    NINDEX = 'i_matsci_N_Index'
    MINDEX = 'i_matsci_M_Index'
    NCELLS = 'i_matsci_N_Cells'
    RADIUS = 'r_matsci_Radius/Ang.'
    LENGTH = 'r_matsci_Length/Ang.'
    MSGWIDTH = 50
    NUMDECIMAL = 3
[docs]    def __init__(self, element1, element2, bondlength, no_double_bonds, nindex,
                 mindex, ncells, termfrag, min_term_frags, is_coarse_grain):
        """
        Create an instance.
        :type element1: str
        :param element1: elemental symbol of the first atom
        :type element2: str
        :param element2: elemental symbol of the second atom
        :type bondlength: float
        :param bondlength: bond length between the first and
            second atoms in Angstrom
        :type no_double_bonds: bool
        :param no_double_bonds: disable the formation of double bonds
        :type nindex: int
        :param nindex: first chiral index
        :type mindex: int
        :param mindex: second chiral index
        :type ncells: int
        :param ncells: number of unit cells
        :type termfrag: str
        :param termfrag: terminate the lattice with a given fragment
        :type min_term_frags: bool
        :param min_term_frags: minimize the geometry of terminating
            fragments
        :type is_coarse_grain: bool
        :param is_coarse_grain: Whether a coarse grain structure is being
            created
        """
        self.element1 = element1
        self.element2 = element2
        self.bondlength = bondlength
        self.no_double_bonds = no_double_bonds
        self.nindex = nindex
        self.mindex = mindex
        self.ncells = ncells
        self.termfrag = termfrag
        self.min_term_frags = min_term_frags
        self.is_coarse_grain = is_coarse_grain
        self.title = None
        self.lattvec1 = None
        self.lattvec2 = None
        self.chiral = None
        self.translat = None
        self.radius = None
        self.length = None
        self.chiral_angle = None
        self.termatoms = None
        self.matchleft = None
        self.matchright = None
        self.frozen_tube_atoms = None
        self.frozenatoms = None
        self.bomsg = None
        self.nanosheet_st = None
        self.nanotube_sheet_st = None
        self.nanotube_st = None
        self.rotmatrix = None
        self.atomic_number1 = None
        self.atomic_number2 = None 
[docs]    def getChiralAngle(self, logger=None):
        """
        Determine the chiral angle of the tube in degrees where
        the chiral angle is angle(lattvec1, chiral) and is in
        [0.0, 30.0], 0.0 for zigzag and 30.0 for armchair and the
        rest are chiral.
        :type logger: logging.getLogger
        :param logger: output logger
        """
        latt_vec_len = numpy.linalg.norm(self.lattvec1)
        chiral_len = numpy.linalg.norm(self.chiral)
        chiral_angle_denom = 2 * chiral_len
        chiral_angle_numer = math.sqrt(3) * self.mindex * latt_vec_len
        self.chiral_angle = math.asin(chiral_angle_numer / chiral_angle_denom)
        self.chiral_angle = math.degrees(self.chiral_angle) 
[docs]    def tubularizeNanoTubeSheet(self, logger=None):
        """
        Tubularize the nanotube sheet.
        :type logger: logging.getLogger
        :param logger: output logger
        """
        self.nanotube_st = self.nanotube_sheet_st.copy()
        chiralunit = transform.get_normalized_vector(self.chiral)
        translatunit = transform.get_normalized_vector(self.translat)
        # get r coordinate, i.e. radius of tube
        self.length, self.radius = get_tube_dimensions(self.chiral,
                                                       self.translat)
        # perform cylindrical transformation on atomic positions
        for atom in self.nanotube_st.atom:
            # these are the plane coords
            cart = numpy.array(atom.xyz)
            cart = numpy.delete(cart, 2)
            # project atomic position onto tube vectors
            cart_proj_chiral = numpy.dot(cart, chiralunit)
            cart_proj_translat = numpy.dot(cart, translatunit)
            # get angular and z coordinate
            thetacoord = cart_proj_chiral / self.radius
            zcoord = cart_proj_translat
            # do r, theta, z -> x, y, z such that y is the tube axis and
            # such that z is shifted so that the origin is not at the center
            # but rather on the +z side of the tube.  Do this for better
            # visualization when viewed together with the nanotube sheet and
            # nanosheet.
            x = self.radius * math.sin(thetacoord)
            y = zcoord
            z = self.radius * math.cos(thetacoord) - self.radius
            # relocate atom
            atom.xyz = [x, y, z] 
[docs]    def rotateTube(self, logger=None):
        """
        Rotate the nanotube so that the tube axis is the translation
        vector.
        """
        yaxis = numpy.array(transform.Y_AXIS)
        translat = numpy.append(self.translat, 0.0)
        self.rotmatrix = transform.get_alignment_matrix(yaxis, translat)
        transform.transform_structure(self.nanotube_st, self.rotmatrix) 
[docs]    def preprocessMatchAtoms(self, inmatch):
        """
        Dangling match atoms require two bonding partners so
        make those atom indicies redundant in the list.
        :type inmatch: list of ints
        :param inmatch: non-redundant list
        :rtype: list of ints
        :return: outmatch, redundant list
        """
        outmatch = []
        for index in inmatch:
            atom = self.nanotube_st.atom[index]
            outmatch.append(atom.index)
            if atom.bond_total == 1 and atom.index not in self.termatoms:
                outmatch.append(atom.index)
        return outmatch 
[docs]    def bondMatchingEdges(self, matchleft, matchright):
        """
        Properly bond the left and right edges which meet each other
        after rolling.
        :type matchleft: list of ints
        :param matchleft: indicies of atoms on the left
        :type matchright: list of ints
        :param matchright: indicies of atoms on the right
        """
        for leftatom, rightatom in zip(matchleft, matchright):
            self.nanotube_st.addBond(leftatom, rightatom, 1) 
[docs]    def doTermination(self, nanosheet_obj, fragment):
        """
        Terminate the nanotube.  Do this by hijacking the HoneycombLattice
        instance and overwriting some attributes.
        :type nanosheet_obj: sheet.HoneycombLattice
        :param nanosheet_obj: contains information shared between this
            instance and the nanotube instance
        :type fragment: str
        :param fragment: fragment name
        :rtype: list of ints
        :return: nanosheet_obj.frozenatoms, those fragment atoms bound to
            the nanotube
        """
        # overwrite nanosheet attrs
        nanosheet_obj.terminatingatoms = list(self.termatoms)
        nanosheet_obj.structure = self.nanotube_st.copy()
        nanosheet_obj.frozenatoms = []
        nanosheet_obj.termfrag = fragment
        # terminate the nanotube
        nanosheet_obj.terminateLattice()
        # overwrite local nanotube attrs
        self.nanotube_st = nanosheet_obj.structure.copy()
        return nanosheet_obj.frozenatoms 
[docs]    def doBondOrders(self, logger=None):
        """
        Assign bond orders to the nanotube.
        :type logger: logging.getLogger
        :param logger: output logger
        """
        bomsg = """A Kekule structure can not be found for this (%d,%d) nanotube,
            i.e. a single double bond could not be defined for all tube atoms.
            Returning a tube containing only single bonds."""
        mm.mmlewis_initialize(mm.MMERR_DEFAULT_HANDLER)
        old_handler = mm.mmlewis_get_error_handler()
        new_handler = ErrorHandler(queued=True, silent=True)
        new_handler.push_level(6)
        mm.mmlewis_set_error_handler(new_handler.handle)
        # see MATSCI-1838 and SHARED-2801 for details, this is to
        # specify that a breadth first algorithm be used for bond
        # order assignment rather than the older protocol which
        # made use of atom ordering, etc.
        mm.mmlewis_set_option(mm.MMLEWIS_OPTION_BOND_PLACEMENT, \
            
mm.MMLEWIS_BOND_PLACEMENT_BFS, 0.0, '')
        try:
            mm.mmlewis_apply(self.nanotube_st)
        except:
            self.bomsg = bomsg % (self.nindex, self.mindex)
        mm.mmlewis_set_error_handler(old_handler)
        mm.mmlewis_terminate() 
[docs]    def removeHydrogens(self):
        """
        Remove all hydrogens from the structure.
        """
        build.delete_hydrogens(self.nanotube_st) 
[docs]    def minTerminatingFrags(self, nanosheet_obj):
        """
        Minimize terminating fragments.  Do this by hijacking
        the HoneycombLattice instance and overwriting some attributes.
        :type nanosheet_obj: sheet.HoneycombLattice
        :param nanosheet_obj: contains information shared between this
            instance and the nanotube instance
        """
        # overwrite nanosheet attrs
        nanosheet_obj.structure = self.nanotube_st.copy()
        nanosheet_obj.frozenatoms = list(self.frozenatoms)
        # minimize the terminating atoms
        nanosheet_obj.minTerminatingFragments()
        # overwrite local nanotube attrs
        self.nanotube_st = nanosheet_obj.structure.copy() 
[docs]    def handleProps(self, chorus_properties):
        """
        Handle the structure properties of the tube.
        :type chorus_properties: list
        :param chorus_properties: contains the nine chorus properties,
            i.e. ax, ay, az, bx, ..., cz
        """
        # delete some unimportant properties that where inherited from
        # the sheets
        ncell1 = sheet.HoneycombLattice.NCELL1
        ncell2 = sheet.HoneycombLattice.NCELL2
        self.nanotube_st.property.pop(ncell1, None)
        self.nanotube_st.property.pop(ncell2, None)
        # set title and entry names
        tag = '%dx(%d,%d)' % (self.ncells, self.nindex, self.mindex)
        self.title = self.TITLENAME + '-' + tag
        self.nanotube_st.property[self.TITLEKEY] = self.title
        self.nanotube_st.property[self.ENTRYKEY] = self.title
        # set dimensions
        self.nanotube_st.property[self.NINDEX] = self.nindex
        self.nanotube_st.property[self.MINDEX] = self.mindex
        self.nanotube_st.property[self.NCELLS] = self.ncells
        self.nanotube_st.property[self.RADIUS] = self.radius
        self.nanotube_st.property[self.LENGTH] = self.length
        # if not terminating then set up PBC
        if self.termfrag == constants.Constants.TERMFRAGS[0]:
            xtal.set_pbc_properties(self.nanotube_st, chorus_properties)
            self.nanotube_st.property[xtal.SPACE_GROUP_KEY] = \
                
xtal.P1_SPACE_GROUP_SYMBOL 
[docs]    def printProps(self, logger=None):
        """
        Print the properties of this nanotube.
        :type logger: logging.getLogger
        :param logger: output logger
        """
        # get parameters
        latt_vec_len = numpy.linalg.norm(self.lattvec1)
        chiral_len = numpy.linalg.norm(self.chiral)
        # get formatted strings
        title = tlog.get_param_string('Title', self.title, self.MSGWIDTH)
        latt_vec_len = tlog.get_param_string(
            'Lattice vector length / Ang.',
            round(latt_vec_len, self.NUMDECIMAL), self.MSGWIDTH)
        nindex = tlog.get_param_string('First chiral index', self.nindex,
                                       self.MSGWIDTH)
        mindex = tlog.get_param_string('Second chiral index', self.mindex,
                                       self.MSGWIDTH)
        ncells = tlog.get_param_string('Number of unit cells', self.ncells,
                                       self.MSGWIDTH)
        chiral_len = tlog.get_param_string('Chiral vector length / Ang.',
                                           round(chiral_len, self.NUMDECIMAL),
                                           self.MSGWIDTH)
        translat_len = tlog.get_param_string(
            'Translation vector length / Ang.',
            round(self.length, self.NUMDECIMAL), self.MSGWIDTH)
        radius = tlog.get_param_string('Tube radius / Ang.',
                                       round(self.radius, self.NUMDECIMAL),
                                       self.MSGWIDTH)
        chiral_angle = tlog.get_param_string(
            'Chiral angle / Deg.', round(self.chiral_angle, self.NUMDECIMAL),
            self.MSGWIDTH)
        # print formatted strings
        logger.info(title)
        logger.info(latt_vec_len)
        logger.info(nindex)
        logger.info(mindex)
        logger.info(ncells)
        logger.info(chiral_len)
        logger.info(translat_len)
        logger.info(radius)
        logger.info(chiral_angle)
        logger.info('') 
[docs]    def getChorusPBC(self):
        """
        Return the chorus box PBC.
        :rtype: list
        :return: contains the nine chorus properties, i.e. ax, ay, az, bx, ..., cz
        """
        a_vec, b_vec, c_vec = get_tube_unit_cell(self.radius, self.length)
        a_vec, b_vec, c_vec = [transform.transform_atom_coordinates(x, \
            
self.rotmatrix) for x in [a_vec, b_vec, c_vec]]
        return list(a_vec) + list(b_vec) + list(c_vec) 
[docs]    def buildTube(self, use_finite_bos=True, logger=None):
        """
        Build a tube.
        :type use_finite_bos: bool
        :param use_finite_bos: use a bond order protocol meant for finite
            molecules
        :type logger: logging.getLogger
        :param logger: output logger
        """
        # get the nanotube sheet to be rolled up
        nanotube_sheet_obj = NanoTubeSheet(self.element1, self.element2,
                                           self.bondlength, self.nindex,
                                           self.mindex, self.ncells,
                                           self.is_coarse_grain)
        nanotube_sheet_obj.buildNanoTubeSheet(self.termfrag, use_finite_bos,
                                              logger)
        self.atomic_number1 = nanotube_sheet_obj.nanosheet_obj.atomic_number1
        self.atomic_number2 = nanotube_sheet_obj.nanosheet_obj.atomic_number2
        # bring important things to this class
        self.lattvec1 = nanotube_sheet_obj.lattvec1
        self.lattvec2 = nanotube_sheet_obj.lattvec2
        self.chiral = nanotube_sheet_obj.chiral
        self.translat = nanotube_sheet_obj.translat
        self.termatoms = nanotube_sheet_obj.termatoms
        self.matchleft = nanotube_sheet_obj.matchleft
        self.matchright = nanotube_sheet_obj.matchright
        self.nanosheet_st = nanotube_sheet_obj.nanosheet_st
        self.nanotube_sheet_st = nanotube_sheet_obj.nanotube_sheet_st
        nanosheet_obj = nanotube_sheet_obj.nanosheet_obj
        # get chiral angle
        self.getChiralAngle()
        # tubularize the nanotube sheet
        self.tubularizeNanoTubeSheet(logger)
        # rotate tube so that the axis is along the translation vector
        self.rotateTube(logger)
        # make dangling atoms redundant in matching lists
        matchleft = self.preprocessMatchAtoms(self.matchleft)
        matchright = self.preprocessMatchAtoms(self.matchright)
        # bond match up atoms to close the tube
        self.bondMatchingEdges(matchleft, matchright)
        # if bond orders are desired then do the bond order
        # assignment but help out by temporarily adding hydrogens
        # MATSCI-11559: bond orders is not supported for coarse grain structures
        if (not self.no_double_bonds and use_finite_bos and
                not self.is_coarse_grain):
            frozenfrag = self.doTermination(nanosheet_obj,
                                            constants.Constants.TERMFRAG)
            self.doBondOrders(logger)
            self.removeHydrogens()
        # terminate the nanotube, if a minimization is desired and there
        # are minimizable terminating atoms then do the minimization
        if self.termfrag != constants.Constants.TERMFRAGS[0]:
            self.frozen_tube_atoms = list(
                range(1,
                      len(self.nanotube_st.atom) + 1))
            frozenfrag = self.doTermination(nanosheet_obj, self.termfrag)
            self.frozenatoms = self.frozen_tube_atoms + frozenfrag
            if len(self.frozenatoms) < len(self.nanotube_st.atom) and \
                
self.min_term_frags:
                self.minTerminatingFrags(nanosheet_obj)
        # handle properties of the tube
        chorus_properties = self.getChorusPBC()
        self.handleProps(chorus_properties)  
[docs]class MultiWalledNanoTube(object):
    """
    Build a multi-walled nanotube by assembling specific NanoTubes.
    """
    NWALLS = 'i_matsci_N_Walls'
    WALLSEP = 'r_matsci_Wall_Sep./Ang.'
    MSGWIDTH = 50
[docs]    def __init__(self, innertube, nwalls, wallsep):
        """
        Create an instance.
        :type innertube: NanoTube
        :param innertube: tube object of inner most tube
        :type nwalls: int
        :param nwalls: number of walls in the multi-walled tube
        :type wallsep: float
        :param wallsep: wall separation in Angstrom for the multi-walled
            tube.
        """
        self.element1 = innertube.element1
        self.element2 = innertube.element2
        self.bondlength = innertube.bondlength
        self.no_double_bonds = innertube.no_double_bonds
        self.ncells = innertube.ncells
        self.termfrag = innertube.termfrag
        self.min_term_frags = innertube.min_term_frags
        self.is_coarse_grain = innertube.is_coarse_grain
        self.lattvec1 = innertube.lattvec1
        self.lattvec2 = innertube.lattvec2
        self.chiral_inner = innertube.chiral
        self.nindex_inner = innertube.nindex
        self.mindex_inner = innertube.mindex
        self.atomic_number1 = innertube.atomic_number1
        self.atomic_number2 = innertube.atomic_number2
        self.nwalls = nwalls
        self.wallsep = wallsep
        self.tmax = None
        self.tubes = [innertube]
        self.tubespacings = []
        self.nanotube_st = None 
[docs]    def areSameLength(self):
        """
        Return True if the tubes in the multi-walled nanotube
        are the same length, False otherwise.
        :rtype: bool
        :return: True if the tubes in the multi-walled nanotube
            are the same length, False otherwise
        """
        return self.nindex_inner == self.mindex_inner or self.mindex_inner == 0 
[docs]    def getRadius(self):
        """
        Return the radius in Ang.
        :rtype: float
        :return: the radius in Ang.
        """
        return self.tubes[-1].radius 
[docs]    def getTerminatingIdxs(self):
        """
        Return the indices of terminating atoms.
        :rtype: list
        :return: indices of terminating atoms
        """
        offset = 0
        term_idxs = []
        for tube in self.tubes:
            for term_idx in tube.termatoms:
                term_idxs.append(term_idx + offset)
            offset += tube.nanotube_st.atom_total
        return term_idxs 
[docs]    def getOuterChiralIndicies(self, wallindex, logger=None):
        """
        Get the chiral indicies for this outer tube.
        :type wallindex: int
        :param wallindex: index of this outer tube
        :type logger: logging.getLogger
        :param logger: output logger
        :rtype: int, int
        :rtype: nindex, mindex, chiral indicies for outer tube
        """
        # get outer tube length
        innerlen = numpy.linalg.norm(self.chiral_inner)
        delta = (wallindex - 1) * TWOPI * self.wallsep
        outerlen = innerlen + delta
        # it might not be possible to find an outer tube with integer
        # (n, m) that has the desired outer tube length however
        # there are several outer tubes with lengths that are close
        # to the desired length, i.e.
        # length ~ math.sqrt(n**2 + m**2 + nm), we will by
        # default choose an outer tube with the same chiral angle
        # as the inside tube, this will likely result in non-integer
        # (n, m) which will then be rounded to the nearest integer
        # thus resulting in an outer tube of slightly different length
        # than that desired, in the equation below for mindex one
        # can show that:
        # mindex = self.mindex_inner*(1 + self.wallsep/radiusinner)
        # which means that in order for mindex to be integer the
        # wall separation parameter must be a multiple of the radius
        # of the inner tube
        mindex = outerlen * self.mindex_inner / innerlen
        # solve well behaved quadratic equation for nindex and take
        # the positive root
        latt_vec_len = numpy.linalg.norm(self.lattvec1)
        amo = latt_vec_len * mindex
        nindex = 4 * outerlen**2 - 3 * amo**2
        nindex = math.sqrt(nindex)
        nindex = nindex - amo
        nindex = nindex / latt_vec_len
        nindex = nindex / 2
        # round to nearest integer
        nindex = int(round(nindex))
        mindex = int(round(mindex))
        return nindex, mindex 
[docs]    def getOuterTubeVectors(self, nindex, mindex):
        """
        Return the tube vectors for the given (n, m).
        :type nindex: int
        :param nindex: first chiral index
        :type mindex: int
        :param mindex: second chiral index
        :rtype: numpy.array, numpy.array
        :return: chiral, translat, the tube vectors
        """
        asheet = NanoTubeSheet(self.element1, self.element2, self.bondlength,
                               nindex, mindex, self.ncells,
                               self.is_coarse_grain)
        asheet.lattvec1 = self.lattvec1
        asheet.lattvec2 = self.lattvec2
        chiral, translat = get_tube_vectors(ncells=asheet.ncells,
                                            nindex=asheet.nindex,
                                            mindex=asheet.mindex,
                                            lattvec1=asheet.lattvec1,
                                            lattvec2=asheet.lattvec2)
        return chiral, translat 
[docs]    def findLargestTranslat(self):
        """
        Return the length of the wall with the longest translation vector.
        :rtype: float
        :return: tmax, length of longest vector in Angstrom
        """
        tlengths = [numpy.linalg.norm(tube.translat) for tube in self.tubes]
        tmax = max(tlengths)
        return tmax 
[docs]    def getNumUnitCells(self, translat):
        """
        Return the number of unit cells to use for the given wall.
        :type translat: numpy.array
        :param translat: translation vector of the given wall
        :rtype: int
        :return: ncells, the number of cells to use for the given wall
        """
        translat_len = numpy.linalg.norm(translat)
        ncells = self.tmax / translat_len
        ncells = int(round(ncells))
        return ncells 
[docs]    def alignCenterCollect(self):
        """
        Align and center the tubes and collect tubes into a single
        structure.
        """
        inner_translat = numpy.append(self.tubes[0].translat, 0.0)
        # get centroid of outer tube, ignore fragment atoms, use outer
        # rather than inner to avoid down stream issues regarding the choice
        # of origin
        outer_centroid = transform.get_centroid(
            self.tubes[-1].nanotube_st, self.tubes[-1].frozen_tube_atoms)
        # rotate and translate then collect
        for idx, tube in enumerate(self.tubes):
            tubest = tube.nanotube_st.copy()
            tube_translat = numpy.append(tube.translat, 0.0)
            rotmatrix = transform.get_alignment_matrix(tube_translat,
                                                       inner_translat)
            transform.transform_structure(tubest, rotmatrix)
            tube_centroid = transform.get_centroid(tubest,
                                                   tube.frozen_tube_atoms)
            diff = outer_centroid - tube_centroid
            transform.translate_structure(tubest, diff[0], diff[1], diff[2])
            if not idx:
                self.nanotube_st = tubest.copy()
            else:
                self.nanotube_st.extend(tubest) 
[docs]    def getTubeSpacings(self):
        """
        Determine actual tube spacings in units of Ang.
        """
        # start with inner most tube and determine spacings
        # working our way outward
        innerrad = self.tubes[0].radius
        for tube in self.tubes[1:]:
            spacing = tube.radius - innerrad
            self.tubespacings.append(spacing)
            innerrad = tube.radius 
[docs]    def handleProps(self):
        """
        Handle the structure properties of the multi-walled tube.
        """
        # keep props inherited from inner tube but set some extra
        # dimensions
        self.nanotube_st.property[self.NWALLS] = self.nwalls
        self.nanotube_st.property[self.WALLSEP] = self.wallsep
        if self.termfrag == constants.Constants.TERMFRAGS[
                0] and self.areSameLength():
            # set the PBC for the outermost tube
            chorus_properties = xtal.get_chorus_properties(
                self.tubes[-1].nanotube_st)
            xtal.set_pbc_properties(self.nanotube_st, chorus_properties)
        else:
            remove_pbc(self.nanotube_st) 
[docs]    def printProps(self, logger=None):
        """
        Print the properties of this multi-walled nanotube.
        :type logger: logging.getLogger
        :param logger: output logger
        """
        logger.info('')
        for index, spacing in enumerate(self.tubespacings, 2):
            description = 'Spacing for tubes %s:%s / Ang.' % (index - 1, index)
            spacing = tlog.get_param_string(description,
                                            round(spacing, NanoTube.NUMDECIMAL),
                                            self.MSGWIDTH)
            logger.info(spacing)
        logger.info('') 
[docs]    def buildMultiWallTube(self, use_finite_bos=True, logger=None):
        """
        Assemble the multi-walled tube.
        :type use_finite_bos: bool
        :param use_finite_bos: use a bond order protocol meant for finite
            molecules
        :type logger: logging.getLogger
        :param logger: output logger
        """
        # build the multi-walled nanotube in such a way that the walls are
        # of comparable length, for each wall store a nanotube instance
        # with special (n, m) (locked in from the given wall separation)
        # and the requested number of unit cells, define the chiral and
        # translation vectors for each (n, m) wall, the wall with the
        # largest translation vector will determine the number of unit
        # cells to use for each of the walls, that number of cells then
        # scaled by the requested number of cells in order to allow for
        # repeating the entire multi-walled tube
        for wallindex in range(2, self.nwalls + 1):
            nindex, mindex = self.getOuterChiralIndicies(wallindex, logger)
            tube = NanoTube(self.element1, self.element2, self.bondlength,
                            self.no_double_bonds, nindex, mindex, self.ncells,
                            self.termfrag, self.min_term_frags,
                            self.is_coarse_grain)
            tube.chiral, tube.translat = self.getOuterTubeVectors(
                nindex, mindex)
            self.tubes.append(tube)
        # find length of wall with longest translat
        self.tmax = self.findLargestTranslat()
        # find number of unit cells, scale ncells, and build tubes
        for tube in self.tubes:
            ncells = self.getNumUnitCells(tube.translat)
            tube.ncells = self.ncells * ncells
            tube.buildTube(use_finite_bos, logger)
        # due to the rounding of (n, m) the tubes should be aligned along
        # the translation vector of the inner tube, also center the tubes,
        # and collect them into a single structure
        self.alignCenterCollect()
        # determine tube spacings
        self.getTubeSpacings()
        # handle properties of the multi-walled tube
        self.handleProps()  
[docs]class NanoTubes(object):
    """
    Main class for making nanotubes.
    """
    MSGWIDTH = 50
[docs]    def __init__(self,
                 element1=constants.Constants.ELEMENT1,
                 element2=constants.Constants.ELEMENT2,
                 bondlength=constants.Constants.BONDLENGTH,
                 no_double_bonds=constants.Constants.NO_DOUBLE_BONDS,
                 nindex=constants.Constants.NINDEX,
                 mindex=constants.Constants.MINDEX,
                 ncells=constants.Constants.NCELLS,
                 termfrag=constants.Constants.TERMFRAG,
                 min_term_frags=constants.Constants.MIN_TERM_FRAGS,
                 up_to_nindex=constants.Constants.UP_TO_NINDEX,
                 up_to_mindex=constants.Constants.UP_TO_MINDEX,
                 nwalls=constants.Constants.NWALLS,
                 wallsep=constants.Constants.WALLSEP,
                 orient=False,
                 logger=None,
                 is_coarse_grain=False):
        """
        :type element1: str
        :param element1: elemental symbol of the first atom
        :type element2: str
        :param element2: elemental symbol of the second atom
        :type bondlength: float
        :param bondlength: bond length between the first and
            second atoms in Angstrom
        :type no_double_bonds: bool
        :param no_double_bonds: disable the formation of double bonds
        :type nindex: int
        :param nindex: first chiral index
        :type mindex: int
        :param mindex: second chiral index
        :type ncells: int
        :param ncells: number of unit cells
        :type termfrag: str
        :param termfrag: terminate the lattice with a given fragment
        :type min_term_frags: bool
        :param min_term_frags: minimize the geometry of terminating
            fragments
        :type up_to_nindex: bool
        :param up_to_nindex: enumerate nanotube structures on the n-index
        :type up_to_mindex: bool
        :param up_to_mindex: enumerate nanotube structures on the m-index
        :type nwalls: int
        :param nwalls: number of walls in a multi-wall nanotube
        :type wallsep: float
        :param wallsep: wall separation in Angstrom in a multi-wall
            nanotube
        :type orient: bool
        :param orient: whether to orient the sheets for Maestro
        :type logger: logging.getLogger
        :param logger: output
        :type is_coarse_grain: bool
        :param is_coarse_grain: Whether a coarse grain structure is being
            created
        """
        # check user options
        chkobj = CheckInput()
        chkobj.checkAll(element1, element2, bondlength, nindex, mindex, ncells,
                        no_double_bonds, termfrag, min_term_frags, up_to_nindex,
                        up_to_mindex, nwalls, wallsep, logger, is_coarse_grain)
        self.element1 = chkobj.element1
        self.element2 = chkobj.element2
        self.bondlength = chkobj.bondlength
        self.no_double_bonds = chkobj.no_double_bonds
        self.nindex = chkobj.nindex
        self.mindex = chkobj.mindex
        self.ncells = chkobj.ncells
        self.termfrag = chkobj.termfrag
        self.min_term_frags = chkobj.min_term_frags
        self.up_to_nindex = chkobj.up_to_nindex
        self.up_to_mindex = chkobj.up_to_mindex
        self.nwalls = chkobj.nwalls
        self.wallsep = chkobj.wallsep
        self.orient = orient
        self.is_coarse_grain = is_coarse_grain
        self.structures = []
        # print job parameters
        if logger:
            self.printJobParams(logger)
        # make single-walled nanotubes
        use_finite_bos = self.termfrag != constants.Constants.TERMFRAGS[0]
        self.nanotube_objs = self.makeSingleWalledTubes(use_finite_bos, logger)
        # make multi-walled nanotubes (potentially skip PBC stuff,
        # see MATSCI-2721 for strain model)
        if self.nwalls == 1:
            if logger:
                self.printSingleWalledTubes(logger)
        else:
            self.nanotube_objs = self.makeMultiWalledTubes(
                use_finite_bos, logger)
            if logger:
                self.printMultiWalledTubes(logger)
        # if terminating then return
        if not (self.orient and
                self.termfrag == constants.Constants.TERMFRAGS[0]):
            for tube in self.nanotube_objs:
                self.structures.append(tube.nanotube_st)
            return
        # handle periodicity
        for tube in self.nanotube_objs:
            if self.nwalls == 1:
                has_pbc = True
                radius = tube.radius
                termatoms = tube.termatoms
            else:
                has_pbc = tube.areSameLength()
                radius = tube.getRadius()
                termatoms = tube.getTerminatingIdxs()
            if not has_pbc:
                self.structures.append(tube.nanotube_st)
                continue
            vectors = xtal.get_vectors_from_chorus(tube.nanotube_st)
            tube.nanotube_st, origin, a_vec, b_vec, c_vec = \
                
slab.maestro_rotate_cell(tube.nanotube_st,
                    numpy.array(xtal.ParserWrapper.ORIGIN), *vectors)
            chorus_properties = list(a_vec) + list(b_vec) + list(c_vec)
            xtal.set_pbc_properties(tube.nanotube_st, chorus_properties)
            translate_tube(tube.nanotube_st, radius)
            tube.nanotube_st = sheet.build_cell(tube.nanotube_st,
                                                termatoms,
                                                tube.atomic_number1,
                                                tube.atomic_number2,
                                                self.bondlength,
                                                no_bonding_along=[0, 2])
            if self.is_coarse_grain:
                set_as_coarse_grain(tube.nanotube_st)
            elif not self.no_double_bonds:
                tube.nanotube_st = xtal.assign_bond_orders_w_mmlewis(
                    tube.nanotube_st, fix_metals=False, logger=logger)
            self.structures.append(tube.nanotube_st) 
[docs]    def printJobParams(self, logger=None):
        """
        Print job parameters.
        :type logger: logging.getLogger
        :param logger: output logger
        """
        # build formatted strings first
        element1 = tlog.get_param_string('Element 1', self.element1,
                                         self.MSGWIDTH)
        element2 = tlog.get_param_string('Element 2', self.element2,
                                         self.MSGWIDTH)
        bondlength = tlog.get_param_string('Bond length / Ang.',
                                           self.bondlength, self.MSGWIDTH)
        no_double_bonds = tlog.get_param_string('No double bonds',
                                                self.no_double_bonds,
                                                self.MSGWIDTH)
        nindex = tlog.get_param_string('First chiral index', self.nindex,
                                       self.MSGWIDTH)
        mindex = tlog.get_param_string('Second chiral index', self.mindex,
                                       self.MSGWIDTH)
        ncells = tlog.get_param_string('Number of unit cells', self.ncells,
                                       self.MSGWIDTH)
        termfrag = tlog.get_param_string('Terminating fragment', self.termfrag,
                                         self.MSGWIDTH)
        min_term_frags = tlog.get_param_string('Minimize terminating fragments',
                                               self.min_term_frags,
                                               self.MSGWIDTH)
        up_to_nindex = tlog.get_param_string('Enumerate on n-index',
                                             self.up_to_nindex, self.MSGWIDTH)
        up_to_mindex = tlog.get_param_string('Enumerate on m-index',
                                             self.up_to_mindex, self.MSGWIDTH)
        nwalls = tlog.get_param_string('Number of walls', self.nwalls,
                                       self.MSGWIDTH)
        wallsep = tlog.get_param_string('Wall separation / Ang.', self.wallsep,
                                        self.MSGWIDTH)
        SEPARATOR = self.MSGWIDTH * '='
        # print formatted strings
        logger.info('Job Parameters:')
        logger.info('')
        logger.info(SEPARATOR)
        logger.info('')
        logger.info('Nanotube')
        logger.info('--------')
        logger.info('')
        logger.info(element1)
        logger.info(element2)
        logger.info(bondlength)
        logger.info(no_double_bonds)
        logger.info(nindex)
        logger.info(mindex)
        logger.info(ncells)
        logger.info(termfrag)
        logger.info(min_term_frags)
        logger.info('')
        if self.up_to_nindex or self.up_to_mindex:
            logger.info('Single-walled nanotubes')
            logger.info('-----------------------')
            logger.info('')
            logger.info(up_to_nindex)
            logger.info(up_to_mindex)
            logger.info('')
        if self.nwalls > 1:
            logger.info('Multi-walled nanotubes')
            logger.info('----------------------')
            logger.info('')
            logger.info(nwalls)
            logger.info(wallsep)
            logger.info('')
        logger.info(SEPARATOR)
        logger.info('') 
[docs]    def makeSingleWalledTubes(self, use_finite_bos=True, logger=None):
        """
        Make single-walled nanotubes.
        :type use_finite_bos: bool
        :param use_finite_bos: use a bond order protocol meant for finite
            molecules
        :type logger: logging.getLogger
        :param logger: output logger
        :rtype: list of NanoTube
        :return: singletubes, contains all created single-walled tubes
        """
        # enumerate (n, m) lists
        nindicies = [self.nindex]
        mindicies = [self.mindex]
        if self.up_to_nindex:
            nindicies = list(range(self.mindex, self.nindex + 1))
        elif self.up_to_mindex:
            mindicies = list(range(0, self.mindex + 1))
        # for each (n, m) make a single tube
        singletubes = []
        for nindex in nindicies:
            for mindex in mindicies:
                singletube = NanoTube(self.element1, self.element2,
                                      self.bondlength, self.no_double_bonds,
                                      nindex, mindex, self.ncells,
                                      self.termfrag, self.min_term_frags,
                                      self.is_coarse_grain)
                singletube.buildTube(use_finite_bos, logger)
                singletubes.append(singletube)
        return singletubes 
[docs]    def printSingleWalledTubes(self, logger=None):
        """
        Formatted print of single-walled tubes.
        :type logger: logging.getLogger
        :param logger: output logger
        """
        # get the number of characters in the length of the list
        width = len(str(len(self.nanotube_objs)))
        # print header
        logger.info('Single-walled Nanotube Parameters:')
        logger.info('')
        # loop over tubes and print header, bond order msgs, and tube
        # properties
        for index, tube in enumerate(self.nanotube_objs, 1):
            header = 'SW Nanotube %s' % str(index).rjust(width)
            logger.info(header)
            logger.info('-' * len(header))
            if tube.bomsg:
                logger.info('')
                logger.warning(tube.bomsg)
            tube.printProps(logger) 
[docs]    def makeMultiWalledTubes(self, use_finite_bos=True, logger=None):
        """
        Make multi-walled nanotubes.
        :type use_finite_bos: bool
        :param use_finite_bos: use a bond order protocol meant for finite
            molecules
        :type logger: logging.getLogger
        :param logger: output logger
        :rtype: list of MultiWalledNanoTube
        :return: multitubes, contains all created multi-walled tubes
        """
        # for each inner tube create a multi-walled tube with the given specs
        multitubes = []
        for innertube in self.nanotube_objs:
            multitube = MultiWalledNanoTube(innertube, self.nwalls,
                                            self.wallsep)
            if not use_finite_bos and not multitube.areSameLength():
                use_finite_bos = True
            multitube.buildMultiWallTube(use_finite_bos, logger)
            multitubes.append(multitube)
        return multitubes 
[docs]    def printMultiWalledTubes(self, logger=None):
        """
        Formatted print of multi-walled tubes.
        :type logger: logging.getLogger
        :param logger: output logger
        """
        # get the number of characters in the length of the list
        mwwidth = len(str(len(self.nanotube_objs)))
        # print header
        logger.info('Multi-walled Nanotube Parameters:')
        logger.info('')
        # loop over multi-walled tubes and print header and multi-tube props
        for mwindex, multitube in enumerate(self.nanotube_objs, 1):
            mwheader = 'MW Nanotube %s' % str(mwindex).rjust(mwwidth)
            logger.info(mwheader)
            logger.info('-' * len(mwheader))
            multitube.printProps(logger)
            # get the number of characters in the length of the list
            swwidth = len(str(len(multitube.tubes)))
            # loop over single-walled tubes in the multi-walled tube and
            # print header, bond order msgs, and tube props
            for swindex, tube in enumerate(multitube.tubes, 1):
                swheader = 'SW Nanotube %s' % str(swindex).rjust(swwidth)
                logger.info(swheader)
                logger.info('-' * len(swheader))
                if tube.bomsg:
                    logger.info('')
                    logger.warning(tube.bomsg)
                tube.printProps(logger)  
[docs]def remove_pbc(astructure):
    """
    Remove the PBC definitions from the given structure.
    :type astructure: schrodinger.structure.Structure
    :param astructure: the structure for which to remove the PBC
    """
    cry = xtal.Crystal
    keys = cry.CHORUS_BOX_KEYS[:]
    keys.extend([cry.A_KEY, cry.B_KEY, cry.C_KEY, \
        
cry.ALPHA_KEY, cry.BETA_KEY, cry.GAMMA_KEY, cry.SPACE_GROUP_KEY])
    list(map(lambda x: astructure.property.pop(x, None), keys)) 
[docs]def translate_tube(tube, radius):
    """
    Translate the tube so that it is inside the box.
    :type tube: schrodinger.structure.Structure
    :param tube: the tube structure
    :type radius: float
    :param radius: the radius in Ang.
    """
    transvec = radius * numpy.array(transform.X_AXIS) + \
        
2 * radius * numpy.array(transform.Z_AXIS)
    transform.translate_structure(tube, *transvec) 
[docs]def get_tube_unit_cell(radius, length):
    """
    Get the a, b, and c vectors of the unit cell for a tube of given length and
    radius.
    :param float radius: The radius of the tube in Angstroms
    :param float length: The length of the tube in Angstroms
    :return tuple(numpy.array): Three numpy arrays each of length 3,
        corresponding to the a, b, and c vectors of the unit cell (in
        Angstroms)
    """
    vacuum = constants.Constants.WALLSEP
    a_vec = numpy.array([2 * radius + vacuum, 0.0, 0.0])
    b_vec = numpy.array([0.0, length, 0.0])
    c_vec = numpy.array([0.0, 0.0, 2 * radius + vacuum])
    return a_vec, b_vec, c_vec 
[docs]def get_tube_dimensions(chiral, translat):
    """
    Calculate the length and radius of a nanotube
    :param numpy.array chiral: The chiral vector of the nanutube
    :param numpy.array translat: The translat vector of the nanutube
    :return tuple(float, float): The length and radius of the nanotube,
        respectively. Units are in Angstroms.
    """
    length = numpy.linalg.norm(translat)
    radius = numpy.linalg.norm(chiral) / TWOPI
    return length, radius 
[docs]def get_tube_vectors(ncells, nindex, mindex, lattvec1, lattvec2):
    """
    Return chiral and translation vectors for a nanotube sheet.
    :param int ncells: number of unit cells
    :param int nindex: first chiral index
    :param int mindex: second chiral index
    :param numpy.array lattvec1: first lattice vector
    :param numpy.array lattvec2: second lattice vector
    :return tuple(numpy.array, numpy.array): The chiral and translat tube
        vectors, respectively
    """
    chiral = nindex * lattvec1 + mindex * lattvec2
    t1coef = 2 * mindex + nindex
    t2coef = 2 * nindex + mindex
    translat = t1coef * lattvec1 - t2coef * lattvec2
    gcd = xtal.gcd(t1coef, t2coef)
    # scale translation vector by the desired number of cells
    translat = ncells * translat / gcd
    return chiral, translat