示例#1
0
def mate_genes(old_population, new_population, count, structure, site_list,
               backends, mutation_rate):
    """
    Make some new structures by mating current ones, based on position in
    the population.
    """
    while count > 0:
        index_0 = int(math.floor(len(old_population) * (random.random()**2)))
        index_1 = int(math.floor(len(old_population) * (random.random()**2)))
        old_gene_0 = old_population[index_0]['gene']
        old_gene_1 = old_population[index_1]['gene']
        debug("Mating {} and {}".format(old_gene_0, old_gene_1))
        new_gene = []
        for gene_part_0, gene_part_1 in zip(old_gene_0.split('.'),
                                            old_gene_1.split('.')):
            # Make a random split somewhere inside
            split = random.randint(0, len(gene_part_0))
            new_gene.append(
                mutate(gene_part_0[:split] + gene_part_1[split:],
                       mutation_rate))
        if any(x['gene'] == ".".join(new_gene) for x in new_population):
            # Alraedy in the population, don't include twice
            continue
        elif site_replace(structure,
                          replace_list=site_list,
                          manual_angles=new_gene,
                          backends=backends):
            debug("Successfully mated as {}".format(new_gene))
            individual = {'fitness': 0.0, 'gene': ".".join(new_gene)}
            new_population.append(individual)
            count -= 1
示例#2
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")
示例#3
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)
示例#4
0
def main():
    """
    Main logic to find lowest energy conformation of the functional groups
    through an evolutionary.

    """

    info("Welcome to confswitch; finding your lowest energy conformers")
    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))

    # Only use the cif backend here
    backends = [CifFileBackend()]

    # Optimise each custom string passed to fapswitch
    custom_strings = options.get('custom_strings')
    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))

        optimise_conformation(input_structure, site_list, backends=backends)
示例#5
0
def all_combinations_replace(structure,
                             rotations=12,
                             replace_only=None,
                             groups_only=None,
                             max_different=None,
                             backends=()):
    """
    Replace every functional point with every combination of functional groups.

    """

    if replace_only is not None:
        local_attachments = [
            att_id for att_id in structure.attachments
            if att_id in replace_only
        ]
        debug("Replacing only: %s" % list(local_attachments))
    else:
        local_attachments = structure.attachments
        debug("Replacing all sites: %s" % list(local_attachments))
    sites = powerset(sorted(local_attachments))

    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 max_different is None or max_different <= 0:
        max_different = len(local_groups)

    for site_set in sites:
        for group_set in product(local_groups, repeat=len(site_set)):
            #TODO(tdaff): make this more efficient
            if len(set(group_set)) > max_different:
                continue
            replace_list = zip(group_set, site_set)
            site_replace(structure,
                         replace_list,
                         rotations=rotations,
                         backends=backends)
