Source code for shapepy.scalar.angle

"""
Defines the Angle class

This class is used to handle conversions between radians/degrees/turns
It is an abstraction that to not handle float angle measured in radians
"""

from __future__ import annotations

import re
from numbers import Real

from ..loggers import debug
from ..scalar.reals import Math
from ..tools import Is, To


@debug("shapepy.scalar.angle")
def radians(value: Real) -> Angle:
    """
    Gives an Angle instance for given value measured in radians

    Parameters
    ----------
    value : Real
        The angle measured in radians

    Return
    ------
    Angle
        The Angle instance

    Example
    -------
    >>> radians(0)
    0 deg
    >>> radians(math.pi/2)
    90 deg
    >>> radians(math.pi)
    180 deg
    >>> radians(3*math.pi/2)
    270 deg
    >>> radians(2*math.pi)
    0 deg
    """
    value = To.finite(Math.fmod(value, Math.tau))
    return degrees(Math.degrees(value))


@debug("shapepy.scalar.angle")
def degrees(value: Real) -> Angle:
    """
    Gives an Angle instance for given value measured in degrees

    Parameters
    ----------
    value : Real
        The angle measured in degrees

    Return
    ------
    Angle
        The Angle instance

    Example
    -------
    >>> degrees(0)
    0 deg
    >>> degrees(90)
    90 deg
    >>> degrees(180)
    180 deg
    >>> degrees(270)
    270 deg
    >>> degrees(360)
    0 deg
    >>> degrees(720)
    0 deg
    """
    value = To.finite(value)
    direction = To.integer(round(value / 90))
    part = value - 90 * direction
    part = To.rational(part, 360) if Is.rational(part) else part / 360
    return Angle(direction, part)


@debug("shapepy.scalar.angle")
def turns(value: Real) -> Angle:
    """
    Gives an Angle instance for given value measured in turns

    Parameters
    ----------
    value : Real
        The angle measured in turns

    Return
    ------
    Angle
        The Angle instance

    Example
    -------
    >>> turns(0)
    0 deg
    >>> turns(0.25)
    90 deg
    >>> turns(0.50)
    180 deg
    >>> turns(0.75)
    270 deg
    >>> turns(1)
    0 deg
    >>> turns(2)
    0 deg
    """
    value = 4 * To.finite(value)
    direction = To.integer(round(value))
    part = value - direction
    return Angle(int(direction), part / 4)


@debug("shapepy.scalar.angle")
def arg(xcoord: Real, ycoord: Real) -> Angle:
    """
    Compute the complex argument of the point (x, y)

    Parameters
    ----------
    xcoord : Real
        The x-coordinate of the point
    ycoord : Real
        The y-coordinate of the point

    Returns
    -------
    Angle
        The Angle instance such tangent gives y/x

    Examples
    --------
    >>> arg(1, 0)  # 0 degrees
    0 deg
    >>> arg(1, 1)  # 45 degrees
    45 deg
    >>> arg(0, 1)  # 90 degrees
    90 deg
    >>> arg(-1, 1)  # 135 degrees
    135 deg
    """
    if ycoord == 0:
        return Angle(0, 0) if xcoord >= 0 else Angle(2, 0)
    if xcoord == 0:
        return Angle(1, 0) if ycoord > 0 else Angle(3, 0)
    return radians(Math.atan2(ycoord, xcoord))


