Esempio n. 1
0
def main():
    """
    Initialise everything needed to get the daemon running and start it up.

    """

    info("Welcome to fapswitchd; the daemon interface to fapswitch")
    info("Using fapswitch version {}".format(fapswitch.__version__))

    # Name for a the single structure
    job_name = options.get('job_name')

    # Load it
    input_structure = load_structure(job_name)
    # Structure is ready!

    # Begin processing
    info("Structure attachment sites: "
         "{}".format(list(input_structure.attachments)))
    info("Structure attachment multiplicities :"
         "{}".format(
             dict((key, len(val))
                  for key, val in input_structure.attachments.items())))

    # Functional group library is self initialising
    info("Groups in library: {}".format(functional_groups.group_list))

    #Define some backends for where to send the structures
    backends = []
    backend_options = options.gettuple('backends')

    rotations = options.getint('rotations')
    info("Will rotate each group a maximum of {} times.".format(rotations))

    if 'sqlite' in backend_options:
        # Initialise and add the database writer
        debug("Initialising the sqlite backend")
        try:
            from fapswitch.backend.sql import AlchemyBackend
            backend = AlchemyBackend(job_name)
            backend.populate_groups(functional_groups)
            backends.append(backend)
        except ImportError:
            error("SQLAlchemy not installed; sql backend unavailable")
        # done

    if 'file' in backend_options:
        # Just dumps to a named file
        debug("Initialising cif file writer backend")
        from fapswitch.backend.cif_file import CifFileBackend
        backends.append(CifFileBackend())

    # Make the program die if the daemon is called unsuccessfully
    if fapswitch_deamon(input_structure,
                        backends=backends,
                        rotations=rotations):
        info("Daemon completed successfully")
    else:
        error("Daemon did not complete successfully; check output")
Esempio n. 2
0
    def add_symmetry_structure(self,
                               base_structure,
                               functions,
                               cif_file,
                               manual_angles=None,
                               **kwargs):
        """
        Write out the cif file with a name derived from the base structure
        and the functionalisations.

        """
        if manual_angles is not None and any(manual_angles):
            new_mof_components = []
            for site, manual_angle in zip(functions, manual_angles):
                if manual_angle is not None:
                    angle_str = "%{}".format(manual_angle)
                else:
                    angle_str = ""
                new_mof_components.append("{}@{}{}".format(
                    site[0], site[1], angle_str))
            new_mof_name = ".".join(new_mof_components)
        else:
            new_mof_name = ".".join(["@".join(x) for x in functions])

        hashed_name = hashlib.md5(new_mof_name).hexdigest()

        if self.hash_filenames == 'always':
            cif_filename = '%s_func_%s.cif' % (base_structure, hashed_name)
            with open(cif_filename, 'w') as output_file:
                output_file.writelines(cif_file)
        else:
            cif_filename = '%s_func_%s.cif' % (base_structure, new_mof_name)
            try:
                with open(cif_filename, 'w') as output_file:
                    output_file.writelines(cif_file)
            except IOError:
                if self.hash_filenames == 'never':
                    error('Unable to write file {}'.format(cif_filename))
                    return 1
                # 'onerror', or any other has option leads here; try and use
                # a shorter name
                cif_filename = '%s_func_%s.cif' % (base_structure, hashed_name)
                debug('Automatically shortened file name to '
                      '{}'.format(cif_filename))
                with open(cif_filename, 'w') as output_file:
                    output_file.writelines(cif_file)
Esempio n. 3
0
 def _from_file(self, flib_file_name='functional_groups.flib'):
     """Parse groups from the configparser .ini style file."""
     # just a standard configparser conversion to a dict of
     # FunctionalGroup objects
     mepo_only = options.getbool('mepo_only')
     flib_file = ConfigParser()
     debug("Reading groups from {}".format(flib_file_name))
     flib_file.read(flib_file_name)
     for group_name in flib_file.sections():
         try:
             new_group = FunctionalGroup(group_name,
                                         flib_file.items(group_name))
             if mepo_only and not new_group.mepo_compatible:
                 debug("Skipped non-MEPO {}".format(group_name))
             else:
                 if group_name in self:
                     debug("Overriding group {}".format(group_name))
                 self[group_name] = new_group
         except KeyError:
             error("Group {} is missing data".format(group_name))