示例#6
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))
def sa_score(smiles):
    """
    Return the SA Score for the given smiles representation.

    """

    molecule = Chem.MolFromSmiles(smiles)

    #
    # fragment score
    #

    # use a radius of 2 for circular fingerprint
    try:
        fingerprint = rdMolDescriptors.GetMorganFingerprint(molecule, 2)
        fingerprint = fingerprint.GetNonzeroElements()
    except Exception as error:
        # Will throw a boost error for N+ so we just give a 0 for score
        debug(error)
        return 0

    fragment_score = 0.0
    fragment_count = 0

    # Count frequencies of fragments
    for bit_id, count in fingerprint.items():
        fragment_count += count
        fragment_score += MOLDB.get(bit_id, -4) * count

    fragment_score /= fragment_count

    #
    # features score
    #

    num_atoms = molecule.GetNumAtoms()
    num_chiral_centers = len(
        Chem.FindMolChiralCenters(molecule, includeUnassigned=True))
    num_bridgeheads, num_spiro, num_macrocycles = ring_analysis(molecule)

    size_penalty = (num_atoms**1.005) - num_atoms
    stereo_penalty = math.log10(num_chiral_centers + 1)
    spiro_penalty = math.log10(num_spiro + 1)
    bridge_penalty = math.log10(num_bridgeheads + 1)

    macrocycle_penalty = 0.0
    # ---------------------------------------
    # This differs from the paper, which defines:
    #  macrocycle_penalty = math.log10(num_macrocycles + 1)
    # This form generates better results when 2 or more macrocycles are present
    if num_macrocycles > 0:
        macrocycle_penalty = math.log10(2)

    feature_penalty = (0.0 - size_penalty - stereo_penalty - spiro_penalty -
                       bridge_penalty - macrocycle_penalty)

    #
    # Correction for the fingerprint density.
    # Not in the original publication, added in version 1.1
    # to make highly symmetrical molecules easier to synthetise.
    #
    if num_atoms > len(fingerprint):
        fingerprint_density = math.log(
            float(num_atoms) / len(fingerprint)) * 0.5
    else:
        fingerprint_density = 0.0

    #
    # Total score
    #
    total_score = fragment_score + feature_penalty + fingerprint_density

    # Transform "raw" value into scale between 1 and 10.
    sa_min = -4.0
    sa_max = 2.5
    total_score = 11.0 - (total_score - sa_min + 1) / (sa_max - sa_min) * 9.0
    # smooth the 10-end
    if total_score > 8.0:
        total_score = 8.0 + math.log(total_score + 1.0 - 9.0)

    if total_score > 10.0:
        total_score = 10.0
    elif total_score < 1.0:
        total_score = 1.0

    return total_score
