def test_H(): """Test H substituent.""" elements, coordinates = read_gjf(DATA_DIR / "gjfs" / "H.gjf") radii = get_radii(elements, radii_type="bondi") radii = [1.09 if radius == 1.20 else radius for radius in radii] sterimol = Sterimol(elements, coordinates, 1, 2, radii=radii) assert_almost_equal( (sterimol.L_value, sterimol.B_1_value, sterimol.B_5_value), (2.15, 1.09, 1.09), decimal=2, )
def test_reference(sterimol_data): """Test against Sterimol reference data.""" data = sterimol_data name = data["name"] L, B_1, B_5 = float(data["L"]), float(data["B_1"]), float(data["B_5"]) # Use Paton's Bondi radii of 1.09 for H elements, coordinates = read_gjf(DATA_DIR / "gjfs" / f"{name}.gjf") radii = get_radii(elements, radii_type="bondi") radii = [1.09 if radius == 1.20 else radius for radius in radii] sterimol = Sterimol(elements, coordinates, 1, 2, radii=radii) assert_almost_equal(L, sterimol.L_value, decimal=2) assert_almost_equal(B_1, sterimol.B_1_value, decimal=2) assert_almost_equal(B_5, sterimol.B_5_value, decimal=2)
def __init__( self, elements: Iterable[int] | Iterable[str], coordinates: ArrayLike2D, radii: ArrayLike1D | None = None, radii_type: str = "crc", probe_radius: float = 1.4, density: float = 0.01, ) -> None: # Converting elements to atomic numbers if the are symbols elements = convert_elements(elements, output="numbers") coordinates: Array2DFloat = np.array(coordinates) # Getting radii if they are not supplied if radii is None: radii = get_radii(elements, radii_type=radii_type) # Increment the radii with the probe radius radii: Array1DFloat = np.array(radii) radii = radii + probe_radius # Construct list of atoms atoms = [] for i, (coordinate, radius, element) in enumerate(zip(coordinates, radii, elements), start=1): atom = Atom(element, coordinate, radius, i) atoms.append(atom) # Set up attributes self._atoms = atoms self._density = density self._probe_radius = probe_radius # Determine accessible and occluded points for each atom self._determine_accessible_points() # Calculate atom areas and volumes self._calculate()
def __init__( self, elements: Union[Iterable[int], Iterable[str]], coordinates: ArrayLike2D, radii: Optional[ArrayLike1D] = None, radii_type: str = "rahm", point_surface: bool = True, compute_coefficients: bool = True, density: float = 0.1, excluded_atoms: Optional[Sequence[int]] = None, included_atoms: Optional[Sequence[int]] = None, ) -> None: # Check that only excluded or included atoms are given if excluded_atoms is not None and included_atoms is not None: raise Exception( "Give either excluded or included atoms but not both.") # Converting elements to atomic numbers if the are symbols elements = convert_elements(elements, output="numbers") coordinates = np.array(coordinates) # Set excluded atoms all_atoms = set(range(1, len(elements) + 1)) if included_atoms is not None: included_atoms_ = set(included_atoms) excluded_atoms = list(all_atoms - included_atoms_) elif excluded_atoms is None: excluded_atoms = [] else: excluded_atoms = list(excluded_atoms) self._excluded_atoms = excluded_atoms # Set up self._surface = None self._density = density # Getting radii if they are not supplied if radii is None: radii = get_radii(elements, radii_type=radii_type) radii = np.array(radii) self._radii = radii # Get vdW surface if requested if point_surface: self._surface_from_sasa(elements, coordinates) else: # Get list of atoms as Atom objects atoms: List[Atom] = [] for i, (element, coord, radius) in enumerate(zip(elements, coordinates, radii), start=1): atom = Atom(element, coord, radius, i) atoms.append(atom) self._atoms = atoms # Calculate coefficients if compute_coefficients: self.compute_coefficients(model="id3") # Calculatte P_int values if point_surface and compute_coefficients: self.compute_p_int()
def __init__( self, elements: Iterable[int] | Iterable[str], coordinates: ArrayLike2D, order: int = 8, ) -> None: # Convert elements to atomic numbers elements = convert_elements(elements, output="numbers") coordinates: Array2DFloat = np.array(coordinates) # Load the covalent radii radii = get_radii(elements, radii_type="pyykko") # Set up the atoms objects atoms = [] for i, (element, coordinate, radius) in enumerate(zip(elements, coordinates, radii), start=1): atom = Atom(element, coordinate, radius, i) atoms.append(atom) self._atoms = atoms # Calculate the coordination numbers according to Grimme's recipe. k_1 = 16 k_2 = 4 / 3 k_3 = 4 for cn_atom in atoms: # Get coordinates and radii of all other atoms other_coordinates: Array2DFloat = np.array( [atom.coordinates for atom in atoms if atom is not cn_atom]) other_radii: Array1DFloat = np.array( [atom.radius for atom in atoms if atom is not cn_atom]) # Calculate distances dists = cdist( np.array(cn_atom.coordinates).reshape(-1, 3), other_coordinates.reshape(-1, 3), ) # Calculate coordination number coordination_number = np.sum( 1 / (1 + np.exp(-k_1 * (k_2 * (cn_atom.radius + other_radii) / dists - 1)))) cn_atom.coordination_number = coordination_number # Calculate the C_N coefficients c_n_coefficients: dict[int, list[float]] = { i: [] for i in range(6, order + 1, 2) } for atom in atoms: # Get the reference data for atom reference_data = c6_reference_data[atom.element] cn = atom.coordination_number # Take out the coordination numbers and c6(aa) values c_6_ref = reference_data[:, 0] cn_1 = reference_data[:, 1] cn_2 = reference_data[:, 2] # Calculate c6 and c8 according to the recipe r = (cn - cn_1)**2 + (cn - cn_2)**2 L = np.exp(-k_3 * r) W = np.sum(L) Z = np.sum(c_6_ref * L) c_6 = Z / W temp_coefficients: dict[int, float] = { i: np.nan for i in range(6, order + 1, 2) } for i in range(6, order + 1, 2): if i == 6: temp_coefficients[i] = c_6 if i == 8: c_8 = 3 * c_6 * r2_r4[atom.element]**2 temp_coefficients[i] = c_8 elif i == 10: c_10 = 49.0 / 40.0 * c_8**2 / c_6 temp_coefficients[i] = c_10 elif i > 10: c_n = (temp_coefficients[i - 6] * (temp_coefficients[i - 2] / temp_coefficients[i - 4])**3) temp_coefficients[i] = c_n for key, value in temp_coefficients.items(): c_n_coefficients[key].append(value) # Set up attributes coordination_numbers: Array1DFloat = np.array( [atom.coordination_number for atom in self._atoms]) c_n_coefficients: Array1DFloat = { key: np.array(value) for key, value in c_n_coefficients.items() } self._atoms = atoms self.c_n_coefficients = c_n_coefficients self.coordination_numbers = coordination_numbers
def __init__( # noqa: C901 self, elements: Iterable[int] | Iterable[str], coordinates: ArrayLike2D, atom_1: int, radii: ArrayLike1D | None = None, radii_type: str = "crc", method: str = "libconeangle", ) -> None: # Convert elements to atomic numbers if the are symbols elements = convert_elements(elements, output="numbers") coordinates: Array2DFloat = np.array(coordinates) # Get radii if they are not supplied if radii is None: radii = get_radii(elements, radii_type=radii_type) radii: Array1DFloat = np.array(radii) # Check so that no atom is within vdW distance of atom 1 within = check_distances(elements, coordinates, atom_1, radii=radii) if len(within) > 0: atom_string = " ".join([str(i) for i in within]) raise ValueError("Atoms within vdW radius of central atom:", atom_string) # Set up coordinate array and translate coordinates coordinates -= coordinates[atom_1 - 1] # Get list of atoms as Atom objects atoms: list[Atom] = [] for i, (element, coord, radius) in enumerate( zip(elements, coordinates, radii), start=1 ): if i != atom_1: atom = Atom(element, coord, radius, i) atom.get_cone() atoms.append(atom) self._atoms = atoms # Calculate cone angle if method == "libconeangle": try: from libconeangle import cone_angle angle, axis, tangent_atoms = cone_angle(coordinates, radii, atom_1 - 1) self.cone_angle = angle self.tangent_atoms = [i + 1 for i in tangent_atoms] atoms = [ atom for atom in self._atoms if atom.index in self.tangent_atoms ] self._cone = Cone(self.cone_angle, atoms, axis) except ImportError: warnings.warn( "Failed to import libconeangle. Defaulting to method='internal'" ) self._cone_angle_internal() elif method == "internal": self._cone_angle_internal() else: raise ValueError( "Method not implemented. Choose between 'libconeangle' and 'internal'" )
def __init__( self, elements: Iterable[int] | Iterable[str], coordinates: ArrayLike2D, metal_index: int, excluded_atoms: Sequence[int] | None = None, radii: ArrayLike1D | None = None, include_hs: bool = False, radius: float = 3.5, radii_type: str = "bondi", radii_scale: float = 1.17, density: float = 0.001, z_axis_atoms: Sequence[int] | None = None, xz_plane_atoms: Sequence[int] | None = None, ) -> None: # Get center and and reortient coordinate system coordinates: Array2DFloat = np.array(coordinates) center = coordinates[metal_index - 1] coordinates -= center if excluded_atoms is None: excluded_atoms = [] excluded_atoms = set(excluded_atoms) if metal_index not in excluded_atoms: excluded_atoms.add(metal_index) if z_axis_atoms is not None and xz_plane_atoms is not None: z_axis_coordinates = coordinates[np.array(z_axis_atoms) - 1] z_point = np.mean(z_axis_coordinates, axis=0) xz_plane_coordinates = coordinates[np.array(xz_plane_atoms) - 1] xz_point = np.mean(xz_plane_coordinates, axis=0) v_1 = z_point - center v_2 = xz_point - center # TODO: Remove type ignores when https://github.com/numpy/numpy/pull/21216 is released v_3: Array1DFloat = np.cross(v_2, v_1) real: Array2DFloat = np.vstack([v_1, v_3]) # type: ignore real /= np.linalg.norm(real, axis=1).reshape(-1, 1) ref_1 = np.array([0.0, 0.0, -1.0]) ref_2 = np.array([0.0, 1.0, 0.0]) ref = np.vstack([ref_1, ref_2]) R = kabsch_rotation_matrix(real, ref, center=False) coordinates = (R @ coordinates.T).T elif z_axis_atoms is not None: z_axis_coordinates = coordinates[np.array(z_axis_atoms) - 1] z_point = np.mean(z_axis_coordinates, axis=0) v_1 = z_point - center v_1 = v_1 / np.linalg.norm(v_1) coordinates = rotate_coordinates(coordinates, v_1, np.array([0, 0, -1])) self._z_axis_atoms = z_axis_atoms self._xz_plane_atoms = xz_plane_atoms # Save density and coordinates for steric map plotting. self._density = density self._all_coordinates = coordinates # Converting element ids to atomic numbers if the are symbols elements = convert_elements(elements, output="numbers") # Getting radii if they are not supplied if radii is None: radii = get_radii(elements, radii_type=radii_type, scale=radii_scale) radii: Array1DFloat = np.array(radii) # Get list of atoms as Atom objects atoms = [] for i, (element, radius_, coord) in enumerate(zip(elements, radii, coordinates), start=1): if i in excluded_atoms: continue elif (not include_hs) and element == 1: continue else: atom = Atom(element, coord, radius_, i) atoms.append(atom) # Set variables for outside access and function access. self._atoms = atoms self._excluded_atoms = set(excluded_atoms) # Compute buried volume self._compute_buried_volume(center=center, radius=radius, density=density)
def __init__( self, elements: Union[Iterable[int], Iterable[str]], coordinates: ArrayLike2D, dummy_index: int, attached_index: Union[int, Iterable[int]], radii: Optional[ArrayLike1D] = None, radii_type: str = "crc", n_rot_vectors: int = 3600, excluded_atoms: Optional[Sequence[int]] = None, calculate: bool = True, ) -> None: # Convert elements to atomic numbers if the are symbols elements = convert_elements(elements, output="numbers") coordinates = np.array(coordinates) if excluded_atoms is None: excluded_atoms = [] excluded_atoms = np.array(excluded_atoms) # Get radii if they are not supplied if radii is None: radii = get_radii(elements, radii_type=radii_type) radii = np.array(radii) # Add dummy atom if multiple attached indices are given if isinstance(attached_index, Iterable): attached_dummy_coordinates = np.mean( [coordinates[i - 1] for i in attached_index], axis=0) attached_dummy_coordinates = cast(np.ndarray, attached_dummy_coordinates) coordinates = np.vstack([coordinates, attached_dummy_coordinates]) elements.append(0) radii = np.concatenate([radii, [0.0]]) attached_index = len(elements) # Set up coordinate array all_coordinates = coordinates all_radii = radii # Translate coordinates so origin is at atom 2 origin = all_coordinates[attached_index - 1] all_coordinates -= origin # Get vector pointing from atom 2 to atom 1 vector_2_to_1 = (all_coordinates[attached_index - 1] - all_coordinates[dummy_index - 1]) bond_length = np.linalg.norm(vector_2_to_1) vector_2_to_1 = vector_2_to_1 / np.linalg.norm(vector_2_to_1) # Get rotation quaternion that overlays vector with x-axis x_axis = np.array([[1.0, 0.0, 0.0]]) R = kabsch_rotation_matrix(vector_2_to_1.reshape(1, -1), x_axis, center=False) all_coordinates = (R @ all_coordinates.T).T self._rotation_matrix = R # Get list of atoms as Atom objects atoms = [] for i, (element, radius, coord) in enumerate(zip(elements, all_radii, all_coordinates), start=1): atom = Atom(element, coord, radius, i) atoms.append(atom) if i == dummy_index: dummy_atom = atom if i == attached_index: attached_atom = atom # Set up attributes self._atoms = atoms self._excluded_atoms = set(excluded_atoms) self._origin = origin self._dummy_atom = dummy_atom self._attached_atom = attached_atom self.bond_length = bond_length self._n_rot_vectors = n_rot_vectors if calculate: self.calculate()
def __init__( # noqa: C901 self, elements: Union[Iterable[int], Iterable[str]], coordinates: ArrayLike2D, atom_1: int, radii: Optional[ArrayLike1D] = None, radii_type: str = "crc", ) -> None: # Convert elements to atomic numbers if the are symbols elements = convert_elements(elements, output="numbers") coordinates = np.array(coordinates) # Get radii if they are not supplied if radii is None: radii = get_radii(elements, radii_type=radii_type) radii = np.array(radii) # Check so that no atom is within vdW distance of atom 1 within = check_distances(elements, coordinates, atom_1, radii=radii) if len(within) > 0: atom_string = " ".join([str(i) for i in within]) raise Exception("Atoms within vdW radius of central atom:", atom_string) # Set up coordinate array and translate coordinates coordinates -= coordinates[atom_1 - 1] # Get list of atoms as Atom objects atoms: List[Atom] = [] for i, (element, coord, radius) in enumerate(zip(elements, coordinates, radii), start=1): if i != atom_1: atom = Atom(element, coord, radius, i) atom.get_cone() atoms.append(atom) self._atoms = atoms # Search for cone over single atoms cone = self._search_one_cones() # Prune out atoms that lie in the shadow of another atom's cone if cone is None: loop_atoms = list(atoms) remove_atoms: Set[Atom] = set() for cone_atom in loop_atoms: for test_atom in loop_atoms: if cone_atom is not test_atom: if cone_atom.cone.is_inside(test_atom): remove_atoms.add(test_atom) for atom in remove_atoms: loop_atoms.remove(atom) self._loop_atoms = loop_atoms # Search for cone over pairs of atoms if cone is None: cone = self._search_two_cones() # Search for cones over triples of atoms if cone is None: cone = self._search_three_cones() # Set attributes self._cone = cone self.cone_angle = math.degrees(cone.angle * 2) self.tangent_atoms = [atom.index for atom in cone.atoms]
def __init__( self, elements: Iterable[int] | Iterable[str], coordinates: ArrayLike2D, metal_index: int, include_hs: bool = True, radii: ArrayLike1D | None = None, radii_type: str = "pyykko", radius: float = 3.5, density: float = 0.01, ) -> None: # Set up arrays and get radii elements: Array1DInt = np.array( convert_elements(elements, output="numbers")) coordinates: Array2DFloat = np.array(coordinates) if radii is None: radii = get_radii(elements, radii_type=radii_type) radii: Array1DFloat = np.array(radii) # Check so that no atom is within vdW distance of metal atom within = check_distances(elements, coordinates, metal_index, radii=radii) if len(within) > 0: atom_string = " ".join([str(i) for i in within]) raise Exception("Atoms within vdW radius of central atom:", atom_string) # Center coordinate system around metal and remove it coordinates -= coordinates[metal_index - 1] elements: Array1DFloat = np.delete(elements, metal_index - 1) coordinates: Array2DFloat = np.delete(coordinates, metal_index - 1, axis=0) radii: Array1DFloat = np.delete(radii, metal_index - 1) # Remove H atoms if include_hs is False: h_mask = elements == 1 elements = elements[~h_mask] coordinates = coordinates[~h_mask] radii = radii[~h_mask] # Construct SASA object sasa = SASA( elements, coordinates, radii=radii, radii_type=radii_type, probe_radius=0.0, density=density, ) # Set invisible and proximal masks for each atom atoms = sasa._atoms for atom in atoms: atom.invisible_mask = np.zeros(len(atom.accessible_points), dtype=bool) atom.proximal_mask = np.linalg.norm(atom.accessible_points, axis=1) < radius # Check points on other atoms against cone for each atom atom_coordinates: Array2DFloat = np.array( [atom.coordinates for atom in atoms]) atoms: Array1DAny = np.array(atoms) for atom in atoms: # Calculate distances to other atoms atom.get_cone() cone = Cone(atom.cone.angle, [atom.index], atom.cone.normal) atom_dist = np.linalg.norm(atom.coordinates) other_distances = np.dot(atom_coordinates, cone.normal) # Check whether points are (1) within cone and (2) beyond atom # center check_atoms = atoms[other_distances >= (atom_dist - atom.radius)] for check_atom in check_atoms: if check_atom == atom: continue is_inside_mask = cone.is_inside_points( check_atom.accessible_points, method="cross") is_beyond_mask = (np.dot(check_atom.accessible_points, cone.normal) >= atom_dist) invisible_mask = np.logical_and(is_inside_mask, is_beyond_mask) check_atom.invisible_mask = np.logical_or( check_atom.invisible_mask, invisible_mask) # Calculate visible, invisible and proximal_visible volume visible_volume = 0 invisible_volume = 0 proximal_visible_volume = 0 proximal_volume = 0 visible_area = 0 invisible_area = 0 proximal_visible_area = 0 proximal_area = 0 for atom in atoms: point_areas = atom.point_areas[atom.accessible_mask] point_volumes = atom.point_volumes[atom.accessible_mask] invisible_volume += point_volumes[atom.invisible_mask].sum() visible_volume += point_volumes[~atom.invisible_mask].sum() proximal_visible_volume += point_volumes[np.logical_and( ~atom.invisible_mask, atom.proximal_mask)].sum() proximal_volume += point_volumes[atom.proximal_mask].sum() invisible_area += point_areas[atom.invisible_mask].sum() visible_area += point_areas[~atom.invisible_mask].sum() proximal_visible_area += point_areas[np.logical_and( ~atom.invisible_mask, atom.proximal_mask)].sum() proximal_area += point_areas[atom.proximal_mask].sum() # Store attributes self.total_volume = sasa.volume self.proximal_volume = proximal_volume self.distal_volume = sasa.volume - proximal_volume self.invisible_volume = invisible_volume self.visible_volume = visible_volume self.proximal_visible_volume = proximal_visible_volume self.distal_visible_volume = visible_volume - proximal_visible_volume self.total_area = sasa.area self.proximal_area = proximal_area self.distal_area = sasa.area - proximal_area self.invisible_area = invisible_area self.visible_area = visible_area self.proximal_visible_area = proximal_visible_area self.distal_visible_area = visible_area - proximal_visible_area self._atoms = atoms