Source code for continuedfractions.rational_points

from __future__ import annotations


__all__ = [
    'Dim2RationalCoordinates',
    'Dim3RationalCoordinates',
    'HomogeneousCoordinates',
    'RationalPoint',
    'RationalTuple',
]

# -- IMPORTS --

# -- Standard libraries --
import decimal
import math
import numbers
import typing

from decimal import Decimal
from itertools import pairwise
from typing import Any

# -- 3rd party libraries --

# -- Internal libraries --
from continuedfractions.continuedfraction import Fraction, ContinuedFraction


[docs] class RationalTuple(tuple): """A rational-valued tuple consisting of one or more :py:class:`numbers.Rational` values. The class serves as a generic base for other custom rational-valued tuple types defined in this library: * :py:class:`~continuedfractions.rational_points.Dim2RationalCoordinates` * :py:class:`~continuedfractions.rational_points.Dim3RationalCoordinates` * :py:class:`~continuedfractions.rational_points.HomogeneousCoordinates` * :py:class:`~continuedfractions.rational_points.RationalPoint` It does not contain any mathematical structure. That is left to the other custom types listed above. Examples -------- >>> from fractions import Fraction as F >>> from continuedfractions.continuedfraction import ContinuedFraction as CF >>> RationalTuple(F(1, 2), CF(3, 4)) RationalTuple(1/2, 3/4) """ def __new__(cls, *args: numbers.Rational) -> RationalTuple: """Constructor. """ if args is None or any(not isinstance(arg, numbers.Rational) for arg in args): raise ValueError( 'One or more rational-valued arguments are required.' ) return super().__new__(cls, args) def __repr__(self) -> str: return f'{self.__class__.__name__}({", ".join(map(str, self))})'
[docs] def scale(self, scalar: numbers.Rational) -> RationalTuple: """:py:class:`numbers.Rational` : A new scaled instance of the rational tuple by a rational scalar. Parameters ---------- scalar : numbers.Rational The (rational) scalar with which to scale the rational tuple. Returns ------- RationalTuple The scaled rational tuple. Examples -------- >>> RationalTuple(1, Fraction(-2, 3), ContinuedFraction(4, 5)).scale(15) RationalTuple(15, -10, 12) >>> RationalTuple(1, Fraction(-2, 3), ContinuedFraction(4, 5)).scale(Fraction(1, 2)) RationalTuple(1/2, -1/3, 2/5) >>> RationalTuple(1, Fraction(-2, 3), ContinuedFraction(4, 5)).scale(ContinuedFraction(-5, 6)) RationalTuple(-5/6, 5/9, -2/3) """ if not isinstance(scalar, numbers.Rational): raise ValueError('A rational scalar is required for scaling.') # pragma: no cover return self.__class__(*(scalar * x for x in self))
[docs] class Dim2RationalCoordinates(RationalTuple): """A simple :py:class:`tuple` subtype for a sequence of two rational coordinates representing a point in :math:`\\mathbb{Q}^2`. Examples -------- >>> from fractions import Fraction as F >>> from continuedfractions.continuedfraction import ContinuedFraction as CF >>> c = Dim2RationalCoordinates(F(1, 2), F(3, 4)) >>> c Dim2RationalCoordinates(1/2, 3/4) >>> c.x Fraction(1, 2) >>> c.y Fraction(3, 4) """ def __new__(cls, x: numbers.Rational, y: numbers.Rational) -> Dim2RationalCoordinates: """Constructor. """ return super().__new__(cls, x, y) @property def x(self) -> numbers.Rational: """:py:class:`numbers.Rational` : The ``x``-coordinate. Returns ------- numbers.Rational The :math:`x`-coordinate. Examples -------- >>> Dim2RationalCoordinates(1, Fraction(3, 4)).x 1 >>> Dim2RationalCoordinates(Fraction(1, 2), Fraction(3, 4)).x Fraction(1, 2) >>> Dim2RationalCoordinates(ContinuedFraction(1, 2), Fraction(3, 4)).x ContinuedFraction(1, 2) """ return self[0] @property def y(self) -> numbers.Rational: """:py:class:`numbers.Rational` : The ``y``-coordinate. Returns ------- numbers.Rational The :math:`y`-coordinate. Examples -------- >>> Dim2RationalCoordinates(Fraction(1, 2), 3).y 3 >>> Dim2RationalCoordinates(Fraction(1, 2), Fraction(3, 4)).y Fraction(3, 4) >>> Dim2RationalCoordinates(Fraction(1, 2), ContinuedFraction(3, 4)).y ContinuedFraction(3, 4) """ return self[1]
[docs] class Dim3RationalCoordinates(RationalTuple): """A simple :py:class:`tuple` subtype for a sequence of three rational coordinates representing a point in :math:`\\mathbb{Q}^3`. Examples -------- >>> from fractions import Fraction as F >>> from continuedfractions.continuedfraction import ContinuedFraction as CF >>> c = Dim3RationalCoordinates(1, F(-2, 3), CF(4, 5)) >>> c Dim3RationalCoordinates(1, -2/3, 4/5) >>> c.x 1 >>> c.y Fraction(-2, 3) >>> c.z ContinuedFraction(4, 5) """ def __new__(cls, x: numbers.Rational, y: numbers.Rational, z: numbers.Rational) -> Dim3RationalCoordinates: """Constructor. """ return super().__new__(cls, x, y, z) @property def x(self) -> numbers.Rational: """:py:class:`numbers.Rational` : The ``x``-coordinate. Returns ------- numbers.Rational The :math:`x`-coordinate. Examples -------- >>> Dim3RationalCoordinates(1, Fraction(2, 3), ContinuedFraction(3, 4)).x 1 >>> Dim3RationalCoordinates(Fraction(1, 2), 3, ContinuedFraction(4, 5)).x Fraction(1, 2) >>> Dim3RationalCoordinates(ContinuedFraction(1, 2), Fraction(3, 4), 5).x ContinuedFraction(1, 2) """ return self[0] @property def y(self) -> numbers.Rational: """:py:class:`numbers.Rational` : The ``y``-coordinate. Returns ------- numbers.Rational The :math:`y`-coordinate. Examples -------- >>> Dim3RationalCoordinates(Fraction(1, 2), 3, ContinuedFraction(4, 5)).y 3 >>> Dim3RationalCoordinates(1, Fraction(2, 3), ContinuedFraction(4, 5)).y Fraction(2, 3) >>> Dim3RationalCoordinates(1, ContinuedFraction(2, 3), Fraction(4, 5)).y ContinuedFraction(2, 3) """ return self[1] @property def z(self) -> numbers.Rational: """:py:class:`numbers.Rational` : The ``z``-coordinate. Returns ------- numbers.Rational The :math:`z`-coordinate. Examples -------- >>> Dim3RationalCoordinates(Fraction(1, 2), ContinuedFraction(3, 4), 5).z 5 >>> Dim3RationalCoordinates(1, ContinuedFraction(2, 3), Fraction(4, 5)).z Fraction(4, 5) >>> Dim3RationalCoordinates(1, Fraction(2, 3), ContinuedFraction(4, 5)).z ContinuedFraction(4, 5) """ return self[2]
[docs] class HomogeneousCoordinates(Dim3RationalCoordinates): """A class to represent homogeneous coordinates for rational points in projective space :math:`\\mathbb{P}^2(\\mathbb{Q})`. Examples -------- >>> from fractions import Fraction as F >>> h = HomogeneousCoordinates(3, 4, 5) >>> h = h.scale(F(1, 5)) >>> h HomogeneousCoordinates(3/5, 4/5, 1) >>> h.to_rational_point() RationalPoint(3/5, 4/5) """
[docs] def to_rational_point(self) -> RationalPoint: """:py:class:`~continuedfractions.rational_points.RationalPoint` : Returns a new rational point from this sequence of homogeneous coordinates. Returns ------- RationalPoint A new rational point from this sequence of homogeneous coordinates. Examples -------- >>> HomogeneousCoordinates(3, 4, 5).to_rational_point() RationalPoint(3/5, 4/5) """ return RationalPoint(Fraction(self.x, self.z), Fraction(self.y, self.z))
[docs] class RationalPoint(Dim2RationalCoordinates): """A simple class for representing and operating on rational points as points in the :math:`xy`-plane. These are points in :math:`\\mathbb{R}^2` whose coordinates are rational i.e. elements of :math:`\\mathbb{Q}`, and thus are points of :math:`\\mathbb{Q}^2`. The points form an abelian group under component-wise addition, and form a closed set under scalar left-multiplication by rational numbers. The scalar left-multiplication is distributive over sums of rational points. This implemention of rational points is based essentially on a custom implementation :py:class:`~continuedfractions.rational_points.RationalTuple` of :py:class:`tuple`, with exactly two rational-valued members: in Python this means the arguments must be instances of :py:class:`~numbers.Rational`, which includes values of type: * :py:class:`int` * :py:class:`~fractions.Fraction` * :py:class:`~continuedfractions.continuedfraction.ContinuedFraction` Internally, the rational coordinates of the point are stored as :py:class:`~continuedfractions.continuedfraction.ContinuedFraction` objects. Examples -------- >>> RationalPoint(Fraction(3, 5), Fraction(4, 5)) RationalPoint(3/5, 4/5) >>> RationalPoint(3, ContinuedFraction(4, 5)) RationalPoint(3, 4/5) >>> RationalPoint(Fraction(3, 5), ContinuedFraction(4)) RationalPoint(3/5, 4) >>> RationalPoint(-3, 4) RationalPoint(-3, 4) >>> RationalPoint(2, .5) Traceback (most recent call last): ... ValueError: A `RationalPoint` object must be specified as a pair of rational numbers `r` and `s`, each of type either integer (`int`), or fraction (`Fraction` or `ContinuedFraction`). """ def __new__(cls, x: int | Fraction | ContinuedFraction, y: int | Fraction | ContinuedFraction) -> RationalPoint: """Constructor. """ if not isinstance(x, (int, Fraction, ContinuedFraction)) or not isinstance(y, (int, Fraction, ContinuedFraction)): raise ValueError( 'A `RationalPoint` object must be specified as a pair of ' 'rational numbers `r` and `s`, each of type either integer ' '(`int`), or fraction (`Fraction` or `ContinuedFraction`).' ) return super().__new__(cls, ContinuedFraction(x), ContinuedFraction(y))
[docs] @classmethod def zero(cls) -> RationalPoint: """:py:class:`~continuedfractions.rational_points.RationalPoint` : The zero rational point. Returns ------- RationalPoint The zero rational point. Examples -------- >>> RationalPoint.zero() RationalPoint(0, 0) """ return cls(0, 0)
[docs] @classmethod def sum(cls, *rational_points: RationalPoint) -> RationalPoint: """:py:class:`~continuedfractions.rational_points.RationalPoint` : The sum of a variable number of rational points. This is designed as a helper method for rational point summation because the built-in :py:func:`sum` function only works for :py:class:`~continuedfractions.rational_points.RationalPoint` instances if the ``start`` value is set to ``RP(0, 0)``, which many users may not be aware of. Parameters ---------- rational_points : typing.Iterable A variable number of :py:class:`~continuedfractions.rational_points.RationalPoint` instances to add. Returns ------- RationalPoint The sum of the rational points. Examples -------- >>> from fractions import Fraction as F >>> from continuedfractions.rational_points import RationalPoint as RP >>> RP.sum(RP(-1, 1), RP(1, -1)) RationalPoint(0, 0) >>> RP.sum(RP(1, 1), RP(2, 1), RP(3, 1)) RationalPoint(6, 3) >>> RP.sum(RP(0, 0), RP(1, F(-1, 2)), RP(F(3, 5), F(4, 5)), RP(F(5, 12), 6)) RationalPoint(121/60, 63/10) """ return sum(rational_points, start=cls.zero())
@property def coordinates(self) -> Dim2RationalCoordinates: """ :py:class:`~continuedfractions.rational_points.Dim2RationalCoordinates` : The pair of (rational) coordinates of the rational point. Returns ------- tuple The pair of coordinates of the rational point as a tuple of :py:class:`~continuedfractions.continuedfraction.ContinuedFraction` instances. Examples -------- >>> P = RationalPoint(Fraction(1, 2), Fraction(-3, 4)) >>> P.coordinates Dim2RationalCoordinates(1/2, -3/4) """ return Dim2RationalCoordinates(*self)
[docs] def gradient(self, /, *, other: RationalPoint = None) -> ContinuedFraction: """:py:class:`~continuedfractions.continuedfraction.ContinuedFraction` : Computes the gradient (slope) of the line passing through this rational point and either the origin :math:`(0, 0)` (default) or another point`. If no second rational point is provided the gradient of this rational point is computed with respect to the origin :math:`(0, 0)`: if this rational point has a zero :math:`x`-coordinate, i.e. falls on the :math:`y`-axis, then a :py:class:`None` value will be returned, to indicate that the result is undefined. If another rational point :math:`P' = (x', y')` (as represented by ``other``) is provided then the gradient is computed as: .. math:: \\frac{y' - y}{x' - x}, \\hskip{3em} x' \\neq x where :math:`P = (x, y)` is this rational point (as represented by ``self``). If no non-zero second rational point is provided the gradient is computed with respect to the origin: .. math:: \\frac{y}{x}, \\hskip{3em} x \\neq 0 As the gradient of a vertical line is infinite (or undefined) the method raises a :py:class:`ValueError`, which occurs whenver the second rational point is vertical with respect to this point, i.e. the two points have the same :math:`x`-coordinate. Parameters ---------- other : RationalPoint, default=None An optional second rational point with respect to which the gradient is computed. Returns ------- ContinuedFraction The gradient (slope) of the line connecting this rational point and either the origin :math:`(0, 0)` (default) or another point. Raises ------ ValueError If ``other`` is not a rational point or the point it represents is vertical with respect to this rational point. Examples -------- >>> from fractions import Fraction as F >>> from continuedfractions.rational_points import RationalPoint as RP >>> RP(1, 1).gradient() ContinuedFraction(1, 1) >>> RP(1, 1).gradient(other=RP(2, 1)) ContinuedFraction(0, 1) >>> RP(0, 1).gradient(other=RP(1, 0)) ContinuedFraction(-1, 1) >>> RP(0, 1).gradient() >>> RP(1, 1).gradient(other=RP(1, 2)) Traceback (most recent call last): ... ValueError: If a second rational point is provided, it must be a `RationalPoint` instance, and non-vertical with respect to this point. """ if other is None: return ContinuedFraction(self.y, self.x) if self.x != 0 else None if not isinstance(other, RationalPoint) or other.x == self.x: raise ValueError( 'If a second rational point is provided, it must be a ' '`RationalPoint` instance, and non-vertical with respect ' 'to this point.' ) return ContinuedFraction(other.y - self.y, other.x - self.x)
[docs] def collinear_with(self, *rational_points: RationalPoint) -> bool: """:py:class:`bool` : Tests whether this rational point is collinear with one or more rational points. The collinearity test for three points :math:`P_1 = (x_1, y_1)`, :math:`P_2 = (x_2, y_2)`, and :math:`P_3 = (x_3, y_3)` uses the gradient method: .. math:: \\frac{y_2 - y_1}{x_2 - x_1} = \\frac{y_3 - y_2}{x_3 - x_2}, \\hskip{3em} x_2 \\neq x_1; x_3 \\neq x_2 which can be rearranged as the equation: .. math:: (y_3 - y_2)(x_2 - x_1) - (y_2 - y_1)(x_3 - x_2) = 0 Note that the other rational points do not need to be given in any particular order. Parameters ---------- rational_points : RationalPoint One or more rational points to test for collinearity with this point. Returns ------- bool Whether this point is collinear with the given rational points. Examples -------- >>> from fractions import Fraction as F >>> from continuedfractions.rational_points import RationalPoint as RP >>> RP(1, 1).collinear_with(RP(2, 2), RP(F(-1, 2), F(-1, 2))) True >>> RP(1, 1).collinear_with(RP(2, 2), RP(F(-1, 2), F(-1, 2)), RP(-3, -3)) True >>> RP(1, 1).collinear_with(RP(-2, -2), RP(F(1, 2), F(1, 2)), RP(1, 2)) False >>> RP(1, 1).collinear_with(RP(F(-1, 2), F(-1, 2))) True >>> RP(1, 1).collinear_with(RP(2, 1)) True """ if any(not isinstance(P, self.__class__) for P in rational_points): raise ValueError( 'One or more non-`RationalPoint` instances detected. Please ' 'check the inputs and try again.' ) if len(rational_points) == 1: return True def collinear(P1: RationalPoint, P2: RationalPoint, P3: RationalPoint, /) -> bool: """Inner function to test collinearity for exactly three rational points, including this one (``self``). """ if (P1 == P2) or (P1 == P3) or (P2 == P3): return True x1, y1, x2, y2, x3, y3 = (*P1, *P2, *P3) return (y3 - y2) * (x2 - x1) - (y2 - y1) * (x3 - x2) == 0 for Q, R in pairwise(rational_points): if not collinear(self, Q, R): return False return True
[docs] def collinear_with_origin(self, *rational_points: RationalPoint) -> bool: """:py:class:`bool` : Tests whether this rational point is collinear with one or more rational points and the origin :math:`(0, 0)`. This is to allow easier checking of collinearity of points falling on a line passing through the origin. Note that the other rational points do not need to be given in any particular order. Parameters ---------- rational_points : RationalPoint One or more rational points to test for collinearity with this point and the origin :math:`(0, 0)` Returns ------- bool Whether this point is collinear with the given rational points and the origin :math:`(0, 0)`. Examples -------- >>> from fractions import Fraction as F >>> from continuedfractions.rational_points import RationalPoint as RP >>> RP(1, 1).collinear_with_origin(RP(2, 2), RP(F(-1, 2), F(-1, 2))) True >>> RP(1, 1).collinear_with_origin(RP(2, 2), RP(F(-1, 2), F(1, 2))) False >>> RP(1, 0).collinear_with_origin(RP(-1, 0), RP(F(1, 2), F(1, 2))) False >>> RP(1, 0).collinear_with_origin(RP(-1, 0), RP(F(1, 2), 0)) True """ if any(not isinstance(P, self.__class__) for P in rational_points): raise ValueError( 'One or more non-`RationalPoint` instances detected. Please ' 'check the inputs and try again.' ) return self.collinear_with(self.zero(), *rational_points)
[docs] def angle(self, /, *, other: RationalPoint = None, as_degrees: bool = False) -> Decimal: """:py:class:`~decimal.Decimal`: The radian (or degree) angle between this rational point, as a position vector in :math:`\\mathbb{Q}^2`, and either another rational point or the positive :math:`x`-axis. If another rational point :math:`P'` (as represented by ``other``) is provided, the computed angle is that between the position vector of this rational point :math:`P = (x, y)` (as represented by ``self``) and the other, as given by: .. math:: \\alpha = \\text{arccos}\\left( \\frac{P \\cdot P'}{\\|P\\|\\|P'\\|} \\right) If no other rational point is provided the computed angle is that between the position vector of this rational point and the positive :math:`x`-axis, as given by: .. math:: \\alpha = \\text{atan2}\\left(\\frac{y}{x}\\right) where :math:`\\text{atan2}` refers to the :math:`\\text{arctan}` extension that uses both :math:`x`- and :math:`y`-coordinates of a plane point :math:`P = (x, y)`, as implemented by :py:func:`math.atan2`. For reference any standard book on trigonometry or plane geometry should contain a definition. The optional ``as_degrees`` boolean can be used to return the angle in degrees. Parameters ---------- as_degrees : bool, default=False Whether to return the angle in degrees. Returns ------- decimal.Decimal The angle between this rational point, as a position vector in :math:`\\mathbb{Q}^2`, and either another rational point, if provided, or the positive :math:`x`-axis. Examples -------- >>> from continuedfractions.rational_points import RationalPoint as RP >>> RP(1, 0).angle() Decimal('0') >>> RP(1, 0).angle(as_degrees=True) Decimal('0') >>> RP(1, 1).angle() Decimal('0.78539816339744827899949086713604629039764404296875') >>> RP(1, 1).angle(as_degrees=True) Decimal('45') >>> RP(1, 1).angle(other=RP(0, 1)) Decimal('0.78539816339744827899949086713604629039764404296875') >>> RP(1, 1).angle(other=RP(0, 1), as_degrees=True) Decimal('45') """ if other and other == self: angle = Decimal('0') elif other and other != self: angle = Decimal(math.acos(self.dot(other).as_decimal() / (self.norm * other.norm))) else: angle = Decimal(math.atan2(self.y, self.x)) if not as_degrees: return angle return Decimal(math.degrees(angle))
[docs] def orthogonal(self) -> RationalPoint: """:py:class:`~continuedfractions.rational_points.RationalPoint` : Returns a rational point whose position vector is orthogonal to that of the original point. This is described by the linear transformation: .. math:: \\begin{bmatrix}0 & -1 \\\\1 & 0 \\end{bmatrix} \\begin{bmatrix}\\frac{a}{c} \\\\\\frac{b}{d}\\end{bmatrix} = \\begin{bmatrix} -\\frac{b}{d} \\\\ \\frac{a}{c} \\end{bmatrix} for points :math:`P = \\left(\\frac{a}{c}, \\frac{b}{d}\\right) \\in \\mathbb{Q}^2`, and has the property that :math:`P \\cdot P^{\\perp} = P^{\\perp} \\cdot P = 0` where :math:`\\perp` is the orthogonality relation.. Returns ------- RationalPoint The "orthogonal" of the rational point as defined above. Examples -------- >>> from fractions import Fraction as F >>> from continuedfractions.rational_points import RationalPoint as RP >>> RP(F(1, 2), F(3, 4)).orthogonal() RationalPoint(-3/4, 1/2) >>> RP(1, -2).orthogonal() RationalPoint(2, 1) >>> RP(1, -2).orthogonal().dot(RP(1, -2)) ContinuedFraction(0, 1) """ return self.__class__(-self.y, self.x)
[docs] def permute(self) -> RationalPoint: """:py:class:`~continuedfractions.rational_points.RationalPoint` : Returns a new rational point by swapping the original coordinates. This is described by the linear transformation: .. math:: \\begin{bmatrix}0 & 1 \\\\1 & 0 \\end{bmatrix} \\begin{bmatrix}\\frac{a}{c} \\\\\\frac{b}{d}\\end{bmatrix} = \\begin{bmatrix} \\frac{b}{d} \\\\ \\frac{a}{c} \\end{bmatrix} for points :math:`P = \\left(\\frac{a}{c}, \\frac{b}{d}\\right) \\in \\mathbb{Q}^2`. Returns ------- RationalPoint The permuted rational point with the original coordinates swapped. Examples -------- >>> from continuedfractions.rational_points import RationalPoint as RP >>> RP(1, 2).permute() RationalPoint(2, 1) """ return self.__class__(self.y, self.x)
[docs] def translate(self, /, *, x_by: int | Fraction | ContinuedFraction = 0, y_by: int | Fraction | ContinuedFraction = 0) -> RationalPoint: """:py:class:`~continuedfractions.rational_points.RationalPoint` : Returns a new rational point obtained by translating the original in the :math:`x`- and/or :math:`y`-coordinates by rational scalars. An affine transformation which implements the mapping: .. math:: \\left(\\left(\\frac{a}{c},\\frac{b}{d}\\right), \\lambda, \\mu\\right) \\longmapsto \\left(\\frac{a}{c} + \\lambda, \\frac{b}{d} + \\mu\\right) for rational points :math:`\\left(\\frac{a}{c}, \\frac{b}{d}\\right) \\in \\mathbb{Q}^2` and rational scalars :math:`\\lambda, \\mu \\in \\mathbb{Q}`. This will not be a linear transformation as the origin :math:`(0, 0)` of :math:`\\mathbb{Q}^2` wil be moved for any non-zero scalars. Parameters ---------- x_by : int or Fraction or ContinuedFraction, default=0 The optional parameter for translating the :math:`x`-coordinate, with a default value of :math:`0`. Must be a rational value. y_by : int or Fraction or ContinuedFraction, default=0 The optional parameter for translating the :math:`y`-coordinate, with a default value of :math:`0`. Must be a rational value. Returns ------- RationalPoint A new rational point translated from the original using the given coordinate translation parameters. Raises ------ ValueError If the coordinate translation parameters are not of the expected type. Examples -------- >>> from fractions import Fraction as F >>> from continuedfractions.rational_points import RationalPoint as RP >>> P = RP(F(1, 2), F(-3, 4)); P RationalPoint(1/2, -3/4) >>> P.translate(x_by=F(-1, 4), y_by=2) RationalPoint(1/4, 5/4) >>> P.translate(x_by=-.5) Traceback (most recent call last): ... ValueError: The coordinate translation parameters must be of type `int`, `fractions.Fraction` or `ContinuedFraction`. """ if not (isinstance(x_by, (int, Fraction, ContinuedFraction)) and isinstance(y_by, (int, Fraction, ContinuedFraction))): raise ValueError( 'The coordinate translation parameters must be of type `int`, ' '`fractions.Fraction` or `ContinuedFraction`.' ) return self.__class__(self.x + x_by, self.y + y_by)
[docs] def reflect(self, axis: typing.Literal['x', 'y']) -> RationalPoint: """:py:class:`~continuedfractions.rational_points.RationalPoint` : Returns a new rational point obtained by reflecting the original in the :math:`x`- or :math:`y`-axis. Implements linear transformations given by the mappings: .. math:: \\left(\\frac{a}{c}, \\frac{b}{d} \\right) \\longmapsto \\left(\\frac{a}{c}, -\\frac{b}{d}\\right) for reflection in the :math:`x`-axis, and: .. math:: \\left(\\frac{a}{c}, \\frac{b}{d} \\right) \\longmapsto \\left(-\\frac{a}{c}, \\frac{b}{d}\\right) for reflection in the :math:`y`-axis, with matrices :math:`\\begin{bmatrix}1 & 0\\\\0 & -1\\end{bmatrix}`, and :math:`\\begin{bmatrix}-1 & 0\\\\0 & 1\\end{bmatrix}` respectively. Parameters ---------- axis : str The axis of reflection: should be a string literal which is either ``"x"`` or ``"y"``. Returns ------- RationalPoint A new rational point reflected from the original in the given axis. Raises ------ ValueError If the axis is invalid or incorrectly specified. Examples -------- >>> from fractions import Fraction as F >>> from continuedfractions.rational_points import RationalPoint as RP >>> P = RP(1, 1) >>> P.reflect(axis='x') RationalPoint(1, -1) >>> P.reflect(axis='y') RationalPoint(-1, 1) >>> P.reflect(axis="X") Traceback (most recent call last): ... ValueError: The axis of reflection must be a string literal which is either "x" or "y". """ if not (isinstance(axis, str) and axis in ['x', 'y']): raise ValueError( 'The axis of reflection must be a string literal which is ' 'either "x" or "y".' ) return self.__class__(self.x, -self.y) if axis == 'x' else self.__class__(-self.x, self.y)
[docs] def dot(self, other: RationalPoint, /) -> ContinuedFraction: """:py:class:`~continuedfractions.continuedfraction.ContinuedFraction` : The dot product of two rational points as position vectors in :math:`\\mathbb{Q}^2`. If :math:`P = \\left( \\frac{a}{c}, \\frac{b}{d} \\right)` and :math:`P' = \\left( \\frac{a'}{c'}, \\frac{b'}{d'} \\right)` are two rational points in the plane their dot product :math:`P \\cdot P'` is the rational number: .. math:: \\begin{align} P \\cdot P' &= \\frac{aa'}{cc'} + \\frac{bb'}{dd'} \\\\ &= \\frac{aa'dd' + bb'cc'}{cc'dd'} \\end{align} This value is returned as a :py:class:`~continuedfractions.continuedfraction.ContinuedFraction` object because this is the standard representation of rational numbers in this package. Returns ------- ContinuedFraction The standard Euclidean dot product for two rational points in the plane. Examples -------- >>> P = RationalPoint(Fraction(1, 2), Fraction(3, 4)) >>> Q = RationalPoint(Fraction(1, 3), Fraction(2, 5)) >>> P.dot(Q) ContinuedFraction(7, 15) >>> P.dot(2) Traceback (most recent call last): ... ValueError: The dot product is only defined between `RationalPoint` instances. >>> P.dot(RationalPoint(0, 0)) ContinuedFraction(0, 1) >>> P.dot(RationalPoint(1, 1)) ContinuedFraction(5, 4) """ if not isinstance(other, self.__class__): raise ValueError( 'The dot product is only defined between `RationalPoint` ' 'instances.' ) if self.coordinates == (0, 0) or other.coordinates == (0, 0): return ContinuedFraction(0) return (self.x * other.x) + (self.y * other.y)
[docs] def det(self, other: RationalPoint, /) -> ContinuedFraction: """:py:class:`~continuedfractions.continuedfraction.ContinuedFraction` : The determinant of the :math:`2 \\times 2` matrix formed by the position vectors in :math:`\\mathbb{Q}^2` of this rational point and another. Computes the (rational) determinant: .. math:: \\begin{vmatrix}\\frac{a}{c} & \\frac{a'}{c'}\\\\\\frac{b}{d} & \\frac{b'}{d'}\\end{vmatrix} = \\frac{ab'}{cd'} - \\frac{a'b}{c'd} = \\frac{ab'c'd - a'bcd'}{cc'dd'} of the matrix formed by the position vectors of two plane rational points :math:`P = \\left( \\frac{a}{c}, \\frac{b}{d} \\right)` and :math:`P' = \\left( \\frac{a'}{c'}, \\frac{b'}{d'} \\right)`, where :math:`P` is represented by ``self`` and :math:`P'` by ``other``. Geometrically, the quantity represents the signed area of the plane parallelogram formed by the position vectors of :math:`P` and :math:`P'` and the vector sum :math:`P + P'`, where the sign is positive or negative depending on whether :math:`\\frac{bc}{ad} < \\frac{b'c'}{a'd'}` or :math:`\\frac{bc}{ad} > \\frac{b'c'}{a'd'}` respectively, where :math:`\\frac{bc}{ad}` and :math:`\\frac{b'c'}{a'd'}` are the gradients of the lines passing through the origin :math:`(0, 0)` and :math:`P` and :math:`P'` respectively. The quantity is zero when these lines are collinear, i.e. when :math:`P` and :math:`P'` fall on a single line passing through :math:`(0, 0)`. Returns ------- ContinuedFraction The determinant of the :math:`2 \\times 2` matrix formed by the position vector of this rational point and another, as described above. Examples -------- >>> from fractions import Fraction as F >>> from continuedfractions.rational_points import RationalPoint as RP >>> P, Q, R = RP(F(3, 5), F(4, 5)), RP(1, 1), RP(F(5, 4), 2); P, Q, R (RationalPoint(3/5, 4/5), RationalPoint(1, 1), RationalPoint(5/4, 2)) >>> P.det(Q) ContinuedFraction(-1, 5) >>> P.det(R) ContinuedFraction(1, 5) >>> Q.det(R) ContinuedFraction(3, 4) >>> P.det(P) ContinuedFraction(0, 1) """ if not isinstance(other, self.__class__): raise ValueError( 'The determinant is only defined between `RationalPoint` ' 'instances.' ) if self.coordinates == (0, 0) or other.coordinates == (0, 0): return ContinuedFraction(0) return (self.x * other.y) - (self.y * other.x)
@property def norm_squared(self) -> ContinuedFraction: """:py:class:`~continuedfractions.continuedfraction.ContinuedFraction` : The square of the Euclidean (:math:`\\ell_2`) norm of a rational point in the plane. The Euclidean (:math:`\\ell_2`) norm squared :math:`\\|P\\|_{2}^2` of a rational point :math:`P = \\left(\\frac{a}{c}, \\frac{b}{d} \\right)` in the plane, which is the dot product :math:`P \\cdot P` of :math:`P` with itself: .. math:: \\begin{align} \\|P\\|_{2}^2 = P \\cdot P &= \\frac{a^2}{c^2} + \\frac{b^2}{d^2} \\\\ &= \\frac{a^2d^2 + b^2c^2}{c^2d^2} \\end{align} and is also a rational number. Returns ------- ContinuedFraction The Euclidean norm squared of the rational point. Examples -------- >>> RationalPoint(1, 1).norm_squared ContinuedFraction(2, 1) >>> RationalPoint(Fraction(1, 2), Fraction(3, 5)).norm_squared ContinuedFraction(61, 100) >>> RationalPoint(0, 0).norm_squared ContinuedFraction(0, 1) """ return self.dot(self) @property def norm(self) -> Decimal: """:py:class:`~decimal.Decimal` : The Euclidean norm of a rational point in the plane. The Euclidean norm :math:`\\|P\\|_2` of a rational point :math:`P = \\left(\\frac{a}{c}, \\frac{b}{d} \\right)`, as given by: .. math:: \\begin{align} \\|P\\|_2 = \\sqrt{P \\cdot P} &= \\sqrt{\\frac{a^2}{c^2} + \\frac{b^2}{d^2}} \\\\ &= \\sqrt{\\frac{a^2d^2 + b^2c^2}{c^2d^2}} \\end{align} Returns ------- decimal.Decimal The Euclidean norm of the rational point. Examples -------- >>> RationalPoint(1, 1).norm Decimal('1.414213562373095048801688724') >>> RationalPoint(Fraction(1, 2), Fraction(3, 5)).norm Decimal('0.7810249675906654394129722736') >>> RationalPoint(0, 0).norm Decimal('0') >>> RationalPoint(Fraction(3, 5), Fraction(4, 5)).norm Decimal('1') """ return self.norm_squared.as_decimal().sqrt()
[docs] def distance_squared(self, other: RationalPoint, /) -> ContinuedFraction: """:py:class:`~continuedfractions.continuedfraction.ContinuedFraction` : The square of the Euclidean distance between this point and another rational point in the plane. If :math:`P = \\left( \\frac{a}{c}, \\frac{b}{d} \\right)` and :math:`P' = \\left( \\frac{a'}{c'}, \\frac{b'}{d'} \\right)` are two rational points in the plane the square of their Euclidean distance :math:`\\|P - P'\\|_{2}^2` is the non-negative rational number: .. math:: \\begin{align} \\|P - P'\\|^2 &= \\left( \\frac{a}{c} - \\frac{a'}{c'} \\right)^2 + \\left( \\frac{b}{d} - \\frac{b'}{d'} \\right)^2 \\\\ &= \\left( \\frac{ac' - a'c}{cc'} \\right)^2 + \\left( \\frac{bd' - b'd}{dd'} \\right)^2 \\\\ &= \\frac{\\left(ac' - a'c\\right)^2d^2d'^2 + \\left(bd' - b'd\\right)^2c^2c'^2}{c^2c'^2d^2d'^2} \\end{align} where :math:`\\|P - P'\\|_{2}^2 = 0` if and only if :math:`P = P'`. The Euclidean distance :math:`\\|P - P'\\|_2` is simply the square root of this quantity, but will in general not be a rational number **unless** :math:`\\left( \\frac{ac' - a'c}{cc'} \\right)^2 + \\left( \\frac{bd' - b'd}{dd'} \\right)^2` is a square of a rational number. Returns ------- ContinuedFraction The square of the Euclidean distance between this point and another rational point in the plane. Examples -------- >>> P = RationalPoint(Fraction(3, 5), Fraction(4, 5)) >>> P RationalPoint(3/5, 4/5) >>> P.distance_squared(RationalPoint(0, 0)) ContinuedFraction(1, 1) >>> P.distance_squared(RationalPoint(1, 1)) ContinuedFraction(1, 5) >>> RationalPoint(0, 0).distance_squared(RationalPoint(1, 1)) ContinuedFraction(2, 1) """ if not isinstance(other, RationalPoint): raise ValueError( 'The square of the Euclidean distance is defined only between ' 'two `RationalPoint` instances.' ) if other == self: return ContinuedFraction(0, 1) if other == RationalPoint(0, 0): return self.norm_squared if self == RationalPoint(0, 0): return other.norm_squared return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2)
[docs] def distance(self, other: RationalPoint, /) -> Decimal: """:py:class:`~decimal.Decimal` : The Euclidean distance between this point and another rational point. For rational points :math:`P = \\left( \\frac{a}{c}, \\frac{b}{d} \\right)` and :math:`P' = \\left( \\frac{a'}{c'}, \\frac{b'}{d'} \\right)` this is the square root :math:`\\sqrt{\\|P - P'\\|_{2}^2}` of the distance squared :math:`\\|P - P'\\|_{2}^2` as defined above. And of course :math:`\\|P - P'\\|_{2} = 0` if and only if :math:`P = P'`. Returns ------- decimal.Decimal The Euclidean distance between this point and another rational point in the plane. Examples -------- >>> P = RationalPoint(Fraction(3, 5), Fraction(4, 5)) >>> P RationalPoint(3/5, 4/5) >>> P.distance(RationalPoint(0, 0)) Decimal('1') >>> P.distance_squared(RationalPoint(1, 1)) ContinuedFraction(1, 5) >>> RationalPoint(0, 0).distance(RationalPoint(1, 1)) Decimal('1.414213562373095048801688724') """ if not isinstance(other, RationalPoint): raise ValueError( 'The Euclidean distance is defined only between two ' '`RationalPoint` instances.' ) return self.distance_squared(other).as_decimal().sqrt()
[docs] def perpendicular_distance(self, other: RationalPoint, /) -> Decimal: """:py:class:`~decimal.Decimal` : The perpendicular distance between this rational point and another. Given a non-zero rational point :math:`P = \\left( \\frac{a}{c}, \\frac{b}{d} \\right)`, the line :math:`\\ell_{OP}` passing through the origin :math:`(0, 0)` and :math:`P`, and another rational point :math:`P' = \\left( \\frac{a'}{c'}, \\frac{b'}{d'} \\right)`, the perpendicular distance :math:`d^{\\perp}\\left(P, P'\\right)'` between :math:`P` and :math:`P'` is defined here as the length :math:`d^{\\perp}\\left(P, P'\\right)` of the line segment connecting :math:`P'` and :math:`\\ell_{OP}`, perpendicular to the latter, as given by: .. math:: d^{\\perp}\\left(P, P'\\right) = \\frac{\\lvert\\text{det}(P, P')\\rvert}{\\|P\\|_2} where :math:`\\text{det}(P, P')` is the determinant of :math:`P` and :math:`P'` as described in :py:meth:`~continuedfractions.rational_points.RationalPoint.det`. Returns ------- decimal.Decimal The perpendicular distance between this rational point and another as defined above. Examples -------- >>> from continuedfractions.rational_points import RationalPoint as RP >>> RP(1, 0).perpendicular_distance(RP(0, 1)) Decimal('1') >>> RP(0, 1).perpendicular_distance(RP(1, 0)) Decimal('1') >>> RP(1, 0).perpendicular_distance(RP(1, 0)) Decimal('0') >>> RP(1, 0).perpendicular_distance(RP(-1, 0)) Decimal('0') >>> RP(0, 0).perpendicular_distance(RP(1, 0)) Traceback (most recent call last): ... ValueError: The perpendicular distance is defined only between two `RationalPoint` instances, the first of which must be non-zero, i.e. different from `RationalPoint(0, 0)`. >>> RP(1, 0).perpendicular_distance(1) Traceback (most recent call last): ... ValueError: The perpendicular distance is defined only between two `RationalPoint` instances, the first of which must be non-zero, i.e. different from `RationalPoint(0, 0)`. """ if not isinstance(other, RationalPoint) or self == self.zero(): raise ValueError( 'The perpendicular distance is defined only between two ' '`RationalPoint` instances, the first of which must be ' 'non-zero, i.e. different from `RationalPoint(0, 0)`.' ) # If the two points are collinear with the origin ``(0, 0)`` return 0. a, b = self.angle(as_degrees=True), other.angle(as_degrees=True) if a == b or abs(a) + abs(b) == Decimal('180'): return Decimal('0') # Otherwise compute the value and return return abs(self.det(other)).as_decimal() / self.norm
[docs] def is_integral_lattice_point(self) -> bool: """:py:class:`bool` : Whether the rational point is an integral lattice point, i.e. has integer coordinates. Returns ------- bool Whether the coordinates correspond to integers. Coordinates in fractional form :math:`\\frac{a}{1}` where the numerator :math:`a` is an integer are treated as integers. Examples -------- >>> RationalPoint(1, 2).is_integral_lattice_point() True >>> RationalPoint(Fraction(1, 2), 2).is_integral_lattice_point() False """ return all(coord.denominator == 1 for coord in self.coordinates)
@property def rectilinear_norm(self) -> ContinuedFraction: """:py:class:`~continuedfractions.continuedfraction.ContinuedFraction`: The rectilinear (:math:`\\ell_1` or taxicab) norm of the rational point. The rectilinear (:math:`\\ell_1` or taxicab) norm :math:`\\|r\\|_1` of a rational point :math:`P = \\left( \\frac{a}{c}, \\frac{b}{d} \\right)` is given by: .. math:: \\|P\\|_1 = \\lvert\\frac{a}{c}\\rvert + \\lvert\\frac{b}{d}\\rvert The rectilinear norm of a rational point is a non-negative rational number, hence the property produces :py:class:`~continuedfractions.continuedfraction.ContinuedFraction` objects, as this is the standard representation of rationals in this package. To get the decimal value use the :py:class:`~continuedfractions.continuedfraction.ContinuedFraction.as_decimal` method. Returns ------- ContinuedFraction The rectilinear norm of the rational point, which is always a non-negative rational number. Examples -------- >>> RationalPoint(Fraction(-1, 2), Fraction(3, 4)).rectilinear_norm ContinuedFraction(5, 4) >>> RationalPoint(Fraction(1, 2), Fraction(-3, 4)).rectilinear_norm ContinuedFraction(5, 4) >>> RationalPoint(Fraction(1, 2), Fraction(3, 4)).rectilinear_norm ContinuedFraction(5, 4) """ return sum(map(abs, self.coordinates))
[docs] def rectilinear_distance(self, other: RationalPoint, /) -> ContinuedFraction: """:py:class:`~continuedfractions.continuedfraction.ContinuedFraction` : The rectilinear (:math:`\\ell_1` or taxicab) distance between this rational point and another. If :math:`P = \\left( \\frac{a}{c}, \\frac{b}{d} \\right)` and :math:`P' = \\left( \\frac{a'}{c'}, \\frac{b'}{d'} \\right)` are two rational points in the plane their rectilinear distance :math:`\\|P - P'\\|_1` is the non-negative rational number: .. math:: \\begin{align} \\|P - P'\\|_1 &= \\lvert \\frac{a}{c} - \\frac{a'}{c'} \\rvert + \\lvert \\frac{b}{d} - \\frac{b'}{d'} \\rvert \\\\ &= \\lvert \\frac{ac' - a'c}{cc'} \\rvert + \\lvert \\frac{bd' - b'd}{dd'} \\rvert \\end{align} where :math:`\\|P - P'\\| = 0` if and only if :math:`P = P'`. Returns ------- decimal.Decimal The rectilinear distance between this point and another rational point in the plane. Examples -------- >>> RationalPoint(Fraction(3, 5), Fraction(4, 5)).rectilinear_distance(RationalPoint(1, 1)) ContinuedFraction(3, 5) >>> RationalPoint(0, 0).rectilinear_distance(RationalPoint(1, 1)) ContinuedFraction(2, 1) """ if not isinstance(other, RationalPoint): raise ValueError( 'The Euclidean distance is defined only between two ' '`RationalPoint` instances.' ) return abs(self.x - other.x) + abs(self.y - other.y)
@property def homogeneous_coordinates(self) -> HomogeneousCoordinates: """:py:class:`~continuedfractions.rational_points.HomogeneousCoordinates` : A unique sequence of integer-valued homogeneous coordinates in :math:`\\mathbb{P}^2` for this rational point. For a rational point :math:`P = \\left(\\frac{a}{c},\\frac{b}{d}\\right)` the triple :math:`\\left(\\lambda \\frac{a}{c}, \\lambda \\frac{b}{d}, \\lambda\\right) = \\left(a \\frac{\\lambda}{c}, b \\frac{\\lambda}{d}, \\lambda\\right)`, where :math:`\\lambda = \\text{lcm}(c, d) > 0`, represents a unique representative sequence of homogeneous coordinates for :math:`P` in :math:`\\mathbb{P}^2` such that :math:`\\left(a \\frac{\\lambda}{c}, b \\frac{\\lambda}{d}, \\lambda\\right)` are all integers (not all zero) and :math:`\\text{gcd}\\left(a \\frac{\\lambda}{c}, b \\frac{\\lambda}{d}, \\lambda\\right) = 1`. Returns ------- HomogeneousCoordinates A tuple of "minimal" integer-valued homogeneous coordinates in for this rational point in projective space :math:`\\mathbb{P}^2`. Examples -------- >>> from fractions import Fraction as F >>> RationalPoint(0, 0).homogeneous_coordinates HomogeneousCoordinates(0, 0, 1) >>> RationalPoint(F(1, 2), F(3, 4)).homogeneous_coordinates HomogeneousCoordinates(2, 3, 4) >>> RationalPoint(F(1, 2), F(2, 3)).homogeneous_coordinates HomogeneousCoordinates(3, 4, 6) >>> RationalPoint(-1, 1).homogeneous_coordinates HomogeneousCoordinates(-1, 1, 1) >>> RationalPoint(1, 1).homogeneous_coordinates HomogeneousCoordinates(1, 1, 1) """ lcm = math.lcm(self.x.denominator, self.y.denominator) return HomogeneousCoordinates((lcm * self.x).numerator, (lcm * self.y).numerator, lcm) @property def height(self) -> int: """:py:class:`int` : The height of the rational point in the projective space :math:`\\mathbb{P}^2`. The height :math:`H\\left(\\frac{a}{c},\\frac{b}{d}\\right)` of a rational point :math:`P = \\left(\\frac{a}{c},\\frac{b}{d}\\right)` as given by: .. math:: \\text{max}\\left(|a|\\lvert \\frac{\\lambda}{c} \\rvert, |b|\\lvert \\frac{\\lambda}{d} \\rvert, \\lambda \\right) where :math:`\\lambda = \\text{lcm}(c, d) > 0`, and :math:`\\left(\\lambda \\frac{a}{c}, \\lambda \\frac{b}{d}, \\lambda\\right) = \\left(a \\frac{\\lambda}{c}, b \\frac{\\lambda}{d}, \\lambda\\right)` is a unique sequence of integer-valued homogeneous coordinates :math:`\\left(\\lambda \\frac{a}{c}, \\lambda \\frac{b}{d}, \\lambda\\right) = \\left(a \\frac{\\lambda}{c}, b \\frac{\\lambda}{d}, \\lambda\\right)` of :math:`P` in :math:`\\mathbb{P}^2`. Returns ------- int The height of this rational point as defined above. Examples -------- >>> from fractions import Fraction as F >>> RationalPoint(0, 0).height 1 >>> RationalPoint(1, 1).height 1 >>> RationalPoint(-1, 1).height 1 >>> RationalPoint(F(3, 5), F(4, 5)).height 5 >>> RationalPoint(F(1, 2), F(3, 5)).height 10 """ return max(map(abs, self.homogeneous_coordinates)) @property def log_height(self) -> Decimal: """:py:class:`~decimal.Decimal` : The natural logarithm of the height of the rational point as defined above. The (natural) logarithm of the height of a rational point :math:`P = \\left(\\frac{a}{c},\\frac{b}{d}\\right)` as given by: .. math:: \\text{log}\\left(H\\left(\\frac{a}{c}, \\frac{b}{d}\\right)\\right) = \\text{log}\\left(\\text{max}\\left(|a|\\lvert \\frac{\\lambda}{c} \\rvert, |b|\\lvert \\frac{\\lambda}{d} \\rvert, \\lambda \\right)\\right) where :math:`\\lambda = \\text{lcm}(c, d) > 0` and :math:`\\left(\\lambda \\frac{a}{c}, \\lambda \\frac{b}{d}, \\lambda\\right) = \\left(a \\frac{\\lambda}{c}, b \\frac{\\lambda}{d}, \\lambda\\right)` is a unique sequence of integer-valued homogeneous coordinates for :math:`P` in :math:`\\mathbb{P}^2`, as defined above. Returns ------- decimal.Decimal The (natural) logarithm of the height of this rational point in :math:`\\mathbb{P}^2` as defined above. Examples -------- >>> from fractions import Fraction as F >>> RationalPoint(0, 0).log_height Decimal('0') >>> RationalPoint(1, 1).log_height Decimal('0') >>> RationalPoint(-1, 1).log_height Decimal('0') >>> RationalPoint(F(3, 5), F(4, 5)).log_height Decimal('1.6094379124341002817999424223671667277812957763671875') >>> RationalPoint(F(1, 2), F(3, 5)).log_height Decimal('2.30258509299404590109361379290930926799774169921875') """ return Decimal(math.log(self.height, math.e))
[docs] def __add__(self, other: RationalPoint) -> RationalPoint: """:py:class:`~continuedfractions.rational_points.RationalPoint` : Component-wise addition for two rational points. Implements component-wise addition of two rational points: .. math:: \\left(\\frac{a}{c}, \\frac{b}{d}\\right) + \\left(\\frac{a'}{c'}, \\frac{b'}{d'}\\right) = \\left(\\frac{ac' + a'c}{cc'}, \\frac{bd' + b'd}{dd'}\\right) The second operand as represented by ``other`` must be an instance of :py:class:`~continuedfractions.rational_points.RationalPoint`. """ if not isinstance(other, RationalPoint): raise TypeError( 'Addition is defined only between two `RationalPoint` ' 'instances.' ) return self.__class__(self.x + other.x, self.y + other.y)
[docs] def __sub__(self, other: RationalPoint) -> RationalPoint: """:py:class:`~continuedfractions.rational_points.RationalPoint` : Component-wise subtraction for two rational points. Implements component-wise subtraction of two rational points: .. math:: \\left(\\frac{a}{c}, \\frac{b}{d}\\right) - \\left(\\frac{a'}{c'}, \\frac{b'}{d'}\\right) = \\left(\\frac{ac' - a'c}{cc'}, \\frac{bd' - b'd}{dd'}\\right) The second operand as represented by ``other`` must be an instance of :py:class:`~continuedfractions.rational_points.RationalPoint`. """ if not isinstance(other, RationalPoint): # pragma: no cover raise TypeError( 'Subtraction is defined only between two `RationalPoint` ' 'instances.' ) return self.__class__(self.x - other.x, self.y - other.y)
[docs] def __neg__(self) -> RationalPoint: """:py:class:`~continuedfractions.rational_points.RationalPoint` : Component-wise negation for a rational point. Implements component-wise negation for a rational point: .. math:: -\\left(\\frac{a}{c}, \\frac{b}{d}\\right) = \\left(-\\frac{a}{c}, -\\frac{b}{d}\\right) """ return self.__class__(-self.x, -self.y)
[docs] def __mul__(self, other: Any) -> None: """Does not support component-wise right-multiplication by a scalar to respect notational convention. """ raise NotImplementedError( 'Only rational scalar left-multiplication is supported. This ' 'means the left-most operand must be an instance of ' '`numbers.Rational`, i.e. an `int`, `fractions.Fraction` or ' '`ContinuedFraction`.' )
[docs] def __rmul__(self, other: int | Fraction | ContinuedFraction) -> RationalPoint: """:py:class:`~continuedfractions.rational_points.RationalPoint` : Component-wise scalar left-multiplication of a rational number by an integer or rational number. Implements component-wise left-multiplication of a rational point with a rational scalar :math:`\\lambda`: .. math:: \\lambda \\left(\\frac{a}{c}, \\frac{b}{d}\\right) = \\left(\\lambda \\frac{a}{c}, \\lambda \\frac{b}{d}\\right), \\hspace{1em} \\lambda \\in \\mathbb{Q} The implementation is in terms of ``__rmul__``, because conventionally scalar multiples of a vectors in vector spaces (or subspaces) such as :math:`\\mathbb{R}^n` are written in left-multiplication style. The second operand must be a rational number scalar as given by an instance of :py:class:`int`, :py:class:`~fractions.Fraction`, or :py:class:`~continuedfractions.continuedfraction.ContinuedFraction`. """ if not isinstance(other, (int, Fraction, ContinuedFraction)): raise TypeError( # pragma: no cover 'Only rational scalar left-multiplication is supported. This ' 'means the left-most operand must be an instance of ' '`numbers.Rational`, i.e. an `int`, `fractions.Fraction` or ' '`ContinuedFraction`.' ) if other == 0: return self.__class__(0, 0) return self.__class__(self.x * other, self.y * other)
[docs] def __truediv__(self, other: int | Fraction | ContinuedFraction): """:py:class:`~continuedfractions.rational_points.RationalPoint` : Component-wise division by a non-zero rational scalar. Implements component-wise division of a rational point by a non-zero rational scalar :math:`\\lambda`: .. math:: \\left(\\frac{a}{c}, \\frac{b}{d}\\right) \\div \\lambda = \\frac{1}{\\lambda}\\cdot \\left(\\frac{a}{c}, \\frac{b}{d} \\right) = \\left(\\frac{a}{\\lambda c}, \\frac{b}{\\lambda d} \\right), \\hspace{1em} \\lambda \\in \\mathbb{Q}\\setminus \\{0\\} by scaling the point by :math:`\\frac{1}{\\lambda}`. """ if not isinstance(other, (int, Fraction, ContinuedFraction)): raise TypeError( # pragma: no cover 'The scalar must be a rational, specifically, an instance of ' '`int`, `fractions.Fraction` or `ContinuedFraction`.' ) if other == 0: raise ZeroDivisionError('Division by zero.') return Fraction(1, other) * self
[docs] def __abs__(self) -> Decimal: """:py:class:`~decimal.Decimal` : The absolute value of the rational point as the standard Euclidean norm. For points in :math:`\\mathbb{R}^n` the notion of absolute value and Euclidean norm coincide. Returns ------- decimal.Decimal The absolute value of the rational point as the standard Euclidean norm. Examples -------- >>> abs(RationalPoint(0, 0)) Decimal('0') >>> abs(RationalPoint(Fraction(3, 5), Fraction(4, 5))) Decimal('1') >>> abs(RationalPoint(1, 1)) Decimal('1.414213562373095048801688724') """ return self.norm
if __name__ == "__main__": # pragma: no cover # Doctest the module from the project root using # # PYTHONPATH="src" python3 -m doctest -v src/continuedfractions/rational_points.py # # NOTE: the doctest examples using ``decimal.Decimal`` values are based on # a context precision of 28 digits. decimal.getcontext().prec = 28 import doctest doctest.testmod()