You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
642 lines
20 KiB
642 lines
20 KiB
"""The definition of the base geometrical entity with attributes common to
|
|
all derived geometrical entities.
|
|
|
|
Contains
|
|
========
|
|
|
|
GeometryEntity
|
|
GeometricSet
|
|
|
|
Notes
|
|
=====
|
|
|
|
A GeometryEntity is any object that has special geometric properties.
|
|
A GeometrySet is a superclass of any GeometryEntity that can also
|
|
be viewed as a sympy.sets.Set. In particular, points are the only
|
|
GeometryEntity not considered a Set.
|
|
|
|
Rn is a GeometrySet representing n-dimensional Euclidean space. R2 and
|
|
R3 are currently the only ambient spaces implemented.
|
|
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from sympy.core.basic import Basic
|
|
from sympy.core.containers import Tuple
|
|
from sympy.core.evalf import EvalfMixin, N
|
|
from sympy.core.numbers import oo
|
|
from sympy.core.symbol import Dummy
|
|
from sympy.core.sympify import sympify
|
|
from sympy.functions.elementary.trigonometric import cos, sin, atan
|
|
from sympy.matrices import eye
|
|
from sympy.multipledispatch import dispatch
|
|
from sympy.printing import sstr
|
|
from sympy.sets import Set, Union, FiniteSet
|
|
from sympy.sets.handlers.intersection import intersection_sets
|
|
from sympy.sets.handlers.union import union_sets
|
|
from sympy.solvers.solvers import solve
|
|
from sympy.utilities.misc import func_name
|
|
from sympy.utilities.iterables import is_sequence
|
|
|
|
|
|
# How entities are ordered; used by __cmp__ in GeometryEntity
|
|
ordering_of_classes = [
|
|
"Point2D",
|
|
"Point3D",
|
|
"Point",
|
|
"Segment2D",
|
|
"Ray2D",
|
|
"Line2D",
|
|
"Segment3D",
|
|
"Line3D",
|
|
"Ray3D",
|
|
"Segment",
|
|
"Ray",
|
|
"Line",
|
|
"Plane",
|
|
"Triangle",
|
|
"RegularPolygon",
|
|
"Polygon",
|
|
"Circle",
|
|
"Ellipse",
|
|
"Curve",
|
|
"Parabola"
|
|
]
|
|
|
|
|
|
x, y = [Dummy('entity_dummy') for i in range(2)]
|
|
T = Dummy('entity_dummy', real=True)
|
|
|
|
|
|
class GeometryEntity(Basic, EvalfMixin):
|
|
"""The base class for all geometrical entities.
|
|
|
|
This class does not represent any particular geometric entity, it only
|
|
provides the implementation of some methods common to all subclasses.
|
|
|
|
"""
|
|
|
|
__slots__: tuple[str, ...] = ()
|
|
|
|
def __cmp__(self, other):
|
|
"""Comparison of two GeometryEntities."""
|
|
n1 = self.__class__.__name__
|
|
n2 = other.__class__.__name__
|
|
c = (n1 > n2) - (n1 < n2)
|
|
if not c:
|
|
return 0
|
|
|
|
i1 = -1
|
|
for cls in self.__class__.__mro__:
|
|
try:
|
|
i1 = ordering_of_classes.index(cls.__name__)
|
|
break
|
|
except ValueError:
|
|
i1 = -1
|
|
if i1 == -1:
|
|
return c
|
|
|
|
i2 = -1
|
|
for cls in other.__class__.__mro__:
|
|
try:
|
|
i2 = ordering_of_classes.index(cls.__name__)
|
|
break
|
|
except ValueError:
|
|
i2 = -1
|
|
if i2 == -1:
|
|
return c
|
|
|
|
return (i1 > i2) - (i1 < i2)
|
|
|
|
def __contains__(self, other):
|
|
"""Subclasses should implement this method for anything more complex than equality."""
|
|
if type(self) is type(other):
|
|
return self == other
|
|
raise NotImplementedError()
|
|
|
|
def __getnewargs__(self):
|
|
"""Returns a tuple that will be passed to __new__ on unpickling."""
|
|
return tuple(self.args)
|
|
|
|
def __ne__(self, o):
|
|
"""Test inequality of two geometrical entities."""
|
|
return not self == o
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
# Points are sequences, but they should not
|
|
# be converted to Tuples, so use this detection function instead.
|
|
def is_seq_and_not_point(a):
|
|
# we cannot use isinstance(a, Point) since we cannot import Point
|
|
if hasattr(a, 'is_Point') and a.is_Point:
|
|
return False
|
|
return is_sequence(a)
|
|
|
|
args = [Tuple(*a) if is_seq_and_not_point(a) else sympify(a) for a in args]
|
|
return Basic.__new__(cls, *args)
|
|
|
|
def __radd__(self, a):
|
|
"""Implementation of reverse add method."""
|
|
return a.__add__(self)
|
|
|
|
def __rtruediv__(self, a):
|
|
"""Implementation of reverse division method."""
|
|
return a.__truediv__(self)
|
|
|
|
def __repr__(self):
|
|
"""String representation of a GeometryEntity that can be evaluated
|
|
by sympy."""
|
|
return type(self).__name__ + repr(self.args)
|
|
|
|
def __rmul__(self, a):
|
|
"""Implementation of reverse multiplication method."""
|
|
return a.__mul__(self)
|
|
|
|
def __rsub__(self, a):
|
|
"""Implementation of reverse subtraction method."""
|
|
return a.__sub__(self)
|
|
|
|
def __str__(self):
|
|
"""String representation of a GeometryEntity."""
|
|
return type(self).__name__ + sstr(self.args)
|
|
|
|
def _eval_subs(self, old, new):
|
|
from sympy.geometry.point import Point, Point3D
|
|
if is_sequence(old) or is_sequence(new):
|
|
if isinstance(self, Point3D):
|
|
old = Point3D(old)
|
|
new = Point3D(new)
|
|
else:
|
|
old = Point(old)
|
|
new = Point(new)
|
|
return self._subs(old, new)
|
|
|
|
def _repr_svg_(self):
|
|
"""SVG representation of a GeometryEntity suitable for IPython"""
|
|
|
|
try:
|
|
bounds = self.bounds
|
|
except (NotImplementedError, TypeError):
|
|
# if we have no SVG representation, return None so IPython
|
|
# will fall back to the next representation
|
|
return None
|
|
|
|
if not all(x.is_number and x.is_finite for x in bounds):
|
|
return None
|
|
|
|
svg_top = '''<svg xmlns="http://www.w3.org/2000/svg"
|
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
width="{1}" height="{2}" viewBox="{0}"
|
|
preserveAspectRatio="xMinYMin meet">
|
|
<defs>
|
|
<marker id="markerCircle" markerWidth="8" markerHeight="8"
|
|
refx="5" refy="5" markerUnits="strokeWidth">
|
|
<circle cx="5" cy="5" r="1.5" style="stroke: none; fill:#000000;"/>
|
|
</marker>
|
|
<marker id="markerArrow" markerWidth="13" markerHeight="13" refx="2" refy="4"
|
|
orient="auto" markerUnits="strokeWidth">
|
|
<path d="M2,2 L2,6 L6,4" style="fill: #000000;" />
|
|
</marker>
|
|
<marker id="markerReverseArrow" markerWidth="13" markerHeight="13" refx="6" refy="4"
|
|
orient="auto" markerUnits="strokeWidth">
|
|
<path d="M6,2 L6,6 L2,4" style="fill: #000000;" />
|
|
</marker>
|
|
</defs>'''
|
|
|
|
# Establish SVG canvas that will fit all the data + small space
|
|
xmin, ymin, xmax, ymax = map(N, bounds)
|
|
if xmin == xmax and ymin == ymax:
|
|
# This is a point; buffer using an arbitrary size
|
|
xmin, ymin, xmax, ymax = xmin - .5, ymin -.5, xmax + .5, ymax + .5
|
|
else:
|
|
# Expand bounds by a fraction of the data ranges
|
|
expand = 0.1 # or 10%; this keeps arrowheads in view (R plots use 4%)
|
|
widest_part = max([xmax - xmin, ymax - ymin])
|
|
expand_amount = widest_part * expand
|
|
xmin -= expand_amount
|
|
ymin -= expand_amount
|
|
xmax += expand_amount
|
|
ymax += expand_amount
|
|
dx = xmax - xmin
|
|
dy = ymax - ymin
|
|
width = min([max([100., dx]), 300])
|
|
height = min([max([100., dy]), 300])
|
|
|
|
scale_factor = 1. if max(width, height) == 0 else max(dx, dy) / max(width, height)
|
|
try:
|
|
svg = self._svg(scale_factor)
|
|
except (NotImplementedError, TypeError):
|
|
# if we have no SVG representation, return None so IPython
|
|
# will fall back to the next representation
|
|
return None
|
|
|
|
view_box = "{} {} {} {}".format(xmin, ymin, dx, dy)
|
|
transform = "matrix(1,0,0,-1,0,{})".format(ymax + ymin)
|
|
svg_top = svg_top.format(view_box, width, height)
|
|
|
|
return svg_top + (
|
|
'<g transform="{}">{}</g></svg>'
|
|
).format(transform, svg)
|
|
|
|
def _svg(self, scale_factor=1., fill_color="#66cc99"):
|
|
"""Returns SVG path element for the GeometryEntity.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
scale_factor : float
|
|
Multiplication factor for the SVG stroke-width. Default is 1.
|
|
fill_color : str, optional
|
|
Hex string for fill color. Default is "#66cc99".
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def _sympy_(self):
|
|
return self
|
|
|
|
@property
|
|
def ambient_dimension(self):
|
|
"""What is the dimension of the space that the object is contained in?"""
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def bounds(self):
|
|
"""Return a tuple (xmin, ymin, xmax, ymax) representing the bounding
|
|
rectangle for the geometric figure.
|
|
|
|
"""
|
|
|
|
raise NotImplementedError()
|
|
|
|
def encloses(self, o):
|
|
"""
|
|
Return True if o is inside (not on or outside) the boundaries of self.
|
|
|
|
The object will be decomposed into Points and individual Entities need
|
|
only define an encloses_point method for their class.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.ellipse.Ellipse.encloses_point
|
|
sympy.geometry.polygon.Polygon.encloses_point
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import RegularPolygon, Point, Polygon
|
|
>>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
|
|
>>> t2 = Polygon(*RegularPolygon(Point(0, 0), 2, 3).vertices)
|
|
>>> t2.encloses(t)
|
|
True
|
|
>>> t.encloses(t2)
|
|
False
|
|
|
|
"""
|
|
|
|
from sympy.geometry.point import Point
|
|
from sympy.geometry.line import Segment, Ray, Line
|
|
from sympy.geometry.ellipse import Ellipse
|
|
from sympy.geometry.polygon import Polygon, RegularPolygon
|
|
|
|
if isinstance(o, Point):
|
|
return self.encloses_point(o)
|
|
elif isinstance(o, Segment):
|
|
return all(self.encloses_point(x) for x in o.points)
|
|
elif isinstance(o, (Ray, Line)):
|
|
return False
|
|
elif isinstance(o, Ellipse):
|
|
return self.encloses_point(o.center) and \
|
|
self.encloses_point(
|
|
Point(o.center.x + o.hradius, o.center.y)) and \
|
|
not self.intersection(o)
|
|
elif isinstance(o, Polygon):
|
|
if isinstance(o, RegularPolygon):
|
|
if not self.encloses_point(o.center):
|
|
return False
|
|
return all(self.encloses_point(v) for v in o.vertices)
|
|
raise NotImplementedError()
|
|
|
|
def equals(self, o):
|
|
return self == o
|
|
|
|
def intersection(self, o):
|
|
"""
|
|
Returns a list of all of the intersections of self with o.
|
|
|
|
Notes
|
|
=====
|
|
|
|
An entity is not required to implement this method.
|
|
|
|
If two different types of entities can intersect, the item with
|
|
higher index in ordering_of_classes should implement
|
|
intersections with anything having a lower index.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.util.intersection
|
|
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def is_similar(self, other):
|
|
"""Is this geometrical entity similar to another geometrical entity?
|
|
|
|
Two entities are similar if a uniform scaling (enlarging or
|
|
shrinking) of one of the entities will allow one to obtain the other.
|
|
|
|
Notes
|
|
=====
|
|
|
|
This method is not intended to be used directly but rather
|
|
through the `are_similar` function found in util.py.
|
|
An entity is not required to implement this method.
|
|
If two different types of entities can be similar, it is only
|
|
required that one of them be able to determine this.
|
|
|
|
See Also
|
|
========
|
|
|
|
scale
|
|
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def reflect(self, line):
|
|
"""
|
|
Reflects an object across a line.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
line: Line
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import pi, sqrt, Line, RegularPolygon
|
|
>>> l = Line((0, pi), slope=sqrt(2))
|
|
>>> pent = RegularPolygon((1, 2), 1, 5)
|
|
>>> rpent = pent.reflect(l)
|
|
>>> rpent
|
|
RegularPolygon(Point2D(-2*sqrt(2)*pi/3 - 1/3 + 4*sqrt(2)/3, 2/3 + 2*sqrt(2)/3 + 2*pi/3), -1, 5, -atan(2*sqrt(2)) + 3*pi/5)
|
|
|
|
>>> from sympy import pi, Line, Circle, Point
|
|
>>> l = Line((0, pi), slope=1)
|
|
>>> circ = Circle(Point(0, 0), 5)
|
|
>>> rcirc = circ.reflect(l)
|
|
>>> rcirc
|
|
Circle(Point2D(-pi, pi), -5)
|
|
|
|
"""
|
|
from sympy.geometry.point import Point
|
|
|
|
g = self
|
|
l = line
|
|
o = Point(0, 0)
|
|
if l.slope.is_zero:
|
|
v = l.args[0].y
|
|
if not v: # x-axis
|
|
return g.scale(y=-1)
|
|
reps = [(p, p.translate(y=2*(v - p.y))) for p in g.atoms(Point)]
|
|
elif l.slope is oo:
|
|
v = l.args[0].x
|
|
if not v: # y-axis
|
|
return g.scale(x=-1)
|
|
reps = [(p, p.translate(x=2*(v - p.x))) for p in g.atoms(Point)]
|
|
else:
|
|
if not hasattr(g, 'reflect') and not all(
|
|
isinstance(arg, Point) for arg in g.args):
|
|
raise NotImplementedError(
|
|
'reflect undefined or non-Point args in %s' % g)
|
|
a = atan(l.slope)
|
|
c = l.coefficients
|
|
d = -c[-1]/c[1] # y-intercept
|
|
# apply the transform to a single point
|
|
xf = Point(x, y)
|
|
xf = xf.translate(y=-d).rotate(-a, o).scale(y=-1
|
|
).rotate(a, o).translate(y=d)
|
|
# replace every point using that transform
|
|
reps = [(p, xf.xreplace({x: p.x, y: p.y})) for p in g.atoms(Point)]
|
|
return g.xreplace(dict(reps))
|
|
|
|
def rotate(self, angle, pt=None):
|
|
"""Rotate ``angle`` radians counterclockwise about Point ``pt``.
|
|
|
|
The default pt is the origin, Point(0, 0)
|
|
|
|
See Also
|
|
========
|
|
|
|
scale, translate
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, RegularPolygon, Polygon, pi
|
|
>>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
|
|
>>> t # vertex on x axis
|
|
Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2))
|
|
>>> t.rotate(pi/2) # vertex on y axis now
|
|
Triangle(Point2D(0, 1), Point2D(-sqrt(3)/2, -1/2), Point2D(sqrt(3)/2, -1/2))
|
|
|
|
"""
|
|
newargs = []
|
|
for a in self.args:
|
|
if isinstance(a, GeometryEntity):
|
|
newargs.append(a.rotate(angle, pt))
|
|
else:
|
|
newargs.append(a)
|
|
return type(self)(*newargs)
|
|
|
|
def scale(self, x=1, y=1, pt=None):
|
|
"""Scale the object by multiplying the x,y-coordinates by x and y.
|
|
|
|
If pt is given, the scaling is done relative to that point; the
|
|
object is shifted by -pt, scaled, and shifted by pt.
|
|
|
|
See Also
|
|
========
|
|
|
|
rotate, translate
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import RegularPolygon, Point, Polygon
|
|
>>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
|
|
>>> t
|
|
Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2))
|
|
>>> t.scale(2)
|
|
Triangle(Point2D(2, 0), Point2D(-1, sqrt(3)/2), Point2D(-1, -sqrt(3)/2))
|
|
>>> t.scale(2, 2)
|
|
Triangle(Point2D(2, 0), Point2D(-1, sqrt(3)), Point2D(-1, -sqrt(3)))
|
|
|
|
"""
|
|
from sympy.geometry.point import Point
|
|
if pt:
|
|
pt = Point(pt, dim=2)
|
|
return self.translate(*(-pt).args).scale(x, y).translate(*pt.args)
|
|
return type(self)(*[a.scale(x, y) for a in self.args]) # if this fails, override this class
|
|
|
|
def translate(self, x=0, y=0):
|
|
"""Shift the object by adding to the x,y-coordinates the values x and y.
|
|
|
|
See Also
|
|
========
|
|
|
|
rotate, scale
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import RegularPolygon, Point, Polygon
|
|
>>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
|
|
>>> t
|
|
Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2))
|
|
>>> t.translate(2)
|
|
Triangle(Point2D(3, 0), Point2D(3/2, sqrt(3)/2), Point2D(3/2, -sqrt(3)/2))
|
|
>>> t.translate(2, 2)
|
|
Triangle(Point2D(3, 2), Point2D(3/2, sqrt(3)/2 + 2), Point2D(3/2, 2 - sqrt(3)/2))
|
|
|
|
"""
|
|
newargs = []
|
|
for a in self.args:
|
|
if isinstance(a, GeometryEntity):
|
|
newargs.append(a.translate(x, y))
|
|
else:
|
|
newargs.append(a)
|
|
return self.func(*newargs)
|
|
|
|
def parameter_value(self, other, t):
|
|
"""Return the parameter corresponding to the given point.
|
|
Evaluating an arbitrary point of the entity at this parameter
|
|
value will return the given point.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Line, Point
|
|
>>> from sympy.abc import t
|
|
>>> a = Point(0, 0)
|
|
>>> b = Point(2, 2)
|
|
>>> Line(a, b).parameter_value((1, 1), t)
|
|
{t: 1/2}
|
|
>>> Line(a, b).arbitrary_point(t).subs(_)
|
|
Point2D(1, 1)
|
|
"""
|
|
from sympy.geometry.point import Point
|
|
if not isinstance(other, GeometryEntity):
|
|
other = Point(other, dim=self.ambient_dimension)
|
|
if not isinstance(other, Point):
|
|
raise ValueError("other must be a point")
|
|
sol = solve(self.arbitrary_point(T) - other, T, dict=True)
|
|
if not sol:
|
|
raise ValueError("Given point is not on %s" % func_name(self))
|
|
return {t: sol[0][T]}
|
|
|
|
|
|
class GeometrySet(GeometryEntity, Set):
|
|
"""Parent class of all GeometryEntity that are also Sets
|
|
(compatible with sympy.sets)
|
|
"""
|
|
__slots__ = ()
|
|
|
|
def _contains(self, other):
|
|
"""sympy.sets uses the _contains method, so include it for compatibility."""
|
|
|
|
if isinstance(other, Set) and other.is_FiniteSet:
|
|
return all(self.__contains__(i) for i in other)
|
|
|
|
return self.__contains__(other)
|
|
|
|
@dispatch(GeometrySet, Set) # type:ignore # noqa:F811
|
|
def union_sets(self, o): # noqa:F811
|
|
""" Returns the union of self and o
|
|
for use with sympy.sets.Set, if possible. """
|
|
|
|
|
|
# if its a FiniteSet, merge any points
|
|
# we contain and return a union with the rest
|
|
if o.is_FiniteSet:
|
|
other_points = [p for p in o if not self._contains(p)]
|
|
if len(other_points) == len(o):
|
|
return None
|
|
return Union(self, FiniteSet(*other_points))
|
|
if self._contains(o):
|
|
return self
|
|
return None
|
|
|
|
|
|
@dispatch(GeometrySet, Set) # type: ignore # noqa:F811
|
|
def intersection_sets(self, o): # noqa:F811
|
|
""" Returns a sympy.sets.Set of intersection objects,
|
|
if possible. """
|
|
|
|
from sympy.geometry.point import Point
|
|
|
|
try:
|
|
# if o is a FiniteSet, find the intersection directly
|
|
# to avoid infinite recursion
|
|
if o.is_FiniteSet:
|
|
inter = FiniteSet(*(p for p in o if self.contains(p)))
|
|
else:
|
|
inter = self.intersection(o)
|
|
except NotImplementedError:
|
|
# sympy.sets.Set.reduce expects None if an object
|
|
# doesn't know how to simplify
|
|
return None
|
|
|
|
# put the points in a FiniteSet
|
|
points = FiniteSet(*[p for p in inter if isinstance(p, Point)])
|
|
non_points = [p for p in inter if not isinstance(p, Point)]
|
|
|
|
return Union(*(non_points + [points]))
|
|
|
|
def translate(x, y):
|
|
"""Return the matrix to translate a 2-D point by x and y."""
|
|
rv = eye(3)
|
|
rv[2, 0] = x
|
|
rv[2, 1] = y
|
|
return rv
|
|
|
|
|
|
def scale(x, y, pt=None):
|
|
"""Return the matrix to multiply a 2-D point's coordinates by x and y.
|
|
|
|
If pt is given, the scaling is done relative to that point."""
|
|
rv = eye(3)
|
|
rv[0, 0] = x
|
|
rv[1, 1] = y
|
|
if pt:
|
|
from sympy.geometry.point import Point
|
|
pt = Point(pt, dim=2)
|
|
tr1 = translate(*(-pt).args)
|
|
tr2 = translate(*pt.args)
|
|
return tr1*rv*tr2
|
|
return rv
|
|
|
|
|
|
def rotate(th):
|
|
"""Return the matrix to rotate a 2-D point about the origin by ``angle``.
|
|
|
|
The angle is measured in radians. To Point a point about a point other
|
|
then the origin, translate the Point, do the rotation, and
|
|
translate it back:
|
|
|
|
>>> from sympy.geometry.entity import rotate, translate
|
|
>>> from sympy import Point, pi
|
|
>>> rot_about_11 = translate(-1, -1)*rotate(pi/2)*translate(1, 1)
|
|
>>> Point(1, 1).transform(rot_about_11)
|
|
Point2D(1, 1)
|
|
>>> Point(0, 0).transform(rot_about_11)
|
|
Point2D(2, 0)
|
|
"""
|
|
s = sin(th)
|
|
rv = eye(3)*cos(th)
|
|
rv[0, 1] = s
|
|
rv[1, 0] = -s
|
|
rv[2, 2] = 1
|
|
return rv
|