Exemple #1
0
    def cap_fragments(self, bond: Bond) -> Dict[Atom, Atom]:
        """Delete a bond from :attr:`SplitMol.mol` and cap the resulting fragments with :attr:`SplitMol.cap_type`.

        Parameters
        ----------
        bond : |plams.Bond|
            A PLAMS bond.

        Returns
        -------
        :class:`dict` [|plams.Atom|, |plams.Atom|]
            A dictionary with the old atoms in **bond** as keys and their new capping atoms
            as values.

        """  # noqa
        mol = self.mol
        symbol = self.cap_type

        # Construct the capping atoms
        atom1, atom2 = bond.atom1, bond.atom2
        atom1_cap = Atom(symbol=symbol, coords=atom2.coords)
        atom2_cap = Atom(symbol=symbol, coords=atom1.coords)
        mol.add_atom(atom1_cap, adjacent=[atom1])
        mol.add_atom(atom2_cap, adjacent=[atom2])

        # Resize the capping atom bond
        length1 = atom1.radius + atom1_cap.radius
        length2 = atom2.radius + atom2_cap.radius
        mol.bonds[-2].resize(atom1_cap, length1)
        mol.bonds[-1].resize(atom2_cap, length2)

        # Delete the old bond and return a dictionary containg marking all new bonds
        return {atom1: atom1_cap, atom2: atom2_cap}
Exemple #2
0
    def dfs(at1: Atom, atom_set: Set[Atom]):
        at1._visited = True
        atom_set.add(at1)
        for at2 in at1.neighbors():

            if at2._visited:
                continue
            dfs(at2, atom_set)
Exemple #3
0
def string_array_to_molecule(parser_fun: ParserElement,
                             file_name: PathLike,
                             mol: Optional_[Molecule] = None) -> Molecule:
    """Convert a Numpy string array.

    It takes an array like:
    [['C', '-1.487460', '-0.028670', '-0.000060'],
    ['O', '0.376340', '0.028670', '-0.000060'],
    ['H', '-1.818910', '-1.067060', '-0.000060'],
    ['H', '-1.866470', '0.473700', '0.889930'],
    ['H', '-1.866470', '0.473700', '-0.890040'],
    ['H', '0.756720', '-0.950010', '-0.000060']]

    and covert it to a plams ``Molecule``.
    """
    mols = parse_file(parser_fun, file_name).asList()
    last_mol = np.array(mols[-1])
    elems = last_mol[:, 0]
    coords = np.array(last_mol[:, 1:], dtype=float)

    if mol:
        if len(coords) == len(mol):
            mol.from_array(coords)
        else:
            raise RuntimeError('Output molecule does not match input molecule')

    else:
        mol = Molecule()
        for e, c in zip(elems, coords):
            mol.add_atom(Atom(symbol=e, coords=tuple(c)))
    return mol
Exemple #4
0
def string_array_to_molecule(parser_fun, file_name, mol=None):
    """
    Convert a Numpy string array like:

    [['C', '-1.487460', '-0.028670', '-0.000060'],
    ['O', '0.376340', '0.028670', '-0.000060'],
    ['H', '-1.818910', '-1.067060', '-0.000060'],
    ['H', '-1.866470', '0.473700', '0.889930'],
    ['H', '-1.866470', '0.473700', '-0.890040'],
    ['H', '0.756720', '-0.950010', '-0.000060']]

    To a plams ``Molecule``.
    """
    string_array_to_float = np.vectorize(float)
    mols = parse_file(parser_fun, file_name).asList()
    last_mol = np.array(mols[-1])
    elems = last_mol[:, 0]
    coords = string_array_to_float(last_mol[:, 1:])
    if mol:
        if len(coords) == len(mol):
            plams_mol = mol
            for i in range(len(plams_mol)):
                plams_mol.atoms[i].coords = tuple(
                    [float(c) for c in coords[i]])
        else:
            raise RuntimeError('Output molecule does not match input molecule')
    else:
        plams_mol = Molecule()
        for e, c in zip(elems, coords):
            plams_mol.add_atom(Atom(symbol=e, coords=tuple(c)))
    return plams_mol
Exemple #5
0
 def dfs(at1: Atom, m_append: Callable[[int], None]):
     at1._visited = True  # type: ignore[attr-defined]
     m_append(at1.id)
     for bond in at1.bonds:
         at2 = bond.other_end(at1)
         if not at2._visited:  # type: ignore[attr-defined]
             dfs(at2, m_append)
