Ejemplo n.º 1
0
 def default_command_library_name(self):
     available_libs = self.library_names()
     for lib_name in available_libs:
         if "Dunbrack" in lib_name:
             lib = lib_name
             break
     else:
         if available_libs:
             lib = list(available_libs)[0]
         else:
             raise LimitationError("No rotamer libraries installed")
     return lib
Ejemplo n.º 2
0
    def __init__(self, session, frames):

        Drawing.__init__(self, 'cross fade')
        self.frames = frames
        self.frame = 0
        self.rgba = None

        has_graphics = session.main_view.render is not None
        if not has_graphics:
            raise LimitationError(
                "Unable to do crossfade without rendering images")
        self.capture_image(session)
Ejemplo n.º 3
0
def label_listfonts(session):
    '''Report available fonts.'''
    has_graphics = session.main_view.render is not None
    if not has_graphics:
        from chimerax.core.errors import LimitationError
        raise LimitationError(
            "Unable to do list fonts without being able to render images")
    from PyQt5.QtGui import QFontDatabase
    fdb = QFontDatabase()
    fnames = list(fdb.families())
    fnames.sort()
    session.logger.info('%d fonts available:\n%s' %
                        (len(fnames), '\n'.join(fnames)))
Ejemplo n.º 4
0
 def _menu_show_cb(self, menu, installed_only):
     menu.clear()
     names = self.library_names(installed_only=installed_only)
     if not names:
         raise LimitationError(
             "No rotamer libraries %s!" %
             ("installed" if installed_only else "available"))
     names.sort()
     installed = set(self.library_names(installed_only=True))
     for name in names:
         if name in installed:
             menu.addAction(name)
         else:
             menu.addAction(name + self._uninstalled_suffix)
Ejemplo n.º 5
0
def cmd_save(session, file_name, rest_of_line, *, log=True):
    tokens = []
    remainder = rest_of_line
    while remainder:
        token, token_log, remainder = next_token(remainder)
        remainder = remainder.lstrip()
        tokens.append(token)
    format_name = None
    for i in range(len(tokens)-2, -1, -2):
        test_token = tokens[i].lower()
        if "format".startswith(test_token):
            format_name = tokens[i+1]
    provider_cmd_text = "save " + " ".join([FileNameArg.unparse(file_name)]
        + [StringArg.unparse(token) for token in tokens])

    try:
        from .manager import NoSaverError
        mgr = session.save_command
        data_format= file_format(session, file_name, format_name)
        try:
            provider_args = mgr.save_args(data_format)
        except NoSaverError as e:
            raise LimitationError(str(e))

        # register a private 'save' command that handles the provider's keywords
        registry = RegisteredCommandInfo()
        keywords = {
            'format': DynamicEnum(lambda ses=session: format_names(ses)),
        }
        for keyword, annotation in provider_args.items():
            if keyword in keywords:
                raise ValueError("Save-provider keyword '%s' conflicts with builtin arg"
                    " of same name" % keyword)
            keywords[keyword] = annotation
        # for convenience, allow 'models' to be a second positional argument instead of a keyword
        if 'models' in keywords:
            optional = [('models', keywords['models'])]
            del keywords['models']
        else:
            optional = []
        desc = CmdDesc(required=[('file_name', SaveFileNameArg)], optional=optional,
            keyword=keywords.items(), hidden=mgr.hidden_args(data_format), synopsis="unnecessary")
        register("save", desc, provider_save, registry=registry)
    except BaseException as e:
        # want to log command even for keyboard interrupts
        log_command(session, "save", provider_cmd_text, url=_main_save_CmdDesc.url)
        raise
    Command(session, registry=registry).run(provider_cmd_text, log=log)
Ejemplo n.º 6
0
def register_attr(session, class_obj, attr_name, attr_type):
    if hasattr(class_obj, 'register_attr'):
        from chimerax.atomic.attr_registration import RegistrationConflict
        try:
            class_obj.register_attr(session,
                                    attr_name,
                                    "setattr command",
                                    attr_type=attr_type)
        except RegistrationConflict as e:
            from chimerax.core.errors import LimitationError
            raise LimitationError(str(e))
    else:
        session.logger.warning(
            "Class %s does not support attribute registration; '%s' attribute"
            " will not be preserved in sessions." %
            (class_obj.__name__, attr_name))
Ejemplo n.º 7
0
def unused_chain_id(structure):
    from string import ascii_uppercase as uppercase, ascii_lowercase as lowercase, digits
    existing_ids = set([chain.chain_id for chain in structure.chains])
    for chain_characters in [uppercase, uppercase + digits + lowercase]:
        for id_length in range(1, 5):
            chain_id = _gen_chain_id(existing_ids, "", chain_characters,
                                     id_length - 1)
            if chain_id:
                break
        else:
            continue
        break
    if chain_id is None:
        from chimerax.core.errors import LimitationError
        raise LimitationError(
            "Could not find unused legal chain ID for peptide!")
    return chain_id
Ejemplo n.º 8
0
    def __init__(self, viewer):

        Drawing.__init__(self, 'motion blur')
        self.viewer = viewer
        self.rgba = None
        self.decay_factor = 0.9
        '''
        The Nth previous rendered frame is dimmed by the decay factor to
        the Nth power.  The dimming is achieved by fading to the current
        background color.
        '''
        self.attenuate = 0.5
        "All preceding frames are additionally dimmed by this factor."
        self.changed = True

        has_graphics = viewer.render is not None
        if not has_graphics:
            raise LimitationError(
                "Unable to do motion blur without rendering images")
        self.capture_image()
Ejemplo n.º 9
0
def fetch_info(mgr, file_arg, format_name, database_name):
    if not database_name and exists_locally(file_arg, format_name):
        return None
    if ':' in file_arg:
        db_name, ident = file_arg.split(':', maxsplit=1)
        if len(db_name) < 2:
            return None
    elif database_name:
        db_name = database_name
        ident = file_arg
    elif likely_pdb_id(file_arg, format_name):
        db_name = "pdb"
        ident = file_arg
    else:
        return None
    from .manager import NoOpenerError
    try:
        db_formats = list(mgr.database_info(db_name).keys())
    except NoOpenerError as e:
        raise LimitationError(str(e))
    if format_name and format_name not in db_formats:
        # for backwards compatibiity, accept formal format name or nicknames
        try:
            df = mgr.session.data_formats[format_name]
        except KeyError:
            nicks = []
        else:
            nicks = df.nicknames + [df.name]
        for nick in nicks:
            if nick in db_formats:
                format_name = nick
                break
        else:
            from chimerax.core.commands import commas
            raise UserError("Format '%s' not supported for database '%s'.  Supported"
                " formats are: %s" % (format_name, db_name,
                commas([dbf for dbf in db_formats])))
    return (ident, db_name, format_name)
Ejemplo n.º 10
0
 def run(self, port, use_ssl):
     from http.server import HTTPServer
     import sys
     if port is None:
         # Defaults to any available port
         port = 0
     if use_ssl is None:
         # Defaults to cleartext
         use_ssl = False
     self.httpd = HTTPServer(("localhost", port), RESTHandler)
     self.httpd.chimerax_restserver = self
     if not use_ssl:
         proto = "http"
     else:
         proto = "https"
         try:
             import os.path, ssl
         except ImportError:
             from chimerax.core.errors import LimitationError
             raise LimitationError("SSL is not supported")
         context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
         cert = os.path.join(os.path.dirname(__file__), "server.pem")
         context.load_cert_chain(cert)
         # self.httpd.socket = ssl.wrap_socket(self.httpd.socket,
         #                                     certfile=cert)
         self.httpd.socket = context.wrap_socket(self.httpd.socket,
                                                 server_side=True)
     self.run_increment()  # To match decrement in terminate()
     host, port = self.httpd.server_address
     msg = ("REST server started on host %s port %d" % (host, port))
     self.session.ui.thread_safe(print,
                                 msg,
                                 file=sys.__stdout__,
                                 flush=True)
     msg += ('\nVisit %s://%s:%d/cmdline.html for CLI interface' %
             (proto, host, port))
     self.session.ui.thread_safe(self.session.logger.info, msg)
     self.httpd.serve_forever()