示例#8
0
    def handle_request(self):
        """Generate a random structure and return the rendered page."""

        debug("Arguments: {}".format(self.request.arguments))

        max_trials = 20
        top_50_groups = [
            "Me", "Ph", "Cl", "OMe", "OH", "Et", "OEt", "F", "Br", "NO2",
            "NH2", "CN", "COOEt", "COMe", "COOH", "Bnz", "COOMe", "iPr",
            "pTol", "4ClPh", "tBu", "4OMePh", "CF3", "COPh", "Pr", "NMe2",
            "Bu", "OBnz", "4NO2Ph", "OAc", "4FPh", "I", "4BrPh", "2ClPh",
            "All", "COH", "SMe", "CONH2", "NPh", "24DClPh", "CHex", "Morph",
            "HCO", "3ClPh", "oTol", "2Fur", "iBu", "NCOMe"
        ]
        small_groups = ["F", "Cl", "Me", "NH2", "OH", "CN"]
        # Possible options:
        # replace_only: tuple of sites to replace
        # groups_only: only use specific groups
        # max_different: restrict simultaneous types of groups

        # This is new every time and keeps all the information
        # we need specific to the web version
        backends = [WebStoreBackend()]
        failed = ""

        if 'mof-choice' in self.request.arguments:
            # Selected a specific MOF
            chosen_structure = self.get_argument('mof-choice')
            base_structure = get_structure(chosen_structure)
            replace_list = []
            for site in available_structures[chosen_structure]:
                group = self.get_argument(site, None)
                if group is None or 'None' in group:
                    continue
                elif 'Random' in group:
                    replace_list.append([random.choice(top_50_groups), site])
                else:
                    replace_list.append([group, site])

            # Now make the MOF
            status = site_replace(base_structure,
                                  replace_list=replace_list,
                                  backends=backends)
            if not status:
                # couldn't make it so just use clean structure
                failed = ".".join("{}@{}".format(x[0], x[1])
                                  for x in replace_list)
                site_replace(base_structure,
                             replace_list=[],
                             backends=backends)
        else:
            # Completely random
            chosen_structure = random.choice(list(available_structures))
            # Make sure we have functionalisation sites
            while len(available_structures[chosen_structure]) == 0:
                chosen_structure = random.choice(list(available_structures))
            # Here's the actual structure
            base_structure = get_structure(chosen_structure)

            # Use several combinations to try to get something functionalised
            trial_number = 0
            while trial_number < max_trials:
                if trial_number < max_trials / 4.0:
                    debug("Trial all groups: {}".format(trial_number))
                    status = random_combination_replace(
                        structure=base_structure,
                        backends=backends,
                        max_different=2)
                elif trial_number < 2.0 * max_trials / 4.0:
                    debug("Trial max one group: {}".format(trial_number))
                    status = random_combination_replace(
                        structure=base_structure,
                        backends=backends,
                        max_different=1)
                elif trial_number < 3.0 * max_trials / 4.0:
                    debug("Trial top 50: {}".format(trial_number))
                    status = random_combination_replace(
                        structure=base_structure,
                        backends=backends,
                        groups_only=top_50_groups,
                        max_different=2)
                else:
                    debug("Trial small groups: {}".format(trial_number))
                    status = random_combination_replace(
                        structure=base_structure,
                        backends=backends,
                        groups_only=small_groups,
                        max_different=1)

                # If functionalisation attempted
                if status:
                    if backends[0].cifs[-1]['functions']:
                        # it was successful; done here
                        break
                else:
                    # only increment if we actually tried to add groups
                    trial_number += 1
            else:
                site_replace(base_structure,
                             replace_list=[],
                             backends=backends)
                failed = "{} random combinations".format(max_trials)

        # Should always have a structure, even if it is clean; but failed will
        # be True for that
        cif_info = backends[0].cifs[-1]
        # MEPO compatibility if all groups are okay
        if all(functional_groups[function[0]].mepo_compatible
               for function in cif_info['functions']):
            mepo_compatible = "Yes"
        else:
            mepo_compatible = "No"

        collision_tester = options.get('collision_method')
        collision_cutoff = options.getfloat('collision_scale')

        if cif_info['ligands'] is None:
            ligands = []
        else:
            ligands = cif_info['ligands']

        if ligands:
            sa_score = max(ligand.sa_score for ligand in ligands)
        else:
            sa_score = 0.0

        processed_ligands = make_ligands(ligands)

        extra_info = """<h4>Hypothetical functionalised MOF</h4>
        <p>Functional groups have been added using the crystal
        symmetry. A collision detection routine with a {} radius
        at {:.2f} was used to carry out the functionalisation.
        Note that although atoms may appear close, the bonding
        connectivity defined in the cif file will be correct.</p>
        """.format(collision_tester, collision_cutoff)

        # These references are always required
        local_references = [
            references['Kadantsev2013'], references['Ertl2009'],
            references['Chung2014']
        ]

        # Find all the references and add them too
        for reference in re.findall(r'\[(.*?)\]', extra_info):
            local_references.append(references[reference])

        # Raw HTML anchors. Ugly.
        extra_info = re.sub(
            r'\[(.*?)\]',  # non-greedy(?) find in square brackets
            r'[<a href="#\1">\1</a>]',  # replace raw html
            extra_info)

        page = templates.load('random.html').generate(
            mepo_compatible=mepo_compatible,
            references=local_references,
            functional_groups=functional_groups,
            extra_info=extra_info,
            sa_score=sa_score,
            processed_ligands=processed_ligands,
            available_structures=available_structures,
            failed=failed,
            **cif_info)
        self.write(page)
示例#9
0
 def post(self, url='/'):
     """POST request"""
     debug('POST request')
     self.handle_request()
示例#10
0
 def get(self, url='/'):
     """GET request"""
     debug("GET request")
     self.handle_request()
