Coverage for /usr/lib/python3/dist-packages/sympy/geometry/entity.py: 24%
254 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""The definition of the base geometrical entity with attributes common to
2all derived geometrical entities.
4Contains
5========
7GeometryEntity
8GeometricSet
10Notes
11=====
13A GeometryEntity is any object that has special geometric properties.
14A GeometrySet is a superclass of any GeometryEntity that can also
15be viewed as a sympy.sets.Set. In particular, points are the only
16GeometryEntity not considered a Set.
18Rn is a GeometrySet representing n-dimensional Euclidean space. R2 and
19R3 are currently the only ambient spaces implemented.
21"""
22from __future__ import annotations
24from sympy.core.basic import Basic
25from sympy.core.containers import Tuple
26from sympy.core.evalf import EvalfMixin, N
27from sympy.core.numbers import oo
28from sympy.core.symbol import Dummy
29from sympy.core.sympify import sympify
30from sympy.functions.elementary.trigonometric import cos, sin, atan
31from sympy.matrices import eye
32from sympy.multipledispatch import dispatch
33from sympy.printing import sstr
34from sympy.sets import Set, Union, FiniteSet
35from sympy.sets.handlers.intersection import intersection_sets
36from sympy.sets.handlers.union import union_sets
37from sympy.solvers.solvers import solve
38from sympy.utilities.misc import func_name
39from sympy.utilities.iterables import is_sequence
42# How entities are ordered; used by __cmp__ in GeometryEntity
43ordering_of_classes = [
44 "Point2D",
45 "Point3D",
46 "Point",
47 "Segment2D",
48 "Ray2D",
49 "Line2D",
50 "Segment3D",
51 "Line3D",
52 "Ray3D",
53 "Segment",
54 "Ray",
55 "Line",
56 "Plane",
57 "Triangle",
58 "RegularPolygon",
59 "Polygon",
60 "Circle",
61 "Ellipse",
62 "Curve",
63 "Parabola"
64]
67x, y = [Dummy('entity_dummy') for i in range(2)]
68T = Dummy('entity_dummy', real=True)
71class GeometryEntity(Basic, EvalfMixin):
72 """The base class for all geometrical entities.
74 This class does not represent any particular geometric entity, it only
75 provides the implementation of some methods common to all subclasses.
77 """
79 __slots__: tuple[str, ...] = ()
81 def __cmp__(self, other):
82 """Comparison of two GeometryEntities."""
83 n1 = self.__class__.__name__
84 n2 = other.__class__.__name__
85 c = (n1 > n2) - (n1 < n2)
86 if not c:
87 return 0
89 i1 = -1
90 for cls in self.__class__.__mro__:
91 try:
92 i1 = ordering_of_classes.index(cls.__name__)
93 break
94 except ValueError:
95 i1 = -1
96 if i1 == -1:
97 return c
99 i2 = -1
100 for cls in other.__class__.__mro__:
101 try:
102 i2 = ordering_of_classes.index(cls.__name__)
103 break
104 except ValueError:
105 i2 = -1
106 if i2 == -1:
107 return c
109 return (i1 > i2) - (i1 < i2)
111 def __contains__(self, other):
112 """Subclasses should implement this method for anything more complex than equality."""
113 if type(self) is type(other):
114 return self == other
115 raise NotImplementedError()
117 def __getnewargs__(self):
118 """Returns a tuple that will be passed to __new__ on unpickling."""
119 return tuple(self.args)
121 def __ne__(self, o):
122 """Test inequality of two geometrical entities."""
123 return not self == o
125 def __new__(cls, *args, **kwargs):
126 # Points are sequences, but they should not
127 # be converted to Tuples, so use this detection function instead.
128 def is_seq_and_not_point(a):
129 # we cannot use isinstance(a, Point) since we cannot import Point
130 if hasattr(a, 'is_Point') and a.is_Point:
131 return False
132 return is_sequence(a)
134 args = [Tuple(*a) if is_seq_and_not_point(a) else sympify(a) for a in args]
135 return Basic.__new__(cls, *args)
137 def __radd__(self, a):
138 """Implementation of reverse add method."""
139 return a.__add__(self)
141 def __rtruediv__(self, a):
142 """Implementation of reverse division method."""
143 return a.__truediv__(self)
145 def __repr__(self):
146 """String representation of a GeometryEntity that can be evaluated
147 by sympy."""
148 return type(self).__name__ + repr(self.args)
150 def __rmul__(self, a):
151 """Implementation of reverse multiplication method."""
152 return a.__mul__(self)
154 def __rsub__(self, a):
155 """Implementation of reverse subtraction method."""
156 return a.__sub__(self)
158 def __str__(self):
159 """String representation of a GeometryEntity."""
160 return type(self).__name__ + sstr(self.args)
162 def _eval_subs(self, old, new):
163 from sympy.geometry.point import Point, Point3D
164 if is_sequence(old) or is_sequence(new):
165 if isinstance(self, Point3D):
166 old = Point3D(old)
167 new = Point3D(new)
168 else:
169 old = Point(old)
170 new = Point(new)
171 return self._subs(old, new)
173 def _repr_svg_(self):
174 """SVG representation of a GeometryEntity suitable for IPython"""
176 try:
177 bounds = self.bounds
178 except (NotImplementedError, TypeError):
179 # if we have no SVG representation, return None so IPython
180 # will fall back to the next representation
181 return None
183 if not all(x.is_number and x.is_finite for x in bounds):
184 return None
186 svg_top = '''<svg xmlns="http://www.w3.org/2000/svg"
187 xmlns:xlink="http://www.w3.org/1999/xlink"
188 width="{1}" height="{2}" viewBox="{0}"
189 preserveAspectRatio="xMinYMin meet">
190 <defs>
191 <marker id="markerCircle" markerWidth="8" markerHeight="8"
192 refx="5" refy="5" markerUnits="strokeWidth">
193 <circle cx="5" cy="5" r="1.5" style="stroke: none; fill:#000000;"/>
194 </marker>
195 <marker id="markerArrow" markerWidth="13" markerHeight="13" refx="2" refy="4"
196 orient="auto" markerUnits="strokeWidth">
197 <path d="M2,2 L2,6 L6,4" style="fill: #000000;" />
198 </marker>
199 <marker id="markerReverseArrow" markerWidth="13" markerHeight="13" refx="6" refy="4"
200 orient="auto" markerUnits="strokeWidth">
201 <path d="M6,2 L6,6 L2,4" style="fill: #000000;" />
202 </marker>
203 </defs>'''
205 # Establish SVG canvas that will fit all the data + small space
206 xmin, ymin, xmax, ymax = map(N, bounds)
207 if xmin == xmax and ymin == ymax:
208 # This is a point; buffer using an arbitrary size
209 xmin, ymin, xmax, ymax = xmin - .5, ymin -.5, xmax + .5, ymax + .5
210 else:
211 # Expand bounds by a fraction of the data ranges
212 expand = 0.1 # or 10%; this keeps arrowheads in view (R plots use 4%)
213 widest_part = max([xmax - xmin, ymax - ymin])
214 expand_amount = widest_part * expand
215 xmin -= expand_amount
216 ymin -= expand_amount
217 xmax += expand_amount
218 ymax += expand_amount
219 dx = xmax - xmin
220 dy = ymax - ymin
221 width = min([max([100., dx]), 300])
222 height = min([max([100., dy]), 300])
224 scale_factor = 1. if max(width, height) == 0 else max(dx, dy) / max(width, height)
225 try:
226 svg = self._svg(scale_factor)
227 except (NotImplementedError, TypeError):
228 # if we have no SVG representation, return None so IPython
229 # will fall back to the next representation
230 return None
232 view_box = "{} {} {} {}".format(xmin, ymin, dx, dy)
233 transform = "matrix(1,0,0,-1,0,{})".format(ymax + ymin)
234 svg_top = svg_top.format(view_box, width, height)
236 return svg_top + (
237 '<g transform="{}">{}</g></svg>'
238 ).format(transform, svg)
240 def _svg(self, scale_factor=1., fill_color="#66cc99"):
241 """Returns SVG path element for the GeometryEntity.
243 Parameters
244 ==========
246 scale_factor : float
247 Multiplication factor for the SVG stroke-width. Default is 1.
248 fill_color : str, optional
249 Hex string for fill color. Default is "#66cc99".
250 """
251 raise NotImplementedError()
253 def _sympy_(self):
254 return self
256 @property
257 def ambient_dimension(self):
258 """What is the dimension of the space that the object is contained in?"""
259 raise NotImplementedError()
261 @property
262 def bounds(self):
263 """Return a tuple (xmin, ymin, xmax, ymax) representing the bounding
264 rectangle for the geometric figure.
266 """
268 raise NotImplementedError()
270 def encloses(self, o):
271 """
272 Return True if o is inside (not on or outside) the boundaries of self.
274 The object will be decomposed into Points and individual Entities need
275 only define an encloses_point method for their class.
277 See Also
278 ========
280 sympy.geometry.ellipse.Ellipse.encloses_point
281 sympy.geometry.polygon.Polygon.encloses_point
283 Examples
284 ========
286 >>> from sympy import RegularPolygon, Point, Polygon
287 >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
288 >>> t2 = Polygon(*RegularPolygon(Point(0, 0), 2, 3).vertices)
289 >>> t2.encloses(t)
290 True
291 >>> t.encloses(t2)
292 False
294 """
296 from sympy.geometry.point import Point
297 from sympy.geometry.line import Segment, Ray, Line
298 from sympy.geometry.ellipse import Ellipse
299 from sympy.geometry.polygon import Polygon, RegularPolygon
301 if isinstance(o, Point):
302 return self.encloses_point(o)
303 elif isinstance(o, Segment):
304 return all(self.encloses_point(x) for x in o.points)
305 elif isinstance(o, (Ray, Line)):
306 return False
307 elif isinstance(o, Ellipse):
308 return self.encloses_point(o.center) and \
309 self.encloses_point(
310 Point(o.center.x + o.hradius, o.center.y)) and \
311 not self.intersection(o)
312 elif isinstance(o, Polygon):
313 if isinstance(o, RegularPolygon):
314 if not self.encloses_point(o.center):
315 return False
316 return all(self.encloses_point(v) for v in o.vertices)
317 raise NotImplementedError()
319 def equals(self, o):
320 return self == o
322 def intersection(self, o):
323 """
324 Returns a list of all of the intersections of self with o.
326 Notes
327 =====
329 An entity is not required to implement this method.
331 If two different types of entities can intersect, the item with
332 higher index in ordering_of_classes should implement
333 intersections with anything having a lower index.
335 See Also
336 ========
338 sympy.geometry.util.intersection
340 """
341 raise NotImplementedError()
343 def is_similar(self, other):
344 """Is this geometrical entity similar to another geometrical entity?
346 Two entities are similar if a uniform scaling (enlarging or
347 shrinking) of one of the entities will allow one to obtain the other.
349 Notes
350 =====
352 This method is not intended to be used directly but rather
353 through the `are_similar` function found in util.py.
354 An entity is not required to implement this method.
355 If two different types of entities can be similar, it is only
356 required that one of them be able to determine this.
358 See Also
359 ========
361 scale
363 """
364 raise NotImplementedError()
366 def reflect(self, line):
367 """
368 Reflects an object across a line.
370 Parameters
371 ==========
373 line: Line
375 Examples
376 ========
378 >>> from sympy import pi, sqrt, Line, RegularPolygon
379 >>> l = Line((0, pi), slope=sqrt(2))
380 >>> pent = RegularPolygon((1, 2), 1, 5)
381 >>> rpent = pent.reflect(l)
382 >>> rpent
383 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)
385 >>> from sympy import pi, Line, Circle, Point
386 >>> l = Line((0, pi), slope=1)
387 >>> circ = Circle(Point(0, 0), 5)
388 >>> rcirc = circ.reflect(l)
389 >>> rcirc
390 Circle(Point2D(-pi, pi), -5)
392 """
393 from sympy.geometry.point import Point
395 g = self
396 l = line
397 o = Point(0, 0)
398 if l.slope.is_zero:
399 v = l.args[0].y
400 if not v: # x-axis
401 return g.scale(y=-1)
402 reps = [(p, p.translate(y=2*(v - p.y))) for p in g.atoms(Point)]
403 elif l.slope is oo:
404 v = l.args[0].x
405 if not v: # y-axis
406 return g.scale(x=-1)
407 reps = [(p, p.translate(x=2*(v - p.x))) for p in g.atoms(Point)]
408 else:
409 if not hasattr(g, 'reflect') and not all(
410 isinstance(arg, Point) for arg in g.args):
411 raise NotImplementedError(
412 'reflect undefined or non-Point args in %s' % g)
413 a = atan(l.slope)
414 c = l.coefficients
415 d = -c[-1]/c[1] # y-intercept
416 # apply the transform to a single point
417 xf = Point(x, y)
418 xf = xf.translate(y=-d).rotate(-a, o).scale(y=-1
419 ).rotate(a, o).translate(y=d)
420 # replace every point using that transform
421 reps = [(p, xf.xreplace({x: p.x, y: p.y})) for p in g.atoms(Point)]
422 return g.xreplace(dict(reps))
424 def rotate(self, angle, pt=None):
425 """Rotate ``angle`` radians counterclockwise about Point ``pt``.
427 The default pt is the origin, Point(0, 0)
429 See Also
430 ========
432 scale, translate
434 Examples
435 ========
437 >>> from sympy import Point, RegularPolygon, Polygon, pi
438 >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
439 >>> t # vertex on x axis
440 Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2))
441 >>> t.rotate(pi/2) # vertex on y axis now
442 Triangle(Point2D(0, 1), Point2D(-sqrt(3)/2, -1/2), Point2D(sqrt(3)/2, -1/2))
444 """
445 newargs = []
446 for a in self.args:
447 if isinstance(a, GeometryEntity):
448 newargs.append(a.rotate(angle, pt))
449 else:
450 newargs.append(a)
451 return type(self)(*newargs)
453 def scale(self, x=1, y=1, pt=None):
454 """Scale the object by multiplying the x,y-coordinates by x and y.
456 If pt is given, the scaling is done relative to that point; the
457 object is shifted by -pt, scaled, and shifted by pt.
459 See Also
460 ========
462 rotate, translate
464 Examples
465 ========
467 >>> from sympy import RegularPolygon, Point, Polygon
468 >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
469 >>> t
470 Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2))
471 >>> t.scale(2)
472 Triangle(Point2D(2, 0), Point2D(-1, sqrt(3)/2), Point2D(-1, -sqrt(3)/2))
473 >>> t.scale(2, 2)
474 Triangle(Point2D(2, 0), Point2D(-1, sqrt(3)), Point2D(-1, -sqrt(3)))
476 """
477 from sympy.geometry.point import Point
478 if pt:
479 pt = Point(pt, dim=2)
480 return self.translate(*(-pt).args).scale(x, y).translate(*pt.args)
481 return type(self)(*[a.scale(x, y) for a in self.args]) # if this fails, override this class
483 def translate(self, x=0, y=0):
484 """Shift the object by adding to the x,y-coordinates the values x and y.
486 See Also
487 ========
489 rotate, scale
491 Examples
492 ========
494 >>> from sympy import RegularPolygon, Point, Polygon
495 >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
496 >>> t
497 Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2))
498 >>> t.translate(2)
499 Triangle(Point2D(3, 0), Point2D(3/2, sqrt(3)/2), Point2D(3/2, -sqrt(3)/2))
500 >>> t.translate(2, 2)
501 Triangle(Point2D(3, 2), Point2D(3/2, sqrt(3)/2 + 2), Point2D(3/2, 2 - sqrt(3)/2))
503 """
504 newargs = []
505 for a in self.args:
506 if isinstance(a, GeometryEntity):
507 newargs.append(a.translate(x, y))
508 else:
509 newargs.append(a)
510 return self.func(*newargs)
512 def parameter_value(self, other, t):
513 """Return the parameter corresponding to the given point.
514 Evaluating an arbitrary point of the entity at this parameter
515 value will return the given point.
517 Examples
518 ========
520 >>> from sympy import Line, Point
521 >>> from sympy.abc import t
522 >>> a = Point(0, 0)
523 >>> b = Point(2, 2)
524 >>> Line(a, b).parameter_value((1, 1), t)
525 {t: 1/2}
526 >>> Line(a, b).arbitrary_point(t).subs(_)
527 Point2D(1, 1)
528 """
529 from sympy.geometry.point import Point
530 if not isinstance(other, GeometryEntity):
531 other = Point(other, dim=self.ambient_dimension)
532 if not isinstance(other, Point):
533 raise ValueError("other must be a point")
534 sol = solve(self.arbitrary_point(T) - other, T, dict=True)
535 if not sol:
536 raise ValueError("Given point is not on %s" % func_name(self))
537 return {t: sol[0][T]}
540class GeometrySet(GeometryEntity, Set):
541 """Parent class of all GeometryEntity that are also Sets
542 (compatible with sympy.sets)
543 """
544 __slots__ = ()
546 def _contains(self, other):
547 """sympy.sets uses the _contains method, so include it for compatibility."""
549 if isinstance(other, Set) and other.is_FiniteSet:
550 return all(self.__contains__(i) for i in other)
552 return self.__contains__(other)
554@dispatch(GeometrySet, Set) # type:ignore # noqa:F811
555def union_sets(self, o): # noqa:F811
556 """ Returns the union of self and o
557 for use with sympy.sets.Set, if possible. """
560 # if its a FiniteSet, merge any points
561 # we contain and return a union with the rest
562 if o.is_FiniteSet:
563 other_points = [p for p in o if not self._contains(p)]
564 if len(other_points) == len(o):
565 return None
566 return Union(self, FiniteSet(*other_points))
567 if self._contains(o):
568 return self
569 return None
572@dispatch(GeometrySet, Set) # type: ignore # noqa:F811
573def intersection_sets(self, o): # noqa:F811
574 """ Returns a sympy.sets.Set of intersection objects,
575 if possible. """
577 from sympy.geometry.point import Point
579 try:
580 # if o is a FiniteSet, find the intersection directly
581 # to avoid infinite recursion
582 if o.is_FiniteSet:
583 inter = FiniteSet(*(p for p in o if self.contains(p)))
584 else:
585 inter = self.intersection(o)
586 except NotImplementedError:
587 # sympy.sets.Set.reduce expects None if an object
588 # doesn't know how to simplify
589 return None
591 # put the points in a FiniteSet
592 points = FiniteSet(*[p for p in inter if isinstance(p, Point)])
593 non_points = [p for p in inter if not isinstance(p, Point)]
595 return Union(*(non_points + [points]))
597def translate(x, y):
598 """Return the matrix to translate a 2-D point by x and y."""
599 rv = eye(3)
600 rv[2, 0] = x
601 rv[2, 1] = y
602 return rv
605def scale(x, y, pt=None):
606 """Return the matrix to multiply a 2-D point's coordinates by x and y.
608 If pt is given, the scaling is done relative to that point."""
609 rv = eye(3)
610 rv[0, 0] = x
611 rv[1, 1] = y
612 if pt:
613 from sympy.geometry.point import Point
614 pt = Point(pt, dim=2)
615 tr1 = translate(*(-pt).args)
616 tr2 = translate(*pt.args)
617 return tr1*rv*tr2
618 return rv
621def rotate(th):
622 """Return the matrix to rotate a 2-D point about the origin by ``angle``.
624 The angle is measured in radians. To Point a point about a point other
625 then the origin, translate the Point, do the rotation, and
626 translate it back:
628 >>> from sympy.geometry.entity import rotate, translate
629 >>> from sympy import Point, pi
630 >>> rot_about_11 = translate(-1, -1)*rotate(pi/2)*translate(1, 1)
631 >>> Point(1, 1).transform(rot_about_11)
632 Point2D(1, 1)
633 >>> Point(0, 0).transform(rot_about_11)
634 Point2D(2, 0)
635 """
636 s = sin(th)
637 rv = eye(3)*cos(th)
638 rv[0, 1] = s
639 rv[1, 0] = -s
640 rv[2, 2] = 1
641 return rv