Ejemplo n.º 11
0
def save_image(session,
               path,
               format_name,
               width=None,
               height=None,
               supersample=3,
               pixel_size=None,
               transparent_background=False,
               quality=95):
    '''
    Save an image of the current graphics window contents.
    '''
    from chimerax.core.errors import UserError, LimitationError
    has_graphics = session.main_view.render is not None
    if not has_graphics:
        raise LimitationError(
            "Unable to save images because OpenGL rendering is not available")
    from os.path import dirname, exists
    dir = dirname(path)
    if dir and not exists(dir):
        raise UserError('Directory "%s" does not exist' % dir)

    if pixel_size is not None:
        if width is not None or height is not None:
            raise UserError(
                'Cannot specify width or height if pixel_size is given')
        v = session.main_view
        b = v.drawing_bounds()
        if b is None:
            raise UserError(
                'Cannot specify use pixel_size option when nothing is shown')
        psize = v.pixel_size(b.center())
        if psize > 0 and pixel_size > 0:
            f = psize / pixel_size
            w, h = v.window_size
            from math import ceil
            width, height = int(ceil(f * w)), int(ceil(f * h))
        else:
            raise UserError(
                'Pixel size option (%g) and screen pixel size (%g) must be positive'
                % (pixel_size, psize))

    from chimerax.core.session import standard_metadata
    std_metadata = standard_metadata()
    metadata = {}
    if format_name == 'PNG':
        metadata['optimize'] = True
        # if dpi is not None:
        #     metadata['dpi'] = (dpi, dpi)
        if session.main_view.render.opengl_context.pixel_scale() == 2:
            metadata['dpi'] = (144, 144)
        from PIL import PngImagePlugin
        pnginfo = PngImagePlugin.PngInfo()

        # tags are from <https://www.w3.org/TR/PNG/#11textinfo>

        def add_text(keyword, value):
            try:
                b = value.encode('latin-1')
            except UnicodeEncodeError:
                pnginfo.add_itxt(keyword, value)
            else:
                pnginfo.add_text(keyword, b)

        # add_text('Title', description)
        add_text('Creation Time', std_metadata['created'])
        add_text('Software', std_metadata['generator'])
        add_text('Author', std_metadata['creator'])
        add_text('Copy' 'right', std_metadata['dateCopyrighted'])
        metadata['pnginfo'] = pnginfo
    elif format_name == 'TIFF':
        # metadata['compression'] = 'lzw:2'
        # metadata['description'] = description
        metadata['software'] = std_metadata['generator']
        # TIFF dates are YYYY:MM:DD HH:MM:SS (local timezone)
        import datetime as dt
        metadata['date_time'] = dt.datetime.now().strftime('%Y:%m:%d %H:%M:%S')
        metadata['artist'] = std_metadata['creator']
        # TIFF copy right is ASCII, so no Unicode symbols
        cp = std_metadata['dateCopyrighted']
        if cp[0] == '\N{COPYRIGHT SIGN}':
            cp = 'Copy' 'right' + cp[1:]
        metadata['copy' 'right'] = cp
        # if units == 'pixels':
        #     dpi = None
        # elif units in ('points', 'inches'):
        #     metadata['resolution unit'] = 'inch'
        #     metadata['x resolution'] = dpi
        #     metadata['y resolution'] = dpi
        # elif units in ('millimeters', 'centimeters'):
        #     adjust = convert['centimeters'] / convert['inches']
        #     dpcm = dpi * adjust
        #     metadata['resolution unit'] = 'cm'
        #     metadata['x resolution'] = dpcm
        #     metadata['y resolution'] = dpcm
    elif format_name == 'JPEG':
        metadata['quality'] = quality
        # if dpi is not None:
        #     # PIL's jpeg_encoder requires integer dpi values
        #     metadata['dpi'] = (int(dpi), int(dpi))
        # TODO: create exif with metadata using piexif package?
        # metadata['exif'] = exif

    view = session.main_view
    view.render.make_current()
    max_size = view.render.max_framebuffer_size()
    if max_size and ((width is not None and width > max_size) or
                     (height is not None and height > max_size)):
        raise UserError(
            'Image size %d x %d too large, exceeds maximum OpenGL render buffer size %d'
            % (width, height, max_size))

    i = view.image(width,
                   height,
                   supersample=supersample,
                   transparent_background=transparent_background)
    if i is not None:
        try:
            i.save(path, format_name, **metadata)
        except PermissionError:
            from chimerax.core.errors import UserError
            raise UserError('Permission denied writing file %s' % path)
    else:
        msg = "Unable to save image"
        if width is not None:
            msg += ', width %d' % width
        if height is not None:
            msg += ', height %d' % height
        session.logger.warning(msg)
Ejemplo n.º 12
0
def use_rotamer(session, res, rots, retain=False, log=False, bfactor=None):
    """Takes a Residue instance and either a list or dictionary of rotamers (as returned by get_rotamers,
       i.e. with backbone already matched) and swaps the Residue's side chain with the given rotamers.

       If the rotamers are a dictionary, then the keys should match the alt locs of the CA atom, and
       the corresponding rotamer will be used for that alt loc.  If the alt locs are a list, if the list
       has only one rotamer then that rotamer will be used for each CA alt loc.  If the list has multiple
       rotamers, then the CA must have only one alt loc (namely ' ') and all the rotamers will be attached,
       using different alt loc characters for each.

       If 'retain' is True, existing side chains will be retained.  If 'bfactor' is None, then the
       current highest existing bfactor in the residue will be used.
    """
    N = res.find_atom("N")
    CA = res.find_atom("CA")
    C = res.find_atom("C")
    if not N or not C or not CA:
        raise LimitationError(
            "N, CA, or C missing from %s: needed for side-chain pruning algorithm"
            % res)
    import string
    alt_locs = string.ascii_uppercase + string.ascii_lowercase + string.digits + string.punctuation
    if retain and CA.alt_locs:
        raise LimitationError(
            "Cannot retain side chains if multiple CA alt locs")
    ca_alt_locs = [' '] if not CA.alt_locs else CA.alt_locs
    if not isinstance(rots, dict):
        # reformat as dictionary
        if CA.alt_locs and len(rots) > 1:
            raise LimitationError(
                "Cannot add multiple rotamers to multi-position backbone")
        retained_alt_locs = side_chain_locs(res) if retain else []
        num_retained = len(retained_alt_locs)
        if len(rots) + num_retained > len(alt_locs):
            raise LimitationError("Don't have enough unique alternate "
                                  "location characters to place %d rotamers." %
                                  len(rots))
        if len(rots) + num_retained > 1:
            rots = {
                loc: rot
                for loc, rot in zip(
                    [c for c in alt_locs
                     if c not in retained_alt_locs][:len(rots)], rots)
            }
        else:
            rots = {alt_loc: rots[0] for alt_loc in ca_alt_locs}
    swap_type = list(rots.values())[0].residues[0].name
    if retain and res.name != swap_type:
        raise LimitationError(
            "Cannot retain side chains if rotamers are a different residue type"
        )
    rot_anchors = {}
    for rot in rots.values():
        rot_res = rot.residues[0]
        rot_N, rot_CA = rot_res.find_atom("N"), rot_res.find_atom("CA")
        if not rot_N or not rot_CA:
            raise LimitationError(
                "N or CA missing from rotamer: cannot matchup with original residue"
            )
        rot_anchors[rot] = (rot_N, rot_CA)
    color_by_element = N.color != CA.color
    if color_by_element:
        carbon_color = CA.color
    else:
        uniform_color = N.color
    # prune old side chain
    bfactor = bfactor_for_res(res, bfactor)
    if not retain:
        res_atoms = res.atoms
        side_atoms = res_atoms.filter(res_atoms.is_side_onlys)
        serials = {a.name: a.serial_number for a in side_atoms}
        side_atoms.delete()
    else:
        serials = {}
    # for proline, also prune amide hydrogens
    if swap_type == "PRO":
        for nnb in N.neighbors[:]:
            if nnb.element.number == 1:
                N.structure.delete_atom(nnb)

    tot_prob = sum([r.rotamer_prob for r in rots.values()])
    with CA.suppress_alt_loc_change_notifications():
        res.name = swap_type
        from chimerax.atomic.struct_edit import add_atom, add_bond
        for alt_loc, rot in rots.items():
            if CA.alt_locs:
                CA.alt_loc = alt_loc
            if log:
                extra = " using alt loc %s" % alt_loc if alt_loc != ' ' else ""
                session.logger.info(
                    "Applying %s rotamer (chi angles: %s) to %s%s" %
                    (rot_res.name, " ".join(["%.1f" % c
                                             for c in rot.chis]), res, extra))
            # add new side chain
            rot_N, rot_CA = rot_anchors[rot]
            visited = set([N, CA, C])
            sprouts = [rot_CA]
            while sprouts:
                sprout = sprouts.pop()
                built_sprout = res.find_atom(sprout.name)
                for nb in sprout.neighbors:
                    built_nb = res.find_atom(nb.name)
                    if tot_prob == 0.0:
                        # some rotamers in Dunbrack are zero prob!
                        occupancy = 1.0 / len(rots)
                    else:
                        occupancy = rot.rotamer_prob / tot_prob
                    if not built_nb:
                        serial = serials.get(nb.name, None)
                        built_nb = add_atom(nb.name,
                                            nb.element,
                                            res,
                                            nb.coord,
                                            serial_number=serial,
                                            bonded_to=built_sprout,
                                            alt_loc=alt_loc)
                        built_nb.occupancy = occupancy
                        built_nb.bfactor = bfactor
                        if color_by_element:
                            if built_nb.element.name == "C":
                                built_nb.color = carbon_color
                            else:
                                from chimerax.atomic.colors import element_color
                                built_nb.color = element_color(
                                    built_nb.element.number)
                        else:
                            built_nb.color = uniform_color
                    elif built_nb not in visited:
                        built_nb.set_alt_loc(alt_loc, True)
                        built_nb.coord = nb.coord
                        built_nb.occupancy = occupancy
                        built_nb.bfactor = bfactor
                    if built_nb not in visited:
                        sprouts.append(nb)
                        visited.add(built_nb)
                    if built_nb not in built_sprout.neighbors:
                        add_bond(built_sprout, built_nb)