Exemple #6
0
 def repr_Atom(self, obj: Atom, level: int) -> str:  # noqa: N802
     """Create a :class:`str` representation of a |plams.Atom| instance."""
     decimal = self.maxfloat
     space = 14 - (
         6 - decimal
     )  # The default PLAMS values for space and decimal are 14 and 6
     ret = obj.str(decimal=decimal, space=space).strip()
     return f'{obj.__class__.__name__}({ret})'
Exemple #7
0
def tuplesXYZ_to_plams(xs):
    """ Transform a list of namedTuples to a Plams molecule """
    plams_mol = Molecule()
    for at in xs:
        symb = at.symbol
        cs = at.xyz
        plams_mol.add_atom(Atom(symbol=symb, coords=tuple(cs)))

    return plams_mol
def geometry_to_molecule(geometry):
    """
    Convert a list of XYZ coordinates to a Molecule object
    """
    mol = Molecule()

    for i in range(0, len(geometry)):
        mol.add_atom(
            Atom(symbol=geometry[i][0],
                 coords=(geometry[i][1], geometry[i][2], geometry[i][3])))

    return mol
Exemple #9
0
def parse_molecule_traj(file_traj: PathLike) -> Molecule:
    """Read Molecules from the job_name.traj file."""
    mols = manyXYZ(file_traj)
    # Last geometry corresponds to the optimized structure
    opt_mol = mols[-1]

    plams_mol = Molecule()
    for at in opt_mol:
        symb = at.symbol
        cs = at.xyz
        plams_mol.add_atom(Atom(symbol=symb, coords=tuple(cs)))

    return plams_mol
Exemple #10
0
def _test_distribute(mol: Molecule, symbol: str, **kwargs) -> Molecule:
    """Helper function for :func:`test_distribute`."""
    if not isinstance(mol, Molecule):
        mol = Molecule(mol)

    _idx_in = [i for i, at in enumerate(mol) if at.symbol == symbol]
    idx_in = np.fromiter(_idx_in, count=len(_idx_in), dtype=int)
    idx_out = distribute_idx(mol, idx_in, **kwargs)

    a = symbol
    b = 'I' if a != 'I' else 'Br'
    mol2 = Molecule()
    for i, at in enumerate(mol):
        if at.symbol != symbol:
            continue
        symbol_new = a if i not in idx_out else b
        mol2.add_atom(Atom(symbol=symbol_new, coords=at.coords, mol=mol2))
    return mol2
Exemple #11
0
def _parse_ion(ion: Molecule | str | int) -> Tuple[Molecule, Atom]:
    """Interpret and parse the **ion** argument in :func:`.get_xyn`.

    Construct and return a new :math:`XY_{n=0}` molecule and the atom :math:`X` itself.
    If **ion** is a polyatomic ion then :math:`XY_{n=0}` is a copy of **ion** and :math:`X`
    is the first atom with a non-zero charge.

    Parameters
    ----------
    ion : |str|_, |int|_ or |plams.Molecule|_
        An ion (:math:`X`), be it mono- (*e.g.* atomic number or symbol) or poly-atomic.

    Returns
    -------
    |plams.Molecule|_ and |plams.Atom|_
        A :math:`XY_{n=0}` molecule and the the charged atom from :math:`X`.

    Raises
    ------
    MoleculeError
        Raised if ion is an instance of :math:`Molecule` but does not contain any charged atoms.

    """
    if isinstance(ion, Molecule):
        XYn = ion.copy()
        for i, at in enumerate(XYn, 1):
            if not at.properties.charge:
                continue

            # Found an atom with non-zero charge; return a copy
            ret = XYn.copy()
            return ret, ret[i]

        raise MoleculeError(
            "No atoms were found in 'ion' with a non-zero charge")

    else:
        # Ion is an atomic number or symbol
        X = Atom(atnum=to_atnum(ion))
        XYn = Molecule()
        XYn.add_atom(X)
        return XYn, X