Esempio n. 4
0
def fapswitch_deamon(structure, backends, rotations=12):
    """
    Use sockets to listen and receive structures.

    """
    timeout = options.getint('timeout')
    # set this to zero for random available port
    port = options.getint('port')
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # no host as this is running locally
    listener.bind(('', port))
    port = listener.getsockname()[1]

    # Ensure that we always know the port
    critical("Listening on port {} ...".format(port))

    listener.settimeout(timeout)
    listener.listen(1)
    # We only wait for a little bit so that process doesn't zombie forever
    try:
        conn, addr = listener.accept()
    except socket.timeout:
        error("No connection within {} seconds; exiting".format(timeout))
        listener.close()
        return False
    info('Connected by {}'.format(addr))

    # Server will continue until an empty input is sent or something times out
    conn.settimeout(timeout)
    while 1:
        try:
            line = conn.recv(1024).decode('utf-8')
        except socket.timeout:
            error(
                "Timed out after {} seconds waiting for input".format(timeout))
            return False

        # Empty input closes server
        if not line:
            break

        # collect information to send what has been done back
        processed = []

        # freeform strings are in braces {}, no spaces
        free_strings = re.findall('{(.*?)}', line)
        debug("Freeform strings: {}".format(free_strings))
        for free_string in free_strings:
            complete = freeform_replace(structure,
                                        custom=free_string,
                                        backends=backends,
                                        rotations=rotations)
            processed.append('{{{}}}'.format(free_string))
            processed.append('{}'.format(complete))

        # site replacements in square brackets [], no spaces
        site_strings = re.findall(r'\[(.*?)\]', line)
        debug("Site replacement strings: {}".format(site_strings))
        for site_string in site_strings:
            site_list = []
            manual_angles = []
            for site in [x for x in site_string.split('.') if x]:
                site_id, functionalisation = site.split('@')
                if '%' in functionalisation:
                    functionalisation, manual = functionalisation.split('%')
                else:
                    manual = None
                site_list.append([site_id, functionalisation])
                manual_angles.append(manual)
            debug("{}".format(site_list))
            debug("{}".format(manual_angles))
            complete = site_replace(structure,
                                    site_list,
                                    backends=backends,
                                    rotations=rotations,
                                    manual_angles=manual_angles)
            processed.append('[{}]'.format(site_string))
            processed.append('{}'.format(complete))

        try:
            conn.sendall((':'.join(processed)).encode('utf-8'))
        except conn.timeout:
            error("Timed out sending status after {} seconds".format(timeout))
            return False

    conn.close()
    return True