Ejemplo n.º 13
0
def get_rotamers(session,
                 res,
                 phi=None,
                 psi=None,
                 cis=False,
                 res_type=None,
                 rot_lib="Dunbrack",
                 log=False):
    """Takes a Residue instance and optionally phi/psi angles (if different from the Residue), residue
       type (e.g. "TYR"), and/or rotamer library name.  Returns a list of AtomicStructure instances (sublass of
       AtomicStructure).  The AtomicStructure are each a single residue (a rotamer) and are in descending
       probability order.  Each has an attribute "rotamer_prob" for the probability and "chis" for the
       chi angles.
    """
    res_type = res_type or res.name
    if res_type == "ALA" or res_type == "GLY":
        raise NoResidueRotamersError("No rotamers for %s" % res_type)

    if not isinstance(rot_lib, RotamerLibrary):
        rot_lib = session.rotamers.library(rot_lib)

    # check that the residue has the n/c/ca atoms needed to position the rotamer
    # and to ensure that it is an amino acid
    from chimerax.atomic import Residue
    match_atoms = {}
    for bb_name in Residue.aa_min_backbone_names:
        match_atoms[bb_name] = a = res.find_atom(bb_name)
        if a is None:
            raise LimitationError("%s missing from %s; needed to position CB" %
                                  (bb_name, res))
    match_atoms["CB"] = res.find_atom("CB")
    if not phi and not psi:
        phi, psi = res.phi, res.psi
        omega = res.omega
        cis = False if omega is None or abs(omega) > 90 else True
        if log:

            def _info(ang):
                if ang is None:
                    return "none"
                return "%.1f" % ang

            if match_atoms["CA"].alt_locs:
                al_info = " (alt loc %s)" % match_atoms["CA"].alt_loc
            else:
                al_info = ""
            session.logger.info("%s%s: phi %s, psi %s %s" %
                                (res, al_info, _info(phi), _info(psi),
                                 "cis" if cis else "trans"))
    session.logger.status("Retrieving rotamers from %s library" %
                          rot_lib.display_name)
    res_template_func = rot_lib.res_template_func
    params = rot_lib.rotamer_params(res_type, phi, psi, cis=cis)
    session.logger.status("Rotamers retrieved from %s library" %
                          rot_lib.display_name)

    mapped_res_type = rot_lib.res_name_mapping.get(res_type, res_type)
    template = rot_lib.res_template_func(mapped_res_type)
    tmpl_N = template.find_atom("N")
    tmpl_CA = template.find_atom("CA")
    tmpl_C = template.find_atom("C")
    tmpl_CB = template.find_atom("CB")
    if match_atoms['CB']:
        res_match_atoms, tmpl_match_atoms = [
            match_atoms[x] for x in ("C", "CA", "CB")
        ], [tmpl_C, tmpl_CA, tmpl_CB]
    else:
        res_match_atoms, tmpl_match_atoms = [
            match_atoms[x] for x in ("N", "CA", "C")
        ], [tmpl_N, tmpl_CA, tmpl_C]
    from chimerax.geometry import align_points
    from numpy import array
    xform, rmsd = align_points(array([fa.coord for fa in tmpl_match_atoms]),
                               array([ta.coord for ta in res_match_atoms]))
    n_coord = xform * tmpl_N.coord
    ca_coord = xform * tmpl_CA.coord
    cb_coord = xform * tmpl_CB.coord
    info = Residue.chi_info[mapped_res_type]
    bond_cache = {}
    angle_cache = {}
    from chimerax.atomic.struct_edit import add_atom, add_dihedral_atom, add_bond
    structs = []
    middles = {}
    ends = {}
    for i, rp in enumerate(params):
        s = AtomicStructure(session, name="rotamer %d" % (i + 1))
        structs.append(s)
        r = s.new_residue(mapped_res_type, 'A', 1)
        registerer = "swap_res get_rotamers"
        AtomicStructure.register_attr(session,
                                      "rotamer_prob",
                                      registerer,
                                      attr_type=float)
        s.rotamer_prob = rp.p
        AtomicStructure.register_attr(session, "chis", registerer)
        s.chis = rp.chis
        rot_N = add_atom("N", tmpl_N.element, r, n_coord)
        rot_CA = add_atom("CA", tmpl_CA.element, r, ca_coord, bonded_to=rot_N)
        rot_CB = add_atom("CB", tmpl_CB.element, r, cb_coord, bonded_to=rot_CA)
        todo = []
        for j, chi in enumerate(rp.chis):
            n3, n2, n1, new = info[j]
            b_len, angle = _len_angle(new, n1, n2, template, bond_cache,
                                      angle_cache)
            n3 = r.find_atom(n3)
            n2 = r.find_atom(n2)
            n1 = r.find_atom(n1)
            new = template.find_atom(new)
            a = add_dihedral_atom(new.name,
                                  new.element,
                                  n1,
                                  n2,
                                  n3,
                                  b_len,
                                  angle,
                                  chi,
                                  bonded=True)
            todo.append(a)
            middles[n1] = [a, n1, n2]
            ends[a] = [a, n1, n2]

        # if there are any heavy non-backbone atoms bonded to template
        # N and they haven't been added by the above (which is the
        # case for Richardson proline parameters) place them now
        for tnnb in tmpl_N.neighbors:
            if r.find_atom(tnnb.name) or tnnb.element.number == 1:
                continue
            tnnb_coord = xform * tnnb.coord
            add_atom(tnnb.name, tnnb.element, r, tnnb_coord, bonded_to=rot_N)

        # fill out bonds and remaining heavy atoms
        from chimerax.geometry import distance, align_points
        done = set([rot_N, rot_CA])
        while todo:
            a = todo.pop(0)
            if a in done:
                continue
            tmpl_A = template.find_atom(a.name)
            for bonded, bond in zip(tmpl_A.neighbors, tmpl_A.bonds):
                if bonded.element.number == 1:
                    continue
                rbonded = r.find_atom(bonded.name)
                if rbonded is None:
                    # use middles if possible...
                    try:
                        p1, p2, p3 = middles[a]
                        conn = p3
                    except KeyError:
                        p1, p2, p3 = ends[a]
                        conn = p2
                    t1 = template.find_atom(p1.name)
                    t2 = template.find_atom(p2.name)
                    t3 = template.find_atom(p3.name)
                    xform = align_points(
                        array([t.coord for t in [t1, t2, t3]]),
                        array([p.coord for p in [p1, p2, p3]]))[0]
                    pos = xform * template.find_atom(bonded.name).coord
                    rbonded = add_atom(bonded.name,
                                       bonded.element,
                                       r,
                                       pos,
                                       bonded_to=a)
                    middles[a] = [rbonded, a, conn]
                    ends[rbonded] = [rbonded, a, conn]
                if a not in rbonded.neighbors:
                    add_bond(a, rbonded)
                if rbonded not in done:
                    todo.append(rbonded)
            done.add(a)
    return structs
