def get_linear_bend_inds(coords3d, cbm, bends, min_deg=175, max_bonds=4, logger=None): linear_bends = list() complements = list() if min_deg is None: return linear_bends, complements bm = squareform(cbm) for bend in bends: deg = np.rad2deg(Bend._calculate(coords3d, bend)) bonds = sum(bm[bend[1]]) if (deg >= min_deg) and (bonds <= max_bonds): log( logger, f"Bend {bend}={deg:.1f}° is (close to) linear. " "Creating linear bend & complement.", ) linear_bends.append(bend) complements.append(bend) return linear_bends, complements
def get_hydrogen_bond_inds(atoms, coords3d, bond_inds, logger=None): tmp_sets = [frozenset(bi) for bi in bond_inds] # Check for hydrogen bonds as described in [1] A.1 . # Find hydrogens bonded to small electronegative atoms X = (N, O # F, P, S, Cl). hydrogen_inds = [i for i, a in enumerate(atoms) if a.lower() == "h"] x_inds = [ i for i, a in enumerate(atoms) if a.lower() in "n o f p s cl".split() ] hydrogen_bond_inds = list() for h_ind, x_ind in it.product(hydrogen_inds, x_inds): as_set = set((h_ind, x_ind)) if as_set not in tmp_sets: continue # Check if distance of H to another electronegative atom Y is # greater than the sum of their covalent radii but smaller than # the 0.9 times the sum of their van der Waals radii. If the # angle X-H-Y is greater than 90° a hydrogen bond is asigned. y_inds = set(x_inds) - set((x_ind, )) for y_ind in y_inds: y_atom = atoms[y_ind].lower() cov_rad_sum = CR["h"] + CR[y_atom] distance = Stretch._calculate(coords3d, (h_ind, y_ind)) vdw = 0.9 * (VDW_RADII["h"] + VDW_RADII[y_atom]) angle = Bend._calculate(coords3d, (x_ind, h_ind, y_ind)) if (cov_rad_sum < distance < vdw) and (angle > np.pi / 2): hydrogen_bond_inds.append((h_ind, y_ind)) log( logger, f"Detected hydrogen bond between atoms {h_ind} " f"({atoms[h_ind]}) and {y_ind} ({atoms[y_ind]})", ) return hydrogen_bond_inds
def dihedrals_are_valid(coords3d, dihedral_inds, logger=None): valid = [dihedral_valid(coords3d, inds) for inds in dihedral_inds] invalid = [dihedral_ind for dihedral_ind, v in zip(dihedral_inds, valid) if not v] if invalid: log(logger, f"Invalid dihedrals: {invalid}") all_valid = all(valid) return all_valid
def augment_bonds(geom, root=0, proj=False): assert geom.coord_type != "cart" log(logger, "Trying to augment bonds.") hessian = geom.cart_hessian try: energy = geom.energy except AttributeError: energy = None func = find_missing_bonds_by_projection if proj else find_missing_strong_bonds missing_bonds = func(geom, hessian, root=root) if missing_bonds: aux_bond_pt = PrimTypes.AUX_BOND missing_aux_bonds = [(aux_bond_pt, *mbond) for mbond in missing_bonds] print("\t@Missing bonds:", missing_bonds) new_geom = Geometry( geom.atoms, geom.cart_coords, coord_type=geom.coord_type, coord_kwargs={ "define_prims": missing_aux_bonds, }, ) new_geom.set_calculator(geom.calculator) new_geom.energy = energy new_geom.cart_hessian = hessian return new_geom else: return geom
def update_internals(new_coords3d, old_internals, primitives, dihedral_inds, check_dihedrals=False, logger=None): prim_internals = eval_primitives(new_coords3d, primitives) new_internals = [prim_int.val for prim_int in prim_internals] internal_diffs = np.array(new_internals) - old_internals dihedrals = [prim_internals[i] for i in dihedral_inds] dihedral_num = len(dihedrals) dihedral_diffs = internal_diffs[-dihedral_num:] # Find differences that are shifted by 2*pi shifted_by_2pi = np.abs(np.abs(dihedral_diffs) - 2 * np.pi) < np.pi / 2 new_dihedrals = np.array([dihed.val for dihed in dihedrals]) if any(shifted_by_2pi): new_dihedrals[shifted_by_2pi] -= ( 2 * np.pi * np.sign(dihedral_diffs[shifted_by_2pi]) ) # Update values for dihed, new_val in zip(dihedrals, new_dihedrals): dihed.val = new_val if check_dihedrals and ( not dihedrals_are_valid(new_coords3d, [prim_int.inds for prim_int in dihedrals]) ): log(logger, "Dihedral(s) became invalid! Need new internal coordinates!") raise NeedNewInternalsException(new_coords3d) return prim_internals
def do_line_search(e0, e1, g0, g1, prev_step, maximize, logger=None): poly_fit_kwargs = { "e0": e0, "e1": e1, "g0": g0, "g1": g1, "maximize": maximize, } if not maximize: poly_fit_kwargs.update({ "g0": prev_step.dot(g0), "g1": prev_step.dot(g1), }) prefix = "Max" if maximize else "Min" fit_result = poly_fit.quartic_fit(**poly_fit_kwargs) fit_energy = None fit_grad = None fit_step = None if fit_result and (0.0 < fit_result.x <= 2.0): x = fit_result.x log(logger, f"{prefix}-subpsace interpolation succeeded: x={x:.6f}") fit_energy = fit_result.y fit_step = (1 - x) * -prev_step fit_grad = (1 - x) * g0 + x * g1 return fit_energy, fit_grad, fit_step
def connect_fragments(cdm, fragments, max_aux=3.78, aux_factor=1.3, logger=None): """Determine the smallest interfragment bond for a list of fragments and a condensed distance matrix.""" if len(fragments) > 1: log( logger, f"Detected {len(fragments)} fragments. Generating interfragment bonds.", ) dist_mat = squareform(cdm) interfrag_inds = list() aux_interfrag_inds = list() for frag1, frag2 in it.combinations(fragments, 2): log(logger, f"\tConnecting {len(frag1)} atom and {len(frag2)} atom fragment") inds = [(i1, i2) for i1, i2 in it.product(frag1, frag2)] distances = np.array([dist_mat[ind] for ind in inds]) # Determine minimum distance bond min_ind = distances.argmin() min_dist = distances[min_ind] interfrag_bond = tuple(inds[min_ind]) interfrag_inds.append(interfrag_bond) log(logger, f"\tMinimum distance bond: {interfrag_bond}, {min_dist:.4f} au") # Determine auxiliary interfragment bonds that are either below max_aux # (default 2 Å, ≈ 3.78 au), or less than aux_factor (default 1.3) times the # minimum interfragment distance. below_max_aux = [ ind for ind in inds if (dist_mat[ind] < max_aux) and (ind != interfrag_bond) ] if below_max_aux: log( logger, f"\tAux. interfrag bonds below {max_aux*BOHR2ANG:.2f} Å:\n" + "\n".join( [f"\t\t{ind}: {dist_mat[ind]:.4f} au" for ind in below_max_aux] ), ) scaled_min_dist = aux_factor * min_dist above_min_dist = [ ind for ind in inds if (dist_mat[ind] < scaled_min_dist) and (ind != interfrag_bond) and (ind not in below_max_aux) ] if above_min_dist: log( logger, f"\tAux. interfrag bonds below {aux_factor:.2f} * min_dist:\n" + "\n".join( [f"\t\t{ind}: {dist_mat[ind]:.4f} au" for ind in above_min_dist] ), ) aux_interfrag_inds.extend(below_max_aux) aux_interfrag_inds.extend(above_min_dist) # Or as Philipp proposed: two loops over the fragments and only # generate interfragment distances. So we get a full matrix with # the original indices but only the required distances. return interfrag_inds, aux_interfrag_inds
def get_primitives(coords3d, typed_prims, logger=None): primitives = list() for type_, *indices in typed_prims: cls = PrimMap[type_] primitives.append(cls(indices=indices)) msg = ("Defined primitives\n" + "\n".join([ f"\t{i:03d}: {str(p.indices): >14}" for i, p in enumerate(primitives) ]) + "\n") log(logger, msg) return primitives
def get_bend_inds(coords3d, bond_inds, min_deg, max_deg, logger=None): bond_sets = {frozenset(bi) for bi in bond_inds} bend_inds = list() for bond_set1, bond_set2 in it.combinations(bond_sets, 2): union = bond_set1 | bond_set2 if len(union) == 3: indices, _ = sort_by_central(bond_set1, bond_set2) if not bend_valid(coords3d, indices, min_deg, max_deg): log(logger, f"Bend {indices} is not valid!") continue bend_inds.append(indices) return bend_inds
def get_lindh_precon( atoms, coords, bonds=None, bends=None, dihedrals=None, c_stab=0.0103, logger=None, ): """c_stab = 0.00103 hartree/bohr² corresponds to 0.1 eV/Ų as given in the paper.""" if bonds is None: bonds = list() if bends is None: bends = list() if dihedrals is None: dihedrals = list() dim = coords.size c3d = coords.reshape(-1, 3) # Calculate Lindh force constants ks = get_lindh_k(atoms, c3d, bonds, bends) grad_funcs = { 2: dq_b, # Bond 3: dq_a, # Bend 4: dq_d, # Dihedral } P = dok_matrix((dim, dim)) for inds, k in zip(it.chain(bonds, bends, dihedrals), ks): # First derivatives of internal coordinates w.r.t cartesian coordinates int_grad = grad_funcs[len(inds)](*c3d[inds].flatten()) # Assign to the correct cartesian indices cart_inds = np.array( list(it.chain(*[range(3 * i, 3 * i + 3) for i in inds]))) P[cart_inds[:, None], cart_inds[None, :]] += abs(k) * np.outer(int_grad, int_grad) # Add stabilization to diagonal P[np.diag_indices(dim)] += c_stab P = P.tocsc() filled = P.size / dim**2 log(logger, f"Preconditioner P has {P.size} entries ({filled:.2%} filled)") return P
def update_internals( new_coords3d, old_internals, primitives, dihedral_inds, check_dihedrals=False, logger=None, ): prim_internals = eval_primitives(new_coords3d, primitives) new_internals = [prim_int.val for prim_int in prim_internals] internal_diffs = np.array(new_internals) - old_internals dihedrals = [prim_internals[i] for i in dihedral_inds] dihedral_num = len(dihedrals) dihedral_diffs = internal_diffs[-dihedral_num:] # Find differences that are shifted by 2*pi shifted_by_2pi = np.abs(np.abs(dihedral_diffs) - 2 * np.pi) < np.pi / 2 new_dihedrals = np.array([dihed.val for dihed in dihedrals]) if any(shifted_by_2pi): new_dihedrals[shifted_by_2pi] -= ( 2 * np.pi * np.sign(dihedral_diffs[shifted_by_2pi])) # Update values for dihed, new_val in zip(dihedrals, new_dihedrals): dihed.val = new_val # See if dihedrals became invalid (collinear atoms) if check_dihedrals: are_valid = [ dihedral_valid(new_coords3d, prim.inds) for prim in dihedrals ] try: first_dihedral = dihedral_inds[0] except IndexError: first_dihedral = 0 invalid_inds = [ i + first_dihedral for i, is_valid in enumerate(are_valid) if not is_valid ] if len(invalid_inds) > 0: invalid_prims = [primitives[i] for i in invalid_inds] log(logger, "Dihedral(s) became invalid! Need new internal coordinates!") raise NeedNewInternalsException(new_coords3d, invalid_inds=invalid_inds, invalid_prims=invalid_prims) return prim_internals
def find_bonds_bends_dihedrals(geom, bond_factor=1.3, min_deg=15, max_deg=175): log(logger, f"Detecting bonds, bends and dihedrals for {len(geom.atoms)} atoms.") bonds, bends = find_bonds_bends(geom, bond_factor=bond_factor, min_deg=min_deg, max_deg=max_deg) proper_diheds, improper_diheds = get_dihedral_inds(geom.coords3d, bonds, bends, max_deg=max_deg) log( logger, f"Found {len(proper_diheds)} proper and improper " f"{len(improper_diheds)} dihedrals.", ) return bonds, bends, proper_diheds + improper_diheds
def afir_closure(fragment_indices, cov_radii, gamma, rho=1, p=6, prefactor=1.0, logger=None): """rho=1 pushes fragments together, rho=-1 pulls fragments apart.""" # See https://onlinelibrary.wiley.com/doi/full/10.1002/qua.24757 # Eq. (9) for extension to 3 fragments. assert len(fragment_indices) == 2 inds = np.array(list(it.product(*fragment_indices))) cov_rad_sums = cov_radii[inds].sum(axis=1) # 3.8164 Angstrom in Bohr R0 = 7.21195 # 1.0061 kJ/mol to Hartree epsilon = 0.000383203368 # Avoid division by zero for gamma = 0. if gamma == 0.0: alpha = 0.0 else: alpha = gamma / ((2**(-1 / 6) - (1 + (1 + gamma / epsilon)**0.5)**(-1 / 6)) * R0) rho_verbose = {1: ("pushing", "together"), -1: ("pulling", "apart")} w1, w2 = rho_verbose[rho] log( logger, f"Creating AFIR closure with α={alpha:.6f}, prefactor {prefactor:.6f}, " f"rho={rho}, {w1} framgents {w2}", ) def afir_func(coords3d): diffs = anp.diff(coords3d[inds], axis=1).reshape(-1, 3) rs = anp.linalg.norm(diffs, axis=1) omegas = (cov_rad_sums / rs)**p f = prefactor * alpha * rho * (omegas * rs).sum() / omegas.sum() return f return afir_func
def find_bonds_bends(geom, bond_factor=1.3, min_deg=15, max_deg=175): log(logger, "Starting detection of bonds and bends.") bonds = find_bonds(geom, bond_factor=bond_factor) log(logger, f"Found {len(bonds)} bonds.") bends = find_bends(geom.coords3d, bonds, min_deg=min_deg, max_deg=max_deg) log(logger, f"Found {len(bends)} bends.") return bonds, bends
def precon_getter(geom, c_stab=0.0103, kind="full", logger=None): valid_kinds = ("full", "full_fast", "bonds", "bonds_bends") assert kind in valid_kinds, f"Invalid kind='{kind}'! Valid kinds are: {valid_kinds}" atoms = geom.atoms # Default empty lists for coordinates that may be skipped # for kind != "full". bends = list() dihedrals = list() if kind == "full": internal = RedundantCoords(atoms, geom.cart_coords) bonds = internal.bond_indices bends = internal.bending_indices dihedrals = internal.dihedrals elif kind == "full_fast": bonds, bends, dihedrals = find_bonds_bends_dihedrals(geom) elif kind == "bonds_bends": bonds, bends = find_bonds_bends(geom) elif kind == "bonds": bonds = find_bonds(geom) msg = ( f"Constructing preconditioner from {len(bonds)} bonds, {len(bends)} bends " f"and {len(dihedrals)} dihedrals (kind='{kind}').") log(logger, msg) def wrapper(coords): P = get_lindh_precon( atoms, coords, bonds, bends, dihedrals, c_stab=c_stab, logger=logger, ) return P return wrapper
def check_typed_prims( coords3d, typed_prims, bend_min_deg, dihed_max_deg, lb_min_deg, logger=None, check_bends=True, ): if check_bends: bend_func = lambda indices: bend_still_valid( coords3d, indices, min_deg=bend_min_deg, max_deg=lb_min_deg ) else: bend_func = lambda indices: True funcs = { PrimTypes.BEND: bend_func, PrimTypes.PROPER_DIHEDRAL: lambda indices: dihedral_valid( coords3d, indices, deg_thresh=dihed_max_deg, ), PrimTypes.IMPROPER_DIHEDRAL: lambda indices: dihedral_valid( coords3d, indices, deg_thresh=dihed_max_deg, ), } valid_typed_prims = list() for i, (type_, *indices) in enumerate(typed_prims): try: valid = funcs[type_](indices) except KeyError: valid = True if valid: valid_typed_prims.append((type_, *indices)) else: log(logger, f"Primitive {(type_, *indices)} is invalid!") return valid_typed_prims
def transform_int_step( int_step, old_cart_coords, cur_internals, B_prim, primitives, check_dihedrals=False, cart_rms_thresh=1e-6, rcond=1e-8, logger=None, ): """Transformation is done in primitive internals, so int_step must be given in primitive internals and not in DLC!""" new_cart_coords = old_cart_coords.copy() remaining_int_step = int_step target_internals = cur_internals + int_step Bt_inv_prim = np.linalg.pinv(B_prim.dot(B_prim.T), rcond=rcond).dot(B_prim) dihedral_inds = np.array( [i for i, primitive in enumerate(primitives) if isinstance(primitive, Torsion)] ) last_rms = 9999 old_internals = cur_internals backtransform_failed = True for i in range(25): cart_step = Bt_inv_prim.T.dot(remaining_int_step) cart_rms = np.sqrt(np.mean(cart_step ** 2)) # Update cartesian coordinates new_cart_coords += cart_step # Determine new internal coordinates new_prim_ints = update_internals( new_cart_coords.reshape(-1, 3), old_internals, primitives, dihedral_inds, check_dihedrals=check_dihedrals, logger=logger, ) new_internals = [prim.val for prim in new_prim_ints] remaining_int_step = target_internals - new_internals internal_rms = np.sqrt(np.mean(remaining_int_step ** 2)) log(logger, f"Cycle {i}: rms(Δcart)={cart_rms:1.4e}, rms(Δint.) = {internal_rms:1.5e}") # This assumes the first cart_rms won't be > 9999 ;) if cart_rms < last_rms: # Store results of the conversion cycle for laster use, if # the internal-cartesian-transformation goes bad. best_cycle = (new_cart_coords.copy(), new_internals.copy()) best_cycle_ind = i elif i != 0: # If the conversion somehow fails we fallback to the best previous step. log(logger, f"Backconversion failed! Falling back to step {best_cycle_ind}") new_cart_coords, new_internals = best_cycle break else: raise Exception( "Internal-cartesian back-transformation already " "failed in the first step. Aborting!" ) old_internals = new_internals last_rms = cart_rms if cart_rms < cart_rms_thresh: log(logger, f"Internal->Cartesian transformation converged in {i+1} cycle(s)!") backtransform_failed = False break # if check_dihedrals and ( # not dihedrals_are_valid(new_cart_coords.reshape(-1, 3), dihedral_inds) # ): # raise NeedNewInternalsException(new_cart_coords) log(logger, "") # Return the difference between the new cartesian coordinates that yield # the desired internal coordinates and the old cartesian coordinates. cart_step = new_cart_coords - old_cart_coords return new_prim_ints, cart_step, backtransform_failed
def md(geom, v0, steps, dt, remove_com_v=True, thermostat=None, T=298.15, timecon=100, term_funcs=None, constraints=None, constraint_kwargs=None, verbose=True): """Velocity verlet integrator. Parameters ---------- geom : Geometry The system for which the dynamics are to be run. v0 : np.array, floats Initial velocities in Bohr/fs. steps : float Number of simulation steps. dt : float Timestep in fs. remove_com_v : bool, default=True Remove center-of-mass velocity. thermostat : str, optional, default None Which and whether to use a thermostat. T : float, optional, default=None Desired temperature in thermostated runs. timecon : float Timeconsanst of the thermostat in fs. term_funcs : dict, optional Iterable of functions that are called with the atomic coordinates in every MD cycle and result in termination constraints : 2d iterable, optional 2D iterable containing atom indices describing constrained bond lengths. of the MD integration when they evaluate to true. constraint_kwargs : dict, optional Keyword arguments for the constraint algorithm. verbose : bool, default=True Do additional printing when True. """ assert geom.coord_type == "cart" if term_funcs is None: term_funcs = dict() if verbose: t_ps = steps * dt * 1e-3 # Total simulation time print( f"Doing {steps} steps of {dt:.4f} fs for a total of {t_ps:.2f} ps." ) energy_forces_getter = energy_forces_getter_closure(geom) if constraint_kwargs is None: constraint_kwargs = dict() if remove_com_v and (not thermostat): print( "Center of mass velocity removal requested, but thermostat is disabled. " "Disabling velocity removal.") remove_com_v = False # Fixed degrees of freedom fixed_dof = 0 if remove_com_v: fixed_dof += 3 constrained_md = constraints is not None # Get RATTLE function from closure for constrained MD if constrained_md: fixed_dof += len(constraints) rattle = rattle_closure(geom, constraints, dt, energy_forces_getter=energy_forces_getter, **constraint_kwargs) if thermostat is not None: thermo_func = THERMOSTATS[thermostat] tau_t = dt / timecon sigma = kinetic_energy_for_temperature(len(geom.atoms), T, fixed_dof=fixed_dof) # In amu masses = geom.masses masses_rep = geom.masses_rep total_mass = masses.sum() x = geom.cart_coords # v is given in Bohr/fs v = v0 a_prev = np.zeros_like(x) xs = list() Ts = list() E_tots = list() E_pot, forces = energy_forces_getter(geom.coords) t_cur = 0 terminate = False terminate_key = None T_avg = 0 log(logger, f"Running MD with Δt={dt:.2f} fs for {steps} steps.") for step in range(steps): xs.append(x.copy()) E_kin = kinetic_energy_from_velocities(masses, v.reshape(-1, 3)) T = temperature_for_kinetic_energy(len(masses), E_kin, fixed_dof=fixed_dof) T_avg += T Ts.append(T) E_tot = E_pot + E_kin E_tots.append(E_tot) status_msg = ( f"Step {step:05d} {t_cur*1e-3: >6.2f} ps E={E_tot: >8.6f} E_h " f"T={T: >8.2f} K <T>={T_avg/(step+1): >8.2f}") if (step % 25) == 0: log(logger, status_msg) if verbose: print(status_msg) if thermostat: E_kin_new = thermo_func(E_kin, sigma, v.size - fixed_dof, tau_t) scale = (E_kin_new / E_kin)**0.5 v *= scale # RATTLE algorithm if constrained_md: x, v, E_pot, forces = rattle(x, v, forces) # Simple Velocity-Verlet integration else: E_pot, forces = energy_forces_getter(geom.coords) # Acceleration, convert from Hartree / (Bohr * amu) to Bohr/fs² a = forces / masses_rep * FORCE2ACC v += .5 * (a + a_prev) * dt if remove_com_v: v -= v * masses_rep / total_mass # v*dt = Bohr/fs * fs -> Bohr # a*dt**2 = Bohr/fs² * fs² -> Bohr x += v * dt + .5 * a * dt**2 a_prev = a # Update coordinates geom.coords = x for name, func in term_funcs.items(): if func(x.reshape(-1, 3)): terminate = True terminate_key = name break if terminate: log(logger, f"Termination function '{name}' evaluted to True. Breaking.") break if check_for_stop_sign(): break # Advance time t_cur += dt log(logger, "") md_result = MDResult( coords=np.array(xs), t_ps=t_cur * 1e-3, step=step, terminated=terminate_key, T=np.array(Ts), E_tot=np.array(E_tots), ) return md_result
def log_dihed_skip(inds): log( logger, f"Skipping generation of dihedral {inds} " "as some of the the atoms are (close too) linear.", )
def get_dihedral_inds(coords3d, bond_inds, bend_inds, max_deg, logger=None): max_rad = np.deg2rad(max_deg) bond_dict = dict() for from_, to_ in bond_inds: bond_dict.setdefault(from_, list()).append(to_) bond_dict.setdefault(to_, list()).append(from_) proper_dihedral_inds = list() improper_candidates = list() improper_dihedral_inds = list() def log_dihed_skip(inds): log( logger, f"Skipping generation of dihedral {inds} " "as some of the the atoms are (close too) linear.", ) def set_dihedral_index(dihedral_ind, proper=True): dihed = tuple(dihedral_ind) check_in = proper_dihedral_inds if proper else improper_dihedral_inds # Check if this dihedral is already present if (dihed in check_in) or (dihed[::-1] in check_in): return # Assure that the angles are below 175° (3.054326 rad) if not dihedral_valid(coords3d, dihedral_ind, deg_thresh=max_deg): log_dihed_skip(dihedral_ind) return if proper: proper_dihedral_inds.append(dihed) else: improper_dihedral_inds.append(dihed) for bond, bend in it.product(bond_inds, bend_inds): # print("bond", bond, "bend", bend) central = bend[1] bend_set = set(bend) bond_set = set(bond) # Check if the two sets share one common atom. If not continue. intersect = bend_set & bond_set # print("intersect", intersect) if len(intersect) != 1: continue # if bond == frozenset((0, 11)) and bend == (0, 3, 4): # import pdb; pdb.set_trace() # pass # TODO: check collinearity of bond and bend. # When the common atom between bond and bend is a terminal, and not a central atom # in the bend we create a proper dihedral. Improper dihedrals are only created # when no proper dihedrals have been found. if central not in bond_set: # The new terminal atom in the dihedral is the one, that doesn' intersect. terminal = tuple(bond_set - intersect)[0] intersecting_atom = tuple(intersect)[0] bend_terminal = tuple(bend_set - {central} - intersect)[0] bend_rad = Bend._calculate(coords3d, bend) # Bend atoms are nearly collinear. Check if we can skip the central bend atom # and use an atom that is conneced to the terminal atom of the bend or bond. if bend_rad >= max_rad: bend_terminal_bonds = set(bond_dict[bend_terminal]) - bend_set bond_terminal_bonds = set(bond_dict[terminal]) - bond_set set_dihedrals = [ (terminal, intersecting_atom, bend_terminal, betb) for betb in bend_terminal_bonds ] + [(bend_terminal, intersecting_atom, terminal, botb) for botb in bond_terminal_bonds] # Hardcoded for now ... look ahead to next shell of atoms if not any([ dihedral_valid(coords3d, inds, deg_thresh=max_deg) for inds in set_dihedrals ]): set_dihedrals = [] for betb in bend_terminal_bonds: bend_terminal_bonds_v2 = set( bond_dict[betb]) - bend_set - bond_set set_dihedrals = [(terminal, intersecting_atom, betb, betb_v2) for betb_v2 in bend_terminal_bonds_v2] for botb in bond_terminal_bonds: bond_terminal_bonds_v2 = set( bond_dict[botb]) - bend_set - bond_set set_dihedrals = [(bend_terminal, intersecting_atom, botb, botb_v2) for botb_v2 in bond_terminal_bonds_v2] elif intersecting_atom == bend[0]: set_dihedrals = [[terminal] + list(bend)] else: set_dihedrals = [list(bend) + [terminal]] [set_dihedral_index(dihed) for dihed in set_dihedrals] # If the common atom is the central atom we try to form an out # of plane bend / improper torsion. They may be created later on. else: fourth_atom = list(bond_set - intersect) dihedral_ind = list(bend) + fourth_atom # This way dihedrals may be generated that contain linear # atoms and these would be undefinied. So we check for this. if dihedral_valid(coords3d, dihedral_ind, deg_thresh=max_deg): improper_candidates.append(dihedral_ind) else: log_dihed_skip(dihedral_ind) # Now try to create the remaining improper dihedrals. if (len(coords3d) >= 4) and (len(proper_dihedral_inds) == 0): log( logger, "Could not define any proper dihedrals! Generating improper dihedrals!", ) for improp in improper_candidates: set_dihedral_index(improp, proper=False) log( logger, "Permutational symmetry not considerd in generation of " "improper dihedrals.", ) return proper_dihedral_inds, improper_dihedral_inds
def setup_redundant( atoms, coords3d, factor=1.3, define_prims=None, min_deg=15, dihed_max_deg=175.0, lb_min_deg=None, lb_max_bonds=4, min_weight=None, logger=None, ): if define_prims is None: define_prims = list() log(logger, f"Detecting primitive internals for {len(atoms)} atoms.") def keep_coord(prim_cls, prim_inds): return (True if (min_weight is None) else ( prim_cls._weight(atoms, coords3d, prim_inds, 0.12) >= min_weight)) def keep_coords(prims, prim_cls): return [prim for prim in prims if keep_coord(prim_cls, prim)] # Bonds bonds, cdm, cbm = get_bond_sets( atoms, coords3d, bond_factor=factor, return_cdm=True, return_cbm=True, ) bonds = [tuple(bond) for bond in bonds] bonds = keep_coords(bonds, Stretch) # Fragments fragments = merge_sets(bonds) # Check for unbonded single atoms and create fragments for them. bonded_set = set(tuple(np.ravel(bonds))) unbonded_set = set(range(len(atoms))) - bonded_set fragments.extend([frozenset((atom, )) for atom in unbonded_set]) # Check for disconnected fragments. If they are present, create interfragment # bonds between them. interfrag_bonds, aux_interfrag_bonds = connect_fragments(cdm, fragments, logger=logger) # Hydrogen bonds hydrogen_bonds = get_hydrogen_bond_inds(atoms, coords3d, bonds, logger=logger) hydrogen_set = [frozenset(bond) for bond in hydrogen_bonds] interfrag_bonds = [ bond for bond in interfrag_bonds if set(bond) not in hydrogen_set ] aux_interfrag_bonds = [ bond for bond in aux_interfrag_bonds if set(bond) not in hydrogen_set ] bonds = [bond for bond in bonds if set(bond) not in hydrogen_set] aux_bonds = list() # Don't use auxilary interfragment bonds for bend detection bonds_for_bends = set( [frozenset(bond) for bond in bonds + hydrogen_bonds + interfrag_bonds]) # Bends bends = get_bend_inds( coords3d, bonds_for_bends, min_deg=min_deg, max_deg=180.0, logger=logger, ) # All bends will be checked, for being linear bends and will be removed from # bend_inds, if needed. bends = keep_coords(bends, Bend) # Linear Bends and orthogonal complements linear_bends, linear_bend_complements = get_linear_bend_inds( coords3d, cbm, bends, min_deg=lb_min_deg, max_bonds=lb_max_bonds, logger=logger, ) # Remove linear bends from bends bends = [bend for bend in bends if bend not in linear_bends] linear_bends = keep_coords(linear_bends, LinearBend) linear_bend_complements = keep_coords(linear_bend_complements, LinearBend) # Dihedrals bends_for_dihedrals = bends + linear_bends proper_dihedrals, improper_dihedrals = get_dihedral_inds( # coords3d, bonds_for_bends, bends, max_deg=dihed_max_deg, logger=logger coords3d, bonds_for_bends, bends_for_dihedrals, max_deg=dihed_max_deg, logger=logger) proper_dihedrals = keep_coords(proper_dihedrals, Torsion) improper_dihedrals = keep_coords(improper_dihedrals, Torsion) # Additional primitives to be defined. define_map = { PrimTypes.BOND: "bonds", PrimTypes.AUX_BOND: "aux_bonds", PrimTypes.HYDROGEN_BOND: "hydrogen_bonds", PrimTypes.INTERFRAG_BOND: "interfrag_bonds", PrimTypes.AUX_INTERFRAG_BOND: "aux_interfrag_bonds", PrimTypes.BEND: "bends", PrimTypes.LINEAR_BEND: "linear_bends", PrimTypes.LINEAR_BEND_COMPLEMENT: "linear_bend_complements", PrimTypes.PROPER_DIHEDRAL: "proper_dihedrals", PrimTypes.IMPROPER_DIHEDRAL: "improper_dihedrals", } for type_, *indices in define_prims: key = define_map[type_] locals()[key].append(tuple(indices)) pt = PrimTypes typed_prims = ( # Bonds, two indices [(pt.BOND, *bond) for bond in bonds] + [(pt.AUX_BOND, *abond) for abond in aux_bonds] + [(pt.HYDROGEN_BOND, *hbond) for hbond in hydrogen_bonds] + [(pt.INTERFRAG_BOND, *ifbond) for ifbond in interfrag_bonds] + [(pt.AUX_INTERFRAG_BOND, *aifbond) for aifbond in aux_interfrag_bonds] # Bends, three indices + [(pt.BEND, *bend) for bend in bends] + [(pt.LINEAR_BEND, *lbend) for lbend in linear_bends] + [(pt.LINEAR_BEND_COMPLEMENT, *lbendc) for lbendc in linear_bend_complements] # Dihedral, four indices + [(pt.PROPER_DIHEDRAL, *pdihedral) for pdihedral in proper_dihedrals] + [(pt.IMPROPER_DIHEDRAL, *idihedral) for idihedral in improper_dihedrals]) coord_info = CoordInfo( bonds=bonds, hydrogen_bonds=hydrogen_bonds, interfrag_bonds=interfrag_bonds, aux_interfrag_bonds=aux_interfrag_bonds, bends=bends, linear_bends=linear_bends, linear_bend_complements=linear_bend_complements, proper_dihedrals=proper_dihedrals, improper_dihedrals=improper_dihedrals, typed_prims=typed_prims, fragments=fragments, ) return coord_info