Esempio n. 5
0
def main():
    """
    Simple application that will read options and run the substitution
    for an input structure.

    """

    info("Welcome to cliswitch; the command line interface to fapswitch")
    info("Using fapswitch version {}".format(fapswitch.__version__))

    # Name for a the single structure
    job_name = options.get('job_name')

    # Load it
    input_structure = load_structure(job_name)
    # Structure is ready!

    # Begin processing
    info("Structure attachment sites: "
         "{}".format(list(input_structure.attachments)))
    info("Structure attachment multiplicities: "
         "{}".format(dict((key, len(val)) for key, val in
                          input_structure.attachments.items())))

    # Will use selected sites if specified, otherwise use all
    replace_only = options.gettuple('replace_only')
    if replace_only == ():
        replace_only = None

    # Functional group library is self initialising
    info("Groups in library: {}".format(functional_groups.group_list))

    #Define some backends for where to send the structures
    backends = []
    backend_options = options.gettuple('backends')

    if 'sqlite' in backend_options:
        # Initialise and add the database writer
        debug("Initialising the sqlite backend")
        try:
            from fapswitch.backend.sql import AlchemyBackend
            backend = AlchemyBackend(job_name)
            backend.populate_groups(functional_groups)
            backends.append(backend)
        except ImportError:
            error("SQLAlchemy not installed; sql backend unavailable")
        # done

    if 'file' in backend_options:
        # Just dumps to a named file
        debug("Initialising cif file writer backend")
        from fapswitch.backend.cif_file import CifFileBackend
        backends.append(CifFileBackend())

    ##
    # User defined, single-shot functionalisations
    ##

    rotations = options.getint('rotations')
    info("Will rotate each group a maximum of {} times.".format(rotations))

    custom_strings = options.get('custom_strings')
    # Pattern matching same as in the daemon
    # freeform strings are in braces {}, no spaces
    freeform_strings = re.findall('{(.*?)}', custom_strings)
    debug("Freeform option strings: {}".format(freeform_strings))
    for freeform_string in freeform_strings:
        freeform_replace(input_structure, custom=freeform_string,
                         backends=backends, rotations=rotations)

    # site replacements in square brackets [], no spaces
    site_strings = re.findall(r'\[(.*?)\]', custom_strings)
    debug("Site replacement options strings: {}".format(site_strings))
    for site_string in site_strings:
        # These should be [email protected]_group2@site2
        # with optional %angle
        site_list = []
        manual_angles = []
        for site in [x for x in site_string.split('.') if x]:
            site_id, functionalisation = site.split('@')
            if '%' in functionalisation:
                functionalisation, manual = functionalisation.split('%')
            else:
                manual = None
            site_list.append([site_id, functionalisation])
            manual_angles.append(manual)

        debug(str(site_list))
        debug(str(manual_angles))

        site_replace(input_structure, site_list, backends=backends,
                     rotations=rotations, manual_angles=manual_angles)

    ##
    # Full systematic replacement of everything start here
    ##

    # Only use these functional groups for replacements
    replace_groups = options.gettuple('replace_groups')
    if replace_groups == ():
        replace_groups = None

    max_different = options.getint('max_different')

    prob_unfunc = options.getfloat('unfunctionalised_probability')

    # Do absolutely every combination (might take a while)
    if options.getbool('replace_all_sites'):
        all_combinations_replace(input_structure,
                                 replace_only=replace_only,
                                 groups_only=replace_groups,
                                 max_different=max_different,
                                 backends=backends,
                                 rotations=rotations)

    # group@site randomisations
    random_count = options.getint('site_random_count')
    successful_randoms = 0
    while successful_randoms < random_count:
        #function returns true if structure is generated
        if random_combination_replace(input_structure,
                                      replace_only=replace_only,
                                      groups_only=replace_groups,
                                      max_different=max_different,
                                      prob_unfunc=prob_unfunc,
                                      backends=backends,
                                      rotations=rotations):
            successful_randoms += 1
            info("Generated %i of %i site random structures" %
                 (successful_randoms, random_count))

    # fully freeform randomisations
    random_count = options.getint('full_random_count')
    successful_randoms = 0
    while successful_randoms < random_count:
        #function returns true if structure is generated
        if freeform_replace(input_structure,
                            replace_only=replace_only,
                            groups_only=replace_groups,
                            max_different=max_different,
                            prob_unfunc=prob_unfunc,
                            backends=backends,
                            rotations=rotations):
            successful_randoms += 1
            info("Generated %i of %i fully random structures" %
                 (successful_randoms, random_count))