Ejemplo n.º 14
0
def swap_aa(session,
            residues,
            res_type,
            *,
            bfactor=None,
            clash_hbond_allowance=None,
            clash_score_method="sum",
            clash_overlap_cutoff=None,
            criteria=default_criteria,
            density=None,
            hbond_angle_slop=None,
            hbond_dist_slop=None,
            hbond_relax=True,
            ignore_other_models=False,
            rot_lib=defaults['library'],
            log=True,
            preserve=None,
            retain=False):
    """backend implementation of "swapaa" command."""
    rotamers = {}
    destroy_list = []
    for res in residues:
        if res_type == "same":
            r_type = res.name
        else:
            r_type = res_type.upper()
        CA = res.find_atom("CA")
        if not CA:
            raise LimitationError("Residue %s is missing CA atom" % res)
        alt_locs = [' '] if CA.alt_loc == ' ' else CA.alt_locs
        with CA.suppress_alt_loc_change_notifications():
            rotamers[res] = by_alt_loc = {}
            for alt_loc in alt_locs:
                CA.alt_loc = alt_loc
                try:
                    rots = get_rotamers(session,
                                        res,
                                        res_type=r_type,
                                        rot_lib=rot_lib,
                                        log=log)
                except UnsupportedResTypeError:
                    raise LimitationError(
                        "%s rotamer library does not support %s" %
                        (rot_lib, r_type))
                except NoResidueRotamersError:
                    if log:
                        session.logger.info("Swapping %s to %s\n" %
                                            (res, r_type))
                    try:
                        template_swap_res(res, r_type, bfactor=bfactor)
                    except TemplateSwapError as e:
                        raise UserError(str(e))
                    continue
                except NoRotamerLibraryError:
                    raise UserError("No rotamer library named '%s'" % rot_lib)
                if preserve is not None:
                    rots = prune_by_chis(session, rots, res, preserve, log=log)
                by_alt_loc[alt_loc] = rots
                destroy_list.extend(rots)
            if not by_alt_loc:
                del rotamers[res]
    if not rotamers:
        return

    if isinstance(criteria, str):
        # this implementation allows tie-breaking criteria to be skipped if
        # there are no ties
        cmp = lambda p1, p2: 1 if p1 > p2 else (0 if p1 == p2 else -1)
        for char in criteria:
            if char == "d":
                # density
                from chimerax.map import Volume
                maps = [m for m in session.models if isinstance(m, Volume)]
                if not maps:
                    if criteria is default_criteria:
                        continue
                    raise UserError(
                        "Density criteria requested but no volume models are open"
                    )
                elif len(maps) > 1:
                    if density is None:
                        raise UserError(
                            "Density criteria with multiple volume models open;\n"
                            "Need to specify one to use via 'density' keyword."
                        )
                    map = density
                else:
                    map = maps[0]
                for res, by_alt_loc in rotamers.items():
                    process_volume(session, res, by_alt_loc, map)
                fetch = lambda r: r.volume_score
                test = cmp
            elif char == "c":
                # clash
                if clash_hbond_allowance is None or clash_overlap_cutoff is None:
                    from chimerax.clashes.settings import defaults
                    if clash_hbond_allowance is None:
                        clash_hbond_allowance = defaults[
                            'clash_hbond_allowance']
                    if clash_overlap_cutoff is None:
                        clash_overlap_cutoff = defaults['clash_threshold']
                for res, by_alt_loc in rotamers.items():
                    process_clashes(session, res, by_alt_loc,
                                    clash_overlap_cutoff,
                                    clash_hbond_allowance, clash_score_method,
                                    False, None, None, ignore_other_models)
                fetch = lambda r: r.clash_score
                test = lambda s1, s2: cmp(s2, s1)  # _lowest_ clash score
            elif char == 'h':
                # H bonds
                if hbond_angle_slop is None or hbond_dist_slop is None:
                    from chimerax.hbonds import rec_angle_slop, rec_dist_slop
                    if hbond_angle_slop is None:
                        hbond_angle_slop = rec_angle_slop
                    if hbond_dist_slop is None:
                        hbond_dist_slop = rec_dist_slop
                session.logger.status("Processing H-bonds for %s" % res)
                for res, by_alt_loc in rotamers.items():
                    process_hbonds(session,
                                   res,
                                   by_alt_loc,
                                   False,
                                   None,
                                   None,
                                   hbond_relax,
                                   hbond_dist_slop,
                                   hbond_angle_slop,
                                   False,
                                   None,
                                   ignore_other_models,
                                   cache_da=True)
                session.logger.status("")
                from chimerax.hbonds import flush_cache
                flush_cache()
                fetch = lambda r: r.num_hbonds
                test = cmp
            elif char == 'p':
                # most probable
                fetch = lambda r: r.rotamer_prob
                test = cmp
            elif isinstance(criteria, int):
                # Nth most probable
                index = criteria - 1
                for res, by_alt_loc in rotamers.items():
                    for alt_loc, rots in list(by_alt_loc.items()):
                        if index >= len(rots):
                            if log:
                                session.logger.status(
                                    "Residue %s does not have %d %s"
                                    " rotamers; skipping" %
                                    (res, criteria, r_type),
                                    log=True,
                                    color="red")
                            return
                        by_alt_loc[alt_loc] = [rots[index]]
                fetch = lambda r: 1
                test = lambda v1, v2: 1

            still_multiple_choices = False
            for res, by_alt_loc in rotamers.items():
                for alt_loc, rots in list(by_alt_loc.items()):
                    if len(rots) == 1:
                        continue
                    best = None
                    for rot in rots:
                        val = fetch(rot)
                        if best == None or test(val, best_val) > 0:
                            best = [rot]
                            best_val = val
                        elif test(val, best_val) == 0:
                            best.append(rot)
                    by_alt_loc[alt_loc] = best
                    if len(best) > 1:
                        still_multiple_choices = True
            if not still_multiple_choices:
                break
        for res, by_alt_loc in rotamers.items():
            for alt_loc, rots in list(by_alt_loc.items()):
                if len(rots) > 1:
                    if log:
                        session.logger.info("%s has %d equal-value rotamers;"
                                            " choosing one arbitrarily." %
                                            (res, len(rots)))
                by_alt_loc[alt_loc] = rots[0]
            use_rotamer(session,
                        res,
                        rotamers[res],
                        retain=retain,
                        log=log,
                        bfactor=bfactor)
    else:
        # Nth-most-probable rotamer(s)
        for res, by_alt_loc in list(rotamers.items()):
            if len(by_alt_loc) > 1:
                if len(critera) > 1:
                    raise LimitationError(
                        "Cannot assign multiple rotamers to multiple alt locs")
                for alt_loc, rots in list(by_alt_loc.items()):
                    try:
                        by_alt_loc[alt_loc] = rots[criteria[0] - 1]
                    except IndexError:
                        raise UserError("Less that %d rotamers for %s" %
                                        (criteria[0], res))
            else:
                rots = list(by_alt_loc.values())[0]
                try:
                    p_rots = [rots[i - 1] for i in criteria]
                except IndexError:
                    raise UserError("Only %d rotamers for %s" %
                                    (len(rots), res))
                rotamers[res] = p_rots
        for res in rotamers:
            use_rotamer(session,
                        res,
                        rotamers[res],
                        retain=retain,
                        log=log,
                        bfactor=bfactor)

    for rot in destroy_list:
        rot.delete()
Ejemplo n.º 15
0
def add_dimensions(name, info, session=None):
    from chimerax.core.errors import LimitationError
    raise LimitationError("Custom dimensions are not supported at this time")
    # TODO: rest of this
    """
Ejemplo n.º 16
0
def remove_dimensions(name):
    from chimerax.core.errors import LimitationError
    raise LimitationError("Custom dimensions are not supported at this time")
    # TODO: rest of this
    """
Ejemplo n.º 17
0
def label(session,
          objects=None,
          object_type=None,
          text=None,
          offset=None,
          color=None,
          bg_color=None,
          attribute=None,
          size=None,
          height=None,
          default_height=None,
          font=None,
          on_top=None):
    '''Create atom labels. The belong to a child model named "labels" of the structure.

    Parameters
    ----------
    objects : Objects or None
      Create labels on specified atoms, residues, pseudobonds, or bonds.
      If None then adjust settings of all existing labels.
    object_type : 'atoms', 'residues', 'pseudobonds', 'bonds'
      What type of object to label.
    text : string or "default"
      Displayed text of the label.
    offset : float 3-tuple or "default"
      Offset of label from atom center in screen coordinates in physical units (Angstroms)
    color : Color or "default"
      Color of the label text.  If no color is specified black is used on light backgrounds
      and white is used on dark backgrounds.
    bg_color : Color or "none"
      Draw rectangular label background in this color, or if "none", background is transparent.
    attribute : string
      Attribute name whose value to display as text
    size : int or "default"
      Font size in points (1/72 inch). Default 48.
    height : float or "fixed"
      Text height in scene units.  Or if "fixed" use fixed pixel height on screen.  Initial value 0.7.
    default_height : float
      Default height value if not specified.  Initial value 0.7.
    font : string or "default"
      Font name.  This must be a true type font installed on Mac in /Library/Fonts
      and is the name of the font file without the ".ttf" suffix.  Default "Arial".
    on_top : bool
      Whether labels always appear on top of other graphics (cannot be occluded).
      This is a per-structure attribute.  Default True.
    '''
    if object_type is None:
        if objects is None:
            otypes = ['atoms', 'residues', 'pseudobonds', 'bonds']
        elif len(objects.atoms) == 0:
            otypes = ['pseudobonds']
        else:
            otypes = ['residues']
    else:
        otypes = [object_type]

    from chimerax.core.errors import UserError
    if text is not None and attribute is not None:
        raise UserError("Cannot specify both 'text' and 'attribute' keywords")

    has_graphics = session.main_view.render is not None
    if not has_graphics:
        from chimerax.core.errors import LimitationError
        raise LimitationError(
            "Unable to draw 3D labels without rendering images")

    settings = {}
    if text == 'default':
        settings['text'] = None
    elif text is not None:
        settings['text'] = text
    if offset == 'default':
        settings['offset'] = None
    elif offset is not None:
        settings['offset'] = offset
    from chimerax.core.colors import Color
    if isinstance(color, Color):
        settings['color'] = color.uint8x4()
    elif color == 'default':
        settings['color'] = None
    if isinstance(bg_color, Color):
        settings['background'] = bg_color.uint8x4()
    elif bg_color == 'none':
        settings['background'] = None
    if size == 'default':
        settings['size'] = 48
    elif size is not None:
        settings['size'] = size
    if height == 'fixed':
        settings['height'] = None
    elif height is not None:
        settings['height'] = height
    if default_height is not None:
        from .settings import settings as prefs
        prefs.label_height = default_height
    if font == 'default':
        settings['font'] = 'Arial'
    elif font is not None:
        settings['font'] = font
    if 'text' in settings:
        settings['attribute'] = False
    elif attribute is not None:
        settings['text'] = False
        settings['attribute'] = attribute

    if objects is None and len(settings) == 0 and on_top is None:
        return  # Get this when setting default height.

    view = session.main_view
    lcount = 0
    for otype in otypes:
        if objects is None:
            mo = labeled_objects_by_model(session, otype)
        else:
            mo = objects_by_model(objects, otype)
        object_class = label_object_class(otype)
        for m, mobjects in mo:
            lm = labels_model(m, create=True)
            lm.add_labels(mobjects, object_class, view, settings, on_top)
            lcount += len(mobjects)
    if objects is None and lcount == 0 and default_height is None:
        raise UserError(
            'Label command requires an atom specifier to create labels.')
