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
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")
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)
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)
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)
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
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)
def post(self, url='/'): """POST request""" debug('POST request') self.handle_request()
def get(self, url='/'): """GET request""" debug("GET request") self.handle_request()
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
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("")
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)
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
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
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
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
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))