示例#11
0
def atoms_to_identifiers(atoms, bonds):
    """Derive the smiles for all the organic ligands."""
    try:
        import openbabel as ob
        import pybel
    except ImportError:
        # Don't bother if no openbabel'
        return

    obmol = ob.OBMol()
    obmol.BeginModify()

    # Translation table for indexes
    seen_atoms = {}

    babel_idx = 1

    for idx, atom in enumerate(atoms):
        if atom is None or atom.is_metal:  # or atom.atomic_number == 1:
            # If we ignore them it should split the
            # ligands into fragments
            continue
        else:
            new_atom = obmol.NewAtom()
            new_atom.SetAtomicNum(atom.atomic_number)
            # so we correlate the bond index
            # to the index for the babel_mol
            seen_atoms[idx] = babel_idx
            babel_idx += 1

    for bond, bond_info in bonds.items():
        if bond[0] in seen_atoms and bond[1] in seen_atoms:

            obmol.AddBond(seen_atoms[bond[0]], seen_atoms[bond[1]],
                          OB_BOND_ORDERS[bond_info[1]])

    obmol.EndModify()

    pybelmol = pybel.Molecule(obmol)

    # Strip out stereochemistry
    full_molecule = pybelmol.write('can', opt={'i': None}).strip()

    if full_molecule == '':
        debug("OpenBabel conversion failed; try newer version")
        return

    # Fix for delocalised carboxylate detached from metals
    full_molecule = re.sub(r'C\(O\)O([)$.])', r'C(=O)O\1', full_molecule)

    # remove any lone atoms
    unique_smiles = (set(full_molecule.split(".")) - {'O', 'H', 'N'})

    identifiers = []
    for smile in unique_smiles:
        pybelmol = pybel.readstring('smi', smile)
        can_smiles = pybelmol.write('can', opt={'i': None}).strip()
        smol = Ligand(can_smiles,
                      pybelmol.write('inchi', opt={
                          'w': None
                      }).strip(),
                      pybelmol.write('inchikey', opt={
                          'w': None
                      }).strip(), sa_score(can_smiles))
        identifiers.append(smol)

    return identifiers
示例#12
0
    def log_info(self):
        """Send all the information about the group to the logging functions."""
        info("[{}]".format(self.ident))
        info("name = {}".format(self.name))
        info("smiles = {}".format(self.smiles))
        info("mepo_compatible = {}".format(self.mepo_compatible))

        debug("atoms =")
        for atom in self.atoms:
            debug("    {0:4} {1:5} {2[0]:10.6f} {2[1]:10.6f} {2[2]:10.6f}".
                  format(atom.type, atom.uff_type, atom.pos))
        debug("orientation = {0[0]:.1f} {0[1]:.1f} {0[2]:.1f}".format(
            self.orientation))
        debug("normal = {0[0]:.1f} {0[1]:.1f} {0[2]:.1f}".format(self.normal))
        debug("carbon_bond = {}".format(self.bond_length))
        debug("bonds =")
        for bond in self.bonds:
            debug("    {0[0]:4} {0[1]:4} {1[1]:5.2f}".format(
                bond, self.bonds[bond]))
        info("")
示例#13
0
def random_combination_replace(structure,
                               rotations=12,
                               replace_only=None,
                               groups_only=None,
                               max_different=0,
                               prob_unfunc=-1.0,
                               backends=()):
    """
    Make a random structure in the site symmetry constrained sample space.

    """

    if replace_only is not None:
        local_attachments = [
            att_id for att_id in structure.attachments
            if att_id in replace_only
        ]
        debug("Replacing only: %s" % list(local_attachments))
    else:
        local_attachments = structure.attachments
        debug("Replacing all sites: %s" % list(local_attachments))

    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)

    # Limit mixing chemistry with many groups
    if len(local_groups) > max_different > 0:
        local_groups = random.sample(local_groups, max_different)
        debug("Restricted to: %s" % local_groups)

    replace_list = []

    if prob_unfunc < 0:
        # Negative probability means try a random proportion of
        # functionalisation
        prob_unfunc = random.random()
        debug("Random functionalisation proportion ({})".format(prob_unfunc))

    for site in sorted(local_attachments):
        if random.random() < prob_unfunc:
            # no functional group here
            continue
        else:
            replace_list.append((random.choice(local_groups), site))
    # Do the replacement
    return site_replace(structure,
                        replace_list,
                        rotations=rotations,
                        backends=backends)
示例#14
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
示例#15
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
示例#16
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
示例#17
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
示例#18
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))