Ejemplo n.º 18
0
def label_create(session,
                 name,
                 text='',
                 color=None,
                 bg_color=None,
                 size=24,
                 font='Arial',
                 bold=None,
                 italic=None,
                 xpos=0.5,
                 ypos=0.5,
                 visibility=True,
                 margin=0,
                 outline=0):
    '''Create a label at a fixed position in the graphics window.

    Parameters
    ----------
    name : string
      Identifier for the label used to change or delete label.
    text : string
      Displayed text of the label.
    color : Color
      Color of the label text.  If no color is specified black is used on light backgrounds
      and white is used on dark backgrounds.
    bg_color : Color
      Draw rectangular label background in this color.  If omitted, background is transparent.
    size : int
      Font size in points.
    font : string
      Font name.  This must be a true type font installed on Mac in /Library/Fonts
      and is the name of the font file without the ".ttf" suffix.
    xpos : float
      Placement of left edge of text. Range 0 - 1 covers full width of graphics window.
    ypos : float
      Placement of bottom edge of text. Range 0 - 1 covers full height of graphics window.
    visibility : bool
      Whether or not to display the label.
    margin : float
      Amount of padding to add around text
    outline : float
      width of contrasting outline to place around background/margin
    '''
    if name == 'all':
        from chimerax.core.errors import UserError
        raise UserError("'all' is reserved to refer to all labels")
    elif name:
        lm = session_labels(session)
        if lm and lm.named_label(name) is not None:
            from chimerax.core.errors import UserError
            raise UserError('Label "%s" already exists' % name)

    kw = {
        'text': text,
        'color': color,
        'size': size,
        'font': font,
        'bold': bold,
        'italic': italic,
        'xpos': xpos,
        'ypos': ypos,
        'visibility': visibility,
        'margin': margin,
        'outline_width': outline
    }

    from chimerax.core.colors import Color
    if isinstance(color, Color):
        kw['color'] = color.uint8x4()
    elif color == 'default':
        kw['color'] = None

    if isinstance(bg_color, Color):
        kw['background'] = bg_color.uint8x4()
    elif bg_color == 'none':
        kw['background'] = None

    has_graphics = session.main_view.render is not None
    if not has_graphics:
        from chimerax.core.errors import LimitationError
        raise LimitationError(
            "Unable to draw 2D labels without rendering images")

    return Label(session, name, **kw)
Ejemplo n.º 19
0
def match(session,
          chain_pairing,
          match_items,
          matrix,
          alg,
          gap_open,
          gap_extend,
          *,
          cutoff_distance=None,
          show_alignment=defaults['show_alignment'],
          align=align,
          domain_residues=(None, None),
          bring=None,
          verbose=defaults['verbose_logging'],
          always_raise_errors=False,
          keep_computed_ss=defaults['overwrite_ss'],
          **align_kw):
    """Superimpose structures based on sequence alignment
       
       Returns a list of tuples, one per chain pairing.  The tuples are:
       (ref-Atoms-used, match-Atoms-used, paired-RMSD, overall-RMSD, transformation-matrix)
       "Atoms-used" means after any pruning due to iteration.

       'chain_pairing' is the method of pairing chains to match:

       CP_SPECIFIC_SPECIFIC --
       Each reference chain is paired with a specified match chain
       ('match_items' is sequence of (ref_chain, match_chain) tuples)

       CP_SPECIFIC_BEST --
       Single reference chain is paired with best seq-aligning
       chain from one or more structures
       ('match_items' is (reference_chain, [match_structures]))

       CP_BEST_BEST --
       Best seq-aligning pair of chains from reference structure and
       match structure(s) is used
       ('match_items' is (ref_structure, [match_structures]))

       'matrix' is name of similarity matrix

       'alg' is the alignment algorithm: AA_NEEDLEMAN_WUNSCH or AA_SMITH_WATERMAN

       'gap_open' and 'gap_extend' are the gap open/extend penalties used
       for the initial sequence alignment

       'cutoff_distance' is the cutoff used for iterative superposition -- iteration stops
       when all remaining distances are below the cutoff.  If None, no iteration.

       'show_alignment' controls whether the sequence alignment is also shown in a
       sequence viewer.

       'align' allows specification of the actual function align/score one chain to
       another.  See the align() function above.

       'domain_residues' allows matching to be restricted to a subset of the chain(s).
       If given, should be (ref_Residues_collection, match_Residues_collection)

       'bring' specifies other structures that should be transformed along with the
       match structure (so, there must be only one match structure in such a case).

       'verbose', if True, produces additional output to the log, If None, the parameter
       table will not be logged.

       If 'always_raise_errors' is True, then an iteration that goes to too few
       matched atoms will immediately raise an error instead of noting the
       failure in the log and continuing on to other pairings.
    """
    dssp_cache = {}
    try:
        alg = alg.lower()
        if alg == "nw" or alg.startswith("needle"):
            alg = "nw"
            alg_name = "Needleman-Wunsch"
        elif alg == "sw" or alg.startswith("smith"):
            alg = "sw"
            alg_name = "Smith-Waterman"
        else:
            raise ValueError("Unknown sequence alignment algorithm: %s" % alg)
        pairings = {}
        small_mol_err_msg = "Reference and/or match model contains no nucleic or"\
            " amino acid chains.\nUse the command-line 'align' command" \
            " to superimpose small molecules/ligands."
        rd_res, md_res = domain_residues
        from chimerax.sim_matrices import matrix_compatible
        if chain_pairing == CP_SPECIFIC_SPECIFIC:
            # specific chain(s) in each

            # various sanity checks
            #
            # (1) can't have same chain matched to multiple refs
            # (2) reference structure can't be a match structure
            match_chains = {}
            match_mols = {}
            ref_mols = {}
            for ref, match in match_items:
                if not matrix_compatible(ref, matrix, session.logger):
                    raise UserError("Reference chain (%s) not"
                                    " compatible with %s similarity"
                                    " matrix" % (ref.full_name, matrix))
                if not matrix_compatible(match, matrix, session.logger):
                    raise UserError("Match chain (%s) not"
                                    " compatible with %s similarity"
                                    " matrix" % (match.full_name, matrix))
                if match in match_chains:
                    raise UserError("Cannot match the same chain"
                                    " to multiple reference chains")
                match_chains[match] = ref
                if match.structure in ref_mols \
                or ref.structure in match_mols \
                or match.structure == ref.structure:
                    raise UserError("Cannot have same molecule"
                                    " model provide both reference and"
                                    " match chains")
                match_mols[match.structure] = ref
                ref_mols[ref.structure] = match

            if not match_chains:
                raise UserError("Must select at least one reference"
                                " chain.\n")

            for match, ref in match_chains.items():
                match, ref = [
                    check_domain_matching([ch], dr)[0]
                    for ch, dr in ((match, md_res), (ref, rd_res))
                ]
                score, s1, s2 = align(session, ref, match, matrix, alg,
                                      gap_open, gap_extend, dssp_cache,
                                      **align_kw)
                pairings.setdefault(s2.structure, []).append((score, s1, s2))

        elif chain_pairing == CP_SPECIFIC_BEST:
            # specific chain in reference;
            # best seq-aligning chain in match model(s)
            ref, matches = match_items
            if not ref or not matches:
                raise UserError(
                    "Must select at least one reference and match item.\n")
            if not matrix_compatible(ref, matrix, session.logger):
                raise UserError("Reference chain (%s) not compatible"
                                " with %s similarity matrix" %
                                (ref.full_name, matrix))
            ref = check_domain_matching([ref], rd_res)[0]
            for match in matches:
                best_score = None
                seqs = [
                    s for s in match.chains
                    if matrix_compatible(s, matrix, session.logger)
                ]
                if not seqs and match.chains:
                    raise UserError("No chains in match structure"
                                    " %s compatible with %s similarity"
                                    " matrix" % (match, matrix))
                seqs = check_domain_matching(seqs, md_res)
                for seq in seqs:
                    score, s1, s2 = align(session, ref, seq, matrix, alg,
                                          gap_open, gap_extend, dssp_cache,
                                          **align_kw)
                    if best_score is None or score > best_score:
                        best_score = score
                        pairing = (score, s1, s2)
                if best_score is None:
                    raise LimitationError(small_mol_err_msg)
                pairings[match] = [pairing]

        elif chain_pairing == CP_BEST_BEST:
            # best seq-aligning pair of chains between
            # reference and match structure(s)
            ref, matches = match_items
            if not ref or not matches:
                raise UserError("Must select at least one reference"
                                " and match item in different models.\n")
            rseqs = [
                s for s in check_domain_matching(ref.chains, rd_res)
                if matrix_compatible(s, matrix, session.logger)
            ]
            if not rseqs and ref.chains:
                raise UserError("No chains in reference structure"
                                " %s compatible with %s similarity"
                                " matrix" % (ref, matrix))
            for match in matches:
                best_score = None
                mseqs = [
                    s for s in check_domain_matching(match.chains, md_res)
                    if matrix_compatible(s, matrix, session.logger)
                ]
                if not mseqs and match.chains:
                    raise UserError("No chains in match structure"
                                    " %s compatible with %s similarity"
                                    " matrix" % (match, matrix))
                for mseq in mseqs:
                    for rseq in rseqs:
                        score, s1, s2 = align(session, rseq, mseq, matrix, alg,
                                              gap_open, gap_extend, dssp_cache,
                                              **align_kw)
                        if best_score is None or score > best_score:
                            best_score = score
                            pairing = (score, s1, s2)
                if best_score is None:
                    raise LimitationError(small_mol_err_msg)
                pairings[match] = [pairing]
        else:
            raise ValueError("No such chain-pairing method")
    finally:
        if not keep_computed_ss:
            for s, ss_info in dssp_cache.items():
                ss_ids, ss_types = ss_info
                s.residues.ss_ids = ss_ids
                s.residues.ss_types = ss_types

    logger = session.logger
    ret_vals = []
    logged_params = False
    for match_mol, pairs in pairings.items():
        ref_atoms = []
        match_atoms = []
        region_info = {}
        if verbose:
            seq_pairings = []
        for score, s1, s2 in pairs:
            try:
                ss_matrix = align_kw['ss_matrix']
            except KeyError:
                ss_matrix = default_ss_matrix
            try:
                ss_fraction = align_kw['ss_fraction']
            except KeyError:
                ss_fraction = defaults["ss_mixture"]

            if not logged_params and verbose is not None:
                if ss_fraction is None or ss_fraction is False:
                    ss_rows = """
                        <tr>
                            <td colspan="2" align="center">No secondary-structure guidance used</td>
                        </tr>
                        <tr>
                            <td>Gap open</td>
                            <td>%g</td>
                        </tr>
                        <tr>
                            <td>Gap extend</td>
                            <td>%g</td>
                        </tr>
                    """ % (gap_open, gap_extend)
                else:
                    if 'gap_open_helix' in align_kw:
                        gh = align_kw['gap_open_helix']
                    else:
                        gh = defaults["helix_open"]
                    if 'gap_open_strand' in align_kw:
                        gs = align_kw['gap_open_strand']
                    else:
                        gs = defaults["strand_open"]
                    if 'gap_open_other' in align_kw:
                        go = align_kw['gap_open_other']
                    else:
                        go = defaults["other_open"]
                    ss_rows = """
                        <tr>
                            <td>SS fraction</td>
                            <td>%g</td>
                        </tr>
                        <tr>
                            <td>Gap open (HH/SS/other)</td>
                            <td>%g/%g/%g</td>
                        </tr>
                        <tr>
                            <td>Gap extend</td>
                            <td>%g</td>
                        </tr>
                        <tr>
                            <td>SS matrix</td>
                            <td>
                                <table>
                                    <tr>
                                        <th></th> <th>H</th> <th>S</th> <th>O</th>
                                    </tr>
                                    <tr>
                                        <th>H</th> <td align="right">%g</td> <td align="right">%g</td> <td align="right">%g</td>
                                    </tr>
                                    <tr>
                                        <th>S</th> <td></td> <td align="right">%g</td> <td align="right">%g</td>
                                    </tr>
                                    <tr>
                                        <th>O</th> <td></td> <td></td> <td align="right">%g</td>
                                    </tr>
                                </table>
                            </td>
                        </tr>
                    """ % (ss_fraction, gh, gs, go, gap_extend,
                           ss_matrix[('H', 'H')], ss_matrix[('H', 'S')],
                           ss_matrix[('H', 'O')], ss_matrix[('S', 'S')],
                           ss_matrix[('S', 'O')], ss_matrix[('O', 'O')])
                if cutoff_distance is None:
                    iterate_row = """<tr> <td colspan="2" align="center">No iteration</td> </tr>"""
                else:
                    iterate_row = """<tr> <td>Iteration cutoff</td> <td>%g</td></tr>""" % cutoff_distance
                from chimerax.core.logger import html_table_params
                param_table = """
                    <table %s>
                        <tr>
                            <th colspan="2">Parameters</th>
                        </tr>
                        <tr>
                            <td>Chain pairing</td>
                            <td>%s</td>
                        </tr>
                        <tr>
                            <td>Alignment algorithm</td>
                            <td>%s</td>
                        </tr>
                        <tr>
                            <td>Similarity matrix</td>
                            <td>%s</td>
                        </tr>
                        %s
                        %s
                    </table>
                """ % (html_table_params, chain_pairing, alg_name, matrix,
                       ss_rows, iterate_row)
                logger.info(param_table, is_html=True)
                logged_params = True
            logger.status("Matchmaker %s (#%s) with %s (#%s),"
                          " sequence alignment score = %g" %
                          (s1.name, s1.structure.id_string, s2.name,
                           s2.structure.id_string, score),
                          log=True)
            skip = set()
            viewer = None
            if show_alignment:
                for s in [s1, s2]:
                    if hasattr(s, '_dm_rebuild_info'):
                        residues = s.residues
                        characters = list(s.characters)
                        for i, c, r in s._dm_rebuild_info:
                            g = s.ungapped_to_gapped(i)
                            characters[g] = c
                            residues[i] = r
                            skip.add(r)
                        s.bulk_set(residues, characters)
                alignment = session.alignments.new_alignment(
                    [s1, s2],
                    None,
                    auto_associate=None,
                    name="MatchMaker alignment")
                alignment.auto_associate = True
                for hdr in alignment.headers:
                    hdr.shown = hdr.ident == "rmsd"
            for i in range(len(s1)):
                if s1[i] == "." or s2[i] == ".":
                    continue
                ref_res = s1.residues[s1.gapped_to_ungapped(i)]
                match_res = s2.residues[s2.gapped_to_ungapped(i)]
                if not ref_res:
                    continue
                ref_atom = ref_res.principal_atom
                if not ref_atom:
                    continue
                if not match_res:
                    continue
                match_atom = match_res.principal_atom
                if not match_atom:
                    continue
                if ref_res in skip or match_res in skip:
                    continue
                if ref_atom.name != match_atom.name:
                    # nucleic P-only trace vs. full nucleic
                    if ref_atom.name != "P":
                        ref_atom = ref_atom.residue.find_atom("P")
                        if not ref_atom:
                            continue
                    else:
                        match_atom = match_atom.residue.find_atom("P")
                        if not match_atom:
                            continue
                ref_atoms.append(ref_atom)
                match_atoms.append(match_atom)
                if viewer and cutoff_distance is not None:
                    region_info[ref_atom] = (viewer, i)

            if verbose:
                seq_pairings.append((s1, s2))
        from chimerax.std_commands import align
        if len(match_atoms) < 3:
            msg = "Fewer than 3 residues aligned; cannot match %s with %s" % (
                s1.name, s2.name)
            if always_raise_errors:
                raise align.IterationError(msg)
            logger.error(msg)
            continue
        from chimerax.atomic import Atoms
        try:
            ret_vals.append(
                align.align(session,
                            Atoms(match_atoms),
                            Atoms(ref_atoms),
                            cutoff_distance=cutoff_distance))
        except align.IterationError:
            if always_raise_errors:
                raise
            logger.error("Iteration produces fewer than 3"
                         " residues aligned.\nCannot match %s with %s"
                         " satisfying iteration threshold." %
                         (s1.name, s2.name))
            continue
        if bring is not None:
            xf = ret_vals[-1][-1]
            for m in bring:
                m.scene_position = xf * m.scene_position
        logger.info("")  # separate matches with whitespace
        if region_info:
            by_viewer = {}
            for ra in ret_vals[-1][1]:
                viewer, index = region_info[ra]
                by_viewer.setdefault(viewer, []).append(index)
            for viewer, indices in by_viewer.items():
                indices.sort()
                name, fill, outline = viewer.MATCHED_REGION_INFO
                viewer.new_region(name=name,
                                  columns=indices,
                                  fill=fill,
                                  outline=outline)
                viewer.status(
                    "Residues used in final fit iteration are highlighted")
        if verbose:
            for s1, s2 in seq_pairings:
                logger.info("Sequences:")
                for s in [s1, s2]:
                    logger.info(s.name + "\t" + s.characters)
                logger.info("Residues:")
                for s in [s1, s2]:
                    logger.info(", ".join([str(r) for r in s.residues]))
                logger.info("Residue usage in match (1=used, 0=unused):")
                match_atoms1, match_atoms2 = ret_vals[-1][:2]
                match_residues = set([
                    a.residue for matched in ret_vals[-1][:2] for a in matched
                ])
                for s in [s1, s2]:
                    logger.info(", ".join(
                        [str(int(r in match_residues)) for r in s.residues]))

    global _dm_cleanup
    for seq in _dm_cleanup:
        delattr(seq, '_dm_rebuild_info')
    _dm_cleanup = []
    return ret_vals