Exemple #12
0
def _molecule_from_rdmol(
    rdmol: Chem.Mol,
    smiles: str,
    matches: Iterable[Sequence[int]],
    split: bool = True,
) -> Generator[Molecule, None, None]:
    """Construct a PLAMS molecule from the passed rdkit mol's ``MolBlock``."""
    for tup in matches:
        try:
            i, *_, j = tup  # type: int, Any, None | int
        except ValueError:
            i = tup[0]
            j = None

        # Split the capping atom (j) from the main molecule
        if j is not None and split:
            if i > j:
                i -= 1
            rdmol_edit = Chem.EditableMol(rdmol)
            rdmol_edit.RemoveAtom(j)
            rdmol_new = rdmol_edit.GetMol()
            anchor = rdmol_new.GetAtoms()[i]
            anchor.SetFormalCharge(anchor.GetFormalCharge() - 1)
        else:
            rdmol_new = rdmol

        # Parse the .mol block and convert it into a PLAMS molecule
        mol_block = Chem.MolToMolBlock(rdmol)
        iterator = _iter_mol_block(mol_block, size=len(rdmol.GetAtoms()))
        mol = Molecule()
        mol.atoms = [Atom(symbol=symbol, coords=xyz, mol=mol) for symbol, xyz in iterator]
        for bond in rdmol.GetBonds():
            at1 = mol.atoms[bond.GetBeginAtomIdx()]
            at2 = mol.atoms[bond.GetEndAtomIdx()]
            mol.add_bond(Bond(at1, at2, order=bond.GetBondTypeAsDouble()))

        # Set properties and yield
        mol.properties.smiles = smiles
        mol.properties.dummies = mol.atoms[i]
        mol.properties.anchor = f"{mol.properties.dummies.symbol}{i + 1}"
        yield mol
Exemple #13
0
def from_rdmol(rdkit_mol, confid=-1):
    """
    Translate an RDKit molecule into a PLAMS molecule type.

    :parameter rdkit_mol: RDKit molecule
    :parameter int confid: conformer identifier from which to take coordinates
    :type rdkit_mol: rdkit.Chem.Mol
    :return: a PLAMS molecule
    :rtype: plams.Molecule

    """
    if isinstance(rdkit_mol, Molecule):
        return rdkit_mol
    # Create plams molecule
    plams_mol = Molecule()
    total_charge = 0
    try:
        Chem.Kekulize(rdkit_mol)
    except:
        pass
    conf = rdkit_mol.GetConformer(id=confid)
    for rd_atom in rdkit_mol.GetAtoms():
        pos = conf.GetAtomPosition(rd_atom.GetIdx())
        ch = rd_atom.GetFormalCharge()
        pl_atom = Atom(rd_atom.GetAtomicNum(),
                       coords=(pos.x, pos.y, pos.z),
                       charge=ch)
        if rd_atom.GetPDBResidueInfo():
            pl_atom.properties.pdb_info = get_PDBResidueInfo(rd_atom)
        plams_mol.add_atom(pl_atom)
        total_charge += ch
    for bond in rdkit_mol.GetBonds():
        at1 = plams_mol.atoms[bond.GetBeginAtomIdx()]
        at2 = plams_mol.atoms[bond.GetEndAtomIdx()]
        plams_mol.add_bond(Bond(at1, at2, bond.GetBondTypeAsDouble()))
    plams_mol.charge = total_charge
    for propname in rdkit_mol.GetPropNames():
        plams_mol.properties[propname] = rdkit_mol.GetProp(propname)
    return plams_mol
Exemple #14
0
def test_index():
    """Test :meth:`Molecule.index`."""
    atom = BENZENE[1]
    bond = BENZENE[1, 2]
    atom_test = Atom(coords=[0, 0, 0], symbol='H')

    assert BENZENE.index(atom) == 1
    assert BENZENE.index(bond) == (1, 2)

    try:
        BENZENE.index(None)  # None is of invalid type
    except MoleculeError:
        pass
    else:
        raise AssertionError(
            "'BENZENE.index(None)' failed to raise a 'MoleculeError'")

    try:
        BENZENE.index(atom_test)  # atom_test is not in BENZENE
    except MoleculeError:
        pass
    else:
        raise AssertionError(
            "'BENZENE.index(atom_test)' failed to raise a 'MoleculeError'")
