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 _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 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))
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