Ejemplo n.º 20
0
 def value(self, val):
     if self.group_identical:
         from chimerax.core.errors import LimitationError
         raise LimitationError("Cannot set grouped Chain list")
     self.set_value(val)
Ejemplo n.º 21
0
def model(session,
          targets,
          *,
          block=True,
          multichain=True,
          custom_script=None,
          dist_restraints=None,
          executable_location=None,
          fast=False,
          het_preserve=False,
          hydrogens=False,
          license_key=None,
          num_models=5,
          show_gui=True,
          temp_path=None,
          thorough_opt=False,
          water_preserve=False):
    """
    Generate comparative models for the target sequences.

    Arguments:
    session
        current session
    targets
        list of (alignment, sequence) tuples.  Each sequence will be modelled.
    block
        If True, wait for modelling job to finish before returning and return list of
        (opened) models.  Otherwise return immediately.  Also see 'show_gui' option.
    multichain
        If True, the associated chains of each structure are used individually to generate
        chains in the resulting models (i.e. the models will be multimers).  If False, all
        associated chains are used together as templates to generate a single-chain model
        for the target sequence.
    custom_script
        If provided, the location of a custom Modeller script to use instead of the
        one we would otherwise generate.  Only used when executing locally.
    dist_restraints
        If provided, the location of a file containing additional distance restraints
    executable_location
        If provided, the path to the locally installed Modeller executable.  If not
        provided, use the web service.
    fast
        Whether to use fast but crude generation of models
    het_preserve
        Whether to preserve HET atoms in generated models
    hydrogens
        Whether to generate models with hydrogen atoms
    license_key
        Modeller license key.  If not provided, try to use settings to find one.
    num_models
        Number of models to generate for each template sequence
    show_gui
        If True, show user interface for Modeller results (if ChimeraX is in gui mode).
    temp_path
        If provided, folder to use for temporary files
    thorough_opt
        Whether to perform thorough optimization
    water_preserve
        Whether to preserve water in generated models
    """

    from chimerax.core.errors import LimitationError, UserError
    from .common import modeller_copy
    if multichain:
        # So, first find structure with most associated chains and least non-associated chains.
        # That structure is used as the multimer template.  Chains from other structures are used
        # as "standalone" templates -- each such chain will be on its own line.  Need to allow
        # space on the left and right of the target sequence so that the largest chains can be
        # accomodated.

        # Find the structure we will use as the multimer template
        by_structure = {}
        chain_info = {}
        for alignment, orig_target in targets:
            # Copy the target sequence, changing name to conform to Modeller limitations
            target = modeller_copy(orig_target)
            if not alignment.associations:
                raise UserError("Alignment %s has no associated chains" %
                                alignment.ident)
            for chain, aseq in alignment.associations.items():
                if len(chain.chain_id) > 1:
                    raise LimitationError(
                        "Modeller cannot handle templates with multi-character chain IDs"
                    )
                by_structure.setdefault(chain.structure, []).append(chain)
                chain_info[chain] = (aseq, target)
        max_matched = min_unmatched = None
        for s, match_info in by_structure.items():
            matched = len(match_info)
            unmatched = s.num_chains - len(match_info)
            if max_matched is None or matched > max_matched or (
                    matched == max_matched and (unmatched < min_unmatched)):
                multimer_template = s
                max_matched = matched
                min_unmatched = unmatched
        mm_targets = []
        mm_chains = []
        match_chains = []
        for chain in multimer_template.chains:
            mm_chains.append(chain)
            try:
                aseq, target = chain_info[chain]
            except KeyError:
                mm_targets.append(None)
            else:
                mm_targets.append(target)
                match_chains.append(chain)
        # okay, now form single-chain lines for the other structure associations, that eventually will
        # be handled column by column in exactly the same way as the non-multichain method.
        single_template_lines = []
        for chain, info in chain_info.items():
            if chain.structure == multimer_template:
                continue
            aseq, target = info
            for i, mm_target in enumerate(mm_targets):
                if mm_target != target:
                    continue
                template_line = [None] * len(mm_targets)
                template_line[i] = chain
                single_template_lines.append(template_line)
        # AFAIK, the multimer template chain sequences need to have complete PDB sequence, so may need
        # to prefix and suffix he corresponding alignment sequence with characters for residues
        # outside of the alignment sequence.  For other templates/targets, affix a corresponding number
        # of '-' characters
        prefixes, suffixes = find_affixes(mm_chains, chain_info)
        target_strings = []
        for prefix, suffix, mm_target in zip(prefixes, suffixes, mm_targets):
            if mm_target is None:
                target_strings.append('-')
                continue
            target_strings.append('-' * len(prefix) + mm_target.characters +
                                  '-' * len(suffix))
        templates_strings = []
        templates_info = []
        mm_template_strings = []
        for prefix, suffix, chain in zip(prefixes, suffixes, mm_chains):
            try:
                aseq, target = chain_info[chain]
            except KeyError:
                mm_template_strings.append('-')
                continue
            mm_template_strings.append(
                prefix + regularized_seq(aseq, chain).characters + suffix)
        templates_strings.append(mm_template_strings)
        templates_info.append(None)
        for template_line in single_template_lines:
            template_strings = []
            for prefix, suffix, chain, target in zip(prefixes, suffixes,
                                                     template_line,
                                                     mm_targets):
                if target is None:
                    template_strings.append('-')
                elif chain is None:
                    template_strings.append(
                        '-' * (len(prefix) + len(target) + len(suffix)))
                else:
                    aseq, target = chain_info[chain]
                    template_strings.append(
                        '-' * len(prefix) +
                        regularized_seq(aseq, chain).characters +
                        '-' * len(suffix))
                    templates_info.append((chain, aseq.match_maps[chain]))
            templates_strings.append(template_strings)
        target_name = "target" if len(targets) > 1 else target.name
    else:
        if len(targets) > 1:
            raise LimitationError(
                "Cannot have multiple targets(/alignments) unless creating multimeric model"
            )
        alignment, orig_target = targets[0]
        # Copy the target sequence, changing name to conform to Modeller limitations
        target = modeller_copy(orig_target)
        target_strings = [target.characters]

        templates_strings = []
        templates_info = []
        match_chains = []
        for chain, aseq in alignment.associations.items():
            if len(chain.chain_id) > 1:
                raise LimitationError(
                    "Modeller cannot handle templates with multi-character chain IDs"
                )
            templates_strings.append([regularized_seq(aseq, chain).characters])
            templates_info.append((chain, aseq.match_maps[chain]))
            if not match_chains:
                match_chains.append(chain)

        target_name = target.name

    from .common import write_modeller_scripts, get_license_key
    script_path, config_path, temp_dir = write_modeller_scripts(
        get_license_key(session, license_key), num_models, het_preserve,
        water_preserve, hydrogens, fast, None, custom_script, temp_path,
        thorough_opt, dist_restraints)

    input_file_map = []

    # form the sequences to be written out as a PIR
    from chimerax.atomic import Sequence
    pir_target = Sequence(name=target_name)
    pir_target.description = "sequence:%s:.:.:.:.::::" % pir_target.name
    pir_target.characters = '/'.join(target_strings)
    pir_seqs = [pir_target]

    structures_to_save = set()
    for strings, info in zip(templates_strings, templates_info):
        if info is None:
            # multimer template
            pir_template = Sequence(
                name=structure_save_name(multimer_template))
            pir_template.description = "structure:%s:FIRST:%s::::::" % (
                pir_template.name, multimer_template.chains[0].chain_id)
            structures_to_save.add(multimer_template)
        else:
            # single-chain template
            chain, match_map = info
            first_assoc_pos = 0
            while first_assoc_pos not in match_map:
                first_assoc_pos += 1
            first_assoc_res = match_map[first_assoc_pos]
            pir_template = Sequence(name=chain_save_name(chain))
            pir_template.description = "structure:%s:%d%s:%s:+%d:%s::::" % (
                structure_save_name(chain.structure), first_assoc_res.number,
                first_assoc_res.insertion_code, chain.chain_id, len(match_map),
                chain.chain_id)
            structures_to_save.add(chain.structure)
        pir_template.characters = '/'.join(strings)
        pir_seqs.append(pir_template)
    import os.path
    pir_file = os.path.join(temp_dir.name, "alignment.ali")
    aln = session.alignments.new_alignment(pir_seqs,
                                           False,
                                           auto_associate=False,
                                           create_headers=False)
    aln.save(pir_file, format_name="pir")
    session.alignments.destroy_alignment(aln)
    input_file_map.append(("alignment.ali", "text_file", pir_file))

    # write the namelist.dat file, target seq name on first line, templates on remaining lines
    name_file = os.path.join(temp_dir.name, "namelist.dat")
    input_file_map.append(("namelist.dat", "text_file", name_file))
    with open(name_file, 'w') as f:
        for template_seq in pir_seqs:
            print(template_seq.name, file=f)

    config_name = os.path.basename(config_path)
    input_file_map.append((config_name, "text_file", config_path))

    # save structure files
    import os
    struct_dir = os.path.join(temp_dir.name, "template_struc")
    if not os.path.exists(struct_dir):
        try:
            os.mkdir(struct_dir, mode=0o755)
        except FileExistsError:
            pass
    from chimerax.pdb import save_pdb, standard_polymeric_res_names as std_res_names
    for structure in structures_to_save:
        base_name = structure_save_name(structure) + '.pdb'
        pdb_file_name = os.path.join(struct_dir, base_name)
        input_file_map.append((base_name, "text_file", pdb_file_name))
        ATOM_res_names = structure.in_seq_hets
        ATOM_res_names.update(std_res_names)
        save_pdb(session,
                 pdb_file_name,
                 models=[structure],
                 polymeric_res_names=ATOM_res_names)
        delattr(structure, 'in_seq_hets')

    from chimerax.atomic import Chains
    match_chains = Chains(match_chains)
    if executable_location is None:
        if custom_script is not None:
            raise LimitationError(
                "Custom Modeller scripts only supported when executing locally"
            )
        if dist_restraints is not None:
            raise LimitationError(
                "Distance restraints only supported when executing locally")
        if thorough_opt:
            session.logger.warning(
                "Thorough optimization only supported when executing locally")
        job_runner = ModellerWebService(session, match_chains, num_models,
                                        pir_target.name, input_file_map,
                                        config_name, targets, show_gui)
    else:
        #TODO: job_runner = ModellerLocal(...)
        from chimerax.core.errors import LimitationError
        raise LimitationError("Local Modeller execution not yet implemented")
        # a custom script [only used when executing locally] needs to be copied into the tmp dir...
        if os.path.exists(script_path) \
        and os.path.normpath(temp_dir.name) != os.path.normpath(os.path.dirname(script_path)):
            import shutil
            shutil.copy(script_path, temp_dir.name)

    return job_runner.run(block=block)
