Source code for wulfric.geometry._geometry

# ================================== LICENSE ===================================
# Wulfric - Cell, Atoms, K-path, visualization.
# Copyright (C) 2023-2025 Andrey Rybakov
#
# e-mail: anry@uv.es, web: adrybakov.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# ================================ END LICENSE =================================
from math import cos, sqrt

import numpy as np

from wulfric._numerical import compare_with_tolerance
from wulfric.constants._numerical import TODEGREES, TORADIANS

# Save local scope at this moment
old_dir = set(dir())
old_dir.add("old_dir")


[docs] def get_volume(*args): r""" Computes volume. Three types of arguments are expected: * One argument. Matrix ``cell``. Volume is computed as: .. math:: V = \boldsymbol{v_1} \cdot (\boldsymbol{v_2} \times \boldsymbol{v_3}) * Three arguments. Vectors ``v1``, ``v2``, ``v3``. Volume is computed as: .. math:: V = \boldsymbol{v_1} \cdot (\boldsymbol{v_2} \times \boldsymbol{v_3}) * Six arguments. Parameters ``a``, ``b``, ``c``, ``alpha``, ``beta``, ``gamma``. Volume is computed as: .. math:: V = abc\sqrt{1+2\cos(\alpha)\cos(\beta)\cos(\gamma)-\cos^2(\alpha)-\cos^2(\beta)-\cos^2(\gamma)} Parameters ---------- v1 : (3,) |array-like|_ First vector. v2 : (3,) |array-like|_ Second vector. v3 : (3,) |array-like|_ Third vector. cell : (3, 3) |array-like|_ Matrix of a cell, rows are interpreted as vectors. a : float, default 1 Length of the :math:`v_1` vector. b : float, default 1 Length of the :math:`v_2` vector. c : float, default 1 Length of the :math:`v_3` vector. alpha : float, default 90 Angle between vectors :math:`v_2` and :math:`v_3`. In degrees. beta : float, default 90 Angle between vectors :math:`v_1` and :math:`v_3`. In degrees. gamma : float, default 90 Angle between vectors :math:`v_1` and :math:`v_2`. In degrees. Returns ------- volume : float Volume of corresponding region in space. Examples -------- .. doctest:: >>> import wulfric >>> wulfric.geometry.get_volume([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) 1.0 >>> wulfric.geometry.get_volume([1, 0, 0], [0, 1, 0], [0, 0, 1]) 1.0 >>> wulfric.geometry.get_volume(1, 1, 1, 90, 90, 90) 1.0 """ if len(args) == 1: cell = np.array(args[0]) elif len(args) == 3: cell = np.array(args) elif len(args) == 6: a, b, c, alpha, beta, gamma = args alpha = alpha * TORADIANS beta = beta * TORADIANS gamma = gamma * TORADIANS sq_root = ( 1 + 2 * cos(alpha) * cos(beta) * cos(gamma) - cos(alpha) ** 2 - cos(beta) ** 2 - cos(gamma) ** 2 ) return a * b * c * sqrt(sq_root) else: raise ValueError( "Unable to identify input. " + "Supported: one (3, 3) array-like, or three (3,) array-like, or 6 floats." ) return float(abs(np.linalg.det(cell)))
[docs] def get_angle(v1, v2, radians=False): r""" Computes angle between two vectors. .. math:: \alpha = \dfrac{(\boldsymbol{v_1} \cdot \boldsymbol{v_2})} {\vert\boldsymbol{v_1}\vert\cdot\vert\boldsymbol{v_2}\vert} Parameters ---------- v1 : (3,) |array-like|_ First vector. v2 : (3,) |array-like|_ Second vector. radians : bool, default False Whether to return value in radians. Returns ------- angle: float Angle in degrees or radians (see ``radians``). Raises ------ ValueError If one of the vectors is zero vector (or both). Norm is compared against :numpy:`finfo`\ (float).eps. Examples -------- .. doctest:: >>> import wulfric >>> wulfric.geometry.get_angle([1, 0, 0], [0, 0, 1]) 90.0 >>> wulfric.geometry.get_angle([1, 0, 0], [1, 0, 0]) 0.0 >>> round(wulfric.geometry.get_angle([1, 0, 0], [1, 1, 1]), 4) 54.7356 """ # Normalize vectors v1_norm = np.linalg.norm(v1) v2_norm = np.linalg.norm(v2) if abs(v1_norm) <= np.finfo(float).eps or abs(v2_norm) <= np.finfo(float).eps: raise ValueError("Angle is ill defined (zero vector).") v1 = np.array(v1) / v1_norm v2 = np.array(v2) / v2_norm alpha = np.arccos(np.clip(np.dot(v1, v2), -1.0, 1.0)) if radians: return float(alpha) return float(alpha * TODEGREES)
[docs] def parallelepiped_check( a, b, c, alpha, beta, gamma, raise_error=False, length_tolerance=1e-8, angle_tolerance=1e-4, ): r""" Checks if parallelepiped is valid. The conditions are * :math:`a > 0` * :math:`b > 0` * :math:`c > 0` * :math:`0 < \alpha < 180` * :math:`0 < \beta < 180` * :math:`0 < \gamma < 180` * :math:`\gamma < \alpha + \beta < 360 - \gamma` * :math:`\beta < \alpha + \gamma < 360 - \beta` * :math:`\alpha < \beta + \gamma < 360 - \alpha` Parameters ---------- a : float Length of the :math:`\boldsymbol{v_1}` vector. b : float Length of the :math:`\boldsymbol{v_2}` vector. c : float Length of the :math:`\boldsymbol{v_3}` vector. alpha : float Angle between vectors :math:`\boldsymbol{v_2}` and :math:`\boldsymbol{v_3}`. In degrees. beta : float Angle between vectors :math:`\boldsymbol{v_1}` and :math:`\boldsymbol{v_3}`. In degrees. gamma : float Angle between vectors :math:`\boldsymbol{v_1}` and :math:`\boldsymbol{v_2}`. In degrees. raise_error : bool, default False Whether to raise error if parameters can not form a parallelepiped. length_tolerance : float, default :math:`10^{-8}` Numerical tolerance for the length variables. Default value is chosen in the contexts of condense matter physics, assuming that length is given in Angstroms. Please choose appropriate tolerance for your problem. angle_tolerance : float, default :math:`10^{-4}` Numerical tolerance for the angle variables. Default value is chosen in the contexts of condense matter physics, assuming that angles are in degrees. Please choose appropriate tolerance for your problem. Returns ------- result: bool Whether the parameters could from a parallelepiped. Raises ------ ValueError If parameters can not form a parallelepiped. Only raised if ``raise_error`` is ``True`` (it is ``False`` by default). Examples -------- .. doctest:: >>> import wulfric >>> wulfric.geometry.parallelepiped_check(1, 1, 1, 90, 90, 90) True >>> wulfric.geometry.parallelepiped_check(1, 1, 1, 30, 20, 110) False >>> wulfric.geometry.parallelepiped_check(1, -1, 1, 90, 90, 90) False >>> wulfric.geometry.parallelepiped_check(1, 0, 1, 90, 90, 90) False >>> wulfric.geometry.parallelepiped_check(1, 1, 1, 90, 199, 90) False """ result = ( compare_with_tolerance(a, ">", 0.0, eps=length_tolerance) and compare_with_tolerance(b, ">", 0.0, eps=length_tolerance) and compare_with_tolerance(c, ">", 0.0, eps=length_tolerance) and compare_with_tolerance(alpha, "<", 180.0, eps=angle_tolerance) and compare_with_tolerance(beta, "<", 180.0, eps=angle_tolerance) and compare_with_tolerance(gamma, "<", 180.0, eps=angle_tolerance) and compare_with_tolerance(alpha, ">", 0.0, eps=angle_tolerance) and compare_with_tolerance(beta, ">", 0.0, eps=angle_tolerance) and compare_with_tolerance(gamma, ">", 0.0, eps=angle_tolerance) and compare_with_tolerance(gamma, "<", alpha + beta, eps=angle_tolerance) and compare_with_tolerance( alpha + beta, "<", 360.0 - gamma, eps=angle_tolerance ) and compare_with_tolerance(beta, "<", alpha + gamma, eps=angle_tolerance) and compare_with_tolerance( alpha + gamma, "<", 360.0 - beta, eps=angle_tolerance ) and compare_with_tolerance(alpha, "<", beta + gamma, eps=angle_tolerance) and compare_with_tolerance( beta + gamma, "<", 360.0 - alpha, eps=angle_tolerance ) ) if not result and raise_error: message = "Parameters could not form a parallelepiped:\n" message += f"a = {a}" if not compare_with_tolerance(a, ">", 0.0, eps=length_tolerance): message += f" (a <= 0 with numerical tolerance: {length_tolerance})" message += "\n" message += f"b = {b}" if not compare_with_tolerance(b, ">", 0.0, eps=length_tolerance): message += f" (b <= 0 with numerical tolerance: {length_tolerance})" message += "\n" message += f"c = {c}" if not compare_with_tolerance(c, ">", 0.0, eps=length_tolerance): message += f" (c <= 0 with numerical tolerance: {length_tolerance})" message += "\n" message += f"alpha = {alpha}\n" if not compare_with_tolerance(alpha, "<", 180.0, eps=angle_tolerance): message += f" (alpha >= 180 with numerical tolerance: {angle_tolerance})\n" if not compare_with_tolerance(alpha, ">", 0.0, eps=angle_tolerance): message += f" (alpha <= 0 with numerical tolerance: {angle_tolerance})\n" message += f"beta = {beta}\n" if not compare_with_tolerance(beta, "<", 180.0, eps=angle_tolerance): message += f" (beta >= 180 with numerical tolerance: {angle_tolerance})\n" if not compare_with_tolerance(beta, ">", 0.0, eps=angle_tolerance): message += f" (beta <= 0 with numerical tolerance: {angle_tolerance})\n" message += f"gamma = {gamma}\n" if not compare_with_tolerance(gamma, "<", 180.0, eps=angle_tolerance): message += f" (gamma >= 180 with numerical tolerance: {angle_tolerance})\n" if not compare_with_tolerance(gamma, ">", 0.0, eps=angle_tolerance): message += f" (gamma <= 0 with numerical tolerance: {angle_tolerance})\n" if not compare_with_tolerance(gamma, "<", alpha + beta, eps=angle_tolerance): message += f"Inequality gamma < alpha + beta is not satisfied with numerical tolerance: {angle_tolerance}\n" if not compare_with_tolerance( alpha + beta, "<", 360.0 - gamma, eps=angle_tolerance ): message += f"Inequality alpha + beta < 360 - gamma is not satisfied with numerical tolerance: {angle_tolerance}\n" if not compare_with_tolerance(beta, "<", alpha + gamma, eps=angle_tolerance): message += f"Inequality beta < alpha + gamma is not satisfied with numerical tolerance: {angle_tolerance}\n" if not compare_with_tolerance( alpha + gamma, "<", 360.0 - beta, eps=angle_tolerance ): message += f"Inequality alpha + gamma < 360 - beta is not satisfied with numerical tolerance: {angle_tolerance}\n" if not compare_with_tolerance(alpha, "<", beta + gamma, eps=angle_tolerance): message += f"Inequality alpha < beta + gamma is not satisfied with numerical tolerance: {angle_tolerance}\n" if not compare_with_tolerance( beta + gamma, "<", 360.0 - alpha, eps=angle_tolerance ): message += f"Inequality beta + gamma < 360 - alpha is not satisfied with numerical tolerance: {angle_tolerance}\n" raise ValueError(message) return result
[docs] def get_spherical( vector, in_degrees=True, polar_axis=[0, 0, 1], radial_line_zero=[1, 0, 0] ): R""" Compute |spherical-coordinates|_ of a vector. :math:`(v^x, v^y, v^z) \rightarrow (r, \theta, \phi)` Parameters ---------- vector : (3,) |array-like|_ Vector to be converted. in_degrees : bool, default True Whether to return angles in degrees or radians. If ``True``, then angles are returned in degrees. polar_axis : (3,) |array-like|_ Polar axis (see notes). By default oriented along :math:`+z`. radial_line_zero : (3,) |array-like|_ Zero of the radial line (see notes). By default oriented along :math:`+x`. Returns ------- r : float Length of the ``vector``. polar_angle : float Polar angle. Returned in degrees if ``in_degrees = True``, in radians otherwise. azimuthal_angle : float Azimuthal angle. Returned in degrees if ``in_degrees = True``, in radians otherwise. Notes ----- ``polar_angle`` is defined as the angle between the polar axis and the given ``vector`` with :math:`0 \le \alpha_{polar} \le \pi`. Azimuthal angle is defined as the angle of the rotation of the radial line around the polar axis. This angle is measured from the ``radial_line_zero`` in accordance to the right-hand rule. :math:`0 \le \alpha_{azimuthal} \le 2\pi`. Radial line is the projection of the ``vector`` on the plane perpendicular to the ``polar_axis``. If azimuthal angle is ill-defined, then wulfric returns * :math:`0` if polar angle is :math:`0`. * :math:`\pi` if polar angle is :math:`\pi`. Examples -------- .. doctest:: >>> import wulfric >>> wulfric.geometry.get_spherical([1, 0, 0]) (1.0, 90.0, 0.0) >>> wulfric.geometry.get_spherical([-1, 0, 0]) (1.0, 90.0, 180.0) >>> wulfric.geometry.get_spherical([0, 1, 0]) (1.0, 90.0, 90.0) >>> wulfric.geometry.get_spherical([0, -1, 0]) (1.0, 90.0, 270.0) >>> wulfric.geometry.get_spherical([0, 0, 1]) (1.0, 0.0, 0.0) >>> wulfric.geometry.get_spherical([0, 0, -1]) (1.0, 180.0, 180.0) >>> wulfric.geometry.get_spherical([1, 0, 0], polar_axis=[1, 0, 0]) (1.0, 0.0, 0.0) """ polar_axis = np.array(polar_axis) / np.linalg.norm(polar_axis) radial_line_zero = np.array(radial_line_zero) / np.linalg.norm(radial_line_zero) r = float(np.linalg.norm(vector)) vector = np.array(vector) / r if np.allclose(vector, polar_axis): polar, azimuthal = 0.0, 0.0 elif np.allclose(vector, -polar_axis): polar, azimuthal = np.pi, np.pi else: polar = float(np.arccos(np.clip(np.dot(vector, polar_axis), -1, 1))) vector = vector - polar_axis * np.dot(vector, polar_axis) vector /= np.linalg.norm(vector) # This one is more complex, as it is from 0 to 360 azimuthal = float(np.arccos(np.clip(np.dot(vector, radial_line_zero), -1, 1))) if np.linalg.det([polar_axis, radial_line_zero, vector]) >= 0: pass else: azimuthal = 2 * np.pi - azimuthal if in_degrees: return r, polar * TODEGREES, azimuthal * TODEGREES else: return r, polar, azimuthal
# Populate __all__ with objects defined in this file __all__ = list(set(dir()) - old_dir) # Remove all semi-private objects __all__ = [i for i in __all__ if not i.startswith("_")] del old_dir