[docs] class Angle: """ Class that stores an angle. Handles the operations such as __add__, __sub__, etc """ def __init__(self, direction: int, part: Real): if not Is.integer(direction): raise TypeError(f"Expected integer value, got {type(direction)}") if not Is.finite(part): raise TypeError(f"Expected numeric value, got {type(part)}") if abs(part) > 0.125: raise ValueError(f"Expected {part} be in [-1/8, 1/8]") self.__direction: int = To.integer(direction % 4) self.__part: Real = To.finite(part) @debug("shapepy.scalar.angle") def __eq__(self, other: object) -> bool: other: Angle = To.angle(other) return ( self.direction == other.direction and abs(self.part - other.part) < 1e-6 ) def __float__(self): return float(self.radians) def __invert__(self): return Angle(self.direction + 2, self.part) def __add__(self, other: Angle) -> Angle: other: Angle = To.angle(other) return turns(self.turns + other.turns) def __sub__(self, other: Angle) -> Angle: other: Angle = To.angle(other) return turns(self.turns - other.turns) def __mul__(self, other: Real) -> Angle: return turns(other * self.turns) def __rmul__(self, other: Real) -> Angle: return self.__mul__(other) def __str__(self): return f"{self.degrees} deg" def __repr__(self): return f"Angle({self.direction}, {self.part})" @property def direction(self) -> int: """Gives the nearest axis to the angle Example ------- >>> degrees(0).direction # +x axis 0 >>> degrees(30).direction # +x axis 0 >>> degrees(60).direction # +y axis 1 >>> degrees(90).direction # +y axis 1 >>> degrees(120).direction # +y axis 1 >>> degrees(180).direction # -x axis 2 >>> degrees(270).direction # -y axis 3 """ return self.__direction @property def part(self) -> int: """Gives the distance between the angle and the nearest axis Example ------- >>> degrees(0).part 0 >>> degrees(30).part 0.08333333333333333 >>> degrees(45).part 0.125 >>> degrees(60) -0.08333333333333333 >>> degrees(90).part 0 >>> degrees(120).part 0.08333333333333333 """ return self.__part @property def radians(self) -> Real: """Gives the angle measure in radians Example ------- >>> degrees(0).radians 0 >>> degrees(45).radians 0.7853981633974483 >>> degrees(90).radians 1.5707963267948966 >>> degrees(180).radians 3.141592653589793 >>> degrees(270).radians 4.71238898038469 >>> degrees(360).radians 0 """ return Math.tau * self.turns @property def degrees(self) -> Real: """Gives the angle measure in degrees Example ------- >>> degrees(0).degrees 0 >>> degrees(45).degrees 45 >>> degrees(90).degrees 90 >>> degrees(180).degrees 180 >>> degrees(270).degrees 270 >>> degrees(360).degrees 0 """ return 360 * self.turns @property def turns(self) -> Real: """Gives the angle measure in turns Example ------- >>> degrees(0).turns 0 >>> degrees(45).turns 0.125 >>> degrees(90).turns 0.25 >>> degrees(180).turns 0.5 >>> degrees(270).turns 0.75 >>> degrees(360).turns 0 """ if self.direction == 0 and self.part < 0: return To.rational(1) + self.part return self.part + To.rational(self.direction, 4)
[docs] def sin(self) -> Real: """ Computes the sinus value for the angle Return ------ Real The sinus result of the angle Example ------- >>> degrees(0).sin() 0 >>> degrees(45).sin() 0.7071067811865476 >>> degrees(90).sin() 1 """ if self.part == 0: result = ( 1 if self.direction == 1 else -1 if self.direction == 3 else 0 ) return To.finite(result) if self.direction % 2: result = Math.turcos(self.part) else: result = Math.tursin(self.part) if self.direction > 1: result *= -1 return result
[docs] def cos(self) -> Real: """ Computes the cossinus value for the angle Return ------ Real The cossinus result of the angle Example ------- >>> degrees(0).cos() 1 >>> degrees(45).cos() 0.7071067811865476 >>> degrees(90).cos() 0 """ if self.part == 0: result = ( 1 if self.direction == 0 else -1 if self.direction == 2 else 0 ) return To.finite(result) if self.direction % 2: result = Math.tursin(self.part) else: result = Math.turcos(self.part) if 0 < self.direction < 3: result *= -1 return result
def to_angle(obj: object) -> Angle: """ Converts an object to an Angle instance * If it's already an angle, gives the same instance * If it's a string, decides depending on the content: * "10deg" -> degrees(10) * "0.25tur" -> turns(0.25) * "2.1rad" -> radians(2.1) * If it's any another type, converts to a number, and gives it in radians Example ------- >>> angle("10deg") >>> angle("0.25tur") >>> angle("2.1rad") >>> angle(1.25) """ if Is.instance(obj, Angle): return obj if Is.instance(obj, str): tipo = re.findall(r"([a-zA-Z]+)$", obj)[0] value = To.finite(obj.replace(tipo, "")) if "deg" in tipo: return degrees(value) if "tur" in tipo: return turns(value) return radians(value) return radians(obj)