Esempio n. 6
0
def freeform_replace(structure,
                     replace_only=None,
                     groups_only=None,
                     num_groups=None,
                     custom=None,
                     rotations=36,
                     max_different=0,
                     prob_unfunc=0.5,
                     backends=()):
    """
    Replace sites with no symmetry constraint and with random rotations
    for successive insertion trials (i.e. there will be variation for the
    same structure)

    """
    # Assume that the replace only is passed as a list or iterable
    # default to everything
    if replace_only is None:
        replace_only = list(structure.attachments)
    # Valid list is True where allowed to attach
    valid_list = []
    for attachment, points in structure.attachments.items():
        if attachment in replace_only:
            valid_list.extend([True] * len(points))
        else:
            valid_list.extend([False] * len(points))

    debug("Attachment mask %s" % valid_list)

    nsites = sum(valid_list)

    if groups_only is not None:
        local_groups = [x for x in functional_groups if x in groups_only]
        debug("Using only: %s" % local_groups)
    else:
        local_groups = list(functional_groups)
        debug("Using all groups: %s" % local_groups)

    if len(local_groups) > max_different > 0:
        local_groups = random.sample(local_groups, max_different)
        debug("Restricted to: %s" % local_groups)

    if custom is not None:
        # Specific functionalisation requested
        debug("Processing custom string: %s" % custom)
        func_repr = custom.strip('{}').split(".")
        if len(func_repr) > nsites:
            error("Expected %s sites; got %s" % (nsites, len(func_repr)))
            func_repr = func_repr[:nsites]
            warning("Truncated to {%s}" % ".".join(func_repr))
        elif len(func_repr) < nsites:
            error("Expected %s sites; got %s" % (nsites, len(func_repr)))
            func_repr = func_repr + [''] * (nsites - len(func_repr))
            warning("Padded to {%s}" % ".".join(func_repr))
        for unmasked, site in zip(valid_list, func_repr):
            if unmasked is False and site != '':
                warning("Replacing masked site")
    else:
        # Randomise the selection
        if num_groups is None:
            num_groups = random.randint(1, nsites)
            debug("Randomly replacing %i sites" % num_groups)
        elif num_groups > nsites:
            warning("Too many sites requested; changing all %i" % nsites)
            num_groups = nsites
        func_repr = [random.choice(local_groups) for _ in range(num_groups)]
        # Pad to the correct length
        func_repr.extend([""] * (nsites - num_groups))
        # Randomise
        random.shuffle(func_repr)
        # These need to be put in the unmasked slots
        masked_func_repr = []
        for unmasked in valid_list:
            if unmasked:
                masked_func_repr.append(func_repr.pop(0))
            else:
                masked_func_repr.append('')
        func_repr = masked_func_repr
    # Unique-ish
    unique_name = hashlib.md5(str(func_repr).encode('utf-8')).hexdigest()
    new_mof_name = []
    new_mof = list(structure.atoms)
    new_mof_bonds = dict(structure.bonds)
    rotation_count = 0
    for this_point, this_group in zip(
            chain(*[
                structure.attachments[x] for x in sorted(structure.attachments)
            ]), func_repr):
        if this_group == "":
            new_mof_name.append("")
            continue
        else:
            new_mof_name.append(this_group)
        attachment = functional_groups[this_group]
        attach_id = this_point[0]
        attach_to = this_point[1]
        attach_at = structure.atoms[attach_to].pos
        attach_towards = this_point[2]
        attach_normal = structure.atoms[attach_to].normal
        #extracted_atoms = new_mof[attach_id:attach_id+1]
        new_mof[attach_id:attach_id + 1] = [None]
        start_idx = len(new_mof)
        for trial_rotation in range(rotations):
            incoming_group, incoming_bonds = (attachment.atoms_attached_to(
                attach_at, attach_towards, attach_normal, attach_to,
                start_idx))
            for atom in incoming_group:
                if not test_collision(
                        atom, new_mof, structure.cell, ignore=[attach_to]):
                    rotation_count += 1
                    attach_normal = dot(
                        rotation_about_angle(attach_towards,
                                             random.random() * np.pi * 2),
                        attach_normal)
                    break
            else:
                # Fits, so add and move on
                new_mof.extend(incoming_group)
                new_mof_bonds.update(incoming_bonds)
                break
        else:
            # this_point not valid
            warning("Failed to generate: %s" %
                    ".".join([x or "" for x in func_repr]))
            warning("Stopped after: %s" % ".".join(new_mof_name))
            debug("Needed {} rotations".format(rotation_count))
            return False

    new_mof_name = ".".join(new_mof_name)

    # Counts how many have been made
    identifier = count()
    info("Generated (%i): {%s}" % (identifier, new_mof_name))
    info("With unique name: %s" % unique_name)

    full_mof_name = "%s_free_%s" % (structure.name, new_mof_name)

    ligands = atoms_to_identifiers(new_mof, new_mof_bonds)
    cif_file = atoms_to_cif(new_mof,
                            structure.cell,
                            new_mof_bonds,
                            full_mof_name,
                            identifiers=ligands)

    if ligands is not None:
        ligand_strings = [
            "{}:{}".format(ligand.smiles, ligand.sa_score)
            for ligand in ligands
        ]
        info("Ligands (%i): %s" % (identifier, ", ".join(ligand_strings)))

    for backend in backends:
        backend.add_freeform_structure(structure.name,
                                       func_repr,
                                       cif_file,
                                       ligands=ligands)

    # completed sucessfully
    return True
