Spaces:
Sleeping
Sleeping
"""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 | |
def ambient_dimension(self): | |
"""What is the dimension of the space that the object is contained in?""" | |
raise NotImplementedError() | |
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) | |
# 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 | |
# 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 | |