Exemple #15
0
def run_ff_anionic(mol: Molecule, anchor: Atom, s: Settings) -> None:
    r"""Assign neutral parameters to an anionic species (*e.g.* carboxylate).

    Consists of 4 distinct steps:

    * **mol** is capped with a proton: *e.g.*
      :math:`RCO_2^- \rightarrow RCO_2H`.
    * Parameters are guessed for both fragments (using MATCH_) and then recombined into **mol**.
    * The capping proton is removed again.
    * The atomic charge of **anchor** is adjusted such that
      the total moleculair charge becomes zero.

    Performs an inplace update of **mol**.

    .. _MATCH: http://brooks.chem.lsa.umich.edu/index.php?page=match&subdir=articles/resources/software

    Parameters
    ----------
    mol : :class:`Molecule<scm.plams.mol.molecule.Molecule>`
        A cationic molecule.

    anchor : :class:`Atom<scm.plams.mol.atom.Atom>`
        The atom in **mol** with the formal negative charge.

    s : :class:`Settings<scm.plams.core.settings.Settings>`
        The job Settings to-be passed to :class:`MATCHJob<nanoCAT.ff.match_job.MATCHJob>`.

    See Also
    --------
    :func:`run_match_job()<nanoCAT.ff.ff_assignment.run_match_job>`
        Assign atom types and charges to **mol** based on the results of MATCH_.

    :func:`run_ff_cationic()<nanoCAT.ff.ff_cationic.run_ff_cationic>`
        Assign neutral parameters to a cationic species (*e.g.* ammonium).

    """  # noqa
    if anchor not in mol:
        raise MoleculeError("Passed 'anchor' is not part of 'mol'")
    anchor.properties.charge = 0

    # Cap the anion with a proton
    mol_with_h = add_Hs(mol)
    _cap_h = mol_with_h[-1]
    cap_h = Atom(atnum=_cap_h.atnum,
                 coords=_cap_h.coords,
                 mol=mol,
                 settings=mol[1].properties.copy())

    cap_h.properties.pdb_info.IsHeteroAtom = False
    cap_h.properties.pdb_info.Name = 'Hxx'
    mol.add_atom(cap_h)
    mol.add_bond(Bond(anchor, cap_h, mol=mol))

    # Guess parameters and remove the capping proton
    run_match_job(mol, s)
    mol.delete_atom(cap_h)

    # Set the total charge of the system to 0
    anchor.properties.charge_float -= sum(at.properties.charge_float
                                          for at in mol)
    return None
Exemple #16
0
def run_ff_cationic(mol: Molecule, anchor: Atom, s: Settings) -> None:
    r"""Assign neutral parameters to a cationic species (*e.g.* ammonium).

    Consists of 3 distinct steps:

    * **mol** is converted into two neutral fragments,
      *e.g.* ammonium is converted into two amines:
      :math:`N^+(R)_4 \rightarrow N(R)_3 + RN(H)_2`.
    * Parameters are guessed for both fragments (using MATCH_) and then recombined into **mol**.
    * The atomic charge of **anchor** is adjusted such that
      the total moleculair charge becomes zero.

    Performs an inplace update of **mol**.

    .. _MATCH: http://brooks.chem.lsa.umich.edu/index.php?page=match&subdir=articles/resources/software

    Parameters
    ----------
    mol : :class:`Molecule<scm.plams.mol.molecule.Molecule>`
        A cationic molecule.

    anchor : :class:`Atom<scm.plams.mol.atom.Atom>`
        The atom in **mol** with the formal positive charge.

    s : :class:`Settings<scm.plams.core.settings.Settings>`
        The job Settings to-be passed to :class:`MATCHJob<nanoCAT.ff.match_job.MATCHJob>`.

    See Also
    --------
    :func:`run_match_job()<nanoCAT.ff.ff_assignment.run_match_job>`
        Assign atom types and charges to **mol** based on the results of MATCH_.

    :func:`run_ff_anionic()<nanoCAT.ff.ff_anionic.run_ff_anionic>`
        Assign neutral parameters to an anionic species (*e.g.* carboxylate).

    """  # noqa
    if anchor not in mol:
        raise MoleculeError("Passed 'anchor' is not part of 'mol'")
    anchor.properties.charge = 0

    # Find the first bond attached to the anchor atom which is not part of a ring
    for bond in anchor.bonds:
        if not mol.in_ring(bond):
            break
    else:
        raise MoleculeError(
            "All bonds attached to 'anchor' are part of a ring system")

    with SplitMol(mol, bond) as (frag1, frag2):
        # Identify the amine and the alkylic fragment
        if anchor in frag1:
            amine = frag1
            alkyl = frag2
        else:
            amine = frag2
            alkyl = frag1
        amine.delete_atom(anchor.bonds[-1].other_end(anchor))

        # Change the capping hydrogen into X
        # X is the same atom type as **anchor**
        alkyl_cap = alkyl[-1]
        alkyl_cap.atnum = anchor.atnum
        cap_bond = alkyl_cap.bonds[0]
        bond_length = alkyl_cap.radius + cap_bond.other_end(alkyl_cap).radius
        cap_bond.resize(alkyl_cap, bond_length)

        # Change X into XH_n
        alkyl_with_h = add_Hs(alkyl)
        properties = mol[1].properties
        for at in alkyl_with_h.atoms[len(alkyl):]:
            cap_h = Atom(atnum=at.atnum,
                         coords=at.coords,
                         mol=alkyl,
                         settings=properties.copy())
            cap_h.properties.pdb_info.IsHeteroAtom = False
            cap_h.properties.pdb_info.Name = 'Hxx'
            alkyl.add_atom(cap_h)
            alkyl.add_bond(Bond(alkyl_cap, cap_h, mol=alkyl))

        # Get the match parameters
        run_match_job(amine, s)
        run_match_job(alkyl, s)

    # Set the total charge of the system to 0
    anchor.properties.charge_float -= sum(at.properties.charge_float
                                          for at in mol)
    return None