Esempio n. 7
0
def site_replace(structure,
                 replace_list,
                 rotations=12,
                 backends=(),
                 manual_angles=None):
    """
    Use replace list to modify the structure with (group, site) pairs.

    Will dump a cif to the backends on success, and return 1 for failed attempt.

    """

    rotation_angle = 2 * np.pi / rotations
    new_mof_name = []
    new_mof_friendly_name = []
    # copy the atoms and bonds so we don't alter the original structure
    new_mof = list(structure.atoms)
    new_mof_bonds = dict(structure.bonds)
    rotation_count = 0

    # here we can zip things up
    if manual_angles is None:
        manual_angles = [None for _ in replace_list]
    elif len(manual_angles) != len(replace_list):
        error("All angles must be specified in manual mode")

    for this_replace, this_manual in zip(replace_list, manual_angles):
        # Unpack the two values from replace list, allows us to zip with
        # manual angles
        this_group, this_site = this_replace
        attachment = functional_groups[this_group]

        # pre-calculate all the angles that we will use to zip with the
        # group positions, always generate a list of list of angles
        if this_manual is None or this_manual == '+':
            # generates all the possible angle sets with the same angle on
            # everything
            angles = [
                trial_rotation * rotation_angle
                for trial_rotation in range(rotations)
            ]
            # regenerate the '+' if it is used, otherwise blank it
            angles_str = "" if not this_manual else '+'
        elif len(this_manual) == 1:
            # Assume single angle applies to all groups, just make one set
            angles = [2 * np.pi * (ord(this_manual[0]) - 97) / 26.0]
            angles_str = "%%%s" % this_manual
            debug("{:.1f} rotation for all".format(180 * angles[0] / np.pi))
        elif '+' in this_manual:
            # keep manual angles fixed and scan all angles where it is '+'
            angles = []
            for trial_rotation in range(rotations):
                this_angle = []
                for x in this_manual:
                    if x == '+':
                        this_angle.append(trial_rotation * rotation_angle)
                    else:
                        this_angle.append(2 * np.pi * (ord(x) - 97) / 26.0)
                angles.append(this_angle)
            angles_str = "%%%s" % this_manual
            debug("Angles from: {}".format(
                [round(180 * x / np.pi, 1) for x in angles[0]]))
            debug("Angles to: {}".format(
                [round(180 * x / np.pi, 1) for x in angles[-1]]))
        else:
            # a = 0 degrees, z = 346
            # _ is negative (or anything else negative...)
            angles = [[2 * np.pi * (ord(x) - 97) / 26.0 for x in this_manual]]
            angles_str = "%%%s" % this_manual
            debug("Angles: {}".format(
                [round(180 * x / np.pi, 1) for x in angles[0]]))

        new_mof_name.append("%s@%s%s" % (this_group, this_site, angles_str))
        new_mof_friendly_name.append("%s@%s%s" %
                                     (attachment.name, this_site, angles_str))
        current_mof = list(new_mof)
        current_mof_bonds = dict(new_mof_bonds)
        for this_angle in angles:
            # for single angles, make a list to zip with each site
            if isinstance(this_angle, float):
                this_angle = [this_angle] * len(
                    structure.attachments[this_site])
            if len(this_angle) < len(structure.attachments[this_site]):
                error("Not enough manual angles : {} needed, {} found".format(
                    len(structure.attachments[this_site]), len(this_angle)))
                return False
            for this_point, this_rotation in zip(
                    structure.attachments[this_site], this_angle):
                if this_rotation < 0:
                    # manually specified empty site
                    continue
                attach_id = this_point[0]
                attach_to = this_point[1]
                attach_at = structure.atoms[attach_to].pos
                attach_towards = this_point[2]
                attach_normal = structure.atoms[attach_to].normal
                new_mof[attach_id:attach_id + 1] = [None]
                start_idx = len(new_mof)
                attach_normal = dot(
                    rotation_about_angle(attach_towards, this_rotation),
                    attach_normal)
                incoming_group, incoming_bonds = attachment.atoms_attached_to(
                    attach_at, attach_towards, attach_normal, attach_to,
                    start_idx)
                group_fits = True
                for atom in incoming_group:
                    if not test_collision(
                            atom, new_mof, structure.cell, ignore=[attach_to]):
                        group_fits = False
                        break
                else:
                    # Fits, so add and move on
                    new_mof.extend(incoming_group)
                    new_mof_bonds.update(incoming_bonds)

                if not group_fits:
                    # kill this loop
                    rotation_count += 1
                    new_mof = list(current_mof)
                    new_mof_bonds = dict(current_mof_bonds)
                    break

            else:
                # All sites attached without collision,
                # break out of the rotations loop
                break
        else:
            # Did not attach after all rotations
            fail_name = ".".join(["@".join(x) for x in replace_list])
            warning("Failed: %s@%s from %s" %
                    (this_group, this_site, fail_name))
            debug("Needed {} rotations".format(rotation_count))
            return False

    debug("Needed {} rotations".format(rotation_count))

    new_mof_name = ".".join(new_mof_name)
    new_mof_friendly_name = ".".join(new_mof_friendly_name)

    # Counts how many have been made
    identifier = count()
    info("Generated (%i): [%s]" % (identifier, new_mof_friendly_name))

    full_mof_name = "%s_func_%s" % (structure.name, new_mof_name)

    ligands = atoms_to_identifiers(new_mof, new_mof_bonds)
    cif_file = atoms_to_cif(new_mof,
                            structure.cell,
                            new_mof_bonds,
                            full_mof_name,
                            identifiers=ligands)

    if ligands is not None:
        ligand_strings = [
            "{}:{}".format(ligand.smiles, ligand.sa_score)
            for ligand in ligands
        ]
        info("Ligands (%i): %s" % (identifier, ", ".join(ligand_strings)))

    for backend in backends:
        backend.add_symmetry_structure(structure.name,
                                       replace_list,
                                       cif_file,
                                       ligands=ligands,
                                       manual_angles=manual_angles)

    # successful
    return True