Ejemplo n.º 22
0
def cmd_open(session, file_names, rest_of_line, *, log=True):
    tokens = []
    remainder = rest_of_line
    while remainder:
        token, token_log, remainder = next_token(remainder)
        remainder = remainder.lstrip()
        tokens.append(token)
    provider_cmd_text = "open " + " ".join([FileNameArg.unparse(fn)
        for fn in file_names] + [StringArg.unparse(token) for token in tokens])
    try:
        database_name = format_name = None
        for i in range(len(tokens)-2, -1, -2):
            test_token = tokens[i].lower()
            if "format".startswith(test_token):
                format_name = tokens[i+1]
            elif "fromdatabase".startswith(test_token):
                database_name = tokens[i+1]

        from .manager import NoOpenerError
        mgr = session.open_command
        fetches, files = fetches_vs_files(mgr, file_names, format_name, database_name)
        if fetches:
            try:
                provider_args = mgr.fetch_args(fetches[0][1], format_name=fetches[0][2])
            except NoOpenerError as e:
                raise LimitationError(str(e))
        else:
            data_format = file_format(session, files[0], format_name)
            if data_format is None:
                # let provider_open raise the error, which will show the command
                provider_args = {}
            else:
                try:
                    provider_args = mgr.open_args(data_format)
                except NoOpenerError as e:
                    raise LimitationError(str(e))

        # register a private 'open' command that handles the provider's keywords
        registry = RegisteredCommandInfo()

        def database_names(mgr=mgr):
            return mgr.database_names

        keywords = {
            'format': DynamicEnum(lambda ses=session:format_names(ses)),
            'from_database': DynamicEnum(database_names),
            'ignore_cache': BoolArg,
            'name': StringArg
        }
        for keyword, annotation in provider_args.items():
            if keyword in keywords:
                raise ValueError("Open-provider keyword '%s' conflicts with builtin arg of"
                    " same name" % keyword)
            keywords[keyword] = annotation
        desc = CmdDesc(required=[('names', OpenFileNamesArg)], keyword=keywords.items(),
            synopsis="read and display data")
        register("open", desc, provider_open, registry=registry)
    except BaseException as e:
        # want to log command even for keyboard interrupts
        log_command(session, "open", provider_cmd_text, url=_main_open_CmdDesc.url)
        raise
    return Command(session, registry=registry).run(provider_cmd_text, log=log)