Exemple #17
0
def _construct_xyn(
    ion: str | int | Molecule,
    lig_count: int,
    lig: Molecule,
    lig_at: Atom,
    lig_idx: int,
) -> Tuple[Molecule, Atom]:
    """Construct the :math:`XYn` molecule for :func:`get_xyn`.

    Parameters
    ----------
    ion : |str|_, |int|_ or |plams.Molecule|_
        An ion (:math:`X`), be it mono- (*e.g.* atomic number or symbol) or poly-atomic.

    lig_count : int
        The number of to-be attached ligands per ion.

    lig : |plams.Molecule|_
        A single ligand molecule.

    lig_at : |plams.Atom|_
        The ligand anchor atom.

    lig_idx : int
        The (1-based) index of **lig_at**.

    Returns
    -------
    |plams.Molecule|_ and |plams.Atom|_
        A :math:`XY_{n}` molecule and the the charged atom from :math:`X`.

    """
    # Create a list of n ligands, n anchor atoms, n desired ion-anchor distances and n angles
    lig_gen = (lig.copy() for _ in range(lig_count))
    angle_ar = np.arange(0, 2 * np.pi, 2 * np.pi / lig_count)

    # Prepare vectors for translations and rotations
    vec1 = lig_at.vector_to(np.zeros(3))
    _vec = lig_at.vector_to(lig.get_center_of_mass())
    vec2 = get_perpendicular_vec(_vec)

    # Update the XYn molecule with ligands
    XYn, X = _parse_ion(ion)
    iterator = enumerate(zip(angle_ar, lig_gen), 2)
    for i, (angle, mol) in iterator:
        # Prepare for translations and rotations
        anchor = mol[lig_idx]
        rotmat = axis_rotation_matrix(vec2, angle)
        dist = anchor.radius + X.radius

        # Translate and rotate the ligand
        mol.translate(vec1)
        mol.rotate(rotmat)
        vec3 = anchor.vector_to(mol.get_center_of_mass())
        vec3 /= np.linalg.norm(vec3) / dist
        mol.translate(vec3)

        # Set pdb attributes
        for at in mol:
            at.properties.pdb_info.ResidueNumber = i
            at.properties.pdb_info.ResidueName = 'LIG'

        # Combine the translated and rotated ligand with XYn
        XYn.add_molecule(mol)
        XYn.add_bond(X, anchor)

    return XYn, X
Exemple #18
0
 def _besides_ring(atom: Atom) -> bool:
     """Check if any neighboring atoms are part of a ring system."""
     return any(plams_mol.in_ring(at) for at in atom.neighbors())
Exemple #19
0
 def format_atom(atom: plams.Atom) -> str:
     symbol, mass, coords = atom.symbol, atom._getmass(
     ), atom.coords
     return '{:2s}{:12.4f}{:14.6f}{:14.6f}{:14.6f}\n'.format(
         symbol, mass, *coords)
