From 1fc60067d114a730b7c5eed151fb3c8a67f4c1f5 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 31 Aug 2025 16:14:21 +0200 Subject: [PATCH] feat: refactor Angle class --- src/shapepy/bool2d/base.py | 2 +- src/shapepy/geometry/base.py | 2 +- src/shapepy/geometry/integral.py | 6 +- src/shapepy/geometry/point.py | 8 +- src/shapepy/scalar/__init__.py | 5 +- src/shapepy/scalar/angle.py | 486 +++++++++++++++----------- src/shapepy/scalar/nodes_sample.py | 4 +- tests/analytic/test_polynomial.py | 1 - tests/bool2d/test_density.py | 4 +- tests/bool2d/test_empty_whole.py | 6 +- tests/bool2d/test_transform.py | 8 +- tests/geometry/test_jordan_polygon.py | 6 +- tests/geometry/test_point.py | 56 ++- tests/geometry/test_usegment.py | 5 +- tests/scalar/test_angle.py | 262 +++++++------- 15 files changed, 455 insertions(+), 406 deletions(-) diff --git a/src/shapepy/bool2d/base.py b/src/shapepy/bool2d/base.py index 4945459e..315f0d3d 100644 --- a/src/shapepy/bool2d/base.py +++ b/src/shapepy/bool2d/base.py @@ -143,7 +143,7 @@ def rotate(self, angle: Angle) -> SubSetR2: ----------- >>> from shapepy import Primitive >>> circle = Primitive.circle() - >>> circle.rotate(Angle.degrees(90)) + >>> circle.rotate(degrees(90)) """ raise NotImplementedError diff --git a/src/shapepy/geometry/base.py b/src/shapepy/geometry/base.py index 5eaf661b..817fc939 100644 --- a/src/shapepy/geometry/base.py +++ b/src/shapepy/geometry/base.py @@ -128,7 +128,7 @@ def rotate(self, angle: Angle) -> IGeometricCurve: ----------- >>> from shapepy import Primitive >>> circle = Primitive.circle() - >>> circle.rotate(Angle.degrees(90)) + >>> circle.rotate(degrees(90)) """ raise NotImplementedError diff --git a/src/shapepy/geometry/integral.py b/src/shapepy/geometry/integral.py index fefb7dac..7827d973 100644 --- a/src/shapepy/geometry/integral.py +++ b/src/shapepy/geometry/integral.py @@ -11,7 +11,7 @@ from ..analytic.tools import find_minimum from ..common import derivate from ..loggers import debug -from ..scalar.angle import Angle +from ..scalar.angle import arg from ..scalar.quadrature import AdaptativeIntegrator, IntegratorFactory from ..scalar.reals import Math from ..tools import Is, To @@ -90,8 +90,8 @@ def lebesgue_density_jordan( deltapj = segmentj(1, 1) innerval = inner(deltapi, deltapj) crossval = cross(deltapi, deltapj) - angle = Angle.arg(-innerval, -crossval) - return float(angle) / Math.tau + angle = arg(-innerval, -crossval) + return angle.turns % 1 direct = IntegratorFactory.closed_newton_cotes(3) integrator = AdaptativeIntegrator(direct, 1e-6) diff --git a/src/shapepy/geometry/point.py b/src/shapepy/geometry/point.py index 1e66ecdf..b92f59f1 100644 --- a/src/shapepy/geometry/point.py +++ b/src/shapepy/geometry/point.py @@ -9,7 +9,7 @@ from typing import Tuple, Union from ..loggers import debug -from ..scalar.angle import Angle +from ..scalar.angle import Angle, arg, degrees from ..scalar.reals import Math, Real from ..tools import Is, To @@ -32,7 +32,7 @@ def polar(radius: Real, angle: Angle) -> Point2D: Creates a Point with polar coordinates """ radius = To.real(radius) - angle = Angle.degrees(0) if radius == 0 else To.angle(angle) + angle = degrees(0) if radius == 0 else To.angle(angle) return Point2D(None, None, radius, angle) @@ -83,7 +83,7 @@ def radius(self) -> Real: def angle(self) -> Angle: """The angle the point (x, y) forms with respect to the horizontal""" if self.__angle is None: - self.__angle = Angle.arg(self.__xcoord, self.__ycoord) + self.__angle = arg(self.__xcoord, self.__ycoord) return self.__angle def __copy__(self) -> Point2D: @@ -117,7 +117,7 @@ def __neg__(self) -> Point2D: -self.xcoord, -self.ycoord, self.radius, - self.angle + Angle.degrees(180), + self.angle + degrees(180), ) def __pos__(self) -> Point2D: diff --git a/src/shapepy/scalar/__init__.py b/src/shapepy/scalar/__init__.py index 21059bd3..51da6c63 100644 --- a/src/shapepy/scalar/__init__.py +++ b/src/shapepy/scalar/__init__.py @@ -4,5 +4,8 @@ defines the class Angle to handle conversions between radians and degrees, defines the methods of numerical integration (quadrature)""" -from .angle import Angle +from ..tools import To +from .angle import Angle, to_angle from .reals import Math, Rational, Real + +To.angle = to_angle diff --git a/src/shapepy/scalar/angle.py b/src/shapepy/scalar/angle.py index ac7c3dd1..e2131b63 100644 --- a/src/shapepy/scalar/angle.py +++ b/src/shapepy/scalar/angle.py @@ -10,222 +10,305 @@ import re from numbers import Real +from ..loggers import debug from ..scalar.reals import Math from ..tools import Is, To -class Angle: +@debug("shapepy.scalar.angle") +def radians(value: Real) -> Angle: """ - Class that stores an angle. + Gives an Angle instance for given value measured in radians - Handles the operations such as __add__, __sub__, etc + 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)) - @classmethod - def radians(cls, value: Real) -> Angle: - """ - Gives an Angle instance for given value measured in radians - Parameters - ---------- - value : Real - The angle measured in radians +@debug("shapepy.scalar.angle") +def degrees(value: Real) -> Angle: + """ + Gives an Angle instance for given value measured in degrees - Return - ------ - Angle - The Angle instance + Parameters + ---------- + value : Real + The angle measured in degrees - Example - ------- - >>> Angle.radians(0) - 0 deg - >>> Angle.radians(math.pi/2) - 90 deg - >>> Angle.radians(math.pi) - 180 deg - >>> Angle.radians(3*math.pi/2) - 270 deg - >>> Angle.radians(2*math.pi) - 0 deg - """ - value = To.finite(Math.fmod(value, Math.tau)) - return cls.degrees(Math.degrees(value)) + Return + ------ + Angle + The Angle instance - @classmethod - def degrees(cls, value: Real) -> Angle: - """ - Gives an Angle instance for given value measured in degrees + 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) - Parameters - ---------- - value : Real - The angle measured in degrees - Return - ------ - Angle - The Angle instance +@debug("shapepy.scalar.angle") +def turns(value: Real) -> Angle: + """ + Gives an Angle instance for given value measured in turns - Example - ------- - >>> Angle.degrees(0) - 0 deg - >>> Angle.degrees(90) - 90 deg - >>> Angle.degrees(180) - 180 deg - >>> Angle.degrees(270) - 270 deg - >>> Angle.degrees(360) - 0 deg - >>> Angle.degrees(720) - 0 deg - """ - value = To.finite(value) - value %= 360 - value = ( - To.rational(value, 360) if Is.rational(value) else (value / 360) - ) - return cls.turns(value) + Parameters + ---------- + value : Real + The angle measured in turns - @classmethod - def turns(cls, value: Real) -> Angle: - """ - Gives an Angle instance for given value measured in turns + Return + ------ + Angle + The Angle instance - Parameters - ---------- - value : Real - The angle measured in turns + 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) - Return - ------ - Angle - The Angle instance - Example - ------- - >>> Angle.turns(0) - 0 deg - >>> Angle.turns(0.25) - 90 deg - >>> Angle.turns(0.50) - 180 deg - >>> Angle.turns(0.75) - 270 deg - >>> Angle.turns(1) - 0 deg - >>> Angle.turns(2) - 0 deg - """ - value = To.finite(value) - quad, part = divmod(4 * value, 1) - return cls(int(quad), part) +@debug("shapepy.scalar.angle") +def arg(xcoord: Real, ycoord: Real) -> Angle: + """ + Compute the complex argument of the point (x, y) - @classmethod - def atan2(cls, ycoord: Real, xcoord: Real): - """ - 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 - Parameters - ---------- - ycoord : Real - The y-coordinate of the point - xcoord : Real - The x-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)) - Returns - ------- - Angle - The Angle instance such tangent gives y/x - - Examples - -------- - >>> Angle.atan2(0, 1) # 0 degrees - 0 deg - >>> Angle.atan2(1, 1) # 45 degrees - 45 deg - >>> Angle.atan2(1, -1) # 135 degrees - 135 deg - >>> Angle.atan2(-1, 1) # -45 degrees - 315 deg - """ - if ycoord == 0: - return cls(0, 0) if xcoord >= 0 else cls(2, 0) - if xcoord == 0: - return cls(1, 0) if ycoord > 0 else cls(3, 0) - return cls.radians(Math.atan2(ycoord, xcoord)) - - @classmethod - def arg(cls, xcoord: Real, ycoord: Real): - """ - 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 +class Angle: + """ + Class that stores an angle. - Returns - ------- - Angle - The Angle instance such tangent gives y/x - - Examples - -------- - >>> Angle.arg(1, 0) # 0 degrees - 0 deg - >>> Angle.arg(1, 1) # 45 degrees - 45 deg - >>> Angle.arg(0, 1) # 90 degrees - 90 deg - >>> Angle.arg(-1, 1) # 135 degrees - 135 deg - """ - return cls.atan2(ycoord, xcoord) + Handles the operations such as __add__, __sub__, etc + """ - def __init__(self, quad: int = 0, part: Real = 0): - if not Is.integer(quad): - raise TypeError(f"Expected integer value, got {type(quad)}") + 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)}") - self.quad: int = quad % 4 - self.part: Real = 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: - if Is.instance(other, Angle): - return self.quad == other.quad and (self.part - other.part == 0) - return self == Angle.radians(other) + other: Angle = To.angle(other) + return ( + self.direction == other.direction + and abs(self.part - other.part) < 1e-6 + ) def __float__(self): - return float(Math.tau * (self.quad + self.part) / 4) + return float(self.radians) def __add__(self, other: Angle) -> Angle: - other = To.angle(other) - return self.__class__.turns( - ((self.quad + other.quad) + (self.part + other.part)) / 4 - ) + other: Angle = To.angle(other) + return turns(self.turns + other.turns) def __sub__(self, other: Angle) -> Angle: - other = To.angle(other) - return self.__class__.turns( - ((self.quad - other.quad) + (self.part - other.part)) / 4 - ) + other: Angle = To.angle(other) + return turns(self.turns - other.turns) def __mul__(self, other: Real) -> Angle: - return self.turns(other * (self.quad + self.part) / 4) + return turns(other * self.turns) def __rmul__(self, other: Real) -> Angle: return self.__mul__(other) def __str__(self): - return f"{90 * (self.quad + self.part)} deg" + return f"{self.degrees} deg" def __repr__(self): - return f"Angle({str(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 + """ + return self.part + To.rational(self.direction, 4) def sin(self) -> Real: """ @@ -238,23 +321,24 @@ def sin(self) -> Real: Example ------- - >>> Angle.degrees(0).sin() + >>> degrees(0).sin() 0 - >>> Angle.degrees(45).sin() + >>> degrees(45).sin() 0.7071067811865476 - >>> Angle.degrees(90).sin() + >>> degrees(90).sin() 1 """ if self.part == 0: - if self.quad % 2: - return To.finite(1 if self.quad == 1 else -1) - return To.finite(0) + result = ( + 1 if self.direction == 1 else -1 if self.direction == 3 else 0 + ) + return To.finite(result) - if self.quad % 2: - result = Math.turcos(self.part / 4) + if self.direction % 2: + result = Math.turcos(self.part) else: - result = Math.tursin(self.part / 4) - if self.quad > 1: + result = Math.tursin(self.part) + if self.direction > 1: result *= -1 return result @@ -269,23 +353,24 @@ def cos(self) -> Real: Example ------- - >>> Angle.degrees(0).cos() + >>> degrees(0).cos() 1 - >>> Angle.degrees(45).cos() + >>> degrees(45).cos() 0.7071067811865476 - >>> Angle.degrees(90).cos() + >>> degrees(90).cos() 0 """ if self.part == 0: - if self.quad % 2: - return To.finite(0) - return To.finite(1 if self.quad == 0 else -1) + result = ( + 1 if self.direction == 0 else -1 if self.direction == 2 else 0 + ) + return To.finite(result) - if self.quad % 2: - result = Math.tursin(self.part / 4) + if self.direction % 2: + result = Math.tursin(self.part) else: - result = Math.turcos(self.part / 4) - if 0 < self.quad < 3: + result = Math.turcos(self.part) + if 0 < self.direction < 3: result *= -1 return result @@ -296,9 +381,9 @@ def to_angle(obj: object) -> Angle: * If it's already an angle, gives the same instance * If it's a string, decides depending on the content: - * "10deg" -> Angle.degrees(10) - * "0.25tur" -> Angle.turns(0.25) - * "2.1rad" -> Angle.radians(2.1) + * "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 @@ -314,11 +399,8 @@ def to_angle(obj: object) -> Angle: tipo = re.findall(r"([a-zA-Z]+)$", obj)[0] value = To.finite(obj.replace(tipo, "")) if "deg" in tipo: - return Angle.degrees(value) + return degrees(value) if "tur" in tipo: - return Angle.turns(value) - return Angle.radians(value) - return Angle.radians(obj) - - -To.angle = to_angle + return turns(value) + return radians(value) + return radians(obj) diff --git a/src/shapepy/scalar/nodes_sample.py b/src/shapepy/scalar/nodes_sample.py index f9776c8b..6cd12000 100644 --- a/src/shapepy/scalar/nodes_sample.py +++ b/src/shapepy/scalar/nodes_sample.py @@ -6,7 +6,7 @@ from typing import Tuple from ..tools import Is, To -from .angle import Angle +from .angle import turns from .reals import Rational, Real @@ -121,7 +121,7 @@ def chebyshev(npts: int) -> Tuple[Real]: (0.02447, 0.20611, 0.5, 0.79389, 0.97553) """ angles = ( - Angle.turns(num / 2) + turns(num / 2) for num in NodeSampleFactory.custom_open_formula(npts)[::-1] ) return tuple(To.rational(1, 2) + angle.cos() / 2 for angle in angles) diff --git a/tests/analytic/test_polynomial.py b/tests/analytic/test_polynomial.py index 7a639a79..6afd97ca 100644 --- a/tests/analytic/test_polynomial.py +++ b/tests/analytic/test_polynomial.py @@ -1,6 +1,5 @@ import random -import numpy as np import pytest from shapepy.analytic.polynomial import Polynomial diff --git a/tests/bool2d/test_density.py b/tests/bool2d/test_density.py index 1a0ecd5d..42cef717 100644 --- a/tests/bool2d/test_density.py +++ b/tests/bool2d/test_density.py @@ -10,7 +10,7 @@ from shapepy.bool2d.primitive import Primitive from shapepy.bool2d.shape import ConnectedShape, DisjointShape from shapepy.geometry.point import polar -from shapepy.scalar.angle import Angle +from shapepy.scalar.angle import degrees @pytest.mark.order(22) @@ -37,7 +37,7 @@ def test_empty_whole(): assert whole.density(point) == 1 for deg in range(0, 360, 30): - angle = Angle.degrees(deg) + angle = degrees(deg) point = polar(float("inf"), angle) assert lebesgue_density(empty, point) == 0 assert lebesgue_density(whole, point) == 1 diff --git a/tests/bool2d/test_empty_whole.py b/tests/bool2d/test_empty_whole.py index 26480253..255ec216 100644 --- a/tests/bool2d/test_empty_whole.py +++ b/tests/bool2d/test_empty_whole.py @@ -11,7 +11,7 @@ from shapepy.bool2d.base import EmptyShape, WholeShape from shapepy.bool2d.primitive import Primitive from shapepy.geometry.point import polar -from shapepy.scalar.angle import Angle +from shapepy.scalar.angle import degrees @pytest.mark.order(21) @@ -162,7 +162,7 @@ def test_scale(): def test_rotate(): empty = EmptyShape() whole = WholeShape() - angle = Angle.degrees(30) + angle = degrees(30) assert empty.rotate(angle) is empty assert whole.rotate(angle) is whole @@ -213,7 +213,7 @@ def test_density(): assert lebesgue_density(whole, point) == 1 for deg in range(0, 360, 30): - angle = Angle.degrees(deg) + angle = degrees(deg) point = polar(float("inf"), angle) assert empty.density(point) == 0 assert whole.density(point) == 1 diff --git a/tests/bool2d/test_transform.py b/tests/bool2d/test_transform.py index 3172e86e..70f0c477 100644 --- a/tests/bool2d/test_transform.py +++ b/tests/bool2d/test_transform.py @@ -5,7 +5,7 @@ from shapepy.bool2d.primitive import Primitive from shapepy.bool2d.shape import ConnectedShape, DisjointShape from shapepy.geometry.box import Box -from shapepy.scalar.angle import Angle +from shapepy.scalar.angle import degrees @pytest.mark.order(24) @@ -46,7 +46,7 @@ def test_rotate_simple(): square = Primitive.square(2) assert square.box() == Box((-1, -1), (1, 1)) - angle = Angle.degrees(90) + angle = degrees(90) square.rotate(angle) assert square.box() == Box((-1, -1), (1, 1)) @@ -83,7 +83,7 @@ def test_rotate_connected(): connected = ConnectedShape([big_square, ~small_square]) assert connected.box() == Box((-2, -2), (2, 2)) - angle = Angle.degrees(90) + angle = degrees(90) connected.rotate(angle) assert connected.box() == Box((-2, -2), (2, 2)) @@ -120,7 +120,7 @@ def test_rotate_disjoint(): disjoint = DisjointShape([left_square, right_square]) assert disjoint.box() == Box((-3, -1), (3, 1)) - angle = Angle.degrees(90) + angle = degrees(90) disjoint.rotate(angle) assert disjoint.box() == Box((-1, -3), (1, 3)) diff --git a/tests/geometry/test_jordan_polygon.py b/tests/geometry/test_jordan_polygon.py index 494c09f0..4aa56975 100644 --- a/tests/geometry/test_jordan_polygon.py +++ b/tests/geometry/test_jordan_polygon.py @@ -10,7 +10,7 @@ from shapepy.geometry.factory import FactoryJordan from shapepy.geometry.integral import lebesgue_density_jordan from shapepy.geometry.jordancurve import clean_jordan -from shapepy.scalar.angle import Angle +from shapepy.scalar.angle import degrees, radians @pytest.mark.order(15) @@ -273,9 +273,9 @@ def test_rotate(self): test_square = FactoryJordan.polygon(test_square_pts) assert test_square == good_square - test_square.rotate(Angle.radians(np.pi / 6)) # 30 degrees + test_square.rotate(radians(np.pi / 6)) # 30 degrees assert test_square != good_square - test_square.rotate(Angle.degrees(60)) + test_square.rotate(degrees(60)) assert test_square == good_square @pytest.mark.order(15) diff --git a/tests/geometry/test_point.py b/tests/geometry/test_point.py index 5ecdd8c4..a45dfef9 100644 --- a/tests/geometry/test_point.py +++ b/tests/geometry/test_point.py @@ -7,7 +7,7 @@ import pytest from shapepy.geometry.point import cartesian, cross, inner, polar -from shapepy.scalar.angle import Angle +from shapepy.scalar.angle import degrees @pytest.mark.order(11) @@ -56,8 +56,7 @@ def test_creation_finite_cartesian(): @pytest.mark.dependency(depends=["test_begin"]) def test_creation_finite_polar(): radius = 0 - for degrees in range(0, 360, 45): - angle = Angle.degrees(degrees) + for angle in map(degrees, range(0, 360, 45)): point = polar(radius, angle) assert point.xcoord == 0 assert point.ycoord == 0 @@ -65,8 +64,7 @@ def test_creation_finite_polar(): assert point.angle == 0 radius = 10 - for degrees in range(0, 360, 45): - angle = Angle.degrees(degrees) + for angle in map(degrees, range(0, 360, 45)): point = polar(radius, angle) assert point.xcoord == radius * angle.cos() assert point.ycoord == radius * angle.sin() @@ -85,49 +83,49 @@ def test_creation_infinite_cartesian(): assert point.xcoord == posinf assert point.ycoord == 0 assert point.radius == posinf - assert point.angle == Angle.degrees(0) + assert point.angle == degrees(0) point = cartesian(posinf, posinf) assert point.xcoord == posinf assert point.ycoord == posinf assert point.radius == posinf - assert point.angle == Angle.degrees(45) + assert point.angle == degrees(45) point = cartesian(0, posinf) assert point.xcoord == 0 assert point.ycoord == posinf assert point.radius == posinf - assert point.angle == Angle.degrees(90) + assert point.angle == degrees(90) point = cartesian(neginf, posinf) assert point.xcoord == neginf assert point.ycoord == posinf assert point.radius == posinf - assert point.angle == Angle.degrees(135) + assert point.angle == degrees(135) point = cartesian(neginf, 0) assert point.xcoord == neginf assert point.ycoord == 0 assert point.radius == posinf - assert point.angle == Angle.degrees(180) + assert point.angle == degrees(180) point = cartesian(neginf, neginf) assert point.xcoord == neginf assert point.ycoord == neginf assert point.radius == posinf - assert point.angle == Angle.degrees(-135) + assert point.angle == degrees(-135) point = cartesian(0, neginf) assert point.xcoord == 0 assert point.ycoord == neginf assert point.radius == posinf - assert point.angle == Angle.degrees(-90) + assert point.angle == degrees(-90) point = cartesian(posinf, neginf) assert point.xcoord == posinf assert point.ycoord == neginf assert point.radius == posinf - assert point.angle == Angle.degrees(-45) + assert point.angle == degrees(-45) @pytest.mark.order(11) @@ -138,56 +136,52 @@ def test_creation_infinite_polar(): posinf = float("+inf") radius = posinf - point = polar(radius, Angle.degrees(0)) + point = polar(radius, degrees(0)) assert point.xcoord == posinf assert point.ycoord == 0 assert point.radius == posinf - assert point.angle == Angle.degrees(0) + assert point.angle == degrees(0) - point = polar(radius, Angle.degrees(90)) + point = polar(radius, degrees(90)) assert point.xcoord == 0 assert point.ycoord == posinf assert point.radius == posinf - assert point.angle == Angle.degrees(90) + assert point.angle == degrees(90) - point = polar(radius, Angle.degrees(180)) + point = polar(radius, degrees(180)) assert point.xcoord == neginf assert point.ycoord == 0 assert point.radius == posinf - assert point.angle == Angle.degrees(180) + assert point.angle == degrees(180) - point = polar(radius, Angle.degrees(270)) + point = polar(radius, degrees(270)) assert point.xcoord == 0 assert point.ycoord == neginf assert point.radius == posinf - assert point.angle == Angle.degrees(270) + assert point.angle == degrees(270) - for degrees in range(1, 90): - angle = Angle.degrees(degrees) + for angle in map(degrees, range(1, 90)): point = polar(radius, angle) assert point.xcoord == posinf assert point.ycoord == posinf assert point.radius == posinf assert point.angle == angle - for degrees in range(91, 180): - angle = Angle.degrees(degrees) + for angle in map(degrees, range(91, 180)): point = polar(radius, angle) assert point.xcoord == neginf assert point.ycoord == posinf assert point.radius == posinf assert point.angle == angle - for degrees in range(181, 270): - angle = Angle.degrees(degrees) + for angle in map(degrees, range(181, 270)): point = polar(radius, angle) assert point.xcoord == neginf assert point.ycoord == neginf assert point.radius == posinf assert point.angle == angle - for degrees in range(271, 360): - angle = Angle.degrees(degrees) + for angle in map(degrees, range(271, 360)): point = polar(radius, angle) assert point.xcoord == posinf assert point.ycoord == neginf @@ -283,7 +277,7 @@ def test_transformations(): assert id(pointb) == id(pointa) assert pointa == (10, 18) - angle = Angle.degrees(90) + angle = degrees(90) pointb = pointa.rotate(angle) assert id(pointb) == id(pointa) assert pointa == (-18, 10) @@ -403,7 +397,7 @@ def test_print(): assert str(pointa) == "(0, 0)" assert str(pointb) == "(1.0, 1.0)" - pointc = polar(float("inf"), Angle.degrees(15)) + pointc = polar(float("inf"), degrees(15)) assert str(pointc) == "(inf:15 deg)" repr(pointa) diff --git a/tests/geometry/test_usegment.py b/tests/geometry/test_usegment.py index a8b7e6fb..a20b089c 100644 --- a/tests/geometry/test_usegment.py +++ b/tests/geometry/test_usegment.py @@ -7,7 +7,7 @@ from shapepy.geometry.box import Box from shapepy.geometry.factory import FactorySegment from shapepy.geometry.unparam import USegment -from shapepy.scalar.angle import Angle +from shapepy.scalar.angle import degrees @pytest.mark.order(14) @@ -78,8 +78,7 @@ def test_scale(): def test_rotate(): segment = FactorySegment.bezier([(0, 0), (3, 4)]) usegment = USegment(segment) - angle = Angle.degrees(90) - usegment.rotate(angle) + usegment.rotate(degrees(90)) good = FactorySegment.bezier([(0, 0), (-4, 3)]) assert usegment.parametrize() == good diff --git a/tests/scalar/test_angle.py b/tests/scalar/test_angle.py index a96e2053..63f79086 100644 --- a/tests/scalar/test_angle.py +++ b/tests/scalar/test_angle.py @@ -3,7 +3,7 @@ import pytest -from shapepy.scalar.angle import Angle, to_angle +from shapepy.scalar.angle import arg, degrees, radians, to_angle, turns from shapepy.scalar.reals import Math, To @@ -16,9 +16,17 @@ scope="session", ) def test_build_radians(): - Angle.radians(0) - Angle.radians(math.pi) - Angle.radians(2 * math.pi) + radians(-math.pi / 6) + radians(-math.pi / 4) + radians(0) + radians(math.pi / 6) + radians(math.pi / 4) + radians(math.pi / 3) + radians(2 * math.pi / 6) + radians(math.pi / 2) + radians(math.pi) + radians(3 * math.pi / 2) + radians(2 * math.pi) @pytest.mark.order(2) @@ -30,11 +38,11 @@ def test_build_radians(): scope="session", ) def test_build_degrees(): - Angle.radians(0) - Angle.radians(90) - Angle.radians(180) - Angle.radians(270) - Angle.radians(360) + degrees(0) + degrees(90) + degrees(180) + degrees(270) + degrees(360) @pytest.mark.order(2) @@ -46,15 +54,15 @@ def test_build_degrees(): scope="session", ) def test_build_turns(): - Angle.turns(0) - Angle.turns(0.125) - Angle.turns(0.250) - Angle.turns(0.375) - Angle.turns(0.500) - Angle.turns(0.625) - Angle.turns(0.750) - Angle.turns(0.875) - Angle.turns(1.000) + turns(0) + turns(0.125) + turns(0.250) + turns(0.375) + turns(0.500) + turns(0.625) + turns(0.750) + turns(0.875) + turns(1.000) @pytest.mark.order(2) @@ -62,24 +70,24 @@ def test_build_turns(): @pytest.mark.dependency( depends=["test_build_radians", "test_build_degrees", "test_build_turns"] ) -def test_build_atan2(): +def test_build_arg(): coords = (-2, -1, 0, 1, 2) for xval in coords: for yval in coords: - Angle.atan2(yval, xval) + arg(xval, yval) for xval in coords: - Angle.atan2(xval, Math.NEGINF) - Angle.atan2(xval, Math.POSINF) + arg(xval, Math.NEGINF) + arg(xval, Math.POSINF) for yval in coords: - Angle.atan2(Math.NEGINF, yval) - Angle.atan2(Math.POSINF, yval) + arg(Math.NEGINF, yval) + arg(Math.POSINF, yval) - Angle.atan2(Math.NEGINF, Math.NEGINF) - Angle.atan2(Math.NEGINF, Math.POSINF) - Angle.atan2(Math.POSINF, Math.NEGINF) - Angle.atan2(Math.POSINF, Math.POSINF) + arg(Math.NEGINF, Math.NEGINF) + arg(Math.NEGINF, Math.POSINF) + arg(Math.POSINF, Math.NEGINF) + arg(Math.POSINF, Math.POSINF) @pytest.mark.order(2) @@ -87,130 +95,84 @@ def test_build_atan2(): @pytest.mark.dependency( depends=["test_build_radians", "test_build_degrees", "test_build_turns"] ) -def test_build_arg(): - coords = (-2, -1, 0, 1, 2) - for xval in coords: - for yval in coords: - Angle.arg(xval, yval) - - for xval in coords: - Angle.arg(xval, Math.NEGINF) - Angle.arg(xval, Math.POSINF) - - for yval in coords: - Angle.arg(Math.NEGINF, yval) - Angle.arg(Math.POSINF, yval) - - Angle.arg(Math.NEGINF, Math.NEGINF) - Angle.arg(Math.NEGINF, Math.POSINF) - Angle.arg(Math.POSINF, Math.NEGINF) - Angle.arg(Math.POSINF, Math.POSINF) +def test_directions(): + for degval in range(-134, -45): + assert degrees(degval).direction % 4 == 3 + for degval in range(-44, 45): + assert degrees(degval).direction % 4 == 0 + for degval in range(46, 134): + assert degrees(degval).direction % 4 == 1 + for degval in range(136, 225): + assert degrees(degval).direction % 4 == 2 + for degval in range(226, 315): + assert degrees(degval).direction % 4 == 3 @pytest.mark.order(2) @pytest.mark.timeout(1) -@pytest.mark.dependency( - depends=["test_build_radians", "test_build_degrees", "test_build_turns"] -) +@pytest.mark.dependency(depends=["test_directions"]) def test_compare(): - assert Angle.radians(0) == Angle.degrees(0) - assert Angle.radians(math.pi / 6) == Angle.degrees(30) - assert Angle.radians(math.pi / 4) == Angle.degrees(45) - assert Angle.radians(math.pi / 3) == Angle.degrees(60) - assert Angle.radians(math.pi / 2) == Angle.degrees(90) - assert Angle.radians(math.pi) == Angle.degrees(180) - assert Angle.radians(3 * math.pi / 2) == Angle.degrees(270) - assert Angle.radians(2 * math.pi) == Angle.degrees(0) + assert radians(0) == degrees(0) + assert radians(math.pi / 6) == degrees(30) + assert radians(math.pi / 4) == degrees(45) + assert radians(math.pi / 3) == degrees(60) + assert radians(math.pi / 2) == degrees(90) + assert radians(math.pi) == degrees(180) + assert radians(3 * math.pi / 2) == degrees(270) + assert radians(2 * math.pi) == degrees(0) for deg in range(0, 360, 15): - assert Angle.degrees(deg) == Angle.turns(To.rational(deg, 360)) + assert degrees(deg) == turns(To.rational(deg, 360)) - assert Angle.degrees(0) == 0 - assert Angle.degrees(180) == math.pi - assert Angle.degrees(90) == math.pi / 2 - assert Angle.degrees(270) == -math.pi / 2 - assert float(Angle.degrees(0)) == 0 - assert float(Angle.degrees(180)) == math.pi - assert float(Angle.degrees(90)) == math.pi / 2 - assert float(Angle.degrees(270)) == 3 * math.pi / 2 - str(Angle()) - repr(Angle()) + assert degrees(0) == radians(0) + assert degrees(180) == radians(math.pi) + assert degrees(90) == radians(math.pi / 2) + assert degrees(270) == radians(-math.pi / 2) + assert float(degrees(0)) == 0 + assert float(degrees(180)) == math.pi + assert float(degrees(90)) == math.pi / 2 + assert float(degrees(270)) == 3 * math.pi / 2 @pytest.mark.order(2) @pytest.mark.timeout(1) -@pytest.mark.dependency( - depends=["test_build_radians", "test_build_degrees", "test_build_turns"] -) +@pytest.mark.dependency(depends=["test_directions", "test_compare"]) def test_evaluate_arg(): - assert Angle.arg(0, 0) == Angle.degrees(0) - assert Angle.arg(1, 0) == Angle.degrees(0) - assert Angle.arg(2, 0) == Angle.degrees(0) - assert Angle.arg(1, 1) == Angle.degrees(45) - assert Angle.arg(0, 1) == Angle.degrees(90) - assert Angle.arg(-1, 1) == Angle.degrees(135) - assert Angle.arg(-1, 0) == Angle.degrees(180) - assert Angle.arg(-1, -1) == Angle.degrees(225) - assert Angle.arg(0, -1) == Angle.degrees(270) - assert Angle.arg(1, -1) == Angle.degrees(315) - assert Angle.arg(1, 0) == Angle.degrees(360) + assert arg(0, 0) == degrees(0) + assert arg(1, 0) == degrees(0) + assert arg(2, 0) == degrees(0) + assert arg(1, 1) == degrees(45) + assert arg(0, 1) == degrees(90) + assert arg(-1, 1) == degrees(135) + assert arg(-1, 0) == degrees(180) + assert arg(-1, -1) == degrees(225) + assert arg(0, -1) == degrees(270) + assert arg(1, -1) == degrees(315) + assert arg(1, 0) == degrees(360) NEGINF = Math.NEGINF POSINF = Math.POSINF - assert Angle.arg(POSINF, 0) == Angle.degrees(0) - assert Angle.arg(POSINF, POSINF) == Angle.degrees(45) - assert Angle.arg(0, POSINF) == Angle.degrees(90) - assert Angle.arg(NEGINF, POSINF) == Angle.degrees(135) - assert Angle.arg(NEGINF, 0) == Angle.degrees(180) - assert Angle.arg(NEGINF, NEGINF) == Angle.degrees(225) - assert Angle.arg(0, NEGINF) == Angle.degrees(270) - assert Angle.arg(POSINF, NEGINF) == Angle.degrees(315) - assert Angle.arg(POSINF, 0) == Angle.degrees(360) + assert arg(POSINF, 0) == degrees(0) + assert arg(POSINF, POSINF) == degrees(45) + assert arg(0, POSINF) == degrees(90) + assert arg(NEGINF, POSINF) == degrees(135) + assert arg(NEGINF, 0) == degrees(180) + assert arg(NEGINF, NEGINF) == degrees(225) + assert arg(0, NEGINF) == degrees(270) + assert arg(POSINF, NEGINF) == degrees(315) + assert arg(POSINF, 0) == degrees(360) @pytest.mark.order(2) @pytest.mark.timeout(1) -@pytest.mark.dependency( - depends=["test_build_radians", "test_build_degrees", "test_build_turns"] -) -def test_evaluate_atan2(): - assert Angle.atan2(0, 0) == Angle.degrees(0) - assert Angle.atan2(0, 1) == Angle.degrees(0) - assert Angle.atan2(0, 2) == Angle.degrees(0) - assert Angle.atan2(1, 1) == Angle.degrees(45) - assert Angle.atan2(1, 0) == Angle.degrees(90) - assert Angle.atan2(1, -1) == Angle.degrees(135) - assert Angle.atan2(0, -1) == Angle.degrees(180) - assert Angle.atan2(-1, -1) == Angle.degrees(225) - assert Angle.atan2(-1, 0) == Angle.degrees(270) - assert Angle.atan2(-1, 1) == Angle.degrees(315) - assert Angle.atan2(0, 1) == Angle.degrees(360) - - NEGINF = Math.NEGINF - POSINF = Math.POSINF - assert Angle.atan2(0, POSINF) == Angle.degrees(0) - assert Angle.atan2(POSINF, POSINF) == Angle.degrees(45) - assert Angle.atan2(POSINF, 0) == Angle.degrees(90) - assert Angle.atan2(POSINF, NEGINF) == Angle.degrees(135) - assert Angle.atan2(0, NEGINF) == Angle.degrees(180) - assert Angle.atan2(NEGINF, NEGINF) == Angle.degrees(225) - assert Angle.atan2(NEGINF, 0) == Angle.degrees(270) - assert Angle.atan2(NEGINF, POSINF) == Angle.degrees(315) - assert Angle.atan2(0, POSINF) == Angle.degrees(360) - - -@pytest.mark.order(2) -@pytest.mark.timeout(1) -@pytest.mark.dependency( - depends=["test_build_radians", "test_build_degrees", "test_build_turns"] -) +@pytest.mark.dependency(depends=["test_directions"]) def test_evaluate_sincos(): degs = (0, 90, 180, 270, 360) coss = (1, 0, -1, 0, 1) sins = (0, 1, 0, -1, 0) for deg, cos, sin in zip(degs, coss, sins): - angle = Angle.degrees(deg) + angle = degrees(deg) assert angle.cos() == cos assert angle.sin() == sin @@ -220,30 +182,28 @@ def test_evaluate_sincos(): sins = (a, a, -a, -a) for deg, cos, sin in zip(degs, coss, sins): - angle = Angle.degrees(deg) + angle = degrees(deg) assert abs(angle.cos() - cos) < 1e-15 assert abs(angle.sin() - sin) < 1e-15 @pytest.mark.order(2) @pytest.mark.timeout(1) -@pytest.mark.dependency( - depends=["test_build_radians", "test_build_degrees", "test_build_turns"] -) +@pytest.mark.dependency(depends=["test_directions", "test_compare"]) def test_evaluate_operations(): random.seed(0) for _ in range(1000): a = random.randint(0, 720) b = random.randint(0, 720) - anglea = Angle.degrees(a) - angleb = Angle.degrees(b) + anglea = degrees(a) + angleb = degrees(b) - anglec = Angle.degrees(a + b) + anglec = degrees(a + b) angled = anglea + angleb assert angled == anglec - anglec = Angle.degrees(a - b) + anglec = degrees(a - b) angled = anglea - angleb assert angled == anglec @@ -252,21 +212,33 @@ def test_evaluate_operations(): @pytest.mark.order(2) @pytest.mark.timeout(1) -@pytest.mark.dependency( - depends=["test_build_radians", "test_build_degrees", "test_build_turns"] -) +@pytest.mark.dependency(depends=["test_directions", "test_compare"]) def test_convert(): + assert to_angle("90deg") == degrees(90) + assert to_angle("0.25tur") == turns(0.25) + assert to_angle("1.25rad") == radians(1.25) - assert to_angle("90deg") == Angle.degrees(90) - assert to_angle("0.25tur") == Angle.turns(0.25) - assert to_angle("1.25rad") == Angle.radians(1.25) - - assert to_angle(1.25) == Angle.radians(1.25) + assert to_angle(1.25) == radians(1.25) - anglea = Angle.degrees(30) + anglea = degrees(30) assert to_angle(anglea) is anglea +@pytest.mark.order(2) +@pytest.mark.timeout(1) +@pytest.mark.dependency(depends=["test_directions", "test_compare"]) +def test_print(): + assert str(degrees(0)) == "0 deg" + assert str(degrees(90)) == "90 deg" + assert str(degrees(180)) == "180 deg" + assert str(degrees(270)) == "270 deg" + + assert repr(degrees(0)) == "Angle(0, 0)" + assert repr(degrees(90)) == "Angle(1, 0)" + assert repr(degrees(180)) == "Angle(2, 0)" + assert repr(degrees(270)) == "Angle(3, 0)" + + @pytest.mark.order(2) @pytest.mark.timeout(1) @pytest.mark.dependency( @@ -274,14 +246,14 @@ def test_convert(): "test_build_radians", "test_build_degrees", "test_build_turns", - "test_build_atan2", "test_build_arg", + "test_directions", "test_compare", "test_evaluate_arg", - "test_evaluate_atan2", "test_evaluate_sincos", "test_evaluate_operations", "test_convert", + "test_print", ] ) def test_all():