Esempio n. 8
0
def load_structure(name):
    """
    Load a structure from a pickle or generate a new one as required.
    Returns an initialised structure. Caches loaded structure on disk.

    """

    info("Structure version {}.{}".format(*DOT_FAPSWITCH_VERSION))
    pickle_file = "__{}.fapswitch".format(name)
    loaded = False
    if path.exists(pickle_file):
        info("Existing structure found: {}; loading...".format(pickle_file))
        #TODO(tdaff): deal with errors
        try:
            with open(pickle_file, 'rb') as p_structure:
                structure = pickle.load(p_structure)
            # Negative versions ensure that very old caches will be removed
            if not hasattr(structure, 'fapswitch_version'):
                structure.fapswitch_version = (-1, -1)
            # Need to make sure it is still valid
            if structure.fapswitch_version[0] < DOT_FAPSWITCH_VERSION[0]:
                error("Old dot-fapswitch detected, re-initialising")
                loaded = False
            elif structure.fapswitch_version[1] < DOT_FAPSWITCH_VERSION[1]:
                warning(
                    "Cached file {} may be out of date".format(pickle_file))
                loaded = True
            else:
                debug("Finished loading")
                loaded = True
        except EOFError:
            warning("Corrupt pickle; re-initialising")
            loaded = False

    if not loaded:
        info("Initialising a new structure. This may take some time.")
        structure = Structure(name)
        structure.from_cif('{}.cif'.format(name))

        # Ensure that atoms in the structure are properly typed
        structure.gen_factional_positions()
        bonding_src = options.get('connectivity')
        if bonding_src == 'file':
            # Rudimentary checks for poor structures
            if not hasattr(structure, 'bonds'):
                error("No bonding in input structure, will probably fail")
            elif len(structure.bonds) == 0:
                error("Zero bonds found, will fail")
            elif not hasattr(structure.atoms[0], 'uff_type'):
                warning("Atoms not properly typed, expect errors")
            else:
                info("Bonding from input file used")
        elif bonding_src == 'bondsonly':
            info("Generating atom types from cif bonding")
            structure.gen_types_from_bonds()
        elif bonding_src == 'openbabel':
            info("Generating topology with Open Babel")
            structure.gen_babel_uff_properties()

        # A couple of structure checks
        structure.check_close_contacts()
        structure.bond_length_check()

        # Initialise the sites after bonds are perceived
        structure.gen_attachment_sites()
        structure.gen_normals()

        # Cache the results
        info("Dumping cache of structure to {}".format(pickle_file))
        structure.fapswitch_version = DOT_FAPSWITCH_VERSION
        with open(pickle_file, 'wb') as p_structure:
            pickle.dump(structure, p_structure, protocol=-1)

    return structure