Exemple #20
0
def create_molecule(name, r=2.25):
    mol = Molecule()
    mol.add_atom(Atom(symbol='Ba', coords=(0, 0, 0)))
    mol.add_atom(Atom(symbol='F', coords=(0, 0, r)))

    return mol
    AtomNamesList = kf.read('Molecule', 'AtomicNumbers')
    AtomSymbolList = kf.read('Molecule', 'AtomSymbols')
    Coords = Units.convert(kf.read('Molecule', 'Coords'), 'Bohr', 'Angstrom')
    energy = Units.convert(kf.read('AMSResults', 'Energy'), 'au',
                           'kcal/mol')  # 1 Hartree = 1 a.u

    npCoords = np.array(Coords)
    npCoords = npCoords.reshape(int(npCoords.size / 3), 3)

    mol = Molecule()
    AtomSymbolList = AtomSymbolList.strip()
    SymbolList = list(AtomSymbolList.split())
    SymbolList = [s.strip() for s in SymbolList]

    for at, coord in zip(SymbolList, npCoords):
        mol.add_atom(Atom(symbol=at, coords=coord))

    # Keep in mind, that coordinate indexing starts from 0;
    pes_at1 = Atom(symbol=SymbolList[pes_atom1_id - 1],
                   coords=npCoords[pes_atom1_id - 1])
    pes_at2 = Atom(symbol=SymbolList[pes_atom2_id - 1],
                   coords=npCoords[pes_atom2_id - 1])
    pes_bond = Bond(pes_at1, pes_at2)
    line = [(pes_atom1_id, pes_atom2_id), direction, PESnum,
            pes_bond.length(), energy]
    data.append(line)

column_names = [
    'id', 'str/sq', 'PES', 'Bond length [A]', 'Energy [kcal / mol]'
]
table_data = pd.DataFrame(data, columns=column_names)
Exemple #22
0
def set_dihed(self,
              angle: float,
              anchor: Atom,
              cap: Sequence[Atom],
              opt: bool = True,
              unit: str = 'degree') -> None:
    """Change all valid dihedral angles into a specific value.

    Performs an inplace update of this instance.

    Parameters
    ----------
    angle : :class:`float`
        The desired dihedral angle.

    anchor : |plams.Atom|
        The ligand anchor atom.

    opt : :class:`bool`
        Whether or not the dihedral adjustment should be followed up by an RDKit UFF optimization.

    unit : :class:`str`
        The input unit.

    """
    cap_atnum = []
    for at in cap:
        cap_atnum.append(at.atnum)
        at.atnum = 0

    angle = Units.convert(angle, unit, 'degree')
    bond_iter = (bond for bond in self.bonds
                 if bond.atom1.atnum != 1 and bond.atom2.atnum != 1
                 and bond.order == 1 and not self.in_ring(bond))

    # Correction factor for, most importantly, tri-valent anchors (e.g. P(R)(R)R)
    dihed_cor = angle / 2
    neighbors = anchor.neighbors()
    if len(neighbors) > 2:
        atom_list = [anchor] + sorted(neighbors, key=lambda at: -at.atnum)[:3]
        improper = get_dihed(atom_list)
        dihed_cor *= np.sign(improper)

    for bond in bond_iter:
        # Gather lists of all non-hydrogen neighbors
        n1, n2 = self.neighbors_mod(bond.atom1), self.neighbors_mod(bond.atom2)

        # Remove all atoms in `bond`
        n1 = [atom for atom in n1 if atom is not bond.atom2]
        n2 = [atom for atom in n2 if atom is not bond.atom1]

        # Remove all non-subsituted atoms
        # A special case consists of anchor atoms; they can stay
        if len(n1) > 1:
            n1 = [
                atom for atom in n1 if (len(self.neighbors_mod(atom)) > 1
                                        or atom is anchor or atom.atnum == 0)
            ]
        if len(n2) > 1:
            n2 = [
                atom for atom in n2 if (len(self.neighbors_mod(atom)) > 1
                                        or atom is anchor or atom.atnum == 0)
            ]

        # Set `bond` in an anti-periplanar conformation
        if n1 and n2:
            dihed = get_dihed((n1[0], bond.atom1, bond.atom2, n2[0]))
            if anchor not in bond:
                self.rotate_bond(bond,
                                 bond.atom1,
                                 angle - dihed,
                                 unit='degree')
            else:
                dihed -= dihed_cor
                self.rotate_bond(bond, bond.atom1, -dihed, unit='degree')
                dihed_cor *= -1

    for at, atnum in zip(cap, cap_atnum):
        at.atnum = atnum

    if opt:
        rdmol = molkit.to_rdmol(self)
        UFF(rdmol).Minimize()
        self.from_rdmol(rdmol)