def place_ligands(atom_dict, lig_info, sites, buffer, fixed_loc, cap): """ Adds the requested amount of ligands at random sites, returns updated atom_dict. """ # Sorry for the length of this function, should probably be broken up in # multiple functions. ligand_type = lig_info["ligand_type"] n_ligands = lig_info["n_ligands"] extension = lig_info["extension"] fail_bool = False id = max(atom_dict) + 1 original_ligand = ligand_type j = 0 tried = [] # Print which ligands are being placed if extension and extension[0] is not False: ex_string = '' for x in extension: if x is not False: ex_string += x[:-4] + ' + ' ex_string = ex_string[:-3] print("\nPlacing " + ligand_type[:-4] + " + " + ex_string) else: print("\nPlacing " + ligand_type[:-4]) # Add ligands until done while j < n_ligands: # preventive in case of rounding errors if len(sites) == 0: break extra_rotation = 0 ligand_type = original_ligand # Randomly choose site, get relevant info, don't pick site that has already been tried sites_list = list(sites).copy() remaining_sites = [x for x in sites_list if x not in tried] if len(remaining_sites) == 0: print("\nUnable to place more ligands of type " + str(original_ligand) + ". Placed " + str(j) + " out of " + str(n_ligands) + " requested ligands.\n") retry = hf.y2true(input("Try again (y), or continue (n)?: ")) if retry: print("Trying again...") return 1 break # Use predetermined sites if required, "loc" variables refer to # connection site and its primary atom if fixed_loc and ligand_type != cap: if len(lig_info["loc_info"]) == 0: break loc_id = random.choice(list(lig_info["loc_info"])) loc_vec = lig_info["loc_info"][loc_id]["loc_vec"] loc_sites = copy.deepcopy(sites[loc_id]['sites_xyz']) random_rotation = lig_info["loc_info"][loc_id]["rotation"] del lig_info["loc_info"][loc_id] # Use random sites else: loc_id = random.choice(remaining_sites) loc_sites = sites[loc_id]['sites_xyz'].copy() loc_vec = random.choice(loc_sites) random_rotation = random.random() * 2 * math.pi loc_primary_xyz = sites[loc_id]['primary_xyz'] tried_loc_vec = [] lig = prep_ligand_file(atom_dict, ligand_type, extension, cap) stop = False # Loop over every site connected to chosen atom while True: # Make sure you don't try same loc_vec twice for vec in tried_loc_vec: if hf.distance_checker(vec, loc_vec) < 0.001: stop = True if stop: break # Get correct rotation for ligand relative to site axis = np.cross(loc_vec, [0, 0, 1]) # Prevent dividing by 0 when vectors are already lined up if np.linalg.norm(axis) == 0: axis = [1, 0, 0] angle = 0 else: angle = -hf.angle_checker(loc_vec, [0, 0, 1]) rot_mat = hf.rotation_matrix(axis, angle) # Place ligand if possible, otherwise rotate while True: xyz_list = [] temp_atom_dict = {} broken = False min_dist_lig = math.inf initial_length = hf.check_bond_len( bond_len_dict, lig[hf.base_atom(lig)]["element"], atom_dict[loc_id]["element"]) for atom, lig_val in lig.items(): lig_coor = copy.deepcopy(lig_val["coor"]) lig_coor[2] += initial_length # Rotate every atom in ligand according to random_rotation rotated_atom = np.dot( hf.rotation_matrix([0, 0, 1], random_rotation + extra_rotation), lig_coor) # Rotate in right direction (with respect to crystal) new_v = np.dot(rot_mat, rotated_atom) atom_xyz = [ c1 + c2 for c1, c2 in zip(new_v, loc_primary_xyz) ] atom_element = lig[atom]['element'] temp_atom_dict[id] = { "coor": atom_xyz, "element": atom_element, "type": "ligand", "ligand_type": ligand_type, "loc_id": loc_id, "loc_vec": loc_vec, "rotation": random_rotation + extra_rotation } xyz_list.append(atom_xyz) id += 1 # Check for overlap if ligand_type != cap: for test_atom, values in atom_dict.items(): space = hf.check_bond_len( bond_len_dict, atom_element, values["element"]) + buffer if test_atom != loc_id: dist = hf.distance_checker( values["coor"], xyz_list[-1]) if dist < space: broken = True break # Check for overlap when capping, use different rotation if ligand_type == cap: new_pos = new_v.copy() changed = False while True: min_dist_lig = math.inf for test_lig, values in atom_dict.items(): if values["type"] == "ligand": dist = hf.distance_checker( values["coor"], [ c1 + c2 for c1, c2 in zip( new_pos, loc_primary_xyz) ]) bond_len_loc = hf.check_bond_len( bond_len_dict, atom_element, values["element"]) # Sorry for magic number if dist < bond_len_loc + 0.3: if dist < min_dist_lig: min_dist_lig = dist min_dist_lig_coor = values["coor"] changed = True if min_dist_lig == math.inf: if changed: xyz_list = [ c1 + c2 for c1, c2 in zip( new_pos, loc_primary_xyz) ] temp_atom_dict[id - 1] = { "coor": xyz_list, "element": atom_element, "type": "ligand", "ligand_type": ligand_type, "loc_id": loc_id, "loc_vec": loc_vec } break # Rotate away from closest atom vec_closest_element = [ c1 - c2 for c1, c2 in zip( min_dist_lig_coor, loc_primary_xyz) ] axis = np.cross(vec_closest_element, new_pos) angle = 0.1 rot_mat = hf.rotation_matrix(axis, angle) new_pos = np.dot(rot_mat, new_pos.copy()) if broken: break # Check for ligand collisions except when last ligand is added if ligand_type != cap: # Rotate if there is a collision if broken and extra_rotation < math.pi * 2: # Rotate less with fixed loc if fixed_loc: extra_rotation += 0.1 else: extra_rotation += 0.2 continue # break if rotated fully elif extra_rotation > math.pi * 2: extra_rotation = 0 tried.append(loc_id) tried_loc_vec.append(loc_vec) fail_bool = True break # Merge dicts if no collision elif not broken: atom_dict = {**atom_dict, **temp_atom_dict} if j % 10 == 0: print(str(j) + " ligands added") if loc_id in sites: del sites[loc_id] tried_loc_vec = [loc_vec] ligand_type = cap lig = prep_ligand_file(atom_dict, cap, [False], cap) j += 1 break else: atom_dict = {**atom_dict, **temp_atom_dict} if loc_id in sites: del sites[loc_id] tried_loc_vec.append(loc_vec) break # Add final ligand type to remaining sites at chosen atom try: loc_vec = loc_sites[(loc_sites.index(loc_vec) + 1) % len(loc_sites)] except ValueError: min_dist = math.inf loc_index = 0 for item in loc_sites: distance = hf.distance_checker(loc_vec, item) if distance < min_dist: min_dist = distance loc_index_min = loc_index loc_index += 1 loc_vec = loc_sites[(loc_index_min + 1) % len(loc_sites)] # Detect failure to place all requested ligands if replacing if fixed_loc and fail_bool: retry = hf.y2true( input( "Not all ligands were able to be put in the same spot, try again? y/n: " )) if retry: print("Trying again...") return 1 else: cont = hf.y2true(input("Continue with fewer ligands? y/n: ")) if not cont: sys.exit() return atom_dict
def replace_ligands(atom_dict, lig_dict, cap, sites, buffer, input_rep, filename, foldername): """ Takes in an atom dict with ligands and replaces the ligands by the requested replacements, saves it to a new quantum dot and returns the new atom dict. """ replacement_list = [] rep_ext_list = [] loc_dict = {cap[:-4]: []} rep_dict = copy.deepcopy(lig_dict) lig_dict_inv = {} # Store the replacements and replacee's in the right way for ligand, lig_val in lig_dict.items(): if lig_val["ligand_type"] != cap: # Use existing input if input_rep: replacement_list = [input_rep[0]] rep_ext_list = [input_rep[1]] rep_dict[ligand]["extension"] = rep_ext_list[-1] # Get new input else: replacement_list.append( input("Replace " + lig_val["ligand_type"] + " with: ") + ".xyz") extend = hf.y2true(input("Extend ligand? y/n: ")) if extend: rep_ext_list.append([input("Extend with: ") + ".xyz"]) rep_dict[ligand]["extension"] = rep_ext_list[-1] else: rep_ext_list.append([False]) rep_dict[ligand]["extension"] = [False] # Store info in dict loc_dict[ligand] = { "loc_id": [], "replacement": replacement_list[-1] } rep_dict[ligand]["ligand_type"] = replacement_list[-1] rep_dict[ligand]["loc_info"] = {} # Reverses key and value lig_dict_inv[lig_val["ligand_type"]] = ligand # Get info on all atoms that need to be deleted, and get the relevant info # of the replacee's atom_del_list = [] for atom, at_values in atom_dict.items(): if at_values["type"] == 'ligand': atom_del_list.append(atom) if at_values["ligand_type"] != cap: rep_dict[lig_dict_inv[at_values["ligand_type"]]]["loc_info"][ at_values["loc_id"]] = { "loc_vec": at_values["loc_vec"], "rotation": at_values["rotation"] } rep_dict_copy = copy.deepcopy(rep_dict) atom_dict_copy = copy.deepcopy(atom_dict) sites_copy = copy.deepcopy(sites) done = False # Actually delete and replace while not done: for item in atom_del_list: del atom_dict[item] for lig, values in rep_dict.items(): atom_dict = place_ligands(atom_dict, values, sites, buffer, True, cap) if atom_dict == 1: atom_dict = copy.deepcopy(atom_dict_copy) sites = copy.deepcopy(sites_copy) rep_dict = copy.deepcopy(rep_dict_copy) break else: done = True hf.dict2file(atom_dict, filename, foldername) return atom_dict, rep_dict
def single_qd(atom_dict, sites, foldername): """ Create a single quantum dot and allow for replacement of ligands for a new one. """ filename = input("Save QD as: ") i = 0 lig_list = [] extension_list = [] coverage_list = [] n_ligands_list = [] lig_dict = {} n_sites = len(sites) # Get all ligands while True: ligand_file = input( "Filename of ligand to be added, or type files to see available ligands: " ) + ".xyz" # Print available ligands if ligand_file[:-4] == "files": hf.print_lig() ligand_file = input("Filename of ligand to be added: ") + ".xyz" lig_list.append(ligand_file) extend = hf.y2true( input( "Extend ligand? Note: this replaces the last atom in the ligand file. y/n: " )) if extend: extension_list.append([input("Extend with: ") + ".xyz"]) while True: extra_ext = input( "Further extension to extension, or type n: ") if extra_ext != 'n': extension_list[-1].append(extra_ext + ".xyz") else: break else: extension_list.append([False]) coverage = float(input("Coverage (fraction): ")) coverage_list.append(coverage) n_ligands_list.append(round(coverage * n_sites)) lig_dict[i] = { "ligand_type": ligand_file, "extension": extension_list[-1], "coverage": coverage, "n_ligands": round(coverage * n_sites) } i += 1 more = hf.y2true(input("Add another type? y/n: ")) if not more: cap = input("Cap remaining sites with (single atom): ") + ".xyz" lig_dict[i] = { "ligand_type": cap, "extension": [False], "coverage": 1, "n_ligands": n_sites } break buffer = float( input( "Buffer (Angstrom) (distance between ligands will be at least buffer + bonding length): " )) sites_copy = copy.deepcopy(sites) atom_dict_copy = copy.deepcopy(atom_dict) # Place ligands while True: stopped = False for ligand, values in lig_dict.items(): atom_dict = place_ligands(atom_dict, values, sites, buffer, False, cap) if atom_dict == 1: atom_dict = copy.deepcopy(atom_dict_copy) sites = copy.deepcopy(sites_copy) stopped = True break if not stopped: break hf.dict2file(atom_dict, filename, foldername) # Replace ligands while True: replacement_ligands = hf.y2true( input("Replace ligands to create new quantum dot? y/n: ")) if replacement_ligands: sites = sites_copy.copy() filename = input("Write to file: ") buffer = float(input("Buffer: ")) replaced = replace_ligands(atom_dict, lig_dict, cap, sites, buffer, False, filename, foldername) atom_dict = replaced[0] lig_dict = replaced[1] else: break
def series_lig(atom_dict, sites, foldername): """ Obtains the lig_dict containing the values of the ligands to be added to the quantum dot. Here this is done by combining all 'bases' and extensions with eachother. e.g. when you put in C10H21 and C9H19 as bases, and COOH and NH2 as extensions you'll get C10H21 + COOH, C10H21 + NH2, C9H19 + COOH C9H19 + NH2 """ base_list = [] ext_list = [] lig_dict = {} n_sites = len(sites) sites_copy = copy.deepcopy(sites) # Obtain bases of ligands while True: base = input( "Add ligand to base list, or type 'files' to show available ligands, or type 'done': " ) if base == 'files': hf.print_lig() if base == 'done': break base_list.append(base + ".xyz") # Obtain extensions while True: ext = input( "Add extension to extension list, or type 'none', or 'done': ") if ext == 'done': break if ext == 'none': ext_list.append([False]) # Extend extensions else: ext_list.append([ext + ".xyz"]) extra_ext = input("Further extension to extension, or type n: ") if extra_ext != 'n': ext_list[-1].append(extra_ext + ".xyz") coverage = float(input("Coverage (fraction): ")) cap = input("Cap remaining sites with (single atom): ") + ".xyz" sec_buffer = float( input( "Buffer size (Angstrom)? Distance between ligands will be at least bonding length + buffer: " )) buffer = float( input( "Initial buffer (Buffer for first QD. Helps create space for later QD's): " )) atom_dict_copy = copy.deepcopy(atom_dict) i = 0 # Create all quantum dots for base in base_list: print(base) for extension in ext_list: filename = base[:-4] for item in extension: if item is not False: filename += "+" + item[:-4] # Create first quantum dot if i == 0: lig_dict[0] = { "ligand_type": base, "extension": extension, "coverage": coverage, "n_ligands": round(coverage * n_sites) } lig_dict[1] = { "ligand_type": cap, "extension": [False], "coverage": 1, "n_ligands": n_sites } while True: stopped = False for ligand, values in lig_dict.items(): atom_dict = place_ligands(atom_dict, values, sites, buffer, False, cap) # Detect failure to place all ligands if atom_dict == 1: atom_dict = copy.deepcopy(atom_dict_copy) sites = copy.deepcopy(sites_copy) stopped = True break if not stopped: i += 1 buffer = sec_buffer hf.dict2file(atom_dict, filename, foldername) break # Replace ligands else: sites = sites_copy.copy() input_rep = [base, extension] replaced = replace_ligands(atom_dict, lig_dict, cap, sites, buffer, input_rep, filename, foldername) atom_dict = replaced[0] lig_dict = replaced[1] # Allow for more replacements while True: replacement_ligands = hf.y2true( input("Replace ligands to create new quantum dot? y/n: ")) if replacement_ligands: sites = sites_copy.copy() filename = input("Write to file: ") buffer = float(input("Buffer: ")) replaced = replace_ligands(atom_dict, lig_dict, cap, sites, buffer, False, filename, foldername) atom_dict = replaced[0] lig_dict = replaced[1] else: break
def crystal_builder(a, atom_a, atom_b, diameter): """ This function takes in the lattice constant, elements in the crystal and the diameter of the nanocrystal to be built. The unit cell is copied in all directions and cutoffs are made at the (111), (110) and (100) planes. ID numbers are assigned and neighbours are calculated. Then coordinates, elements, neighbours and atom id are stored in a dict. """ n_unit = math.ceil(diameter) n_range = np.arange(-n_unit / 2.0 + 0.5, n_unit / 2.0) # Get unit cell uc = unit_cells.zns(a, atom_a, atom_b) all_atoms = [] atom_dict = {} cut_off_distance = diameter * a / 2.0 boundary_111 = 3.0 / math.sqrt(3) * cut_off_distance boundary_110 = 2.0 / math.sqrt(2) * cut_off_distance boundary_100 = cut_off_distance id = 0 for x in n_range: for y in n_range: for z in n_range: # Place atoms one by one in correct spot for atom in uc.atom_xyz: atom_element = atom[0] atom_x = round(x * a + atom[1], 4) atom_y = round(y * a + atom[2], 4) atom_z = round(z * a + atom[3], 4) atom_details = [atom_element, atom_x, atom_y, atom_z] # Make sure not to put multiple atoms in same spot, and make cut-offs if atom_details not in all_atoms and ( abs(atom_x) + abs(atom_y) + abs(atom_z) <= boundary_111 and (abs(atom_x) + abs(atom_y) <= boundary_110 and abs(atom_x) + abs(atom_z) <= boundary_110 and abs(atom_y) + abs(atom_z) <= boundary_110) and (abs(atom_x) <= boundary_100 and abs(atom_y) <= boundary_100 and abs(atom_z) <= boundary_100)): all_atoms.append(atom_details) bound = hf.bond_checker(atom_details, atom_dict, bond_len_dict) atom_dict[id] = { "coor": [atom_x, atom_y, atom_z], "element": atom_element, "bound": bound, "type": "crystal" } # Update bonds for already placed atoms for item in bound: atom_dict[item]["bound"].append(id) id += 1 # Remove all singly bound atoms, if wanted include_singles = hf.y2true(input("Include singly bound atoms? y/n: ")) if not include_singles: while True: for test_id, values in atom_dict.items(): stopped = False # Delete singly bonded atoms, update neighbour if len(values['bound']) == 1: neighbour = values['bound'][0] atom_dict[neighbour]['bound'].remove(test_id) del dict[test_id] stopped = True break # Break if no singly bonded atoms remain if not stopped: break return atom_dict
space = space + 0.25 if test_atom not in values["connected"]: dist = hf.distance_checker(values2["coor"], xyz_list[-1]) if dist < space + 0.25: break atom_dict = {**atom_dict, **temp_atom_dict} break return atom_dict if __name__ == "__main__": # Having a global dict with bonding lengths improves speed a lot global bond_len_dict bond_len_dict = hf.csv2dict("bonding_distances.csv") build = hf.y2true( input("Create new crystal (y) or use existing file (n)?: ")) if build: a = float(input("Specify lattice constant (in Ångström): ")) atom_a = input("Element for first element type: ") atom_b = input("Element for second element type: ") diameter = float(input("Diameter of quantum dot (in unit cells): ")) atom_dict = crystal_builder(a, atom_a, atom_b, diameter) else: crystal_file = input( "Crystal file to use (don't write the file extension): ") + ".xyz" atom_dict = crystal_reader(crystal_file) foldername = input("Save in folder (or main): ") if foldername == "main": foldername = False sites = tetra_sites(atom_dict)