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

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""" 

22from __future__ import annotations 

23 

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 

40 

41 

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] 

65 

66 

67x, y = [Dummy('entity_dummy') for i in range(2)] 

68T = Dummy('entity_dummy', real=True) 

69 

70 

71class GeometryEntity(Basic, EvalfMixin): 

72 """The base class for all geometrical entities. 

73 

74 This class does not represent any particular geometric entity, it only 

75 provides the implementation of some methods common to all subclasses. 

76 

77 """ 

78 

79 __slots__: tuple[str, ...] = () 

80 

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 

88 

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 

98 

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 

108 

109 return (i1 > i2) - (i1 < i2) 

110 

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() 

116 

117 def __getnewargs__(self): 

118 """Returns a tuple that will be passed to __new__ on unpickling.""" 

119 return tuple(self.args) 

120 

121 def __ne__(self, o): 

122 """Test inequality of two geometrical entities.""" 

123 return not self == o 

124 

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) 

133 

134 args = [Tuple(*a) if is_seq_and_not_point(a) else sympify(a) for a in args] 

135 return Basic.__new__(cls, *args) 

136 

137 def __radd__(self, a): 

138 """Implementation of reverse add method.""" 

139 return a.__add__(self) 

140 

141 def __rtruediv__(self, a): 

142 """Implementation of reverse division method.""" 

143 return a.__truediv__(self) 

144 

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) 

149 

150 def __rmul__(self, a): 

151 """Implementation of reverse multiplication method.""" 

152 return a.__mul__(self) 

153 

154 def __rsub__(self, a): 

155 """Implementation of reverse subtraction method.""" 

156 return a.__sub__(self) 

157 

158 def __str__(self): 

159 """String representation of a GeometryEntity.""" 

160 return type(self).__name__ + sstr(self.args) 

161 

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) 

172 

173 def _repr_svg_(self): 

174 """SVG representation of a GeometryEntity suitable for IPython""" 

175 

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 

182 

183 if not all(x.is_number and x.is_finite for x in bounds): 

184 return None 

185 

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>''' 

204 

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]) 

223 

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 

231 

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) 

235 

236 return svg_top + ( 

237 '<g transform="{}">{}</g></svg>' 

238 ).format(transform, svg) 

239 

240 def _svg(self, scale_factor=1., fill_color="#66cc99"): 

241 """Returns SVG path element for the GeometryEntity. 

242 

243 Parameters 

244 ========== 

245 

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() 

252 

253 def _sympy_(self): 

254 return self 

255 

256 @property 

257 def ambient_dimension(self): 

258 """What is the dimension of the space that the object is contained in?""" 

259 raise NotImplementedError() 

260 

261 @property 

262 def bounds(self): 

263 """Return a tuple (xmin, ymin, xmax, ymax) representing the bounding 

264 rectangle for the geometric figure. 

265 

266 """ 

267 

268 raise NotImplementedError() 

269 

270 def encloses(self, o): 

271 """ 

272 Return True if o is inside (not on or outside) the boundaries of self. 

273 

274 The object will be decomposed into Points and individual Entities need 

275 only define an encloses_point method for their class. 

276 

277 See Also 

278 ======== 

279 

280 sympy.geometry.ellipse.Ellipse.encloses_point 

281 sympy.geometry.polygon.Polygon.encloses_point 

282 

283 Examples 

284 ======== 

285 

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 

293 

294 """ 

295 

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 

300 

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() 

318 

319 def equals(self, o): 

320 return self == o 

321 

322 def intersection(self, o): 

323 """ 

324 Returns a list of all of the intersections of self with o. 

325 

326 Notes 

327 ===== 

328 

329 An entity is not required to implement this method. 

330 

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. 

334 

335 See Also 

336 ======== 

337 

338 sympy.geometry.util.intersection 

339 

340 """ 

341 raise NotImplementedError() 

342 

343 def is_similar(self, other): 

344 """Is this geometrical entity similar to another geometrical entity? 

345 

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. 

348 

349 Notes 

350 ===== 

351 

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. 

357 

358 See Also 

359 ======== 

360 

361 scale 

362 

363 """ 

364 raise NotImplementedError() 

365 

366 def reflect(self, line): 

367 """ 

368 Reflects an object across a line. 

369 

370 Parameters 

371 ========== 

372 

373 line: Line 

374 

375 Examples 

376 ======== 

377 

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) 

384 

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) 

391 

392 """ 

393 from sympy.geometry.point import Point 

394 

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)) 

423 

424 def rotate(self, angle, pt=None): 

425 """Rotate ``angle`` radians counterclockwise about Point ``pt``. 

426 

427 The default pt is the origin, Point(0, 0) 

428 

429 See Also 

430 ======== 

431 

432 scale, translate 

433 

434 Examples 

435 ======== 

436 

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)) 

443 

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) 

452 

453 def scale(self, x=1, y=1, pt=None): 

454 """Scale the object by multiplying the x,y-coordinates by x and y. 

455 

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. 

458 

459 See Also 

460 ======== 

461 

462 rotate, translate 

463 

464 Examples 

465 ======== 

466 

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))) 

475 

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 

482 

483 def translate(self, x=0, y=0): 

484 """Shift the object by adding to the x,y-coordinates the values x and y. 

485 

486 See Also 

487 ======== 

488 

489 rotate, scale 

490 

491 Examples 

492 ======== 

493 

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)) 

502 

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) 

511 

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. 

516 

517 Examples 

518 ======== 

519 

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]} 

538 

539 

540class GeometrySet(GeometryEntity, Set): 

541 """Parent class of all GeometryEntity that are also Sets 

542 (compatible with sympy.sets) 

543 """ 

544 __slots__ = () 

545 

546 def _contains(self, other): 

547 """sympy.sets uses the _contains method, so include it for compatibility.""" 

548 

549 if isinstance(other, Set) and other.is_FiniteSet: 

550 return all(self.__contains__(i) for i in other) 

551 

552 return self.__contains__(other) 

553 

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. """ 

558 

559 

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 

570 

571 

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. """ 

576 

577 from sympy.geometry.point import Point 

578 

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 

590 

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)] 

594 

595 return Union(*(non_points + [points])) 

596 

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 

603 

604 

605def scale(x, y, pt=None): 

606 """Return the matrix to multiply a 2-D point's coordinates by x and y. 

607 

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 

619 

620 

621def rotate(th): 

622 """Return the matrix to rotate a 2-D point about the origin by ``angle``. 

623 

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: 

627 

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