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