Source code for flatsurf.geometry.euclidean

r"""
A loose collection of tools for Euclidean geometry in the plane.

.. SEEALSO::

    :mod:`flatsurf.geometry.circle` for everything specific to circles in the plane

"""

######################################################################
#  This file is part of sage-flatsurf.
#
#        Copyright (C) 2016-2020 Vincent Delecroix
#                      2020-2023 Julian Rüth
#
#  sage-flatsurf is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 2 of the License, or
#  (at your option) any later version.
#
#  sage-flatsurf is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with sage-flatsurf. If not, see <https://www.gnu.org/licenses/>.
######################################################################


[docs] def is_cosine_sine_of_rational(cos, sin, scaled=False): r""" Check whether the given pair is a cosine and sine of a same rational angle. INPUT: - ``cos`` -- a number - ``sin`` -- a number - ``scaled`` -- a boolean (default: ``False``); whether to allow ``cos`` and ``sin`` to be scaled by the same positive algebraic number EXAMPLES:: sage: from flatsurf.geometry.euclidean import is_cosine_sine_of_rational sage: c = s = AA(sqrt(2))/2 sage: is_cosine_sine_of_rational(c, s) True sage: c = AA(sqrt(3))/2 sage: s = AA(1/2) sage: is_cosine_sine_of_rational(c, s) True sage: c = AA(sqrt(5)/2) sage: s = (1 - c**2).sqrt() sage: c**2 + s**2 1.000000000000000? sage: is_cosine_sine_of_rational(c, s) False sage: c = (AA(sqrt(5)) + 1)/4 sage: s = (1 - c**2).sqrt() sage: is_cosine_sine_of_rational(c, s) True sage: K.<sqrt2> = NumberField(x**2 - 2, embedding=1.414) sage: is_cosine_sine_of_rational(K.zero(), -K.one()) True TESTS:: sage: from pyexactreal import ExactReals # optional: pyexactreal # random output due to matplotlib warnings with some combinations of setuptools and matplotlib sage: R = ExactReals() # optional: pyexactreal sage: is_cosine_sine_of_rational(R.one(), R.zero()) # optional: pyexactreal True """ from sage.all import AA # We cannot check in AA due to https://github.com/flatsurf/exact-real/issues/172 # We just trust that non-algebraic elements won't allow conversion to AA. # if cos not in AA: # return False # if sin not in AA: # return False if not scaled: if cos**2 + sin**2 != 1: return False try: cos = AA(cos) except ValueError: # This is a replacement for the "in AA" checked disabled above. return False cos = cos.as_number_field_element(embedded=True) # We need an explicit conversion to the number field due to https://github.com/sagemath/sage/issues/35613 cos = cos[0](cos[1]) try: sin = AA(sin) except ValueError: # This is a replacement for the "in AA" checked disabled above. return False sin = sin.as_number_field_element(embedded=True) # We need an explicit conversion to the number field due to https://github.com/sagemath/sage/issues/35613 sin = sin[0](sin[1]) from sage.all import ComplexBallField CBF = ComplexBallField(53) x = CBF(cos) + CBF.gen(0) * CBF(sin) xN = x # Suppose that (cos, sin) are indeed sine and cosine of a rational angle. # Then x = cos + I*sin generates a cyclotomic field C and for some N we # have x^N = ±1. Since C is contained in the compositum of K=Q(cos) and # L=Q(i*sin) and Q(cos) and Q(sin) are both contained in C, the degree of C # is bounded from above by twice (accounting for the imaginary unit) the # degrees of K and L. The degree of C is the totient of N which is bounded # from below by n / (e^γ loglog n + 3 / loglog n) [cf. wikipedia]. degree_bound = 2 * cos.minpoly().degree() * sin.minpoly().degree() from itertools import count for n in count(2): xN *= x c = xN.real() s = xN.imag() if xN.real().contains_zero() or xN.imag().contains_zero(): c, s = cos, sin for i in range(n - 1): c, s = c * cos - s * sin, s * cos + c * sin if c == 0 or s == 0: return True CBF = ComplexBallField(CBF.precision() * 2) x = CBF(cos) + CBF.gen(0) * CBF(sin) xN = x**n from math import log if n / (2.0 * log(log(n)) + 3 / log(log(n))) > 2 * degree_bound: return False
[docs] def acos(cos_angle, numerical=False): r""" Return the arccosine of ``cos_angle`` as a multiple of 2π, i.e., as a value between 0 and 1/2. INPUT: - ``cos_angle`` -- a floating point number, the cosine of an angle - ``numerical`` -- a boolean (default: ``False``); whether to return a numerical approximation of the arccosine or try to reconstruct an exact rational value for the arccosine (in radians.) EXAMPLES:: sage: from flatsurf.geometry.euclidean import acos sage: acos(1) 0 sage: acos(.5) 1/6 sage: acos(0) 1/4 sage: acos(-.5) 1/3 sage: acos(-1) 1/2 sage: acos(.25) Traceback (most recent call last): ... NotImplementedError: cannot recover a rational angle from these numerical results sage: acos(.25, numerical=True) 0.2097846883724169 """ import math angle = math.acos(cos_angle) / (2 * math.pi) assert 0 <= angle <= 0.5 if numerical: return angle # fast and dirty way using floating point approximation from sage.all import RR angle_rat = RR(angle).nearby_rational(0.00000001) if angle_rat.denominator() > 256: raise NotImplementedError( "cannot recover a rational angle from these numerical results" ) return angle_rat
[docs] def angle(u, v, numerical=False): r""" Return the angle between the vectors ``u`` and ``v`` divided by `2 \pi`. INPUT: - ``u``, ``v`` - vectors - ``numerical`` - boolean (default: ``False``), whether to return floating point numbers EXAMPLES:: sage: from flatsurf.geometry.euclidean import angle As the implementation is dirty, we at least check that it works for all denominator up to 20:: sage: u = vector((AA(1),AA(0))) sage: for n in xsrange(1,20): # long time (1.5s) ....: for k in xsrange(1,n): ....: v = vector((AA(cos(2*k*pi/n)), AA(sin(2*k*pi/n)))) ....: assert angle(u,v) == k/n The numerical version (working over floating point numbers):: sage: import math sage: u = (1, 0) sage: for n in xsrange(1,20): ....: for k in xsrange(1,n): ....: a = 2 * k * math.pi / n ....: v = (math.cos(a), math.sin(a)) ....: assert abs(angle(u,v,numerical=True) * 2 * math.pi - a) < 1.e-10 If the angle is not rational, then the method returns an element in the real lazy field:: sage: v = vector((AA(sqrt(2)), AA(sqrt(3)))) sage: a = angle(u, v) Traceback (most recent call last): ... NotImplementedError: cannot recover a rational angle from these numerical results sage: a = angle(u, v, numerical=True) sage: a # abs tol 1e-14 0.14102355421224375 sage: exp(2*pi.n()*CC(0,1)*a) 0.632455532033676 + 0.774596669241483*I sage: v / v.norm() (0.6324555320336758?, 0.774596669241484?) """ import math u0 = float(u[0]) u1 = float(u[1]) v0 = float(v[0]) v1 = float(v[1]) cos_uv = (u0 * v0 + u1 * v1) / math.sqrt((u0 * u0 + u1 * u1) * (v0 * v0 + v1 * v1)) if cos_uv < -1.0: assert cos_uv > -1.0000001 cos_uv = -1.0 elif cos_uv > 1.0: assert cos_uv < 1.0000001 cos_uv = 1.0 angle = acos(cos_uv, numerical=numerical) return 1 - angle if u0 * v1 - u1 * v0 < 0 else angle
# a neater way is provided below by working only with number fields # but this method is slower... # sqnorm_u = u[0]*u[0] + u[1]*u[1] # sqnorm_v = v[0]*v[0] + v[1]*v[1] # # if sqnorm_u != sqnorm_v: # # we need to take a square root in order that u and v have the # # same norm # u = (1 / AA(sqnorm_u)).sqrt() * u.change_ring(AA) # v = (1 / AA(sqnorm_v)).sqrt() * v.change_ring(AA) # sqnorm_u = AA.one() # sqnorm_v = AA.one() # # cos_uv = (u[0]*v[0] + u[1]*v[1]) / sqnorm_u # sin_uv = (u[0]*v[1] - u[1]*v[0]) / sqnorm_u
[docs] def ccw(v, w): r""" Return a positive number if the turn from ``v`` to ``w`` is counterclockwise, a negative number if it is clockwise, and zero if the two vectors are collinear. .. NOTE:: This function is sometimes also referred to as the wedge product or simply the determinant. We chose the more customary name ``ccw`` from computational geometry here. EXAMPLES:: sage: from flatsurf.geometry.euclidean import ccw sage: ccw((1, 0), (0, 1)) 1 sage: ccw((1, 0), (-1, 0)) 0 sage: ccw((1, 0), (0, -1)) -1 sage: ccw((1, 0), (1, 0)) 0 """ return v[0] * w[1] - v[1] * w[0]
[docs] def is_parallel(v, w): r""" Return whether the vectors ``v`` and ``w`` are parallel (but not anti-parallel.) EXAMPLES:: sage: from flatsurf.geometry.euclidean import is_parallel sage: is_parallel((0, 1), (0, 1)) True sage: is_parallel((0, 1), (0, 2)) True sage: is_parallel((0, 1), (0, -2)) False sage: is_parallel((0, 1), (0, 0)) False sage: is_parallel((0, 1), (1, 0)) False TESTS:: sage: V = QQ**2 sage: is_parallel(V((0,1)), V((0,2))) True sage: is_parallel(V((1,-1)), V((2,-2))) True sage: is_parallel(V((4,-2)), V((2,-1))) True sage: is_parallel(V((1,2)), V((2,4))) True sage: is_parallel(V((0,2)), V((0,1))) True sage: is_parallel(V((1,1)), V((1,2))) False sage: is_parallel(V((1,2)), V((2,1))) False sage: is_parallel(V((1,2)), V((1,-2))) False sage: is_parallel(V((1,2)), V((-1,-2))) False sage: is_parallel(V((2,-1)), V((-2,1))) False """ if ccw(v, w) != 0: # vectors are not collinear return False return v[0] * w[0] + v[1] * w[1] > 0
[docs] def is_anti_parallel(v, w): r""" Return whether the vectors ``v`` and ``w`` are anti-parallel, i.e., whether ``v`` and ``-w`` are parallel. EXAMPLES:: sage: from flatsurf.geometry.euclidean import is_anti_parallel sage: V = QQ**2 sage: is_anti_parallel(V((0,1)), V((0,-2))) True sage: is_anti_parallel(V((1,-1)), V((-2,2))) True sage: is_anti_parallel(V((4,-2)), V((-2,1))) True sage: is_anti_parallel(V((-1,-2)), V((2,4))) True sage: is_anti_parallel(V((1,1)), V((1,2))) False sage: is_anti_parallel(V((1,2)), V((2,1))) False sage: is_anti_parallel(V((0,2)), V((0,1))) False sage: is_anti_parallel(V((1,2)), V((1,-2))) False sage: is_anti_parallel(V((1,2)), V((-1,2))) False sage: is_anti_parallel(V((2,-1)), V((-2,-1))) False """ return is_parallel(v, -w)
[docs] def line_intersection(p1, p2, q1, q2): r""" Return the point of intersection between the line joining p1 to p2 and the line joining q1 to q2. If the lines do not have a single point of intersection, we return None. Here p1, p2, q1 and q2 should be vectors in the plane. """ if ccw(p2 - p1, q2 - q1) == 0: return None # Since the wedge product is non-zero, the following is invertible: from sage.all import matrix m = matrix([[p2[0] - p1[0], q1[0] - q2[0]], [p2[1] - p1[1], q1[1] - q2[1]]]) return p1 + (m.inverse() * (q1 - p1))[0] * (p2 - p1)
[docs] def is_segment_intersecting(e1, e2): r""" Return whether the segments ``e1`` and ``e2`` intersect. OUTPUT: - ``0`` - do not intersect - ``1`` - one endpoint in common - ``2`` - non-trivial intersection EXAMPLES:: sage: from flatsurf.geometry.euclidean import is_segment_intersecting sage: is_segment_intersecting(((0,0),(1,0)),((0,1),(0,3))) 0 sage: is_segment_intersecting(((0,0),(1,0)),((0,0),(0,3))) 1 sage: is_segment_intersecting(((0,0),(1,0)),((0,-1),(0,3))) 2 sage: is_segment_intersecting(((-1,-1),(1,1)),((0,0),(2,2))) 2 sage: is_segment_intersecting(((-1,-1),(1,1)),((1,1),(2,2))) 1 """ if e1[0] == e1[1] or e2[0] == e2[1]: raise ValueError("degenerate segments") elts = [e[i][j] for e in (e1, e2) for i in (0, 1) for j in (0, 1)] from sage.structure.element import get_coercion_model cm = get_coercion_model() base_ring = cm.common_parent(*elts) if isinstance(base_ring, type): from sage.structure.coerce import py_scalar_parent base_ring = py_scalar_parent(base_ring) from sage.all import matrix m = matrix(base_ring, 3) xs1, ys1 = map(base_ring, e1[0]) xt1, yt1 = map(base_ring, e1[1]) xs2, ys2 = map(base_ring, e2[0]) xt2, yt2 = map(base_ring, e2[1]) m[0] = [xs1, ys1, 1] m[1] = [xt1, yt1, 1] m[2] = [xs2, ys2, 1] s0 = m.det() m[2] = [xt2, yt2, 1] s1 = m.det() if (s0 > 0 and s1 > 0) or (s0 < 0 and s1 < 0): # e2 stands on one side of the line generated by e1 return 0 m[0] = [xs2, ys2, 1] m[1] = [xt2, yt2, 1] m[2] = [xs1, ys1, 1] s2 = m.det() m[2] = [xt1, yt1, 1] s3 = m.det() if (s2 > 0 and s3 > 0) or (s2 < 0 and s3 < 0): # e1 stands on one side of the line generated by e2 return 0 if s0 == 0 and s1 == 0: assert s2 == 0 and s3 == 0 if xt1 < xs1 or (xt1 == xs1 and yt1 < ys1): xs1, xt1 = xt1, xs1 ys1, yt1 = yt1, ys1 if xt2 < xs2 or (xt2 == xs2 and yt2 < ys2): xs2, xt2 = xt2, xs2 ys2, yt2 = yt2, ys2 if xs1 == xt1 == xs2 == xt2: xs1, xt1, xs2, xt2 = ys1, yt1, ys2, yt2 assert xs1 < xt1 and xs2 < xt2, (xs1, xt1, xs2, xt2) if (xs2 > xt1) or (xt2 < xs1): return 0 # no intersection elif (xs2 == xt1) or (xt2 == xs1): return 1 # one endpoint in common else: assert ( xs1 <= xs2 < xt1 or xs1 < xt2 <= xt1 or (xs2 < xs1 and xt2 > xt1) or (xs2 > xs1 and xt2 < xt1) ), (xs1, xt1, xs2, xt2) return 2 # one dimensional elif s0 == 0 or s1 == 0: # treat alignment here if s2 == 0 or s3 == 0: return 1 # one endpoint in common else: return 2 # intersection in the middle return 2 # middle intersection
[docs] def is_between(e0, e1, f): r""" Check whether the vector ``f`` is strictly in the sector formed by the vectors ``e0`` and ``e1`` (in counter-clockwise order). EXAMPLES:: sage: from flatsurf.geometry.euclidean import is_between sage: is_between((1, 0), (1, 1), (2, 1)) True sage: from itertools import product sage: vecs = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)] sage: for (i, vi), (j, vj), (k, vk) in product(enumerate(vecs), repeat=3): ....: assert is_between(vi, vj, vk) == ((i == j and i != k) or i < k < j or k < j < i or j < i < k), ((i, vi), (j, vj), (k, vk)) """ if e0[0] * e1[1] > e1[0] * e0[1]: # positive determinant # [ e0[0] e1[0] ]^-1 = [ e1[1] -e1[0] ] # [ e0[1] e1[1] ] [-e0[1] e0[0] ] # f[0] * e1[1] - e1[0] * f[1] > 0 # - f[0] * e0[1] + e0[0] * f[1] > 0 return e1[1] * f[0] > e1[0] * f[1] and e0[0] * f[1] > e0[1] * f[0] elif e0[0] * e1[1] == e1[0] * e0[1]: # aligned vector if e0[0] * e1[0] >= 0 and e0[1] * e1[1] >= 0: return f[0] * e0[1] != f[1] * e0[0] or f[0] * e0[0] < 0 or f[1] * e0[1] < 0 else: return e0[0] * f[1] > e0[1] * f[0] else: # negative determinant # [ e1[0] e0[0] ]^-1 = [ e0[1] -e0[0] ] # [ e1[1] e0[1] ] [-e1[1] e1[0] ] # f[0] * e0[1] - e0[0] * f[1] > 0 # - f[0] * e1[1] + e1[0] * f[1] > 0 return e0[1] * f[0] < e0[0] * f[1] or e1[0] * f[1] < e1[1] * f[0]
[docs] def solve(x, u, y, v): r""" Return (a,b) so that: x + au = y + bv INPUT: - ``x``, ``u``, ``y``, ``v`` -- two dimensional vectors EXAMPLES:: sage: from flatsurf.geometry.euclidean import solve sage: K.<sqrt2> = NumberField(x^2 - 2, embedding=AA(2).sqrt()) sage: V = VectorSpace(K,2) sage: x = V((1,-sqrt2)) sage: y = V((1,1)) sage: a = V((0,1)) sage: b = V((-sqrt2, sqrt2+1)) sage: u = V((0,1)) sage: v = V((-sqrt2, sqrt2+1)) sage: a, b = solve(x,u,y,v) sage: x + a*u == y + b*v True sage: u = V((1,1)) sage: v = V((1,sqrt2)) sage: a, b = solve(x,u,y,v) sage: x + a*u == y + b*v True """ d = -u[0] * v[1] + u[1] * v[0] if d.is_zero(): raise ValueError("parallel vectors") a = v[1] * (x[0] - y[0]) + v[0] * (y[1] - x[1]) b = u[1] * (x[0] - y[0]) + u[0] * (y[1] - x[1]) return (a / d, b / d)
[docs] def projectivization(x, y, signed=True, denominator=None): r""" Return a simplified version of the projective coordinate [x: y]. If ``signed`` (the default), the second coordinate is made non-negative; otherwise the coordinates keep their signs. If ``denominator`` is ``False``, returns [x/y: 1] up to sign. Otherwise, the returned coordinates have no denominator and no non-unit gcd. TESTS:: sage: from flatsurf.geometry.euclidean import projectivization sage: projectivization(2/3, -3/5, signed=True, denominator=True) (10, -9) sage: projectivization(2/3, -3/5, signed=False, denominator=True) (-10, 9) sage: projectivization(2/3, -3/5, signed=True, denominator=False) (10/9, -1) sage: projectivization(2/3, -3/5, signed=False, denominator=False) (-10/9, 1) sage: projectivization(-1/2, 0, signed=True, denominator=True) (-1, 0) sage: projectivization(-1/2, 0, signed=False, denominator=True) (1, 0) sage: projectivization(-1/2, 0, signed=True, denominator=False) (-1, 0) sage: projectivization(-1/2, 0, signed=False, denominator=False) (1, 0) """ from sage.all import Sequence parent = Sequence([x, y]).universe() if y: z = x / y if denominator is True or (denominator is None and hasattr(z, "denominator")): d = parent(z.denominator()) else: d = parent(1) if signed and y < 0: d *= -1 return (z * d, d) elif signed and x < 0: return (parent(-1), parent(0)) else: return (parent(1), parent(0))
[docs] def slope(a, rotate=1): r""" Return either ``1`` (positive slope) or ``-1`` (negative slope). If ``rotate`` is set to 1 then consider the edge as if it was rotated counterclockwise infinitesimally. EXAMPLES:: sage: from flatsurf.geometry.euclidean import slope sage: slope((1, 1)) 1 sage: slope((-1, 1)) -1 sage: slope((-1, -1)) 1 sage: slope((1, -1)) -1 sage: slope((1, 0)) 1 sage: slope((0, 1)) -1 sage: slope((-1, 0)) 1 sage: slope((0, -1)) -1 sage: slope((1, 0), rotate=-1) -1 sage: slope((0, 1), rotate=-1) 1 sage: slope((-1, 0), rotate=-1) -1 sage: slope((0, -1), rotate=-1) 1 sage: slope((1, 0), rotate=0) 0 sage: slope((0, 1), rotate=0) 0 sage: slope((-1, 0), rotate=0) 0 sage: slope((0, -1), rotate=0) 0 sage: slope((0, 0)) Traceback (most recent call last): ... ValueError: zero vector """ x, y = a if not x and not y: raise ValueError("zero vector") if (x > 0 and y > 0) or (x < 0 and y < 0): return 1 elif (x > 0 and y < 0) or (x < 0 and y > 0): return -1 if rotate == 0: return 0 if rotate == 1: return 1 if x else -1 if rotate == -1: return 1 if y else -1 raise ValueError("invalid argument rotate={}".format(rotate))