Ejemplo n.º 23
0
def add_atom(name,
             element,
             residue,
             loc,
             serial_number=None,
             bonded_to=None,
             occupancy=None,
             info_from=None,
             alt_loc=None,
             bfactor=None):
    """Add an atom at the Point 'loc'

       'element' can be a string (atomic symbol), integer (atomic number),
       or an Element instance.

       The atom is added to the given residue (and its molecule).
       'loc' can be an array of xyzs if there are multiple coordinate sets.

       If no 'serial_number' is given, then the atom will be given a serial
       number one greater than the largest serial number of the other atoms
       in the structure.

       'bonded_to' is None or an Atom.  If an Atom, then the new atom
       inherits various attributes [display, altloc, style, occupancy]
       from that atom and a bond to that Atom is created.

       If 'info_from' is supplied then the information normally garnered
       from the 'bonded_to' atom will be obtained from the 'info_from'
       atom instead. Typically used when there is no 'bonded_to' atom.

       If 'occupancy' is not None or the 'bonded_to' atom is not None,
       the new atom will be given the corresponding occupancy.

       If 'bfactor' is not None or the 'bonded_to' atom is not None,
       the new atom will be given the corresponding bfactor.

       If 'alt_loc' is specified (must be a single-character string), then
       the new atom will be given that alt loc, otherwise the alt loc will be ' '.

       Returns the new atom.
    """

    if not info_from:
        info_from = bonded_to
    struct = residue.structure
    new_atom = struct.new_atom(name, element)
    residue.add_atom(new_atom)
    if alt_loc is not None:
        new_atom.set_alt_loc(alt_loc, True)
    from numpy import array
    if len(loc.shape) == 1:
        locs = array([loc])
    else:
        locs = loc
    if struct.num_coordsets == 0:
        if len(locs) > 1:
            from chimerax.core.errors import LimitationError
            raise LimitationError(
                "Cannot add_atom() multi-position atom to empty structure")
        new_atom.coord = locs[0]
    else:
        for xyz, cs_id in zip(locs, struct.coordset_ids):
            new_atom.set_coord(xyz, cs_id)
    if serial_number is None:
        import numpy
        serial_number = numpy.max(struct.atoms.serial_numbers) + 1
    new_atom.serial_number = serial_number
    if occupancy is not None or info_from and hasattr(info_from, 'occupancy'):
        new_atom.occupancy = getattr(info_from, 'occupancy', occupancy)
    if bfactor is not None or info_from and hasattr(info_from, 'bfactor'):
        new_atom.bfactor = getattr(info_from, 'bfactor', bfactor)
    if info_from:
        new_atom.display = info_from.display
        new_atom.draw_mode = info_from.draw_mode
    if bonded_to:
        add_bond(new_atom, bonded_to)
    return new_atom
Ejemplo n.º 24
0
def findclash(session,
              spec=None,
              make_pseudobonds=False,
              log=True,
              naming_style="command",
              overlap_cutoff=0.6,
              hbond_allowance=0.4,
              bond_separation=4,
              test="other",
              intra_residue=False):
    from chimerax.core.errors import LimitationError
    from chimerax.atomic import Atoms
    from chimerax.core.commands import atomspec
    from chimerax.atomic.settings import settings
    if test != "self":
        raise LimitationError("findclash test \"%s\" not implemented" % test)
    if hbond_allowance != 0:
        session.logger.warning("Hydrogen bond finding is not implemented.  "
                               "Setting hbond_allowance to 0.0.")
        hbond_allowance = 0
    if naming_style != "command" and naming_style != "command-line":
        raise LimitationError("findclash naming style \"%s\" not implemented" %
                              naming_style)
    if make_pseudobonds:
        raise LimitationError("findclash make pseudobonds not implemented")
    if spec is None:
        spec = atomspec.everything(session)
    results = spec.evaluate(session)
    atoms = results.atoms
    neighbors = _find_neighbors(atoms, bond_separation)
    clashes = []
    for i in range(len(atoms) - 1):
        a = atoms[i]
        if intra_residue:
            # Want intra-residue contacts, so check all atoms
            others = atoms[i + 1:]
        else:
            # Do not want intra-residue contacts, so remove atoms from residue
            from numpy import logical_not
            others = atoms[i + 1:]
            residues = others.residues
            # Generate mask for atoms from same residue
            mask = others.residues.mask(Atoms([a.residue]))
            others = others.filter(logical_not(mask))
        # Remove atoms within "separation" bonds of this atom
        others -= neighbors[a]
        if len(others) > 0:
            clashes.extend(
                _find_clash_self(a, others, overlap_cutoff, hbond_allowance))
    if log:
        session.logger.info("Allowed overlap: %g" % overlap_cutoff)
        session.logger.info("H-bond overlap reduction: %g" % hbond_allowance)
        session.logger.info("Ignored contact between atoms separated by %d "
                            "bonds or less" % bond_separation)
        session.logger.info("%d contacts" % len(clashes))
        session.logger.info("atom1\tatom2\toverlap\tdistance")
        save = settings.atomspec_contents
        settings.atomspec_contents = "command"
        msgs = ["%s\t%s\t%.3f\t%.3f" % c for c in clashes]
        settings.atomspec_contents = save
        session.logger.info('\n'.join(msgs))
        session.logger.info("%d contacts" % len(clashes))
Ejemplo n.º 25
0
def distance(session, objects, *, color=None, dashes=None,
        decimal_places=None, radius=None, symbol=None, signed=False):
    '''
    Show/report distance between two objects.
    '''
    from chimerax.core.errors import UserError, LimitationError
    measurables = [m for m in objects.models if isinstance(m, (SimpleMeasurable, ComplexMeasurable))]
    measurables.extend(objects.atoms)
    if len(measurables) != 2:
        raise UserError("Expected exactly two atoms and/or measurable objects (e.g. axes, planes), got %d"
            % len(measurables))
    if len(objects.atoms) != 2:
        # just report the distance -- no distance monitor
        if len([m for m in measurables if isinstance(m, SimpleMeasurable)]) == 2:
            from chimerax.geometry import distance
            dist = distance(measurables[0].scene_coord, measurables[1].scene_coord)
        else:
            dist = NotImplemented
            if isinstance(measurables[0], ComplexMeasurable):
                dist = measurables[0].distance(measurables[1], signed=signed)
            if dist is NotImplemented and isinstance(measurables[1], ComplexMeasurable):
                dist = measurables[1].distance(measurables[0], signed=signed)
            if dist is NotImplemented:
                raise LimitationError("Don't know how to measure distance between %s and %s"
                    % tuple(measurables))
        session.logger.info(("Distance between %s and %s: " + session.pb_dist_monitor.distance_format)
            % (measurables[0], measurables[1], dist))
        return dist
    a1, a2 = measurables
    grp = session.pb_manager.get_group("distances", create=False)
    from .settings import settings
    if not grp:
        # create group and add to DistMonitor
        grp = session.pb_manager.get_group("distances")
        if color is not None:
            grp.color = color.uint8x4()
        else:
            grp.color = settings.color.uint8x4()
        if radius is not None:
            grp.radius = radius
        else:
            grp.radius = settings.radius
        grp.dashes = settings.dashes
        session.models.add([grp])
        session.pb_dist_monitor.add_group(grp, update_callback=_notify_updates)
    for pb in grp.pseudobonds:
        pa1, pa2 = pb.atoms
        if (pa1 == a1 and pa2 == a2) or (pa1 == a2 and pa2 == a1):
            raise UserError("Distance already exists;"
                " modify distance properties with 'distance style'")
    pb = grp.new_pseudobond(a1, a2)

    if color is not None:
        pb.color = color.uint8x4()
    if dashes is not None:
        grp.dashes = dashes
    if radius is not None:
        pb.radius = radius
    if decimal_places is not None or symbol is not None:
        if decimal_places is not None:
            session.pb_dist_monitor.decimal_places = decimal_places
        if symbol is not None:
            session.pb_dist_monitor.show_units = symbol

    session.logger.info(("Distance between %s and %s: " + session.pb_dist_monitor.distance_format)
        % (a1, a2.string(relative_to=a1), pb.length))