def is_stereo_compatible(tra, sgr1, sgr2): """ is this transformation compatible with the reactant/product stereo assignments? """ cgr1 = without_stereo_parities(sgr1) cgr2 = without_stereo_parities(sgr2) atm_key_dct = full_isomorphism(apply(tra, cgr1), cgr2) # determine the stereo centers which are preserved in the transformation sgr1 = _relabel(sgr1, atm_key_dct) atm_keys = sorted(atom_stereo_keys(sgr1) & atom_stereo_keys(sgr2)) bnd_keys = sorted(bond_stereo_keys(sgr1) & bond_stereo_keys(sgr2)) atm_pars1 = dict_.values_by_key(atom_stereo_parities(sgr1), atm_keys) atm_pars2 = dict_.values_by_key(atom_stereo_parities(sgr2), atm_keys) bnd_pars1 = dict_.values_by_key(bond_stereo_parities(sgr1), bnd_keys) bnd_pars2 = dict_.values_by_key(bond_stereo_parities(sgr2), bnd_keys) atm_ngb_keys_dct1 = atom_neighbor_keys(sgr1) atm_ngb_keys_dct2 = atom_neighbor_keys(sgr2) ret = True for atm_key, par1, par2 in zip(atm_keys, atm_pars1, atm_pars2): atm_ngb_keys1 = stereo_sorted_atom_neighbor_keys( sgr1, atm_key, atm_ngb_keys_dct1[atm_key]) atm_ngb_keys2 = stereo_sorted_atom_neighbor_keys( sgr2, atm_key, atm_ngb_keys_dct2[atm_key]) if _permutation_parity(atm_ngb_keys1, atm_ngb_keys2): ret &= (par1 == par2) else: ret &= (par1 != par2) for bnd_key, par1, par2 in zip(bnd_keys, bnd_pars1, bnd_pars2): atm1_key, atm2_key = bnd_key atm1_ngb_key1 = stereo_sorted_atom_neighbor_keys( sgr1, atm1_key, atm_ngb_keys_dct1[atm1_key] - {atm2_key})[0] atm2_ngb_key1 = stereo_sorted_atom_neighbor_keys( sgr1, atm2_key, atm_ngb_keys_dct1[atm2_key] - {atm1_key})[0] atm1_ngb_key2 = stereo_sorted_atom_neighbor_keys( sgr2, atm1_key, atm_ngb_keys_dct2[atm1_key] - {atm2_key})[0] atm2_ngb_key2 = stereo_sorted_atom_neighbor_keys( sgr2, atm2_key, atm_ngb_keys_dct2[atm2_key] - {atm1_key})[0] if not ((atm1_ngb_key1 != atm1_ngb_key2) ^ (atm2_ngb_key1 != atm2_ngb_key2)): ret &= (par1 == par2) else: ret &= (par1 != par2) return ret
def _bond_stereo_parity_from_geometry(gra, bnd_key, geo, geo_idx_dct): """ get the current stereo parity of a bond from its geometry """ atm1_key, atm2_key = bnd_key atm_ngb_keys_dct = atom_neighbor_keys(gra) atm1_ngb_keys = atm_ngb_keys_dct[atm1_key] - {atm2_key} atm2_ngb_keys = atm_ngb_keys_dct[atm2_key] - {atm1_key} atm1_ngb_keys = stereo_sorted_atom_neighbor_keys(gra, atm1_key, atm1_ngb_keys) atm2_ngb_keys = stereo_sorted_atom_neighbor_keys(gra, atm2_key, atm2_ngb_keys) # get the top priority neighbor keys on each side atm1_ngb_key = atm1_ngb_keys[0] atm2_ngb_key = atm2_ngb_keys[0] # determine the parity based on the coordinates xyzs = automol.geom.coordinates(geo) atm1_xyz = xyzs[geo_idx_dct[atm1_key]] atm2_xyz = xyzs[geo_idx_dct[atm2_key]] atm1_ngb_xyz = xyzs[geo_idx_dct[atm1_ngb_key]] atm2_ngb_xyz = xyzs[geo_idx_dct[atm2_ngb_key]] atm1_bnd_vec = numpy.subtract(atm1_ngb_xyz, atm1_xyz) atm2_bnd_vec = numpy.subtract(atm2_ngb_xyz, atm2_xyz) dot_val = numpy.vdot(atm1_bnd_vec, atm2_bnd_vec) assert dot_val != 0. # for now, assume no collinear par = dot_val > 0. return par
def two_bond_idxs(gra, asymb1='H', cent='C', asymb2='H'): """ Determine triplet of idxs for describing bond idxs = (asymb1_idx, cent_idx, asymb2_idx) """ grps = tuple() neigh_dct = atom_neighbor_keys(gra) idx_symb_dct = atom_symbols(gra) symb_idx_dct = atom_symbol_idxs(gra) cent_idxs = symb_idx_dct.get(cent, tuple()) for cent_idx in cent_idxs: neighs = tuple(neigh_dct[cent_idx]) neigh_symbs = atom_idx_to_symb(neighs, idx_symb_dct) if neigh_symbs == (asymb1, asymb2): grp_idxs = (neighs[0], cent_idx, neighs[1]) elif neigh_symbs == (asymb2, asymb1): grp_idxs = (neighs[1], cent_idx, neighs[0]) else: grp_idxs = () if grp_idxs: grps += ((grp_idxs),) return grps
def isomorphic_radical_graphs(gra): """ Generate a set of graphs that are isomorphic to a graph of a radical species """ # Determine useful keys symbols = atom_symbols(gra) unsat_keys = unsaturated_atom_keys(gra) unsat_key = next(iter(unsat_keys)) h_atm_key = max(symbols.keys()) + 1 iso_gras = [] for aidx, symbol in enumerate(symbols.values()): # Loop over saturated (non-radical) heavy atoms if symbol != 'H' and aidx != unsat_key: # Add hydrogen atom to radical atom new_graph = add_atom_explicit_hydrogen_keys( gra, {unsat_key: [h_atm_key]}) # Remove hydrogen from saturated atom neighbors = atom_neighbor_keys(new_graph) for neigh in neighbors[aidx]: if symbols[neigh] == 'H': aneighbor = neigh break new_graph = remove_atoms(new_graph, [aneighbor]) # Test to see if new radical species is the same as the original inv_atm_key_dct = full_isomorphism(gra, new_graph) if inv_atm_key_dct: iso_gras.append(new_graph) return iso_gras
def _dihedral_increment(sgr, atm1_key, atm2_key, atm3_key, check=False): """ predict dihedral increment for atoms attached to `atm3_key` """ if check: atm_ngb_keys_dct = atom_neighbor_keys(sgr) atm2_ngb_keys = atm_ngb_keys_dct[atm2_key] atm3_ngb_keys = atm_ngb_keys_dct[atm3_key] assert atm2_key in atm3_ngb_keys assert atm1_key in atm2_ngb_keys - {atm3_key} atm_hyb_dct = resonance_dominant_atom_hybridizations(sgr) atm3_hyb = atm_hyb_dct[atm3_key] dih_incr = 2 * numpy.pi / atm3_hyb if not atm3_hyb == 0 else 0. return dih_incr
def neighbors_of_type(gra, aidx, asymb='H'): """ Get the neighbor indices for a certain type """ idx_symb_dct = atom_symbols(gra) neighs = atom_neighbor_keys(gra)[aidx] neigh_symbs = atom_idx_to_symb(neighs, idx_symb_dct) idxs_of_type = tuple() for nidx, nsymb in zip(neighs, neigh_symbs): if nsymb == asymb: idxs_of_type += (nidx,) return idxs_of_type
def stereo_priority_vector(gra, atm_key, atm_ngb_key): """ generates a sortable one-to-one representation of the branch extending from `atm_key` through its bonded neighbor `atm_ngb_key` """ bbn_keys = backbone_keys(gra) exp_hyd_keys = explicit_hydrogen_keys(gra) if atm_ngb_key not in bbn_keys: assert atm_ngb_key in exp_hyd_keys assert frozenset({atm_key, atm_ngb_key}) in bonds(gra) pri_vec = () else: gra = implicit(gra) atm_dct = atoms(gra) bnd_dct = bonds(gra) assert atm_key in bbn_keys assert frozenset({atm_key, atm_ngb_key}) in bnd_dct # here, switch to an implicit graph atm_ngb_keys_dct = atom_neighbor_keys(gra) def _priority_vector(atm1_key, atm2_key, seen_keys): # we keep a list of seen keys to cut off cycles, avoiding infinite # loops bnd_val = bnd_dct[frozenset({atm1_key, atm2_key})] atm_val = atm_dct[atm2_key] bnd_val = _replace_nones_with_negative_infinity(bnd_val) atm_val = _replace_nones_with_negative_infinity(atm_val) if atm2_key in seen_keys: ret = (bnd_val,) else: seen_keys.update({atm1_key, atm2_key}) atm3_keys = atm_ngb_keys_dct[atm2_key] - {atm1_key} if atm3_keys: next_vals, seen_keys = zip(*[ _priority_vector(atm2_key, atm3_key, seen_keys) for atm3_key in atm3_keys]) ret = (bnd_val, atm_val) + next_vals else: ret = (bnd_val, atm_val) return ret, seen_keys pri_vec, _ = _priority_vector(atm_key, atm_ngb_key, set()) return pri_vec
def _atom_stereo_corrected_geometry(gra, atm_ste_par_dct, geo, geo_idx_dct): """ correct the atom stereo parities of a geometry, for a subset of atoms """ ring_atm_keys = set(itertools.chain(*rings_atom_keys(gra))) atm_ngb_keys_dct = atom_neighbor_keys(gra) atm_keys = list(atm_ste_par_dct.keys()) for atm_key in atm_keys: par = atm_ste_par_dct[atm_key] curr_par = _atom_stereo_parity_from_geometry(gra, atm_key, geo, geo_idx_dct) if curr_par != par: atm_ngb_keys = atm_ngb_keys_dct[atm_key] # for now, we simply exclude rings from the pivot keys # (will not work for stereo atom at the intersection of two rings) atm_piv_keys = list(atm_ngb_keys - ring_atm_keys)[:2] assert len(atm_piv_keys) == 2 atm3_key, atm4_key = atm_piv_keys # get coordinates xyzs = automol.geom.coordinates(geo) atm_xyz = xyzs[geo_idx_dct[atm_key]] atm3_xyz = xyzs[geo_idx_dct[atm3_key]] atm4_xyz = xyzs[geo_idx_dct[atm4_key]] # do the rotation rot_axis = cart.vec.unit_bisector(atm3_xyz, atm4_xyz, orig_xyz=atm_xyz) rot_atm_keys = ( atom_keys(branch(gra, atm_key, {atm_key, atm3_key})) | atom_keys(branch(gra, atm_key, {atm_key, atm4_key}))) rot_idxs = list(map(geo_idx_dct.__getitem__, rot_atm_keys)) geo = automol.geom.rotate(geo, rot_axis, numpy.pi, orig_xyz=atm_xyz, idxs=rot_idxs) assert _atom_stereo_parity_from_geometry(gra, atm_key, geo, geo_idx_dct) == par gra = set_atom_stereo_parities(gra, {atm_key: par}) return geo, gra
def _bond_distance(gra, atm1_key, atm2_key, check=True): """ predicted bond distance (currently crude, but could easily be made more sophisticated """ atm_sym_dct = atom_symbols(gra) if check: assert atm2_key in atom_neighbor_keys(gra)[atm1_key] atm1_sym = atm_sym_dct[atm1_key] atm2_sym = atm_sym_dct[atm2_key] if atm1_sym == 'H' or atm2_sym == 'H': dist = 1.1 * qcc.conversion_factor('angstrom', 'bohr') else: dist = 1.5 * qcc.conversion_factor('angstrom', 'bohr') return dist
def _atom_stereo_parity_from_geometry(gra, atm_key, geo, geo_idx_dct): """ get the current stereo parity of an atom from its geometry """ atm_ngb_keys_dct = atom_neighbor_keys(gra) atm_ngb_keys = atm_ngb_keys_dct[atm_key] # sort the neighbor keys by stereo priority atm_ngb_keys = stereo_sorted_atom_neighbor_keys(gra, atm_key, atm_ngb_keys) # determine the parity based on the coordinates xyzs = automol.geom.coordinates(geo) atm_ngb_idxs = dict_.values_by_key(geo_idx_dct, atm_ngb_keys) atm_ngb_xyzs = [xyzs[idx] for idx in atm_ngb_idxs] det_mat = numpy.ones((4, 4)) det_mat[:, :3] = atm_ngb_xyzs det_val = numpy.linalg.det(det_mat) assert det_val != 0. # for now, assume no four-atom planes par = det_val > 0. return par
def stereogenic_bond_keys(gra): """ (unassigned) stereogenic bonds in this graph """ gra = without_bond_orders(gra) gra = explicit(gra) # for simplicity, add the explicit hydrogens back in bnd_keys = dict_.keys_by_value( resonance_dominant_bond_orders(gra), lambda x: 2 in x) # make sure both ends are sp^2 (excludes cumulenes) atm_hyb_dct = resonance_dominant_atom_hybridizations(gra) sp2_atm_keys = dict_.keys_by_value(atm_hyb_dct, lambda x: x == 2) bnd_keys = frozenset({bnd_key for bnd_key in bnd_keys if bnd_key <= sp2_atm_keys}) bnd_keys -= bond_stereo_keys(gra) bnd_keys -= functools.reduce( # remove double bonds in small rings frozenset.union, filter(lambda x: len(x) < 8, rings_bond_keys(gra)), frozenset()) atm_ngb_keys_dct = atom_neighbor_keys(gra) def _is_stereogenic(bnd_key): atm1_key, atm2_key = bnd_key def _is_symmetric_on_bond(atm_key, atm_ngb_key): atm_ngb_keys = list(atm_ngb_keys_dct[atm_key] - {atm_ngb_key}) if not atm_ngb_keys: # C=:O: ret = True elif len(atm_ngb_keys) == 1: # C=N:-X ret = False else: assert len(atm_ngb_keys) == 2 # C=C(-X)-Y ret = (stereo_priority_vector(gra, atm_key, atm_ngb_keys[0]) == stereo_priority_vector(gra, atm_key, atm_ngb_keys[1])) return ret return not (_is_symmetric_on_bond(atm1_key, atm2_key) or _is_symmetric_on_bond(atm2_key, atm1_key)) ste_gen_bnd_keys = frozenset(filter(_is_stereogenic, bnd_keys)) return ste_gen_bnd_keys
def stereogenic_atom_keys(gra): """ (unassigned) stereogenic atoms in this graph """ gra = without_bond_orders(gra) gra = explicit(gra) # for simplicity, add the explicit hydrogens back in atm_keys = dict_.keys_by_value(atom_bond_valences(gra), lambda x: x == 4) atm_keys -= atom_stereo_keys(gra) atm_ngb_keys_dct = atom_neighbor_keys(gra) def _is_stereogenic(atm_key): atm_ngb_keys = list(atm_ngb_keys_dct[atm_key]) pri_vecs = [stereo_priority_vector(gra, atm_key, atm_ngb_key) for atm_ngb_key in atm_ngb_keys] return not any(pv1 == pv2 for pv1, pv2 in itertools.combinations(pri_vecs, r=2)) ste_gen_atm_keys = frozenset(filter(_is_stereogenic, atm_keys)) return ste_gen_atm_keys
def prod_beta_scission(gra): """ products of beta scission """ prod_gras = tuple() rad_idxs = resonance_dominant_radical_atom_keys(gra) single_bonds = bonds_of_order(gra, mbond=1) for rad_idx in rad_idxs: rad_neighs = atom_neighbor_keys(gra)[rad_idx] for single_bond in single_bonds: bond = frozenset(single_bond) if rad_neighs & bond and rad_idx not in bond: gra2 = remove_bonds(gra, [bond]) disconn_gras = automol.graph.connected_components(gra2) prod_gras += (disconn_gras, ) return _unique_gras(prod_gras)
def _bond_angle(sgr, atm1_key, atm2_key, atm3_key, check=True): """ predict the bond angles an atom makes with its neighbors """ if check: atm_ngb_keys_dct = atom_neighbor_keys(sgr) atm2_ngb_keys = atm_ngb_keys_dct[atm2_key] assert {atm1_key, atm3_key} <= atm2_ngb_keys atm_hyb_dct = resonance_dominant_atom_hybridizations(sgr) atm2_hyb = atm_hyb_dct[atm2_key] if atm2_hyb == 3: ang_deg = 109.5 elif atm2_hyb == 2: ang_deg = 120.0 else: assert atm2_hyb == 1 ang_deg = 180.0 ang = ang_deg * qcc.conversion_factor('degree', 'radian') return ang
def _complete_zmatrix_for_branch(gra, atm1_key, atm2_key, atm3_key, zma, zma_key_dct): """ core function for generating geometries; starting from three neighboring atoms at the end of the z-matrix, fills out the z-matrix for the rest of the branch extending out from the third atom returns a z-matrix and a dictionary mapping atom keys to rows of the z-matrix """ atm_sym_dct = atom_symbols(gra) atm_ngb_keys_dct = atom_neighbor_keys(gra) atm1_zma_key = zma_key_dct[atm1_key] atm2_zma_key = zma_key_dct[atm2_key] atm3_zma_key = zma_key_dct[atm3_key] atm4_keys = atm_ngb_keys_dct[atm3_key] - {atm2_key} # first handle the linear bond case, inserting a dummy atom atm_hyb = resonance_dominant_atom_hybridizations(gra)[atm3_key] if atm_hyb == 1 and atm4_keys: assert len(atm4_keys) == 1 atm4_key, = atm4_keys # first, insert the dummy atom dist4_name = automol.zmatrix.new_distance_name(zma) dist4_val = 1. ang4_name = automol.zmatrix.new_central_angle_name(zma) ang4_val = RIT_ANG dih4_name = automol.zmatrix.new_dihedral_angle_name(zma) dih4_val = 0. gra, dummy_atm_key = add_bonded_atom(gra, 'X', atm3_key, bnd_ord=0) dummy_atm_zma_key = automol.zmatrix.count(zma) zma_key_dct[dummy_atm_key] = dummy_atm_zma_key zma = automol.zmatrix.append( zma, 'X', [atm3_zma_key, atm2_zma_key, atm1_zma_key], [dist4_name, ang4_name, dih4_name], [dist4_val, ang4_val, dih4_val]) # shift the keys to include the dummy atom atm1_key, atm2_key = atm2_key, dummy_atm_key atm1_zma_key, atm2_zma_key = atm2_zma_key, dummy_atm_zma_key atm4_sym = atm_sym_dct[atm4_key] dist4_name = automol.zmatrix.new_distance_name(zma) dist4_val = _bond_distance(gra, atm3_key, atm4_key) ang4_name = automol.zmatrix.new_central_angle_name(zma) ang4_val = RIT_ANG dih4_name = automol.zmatrix.new_dihedral_angle_name(zma) dih4_val = LIN_ANG zma_key_dct[atm4_key] = automol.zmatrix.count(zma) zma = automol.zmatrix.append( zma, atm4_sym, [atm3_zma_key, atm2_zma_key, atm1_zma_key], [dist4_name, ang4_name, dih4_name], [dist4_val, ang4_val, dih4_val]) geo = automol.zmatrix.geometry(zma) # recursion 1 zma, zma_key_dct, gra = _complete_zmatrix_for_branch( gra, atm2_key, atm3_key, atm4_key, zma, zma_key_dct) # from here on, we can assume a non-linear bond else: dih_incr = _dihedral_increment(gra, atm1_key, atm2_key, atm3_key) fixed_atm4_keys = set(atm4_keys) & set(zma_key_dct) if fixed_atm4_keys: geo = automol.zmatrix.geometry(zma) atm4_zma_keys = list(map(zma_key_dct.__getitem__, fixed_atm4_keys)) dih4_vals = [ automol.geom.dihedral_angle(geo, atm1_zma_key, atm2_zma_key, atm3_zma_key, atm4_zma_key) for atm4_zma_key in atm4_zma_keys ] dih4_val = max(dih4_vals) else: dih4_val = numpy.pi - dih_incr # subtract off the fixed atom 4 keys in case we're dealing with a ring for atm4_key in atm4_keys - fixed_atm4_keys: atm4_sym = atm_sym_dct[atm4_key] dist4_name = automol.zmatrix.new_distance_name(zma) dist4_val = _bond_distance(gra, atm3_key, atm4_key) ang4_name = automol.zmatrix.new_central_angle_name(zma) ang4_val = _bond_angle(gra, atm2_key, atm3_key, atm4_key) dih4_name = automol.zmatrix.new_dihedral_angle_name(zma) dih4_val += dih_incr dih4_val = numpy.mod(dih4_val, 2 * numpy.pi) zma_key_dct[atm4_key] = automol.zmatrix.count(zma) zma = automol.zmatrix.append( zma, atm4_sym, [atm3_zma_key, atm2_zma_key, atm1_zma_key], [dist4_name, ang4_name, dih4_name], [dist4_val, ang4_val, dih4_val]) # recursion 2 zma, zma_key_dct, gra = _complete_zmatrix_for_branch( gra, atm2_key, atm3_key, atm4_key, zma, zma_key_dct) gra_with_dummies = gra return zma, zma_key_dct, gra_with_dummies
def _start_zmatrix_from_atom(gra, atm_key): """ generates a z-matrix for a single atom and its neighbors returns a z-matrix and a dictionary mapping atom keys to rows of the z-matrix """ atm_ngb_keys_dct = atom_neighbor_keys(gra) dummy_atm_key = None atm_sym_dct = atom_symbols(gra) # sort hydrogens to be first atm_ngb_keys = sorted(atm_ngb_keys_dct[atm_key]) atm_ngb_syms = list(map(atm_sym_dct.__getitem__, atm_ngb_keys)) srt = formula.argsort_symbols(atm_ngb_syms, syms=('H', 'C')) atm_ngb_keys = list(map(atm_ngb_keys.__getitem__, srt)) atm_keys = [atm_key] + atm_ngb_keys zma_key_dct = { atm_key: zma_key for zma_key, atm_key in enumerate(atm_keys) } syms = list(map(atm_sym_dct.__getitem__, atm_keys)) natms = len(atm_keys) key_mat = [[None, None, None], [0, None, None], [0, 1, None], [0, 1, 2], [0, 1, 2]][:natms] name_mat = [[None, None, None], ['R1', None, None], ['R2', 'A2', None], ['R3', 'A3', 'D3'], ['R4', 'A4', 'D4']][:natms] atm_hyb = resonance_dominant_atom_hybridizations(gra)[atm_key] # z-matrix coordinate values val_dct = {} # determine bond distances for dist_name, atm_ngb_key in zip(['R1', 'R2', 'R3', 'R4'], atm_ngb_keys): dist_val = _bond_distance(gra, atm_key, atm_ngb_key) val_dct[dist_name] = dist_val # determine bond angles and dihedral angles if atm_hyb == 3: # for sp3 atoms, use z-matrix for a tetrahedral structure val_dct.update({ 'A2': TET_ANG, 'A3': TET_ANG, 'A4': TET_ANG, 'D3': +TRI_ANG, 'D4': -TRI_ANG }) elif atm_hyb == 2: # for sp2 atoms, use z-matrix for a trigonal planar structure val_dct.update({'A2': TRI_ANG, 'A3': TRI_ANG, 'D3': LIN_ANG}) elif atm_hyb == 1 and natms > 2: # for sp1 atoms, uze z-matrix for a linear structure # if there's only one neighbor, we don't need to set any angles at all assert natms == 3 # insert a dummy atom syms.insert(2, 'X') key_mat.append([0, 2, 1]) name_mat.append(['R3', 'A3', 'D3']) # note: we need to insert the dummy atom bond distance at R2 and shift # over the current value val_dct.update({ 'R2': 1.0, 'R3': val_dct['R2'], 'A2': RIT_ANG, 'A3': RIT_ANG, 'D3': LIN_ANG }) gra, dummy_atm_key = add_bonded_atom(gra, 'X', atm_key, bnd_ord=0) zma_key_dct[dummy_atm_key] = 2 zma_key_dct[atm_keys[2]] = 3 vma = automol.vmatrix.from_data(syms, key_mat, name_mat) names = automol.vmatrix.names(vma) val_dct = {name: val_dct[name] for name in names} zma = automol.zmatrix.from_data(syms, key_mat, name_mat, val_dct) gra_with_dummies = gra return zma, zma_key_dct, dummy_atm_key, gra_with_dummies