plane.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884
  1. """Geometrical Planes.
  2. Contains
  3. ========
  4. Plane
  5. """
  6. from sympy.core import Dummy, Rational, S, Symbol
  7. from sympy.core.symbol import _symbol
  8. from sympy.functions.elementary.trigonometric import cos, sin, acos, asin, sqrt
  9. from .entity import GeometryEntity
  10. from .line import (Line, Ray, Segment, Line3D, LinearEntity, LinearEntity3D,
  11. Ray3D, Segment3D)
  12. from .point import Point, Point3D
  13. from sympy.matrices import Matrix
  14. from sympy.polys.polytools import cancel
  15. from sympy.solvers import solve, linsolve
  16. from sympy.utilities.iterables import uniq, is_sequence
  17. from sympy.utilities.misc import filldedent, func_name, Undecidable
  18. from mpmath.libmp.libmpf import prec_to_dps
  19. import random
  20. x, y, z, t = [Dummy('plane_dummy') for i in range(4)]
  21. class Plane(GeometryEntity):
  22. """
  23. A plane is a flat, two-dimensional surface. A plane is the two-dimensional
  24. analogue of a point (zero-dimensions), a line (one-dimension) and a solid
  25. (three-dimensions). A plane can generally be constructed by two types of
  26. inputs. They are three non-collinear points and a point and the plane's
  27. normal vector.
  28. Attributes
  29. ==========
  30. p1
  31. normal_vector
  32. Examples
  33. ========
  34. >>> from sympy import Plane, Point3D
  35. >>> Plane(Point3D(1, 1, 1), Point3D(2, 3, 4), Point3D(2, 2, 2))
  36. Plane(Point3D(1, 1, 1), (-1, 2, -1))
  37. >>> Plane((1, 1, 1), (2, 3, 4), (2, 2, 2))
  38. Plane(Point3D(1, 1, 1), (-1, 2, -1))
  39. >>> Plane(Point3D(1, 1, 1), normal_vector=(1,4,7))
  40. Plane(Point3D(1, 1, 1), (1, 4, 7))
  41. """
  42. def __new__(cls, p1, a=None, b=None, **kwargs):
  43. p1 = Point3D(p1, dim=3)
  44. if a and b:
  45. p2 = Point(a, dim=3)
  46. p3 = Point(b, dim=3)
  47. if Point3D.are_collinear(p1, p2, p3):
  48. raise ValueError('Enter three non-collinear points')
  49. a = p1.direction_ratio(p2)
  50. b = p1.direction_ratio(p3)
  51. normal_vector = tuple(Matrix(a).cross(Matrix(b)))
  52. else:
  53. a = kwargs.pop('normal_vector', a)
  54. evaluate = kwargs.get('evaluate', True)
  55. if is_sequence(a) and len(a) == 3:
  56. normal_vector = Point3D(a).args if evaluate else a
  57. else:
  58. raise ValueError(filldedent('''
  59. Either provide 3 3D points or a point with a
  60. normal vector expressed as a sequence of length 3'''))
  61. if all(coord.is_zero for coord in normal_vector):
  62. raise ValueError('Normal vector cannot be zero vector')
  63. return GeometryEntity.__new__(cls, p1, normal_vector, **kwargs)
  64. def __contains__(self, o):
  65. k = self.equation(x, y, z)
  66. if isinstance(o, (LinearEntity, LinearEntity3D)):
  67. d = Point3D(o.arbitrary_point(t))
  68. e = k.subs([(x, d.x), (y, d.y), (z, d.z)])
  69. return e.equals(0)
  70. try:
  71. o = Point(o, dim=3, strict=True)
  72. d = k.xreplace(dict(zip((x, y, z), o.args)))
  73. return d.equals(0)
  74. except TypeError:
  75. return False
  76. def _eval_evalf(self, prec=15, **options):
  77. pt, tup = self.args
  78. dps = prec_to_dps(prec)
  79. pt = pt.evalf(n=dps, **options)
  80. tup = tuple([i.evalf(n=dps, **options) for i in tup])
  81. return self.func(pt, normal_vector=tup, evaluate=False)
  82. def angle_between(self, o):
  83. """Angle between the plane and other geometric entity.
  84. Parameters
  85. ==========
  86. LinearEntity3D, Plane.
  87. Returns
  88. =======
  89. angle : angle in radians
  90. Notes
  91. =====
  92. This method accepts only 3D entities as it's parameter, but if you want
  93. to calculate the angle between a 2D entity and a plane you should
  94. first convert to a 3D entity by projecting onto a desired plane and
  95. then proceed to calculate the angle.
  96. Examples
  97. ========
  98. >>> from sympy import Point3D, Line3D, Plane
  99. >>> a = Plane(Point3D(1, 2, 2), normal_vector=(1, 2, 3))
  100. >>> b = Line3D(Point3D(1, 3, 4), Point3D(2, 2, 2))
  101. >>> a.angle_between(b)
  102. -asin(sqrt(21)/6)
  103. """
  104. if isinstance(o, LinearEntity3D):
  105. a = Matrix(self.normal_vector)
  106. b = Matrix(o.direction_ratio)
  107. c = a.dot(b)
  108. d = sqrt(sum([i**2 for i in self.normal_vector]))
  109. e = sqrt(sum([i**2 for i in o.direction_ratio]))
  110. return asin(c/(d*e))
  111. if isinstance(o, Plane):
  112. a = Matrix(self.normal_vector)
  113. b = Matrix(o.normal_vector)
  114. c = a.dot(b)
  115. d = sqrt(sum([i**2 for i in self.normal_vector]))
  116. e = sqrt(sum([i**2 for i in o.normal_vector]))
  117. return acos(c/(d*e))
  118. def arbitrary_point(self, u=None, v=None):
  119. """ Returns an arbitrary point on the Plane. If given two
  120. parameters, the point ranges over the entire plane. If given 1
  121. or no parameters, returns a point with one parameter which,
  122. when varying from 0 to 2*pi, moves the point in a circle of
  123. radius 1 about p1 of the Plane.
  124. Examples
  125. ========
  126. >>> from sympy import Plane, Ray
  127. >>> from sympy.abc import u, v, t, r
  128. >>> p = Plane((1, 1, 1), normal_vector=(1, 0, 0))
  129. >>> p.arbitrary_point(u, v)
  130. Point3D(1, u + 1, v + 1)
  131. >>> p.arbitrary_point(t)
  132. Point3D(1, cos(t) + 1, sin(t) + 1)
  133. While arbitrary values of u and v can move the point anywhere in
  134. the plane, the single-parameter point can be used to construct a
  135. ray whose arbitrary point can be located at angle t and radius
  136. r from p.p1:
  137. >>> Ray(p.p1, _).arbitrary_point(r)
  138. Point3D(1, r*cos(t) + 1, r*sin(t) + 1)
  139. Returns
  140. =======
  141. Point3D
  142. """
  143. circle = v is None
  144. if circle:
  145. u = _symbol(u or 't', real=True)
  146. else:
  147. u = _symbol(u or 'u', real=True)
  148. v = _symbol(v or 'v', real=True)
  149. x, y, z = self.normal_vector
  150. a, b, c = self.p1.args
  151. # x1, y1, z1 is a nonzero vector parallel to the plane
  152. if x.is_zero and y.is_zero:
  153. x1, y1, z1 = S.One, S.Zero, S.Zero
  154. else:
  155. x1, y1, z1 = -y, x, S.Zero
  156. # x2, y2, z2 is also parallel to the plane, and orthogonal to x1, y1, z1
  157. x2, y2, z2 = tuple(Matrix((x, y, z)).cross(Matrix((x1, y1, z1))))
  158. if circle:
  159. x1, y1, z1 = (w/sqrt(x1**2 + y1**2 + z1**2) for w in (x1, y1, z1))
  160. x2, y2, z2 = (w/sqrt(x2**2 + y2**2 + z2**2) for w in (x2, y2, z2))
  161. p = Point3D(a + x1*cos(u) + x2*sin(u), \
  162. b + y1*cos(u) + y2*sin(u), \
  163. c + z1*cos(u) + z2*sin(u))
  164. else:
  165. p = Point3D(a + x1*u + x2*v, b + y1*u + y2*v, c + z1*u + z2*v)
  166. return p
  167. @staticmethod
  168. def are_concurrent(*planes):
  169. """Is a sequence of Planes concurrent?
  170. Two or more Planes are concurrent if their intersections
  171. are a common line.
  172. Parameters
  173. ==========
  174. planes: list
  175. Returns
  176. =======
  177. Boolean
  178. Examples
  179. ========
  180. >>> from sympy import Plane, Point3D
  181. >>> a = Plane(Point3D(5, 0, 0), normal_vector=(1, -1, 1))
  182. >>> b = Plane(Point3D(0, -2, 0), normal_vector=(3, 1, 1))
  183. >>> c = Plane(Point3D(0, -1, 0), normal_vector=(5, -1, 9))
  184. >>> Plane.are_concurrent(a, b)
  185. True
  186. >>> Plane.are_concurrent(a, b, c)
  187. False
  188. """
  189. planes = list(uniq(planes))
  190. for i in planes:
  191. if not isinstance(i, Plane):
  192. raise ValueError('All objects should be Planes but got %s' % i.func)
  193. if len(planes) < 2:
  194. return False
  195. planes = list(planes)
  196. first = planes.pop(0)
  197. sol = first.intersection(planes[0])
  198. if sol == []:
  199. return False
  200. else:
  201. line = sol[0]
  202. for i in planes[1:]:
  203. l = first.intersection(i)
  204. if not l or l[0] not in line:
  205. return False
  206. return True
  207. def distance(self, o):
  208. """Distance between the plane and another geometric entity.
  209. Parameters
  210. ==========
  211. Point3D, LinearEntity3D, Plane.
  212. Returns
  213. =======
  214. distance
  215. Notes
  216. =====
  217. This method accepts only 3D entities as it's parameter, but if you want
  218. to calculate the distance between a 2D entity and a plane you should
  219. first convert to a 3D entity by projecting onto a desired plane and
  220. then proceed to calculate the distance.
  221. Examples
  222. ========
  223. >>> from sympy import Point3D, Line3D, Plane
  224. >>> a = Plane(Point3D(1, 1, 1), normal_vector=(1, 1, 1))
  225. >>> b = Point3D(1, 2, 3)
  226. >>> a.distance(b)
  227. sqrt(3)
  228. >>> c = Line3D(Point3D(2, 3, 1), Point3D(1, 2, 2))
  229. >>> a.distance(c)
  230. 0
  231. """
  232. if self.intersection(o) != []:
  233. return S.Zero
  234. if isinstance(o, (Segment3D, Ray3D)):
  235. a, b = o.p1, o.p2
  236. pi, = self.intersection(Line3D(a, b))
  237. if pi in o:
  238. return self.distance(pi)
  239. elif a in Segment3D(pi, b):
  240. return self.distance(a)
  241. else:
  242. assert isinstance(o, Segment3D) is True
  243. return self.distance(b)
  244. # following code handles `Point3D`, `LinearEntity3D`, `Plane`
  245. a = o if isinstance(o, Point3D) else o.p1
  246. n = Point3D(self.normal_vector).unit
  247. d = (a - self.p1).dot(n)
  248. return abs(d)
  249. def equals(self, o):
  250. """
  251. Returns True if self and o are the same mathematical entities.
  252. Examples
  253. ========
  254. >>> from sympy import Plane, Point3D
  255. >>> a = Plane(Point3D(1, 2, 3), normal_vector=(1, 1, 1))
  256. >>> b = Plane(Point3D(1, 2, 3), normal_vector=(2, 2, 2))
  257. >>> c = Plane(Point3D(1, 2, 3), normal_vector=(-1, 4, 6))
  258. >>> a.equals(a)
  259. True
  260. >>> a.equals(b)
  261. True
  262. >>> a.equals(c)
  263. False
  264. """
  265. if isinstance(o, Plane):
  266. a = self.equation()
  267. b = o.equation()
  268. return cancel(a/b).is_constant()
  269. else:
  270. return False
  271. def equation(self, x=None, y=None, z=None):
  272. """The equation of the Plane.
  273. Examples
  274. ========
  275. >>> from sympy import Point3D, Plane
  276. >>> a = Plane(Point3D(1, 1, 2), Point3D(2, 4, 7), Point3D(3, 5, 1))
  277. >>> a.equation()
  278. -23*x + 11*y - 2*z + 16
  279. >>> a = Plane(Point3D(1, 4, 2), normal_vector=(6, 6, 6))
  280. >>> a.equation()
  281. 6*x + 6*y + 6*z - 42
  282. """
  283. x, y, z = [i if i else Symbol(j, real=True) for i, j in zip((x, y, z), 'xyz')]
  284. a = Point3D(x, y, z)
  285. b = self.p1.direction_ratio(a)
  286. c = self.normal_vector
  287. return (sum(i*j for i, j in zip(b, c)))
  288. def intersection(self, o):
  289. """ The intersection with other geometrical entity.
  290. Parameters
  291. ==========
  292. Point, Point3D, LinearEntity, LinearEntity3D, Plane
  293. Returns
  294. =======
  295. List
  296. Examples
  297. ========
  298. >>> from sympy import Point3D, Line3D, Plane
  299. >>> a = Plane(Point3D(1, 2, 3), normal_vector=(1, 1, 1))
  300. >>> b = Point3D(1, 2, 3)
  301. >>> a.intersection(b)
  302. [Point3D(1, 2, 3)]
  303. >>> c = Line3D(Point3D(1, 4, 7), Point3D(2, 2, 2))
  304. >>> a.intersection(c)
  305. [Point3D(2, 2, 2)]
  306. >>> d = Plane(Point3D(6, 0, 0), normal_vector=(2, -5, 3))
  307. >>> e = Plane(Point3D(2, 0, 0), normal_vector=(3, 4, -3))
  308. >>> d.intersection(e)
  309. [Line3D(Point3D(78/23, -24/23, 0), Point3D(147/23, 321/23, 23))]
  310. """
  311. if not isinstance(o, GeometryEntity):
  312. o = Point(o, dim=3)
  313. if isinstance(o, Point):
  314. if o in self:
  315. return [o]
  316. else:
  317. return []
  318. if isinstance(o, (LinearEntity, LinearEntity3D)):
  319. # recast to 3D
  320. p1, p2 = o.p1, o.p2
  321. if isinstance(o, Segment):
  322. o = Segment3D(p1, p2)
  323. elif isinstance(o, Ray):
  324. o = Ray3D(p1, p2)
  325. elif isinstance(o, Line):
  326. o = Line3D(p1, p2)
  327. else:
  328. raise ValueError('unhandled linear entity: %s' % o.func)
  329. if o in self:
  330. return [o]
  331. else:
  332. a = Point3D(o.arbitrary_point(t))
  333. p1, n = self.p1, Point3D(self.normal_vector)
  334. # TODO: Replace solve with solveset, when this line is tested
  335. c = solve((a - p1).dot(n), t)
  336. if not c:
  337. return []
  338. else:
  339. c = [i for i in c if i.is_real is not False]
  340. if len(c) > 1:
  341. c = [i for i in c if i.is_real]
  342. if len(c) != 1:
  343. raise Undecidable("not sure which point is real")
  344. p = a.subs(t, c[0])
  345. if p not in o:
  346. return [] # e.g. a segment might not intersect a plane
  347. return [p]
  348. if isinstance(o, Plane):
  349. if self.equals(o):
  350. return [self]
  351. if self.is_parallel(o):
  352. return []
  353. else:
  354. x, y, z = map(Dummy, 'xyz')
  355. a, b = Matrix([self.normal_vector]), Matrix([o.normal_vector])
  356. c = list(a.cross(b))
  357. d = self.equation(x, y, z)
  358. e = o.equation(x, y, z)
  359. result = list(linsolve([d, e], x, y, z))[0]
  360. for i in (x, y, z): result = result.subs(i, 0)
  361. return [Line3D(Point3D(result), direction_ratio=c)]
  362. def is_coplanar(self, o):
  363. """ Returns True if `o` is coplanar with self, else False.
  364. Examples
  365. ========
  366. >>> from sympy import Plane
  367. >>> o = (0, 0, 0)
  368. >>> p = Plane(o, (1, 1, 1))
  369. >>> p2 = Plane(o, (2, 2, 2))
  370. >>> p == p2
  371. False
  372. >>> p.is_coplanar(p2)
  373. True
  374. """
  375. if isinstance(o, Plane):
  376. return not cancel(self.equation(x, y, z)/o.equation(x, y, z)).has(x, y, z)
  377. if isinstance(o, Point3D):
  378. return o in self
  379. elif isinstance(o, LinearEntity3D):
  380. return all(i in self for i in self)
  381. elif isinstance(o, GeometryEntity): # XXX should only be handling 2D objects now
  382. return all(i == 0 for i in self.normal_vector[:2])
  383. def is_parallel(self, l):
  384. """Is the given geometric entity parallel to the plane?
  385. Parameters
  386. ==========
  387. LinearEntity3D or Plane
  388. Returns
  389. =======
  390. Boolean
  391. Examples
  392. ========
  393. >>> from sympy import Plane, Point3D
  394. >>> a = Plane(Point3D(1,4,6), normal_vector=(2, 4, 6))
  395. >>> b = Plane(Point3D(3,1,3), normal_vector=(4, 8, 12))
  396. >>> a.is_parallel(b)
  397. True
  398. """
  399. if isinstance(l, LinearEntity3D):
  400. a = l.direction_ratio
  401. b = self.normal_vector
  402. c = sum([i*j for i, j in zip(a, b)])
  403. if c == 0:
  404. return True
  405. else:
  406. return False
  407. elif isinstance(l, Plane):
  408. a = Matrix(l.normal_vector)
  409. b = Matrix(self.normal_vector)
  410. if a.cross(b).is_zero_matrix:
  411. return True
  412. else:
  413. return False
  414. def is_perpendicular(self, l):
  415. """Is the given geometric entity perpendicualar to the given plane?
  416. Parameters
  417. ==========
  418. LinearEntity3D or Plane
  419. Returns
  420. =======
  421. Boolean
  422. Examples
  423. ========
  424. >>> from sympy import Plane, Point3D
  425. >>> a = Plane(Point3D(1,4,6), normal_vector=(2, 4, 6))
  426. >>> b = Plane(Point3D(2, 2, 2), normal_vector=(-1, 2, -1))
  427. >>> a.is_perpendicular(b)
  428. True
  429. """
  430. if isinstance(l, LinearEntity3D):
  431. a = Matrix(l.direction_ratio)
  432. b = Matrix(self.normal_vector)
  433. if a.cross(b).is_zero_matrix:
  434. return True
  435. else:
  436. return False
  437. elif isinstance(l, Plane):
  438. a = Matrix(l.normal_vector)
  439. b = Matrix(self.normal_vector)
  440. if a.dot(b) == 0:
  441. return True
  442. else:
  443. return False
  444. else:
  445. return False
  446. @property
  447. def normal_vector(self):
  448. """Normal vector of the given plane.
  449. Examples
  450. ========
  451. >>> from sympy import Point3D, Plane
  452. >>> a = Plane(Point3D(1, 1, 1), Point3D(2, 3, 4), Point3D(2, 2, 2))
  453. >>> a.normal_vector
  454. (-1, 2, -1)
  455. >>> a = Plane(Point3D(1, 1, 1), normal_vector=(1, 4, 7))
  456. >>> a.normal_vector
  457. (1, 4, 7)
  458. """
  459. return self.args[1]
  460. @property
  461. def p1(self):
  462. """The only defining point of the plane. Others can be obtained from the
  463. arbitrary_point method.
  464. See Also
  465. ========
  466. sympy.geometry.point.Point3D
  467. Examples
  468. ========
  469. >>> from sympy import Point3D, Plane
  470. >>> a = Plane(Point3D(1, 1, 1), Point3D(2, 3, 4), Point3D(2, 2, 2))
  471. >>> a.p1
  472. Point3D(1, 1, 1)
  473. """
  474. return self.args[0]
  475. def parallel_plane(self, pt):
  476. """
  477. Plane parallel to the given plane and passing through the point pt.
  478. Parameters
  479. ==========
  480. pt: Point3D
  481. Returns
  482. =======
  483. Plane
  484. Examples
  485. ========
  486. >>> from sympy import Plane, Point3D
  487. >>> a = Plane(Point3D(1, 4, 6), normal_vector=(2, 4, 6))
  488. >>> a.parallel_plane(Point3D(2, 3, 5))
  489. Plane(Point3D(2, 3, 5), (2, 4, 6))
  490. """
  491. a = self.normal_vector
  492. return Plane(pt, normal_vector=a)
  493. def perpendicular_line(self, pt):
  494. """A line perpendicular to the given plane.
  495. Parameters
  496. ==========
  497. pt: Point3D
  498. Returns
  499. =======
  500. Line3D
  501. Examples
  502. ========
  503. >>> from sympy import Plane, Point3D
  504. >>> a = Plane(Point3D(1,4,6), normal_vector=(2, 4, 6))
  505. >>> a.perpendicular_line(Point3D(9, 8, 7))
  506. Line3D(Point3D(9, 8, 7), Point3D(11, 12, 13))
  507. """
  508. a = self.normal_vector
  509. return Line3D(pt, direction_ratio=a)
  510. def perpendicular_plane(self, *pts):
  511. """
  512. Return a perpendicular passing through the given points. If the
  513. direction ratio between the points is the same as the Plane's normal
  514. vector then, to select from the infinite number of possible planes,
  515. a third point will be chosen on the z-axis (or the y-axis
  516. if the normal vector is already parallel to the z-axis). If less than
  517. two points are given they will be supplied as follows: if no point is
  518. given then pt1 will be self.p1; if a second point is not given it will
  519. be a point through pt1 on a line parallel to the z-axis (if the normal
  520. is not already the z-axis, otherwise on the line parallel to the
  521. y-axis).
  522. Parameters
  523. ==========
  524. pts: 0, 1 or 2 Point3D
  525. Returns
  526. =======
  527. Plane
  528. Examples
  529. ========
  530. >>> from sympy import Plane, Point3D
  531. >>> a, b = Point3D(0, 0, 0), Point3D(0, 1, 0)
  532. >>> Z = (0, 0, 1)
  533. >>> p = Plane(a, normal_vector=Z)
  534. >>> p.perpendicular_plane(a, b)
  535. Plane(Point3D(0, 0, 0), (1, 0, 0))
  536. """
  537. if len(pts) > 2:
  538. raise ValueError('No more than 2 pts should be provided.')
  539. pts = list(pts)
  540. if len(pts) == 0:
  541. pts.append(self.p1)
  542. if len(pts) == 1:
  543. x, y, z = self.normal_vector
  544. if x == y == 0:
  545. dir = (0, 1, 0)
  546. else:
  547. dir = (0, 0, 1)
  548. pts.append(pts[0] + Point3D(*dir))
  549. p1, p2 = [Point(i, dim=3) for i in pts]
  550. l = Line3D(p1, p2)
  551. n = Line3D(p1, direction_ratio=self.normal_vector)
  552. if l in n: # XXX should an error be raised instead?
  553. # there are infinitely many perpendicular planes;
  554. x, y, z = self.normal_vector
  555. if x == y == 0:
  556. # the z axis is the normal so pick a pt on the y-axis
  557. p3 = Point3D(0, 1, 0) # case 1
  558. else:
  559. # else pick a pt on the z axis
  560. p3 = Point3D(0, 0, 1) # case 2
  561. # in case that point is already given, move it a bit
  562. if p3 in l:
  563. p3 *= 2 # case 3
  564. else:
  565. p3 = p1 + Point3D(*self.normal_vector) # case 4
  566. return Plane(p1, p2, p3)
  567. def projection_line(self, line):
  568. """Project the given line onto the plane through the normal plane
  569. containing the line.
  570. Parameters
  571. ==========
  572. LinearEntity or LinearEntity3D
  573. Returns
  574. =======
  575. Point3D, Line3D, Ray3D or Segment3D
  576. Notes
  577. =====
  578. For the interaction between 2D and 3D lines(segments, rays), you should
  579. convert the line to 3D by using this method. For example for finding the
  580. intersection between a 2D and a 3D line, convert the 2D line to a 3D line
  581. by projecting it on a required plane and then proceed to find the
  582. intersection between those lines.
  583. Examples
  584. ========
  585. >>> from sympy import Plane, Line, Line3D, Point3D
  586. >>> a = Plane(Point3D(1, 1, 1), normal_vector=(1, 1, 1))
  587. >>> b = Line(Point3D(1, 1), Point3D(2, 2))
  588. >>> a.projection_line(b)
  589. Line3D(Point3D(4/3, 4/3, 1/3), Point3D(5/3, 5/3, -1/3))
  590. >>> c = Line3D(Point3D(1, 1, 1), Point3D(2, 2, 2))
  591. >>> a.projection_line(c)
  592. Point3D(1, 1, 1)
  593. """
  594. if not isinstance(line, (LinearEntity, LinearEntity3D)):
  595. raise NotImplementedError('Enter a linear entity only')
  596. a, b = self.projection(line.p1), self.projection(line.p2)
  597. if a == b:
  598. # projection does not imply intersection so for
  599. # this case (line parallel to plane's normal) we
  600. # return the projection point
  601. return a
  602. if isinstance(line, (Line, Line3D)):
  603. return Line3D(a, b)
  604. if isinstance(line, (Ray, Ray3D)):
  605. return Ray3D(a, b)
  606. if isinstance(line, (Segment, Segment3D)):
  607. return Segment3D(a, b)
  608. def projection(self, pt):
  609. """Project the given point onto the plane along the plane normal.
  610. Parameters
  611. ==========
  612. Point or Point3D
  613. Returns
  614. =======
  615. Point3D
  616. Examples
  617. ========
  618. >>> from sympy import Plane, Point3D
  619. >>> A = Plane(Point3D(1, 1, 2), normal_vector=(1, 1, 1))
  620. The projection is along the normal vector direction, not the z
  621. axis, so (1, 1) does not project to (1, 1, 2) on the plane A:
  622. >>> b = Point3D(1, 1)
  623. >>> A.projection(b)
  624. Point3D(5/3, 5/3, 2/3)
  625. >>> _ in A
  626. True
  627. But the point (1, 1, 2) projects to (1, 1) on the XY-plane:
  628. >>> XY = Plane((0, 0, 0), (0, 0, 1))
  629. >>> XY.projection((1, 1, 2))
  630. Point3D(1, 1, 0)
  631. """
  632. rv = Point(pt, dim=3)
  633. if rv in self:
  634. return rv
  635. return self.intersection(Line3D(rv, rv + Point3D(self.normal_vector)))[0]
  636. def random_point(self, seed=None):
  637. """ Returns a random point on the Plane.
  638. Returns
  639. =======
  640. Point3D
  641. Examples
  642. ========
  643. >>> from sympy import Plane
  644. >>> p = Plane((1, 0, 0), normal_vector=(0, 1, 0))
  645. >>> r = p.random_point(seed=42) # seed value is optional
  646. >>> r.n(3)
  647. Point3D(2.29, 0, -1.35)
  648. The random point can be moved to lie on the circle of radius
  649. 1 centered on p1:
  650. >>> c = p.p1 + (r - p.p1).unit
  651. >>> c.distance(p.p1).equals(1)
  652. True
  653. """
  654. if seed is not None:
  655. rng = random.Random(seed)
  656. else:
  657. rng = random
  658. params = {
  659. x: 2*Rational(rng.gauss(0, 1)) - 1,
  660. y: 2*Rational(rng.gauss(0, 1)) - 1}
  661. return self.arbitrary_point(x, y).subs(params)
  662. def parameter_value(self, other, u, v=None):
  663. """Return the parameter(s) corresponding to the given point.
  664. Examples
  665. ========
  666. >>> from sympy import pi, Plane
  667. >>> from sympy.abc import t, u, v
  668. >>> p = Plane((2, 0, 0), (0, 0, 1), (0, 1, 0))
  669. By default, the parameter value returned defines a point
  670. that is a distance of 1 from the Plane's p1 value and
  671. in line with the given point:
  672. >>> on_circle = p.arbitrary_point(t).subs(t, pi/4)
  673. >>> on_circle.distance(p.p1)
  674. 1
  675. >>> p.parameter_value(on_circle, t)
  676. {t: pi/4}
  677. Moving the point twice as far from p1 does not change
  678. the parameter value:
  679. >>> off_circle = p.p1 + (on_circle - p.p1)*2
  680. >>> off_circle.distance(p.p1)
  681. 2
  682. >>> p.parameter_value(off_circle, t)
  683. {t: pi/4}
  684. If the 2-value parameter is desired, supply the two
  685. parameter symbols and a replacement dictionary will
  686. be returned:
  687. >>> p.parameter_value(on_circle, u, v)
  688. {u: sqrt(10)/10, v: sqrt(10)/30}
  689. >>> p.parameter_value(off_circle, u, v)
  690. {u: sqrt(10)/5, v: sqrt(10)/15}
  691. """
  692. if not isinstance(other, GeometryEntity):
  693. other = Point(other, dim=self.ambient_dimension)
  694. if not isinstance(other, Point):
  695. raise ValueError("other must be a point")
  696. if other == self.p1:
  697. return other
  698. if isinstance(u, Symbol) and v is None:
  699. delta = self.arbitrary_point(u) - self.p1
  700. eq = delta - (other - self.p1).unit
  701. sol = solve(eq, u, dict=True)
  702. elif isinstance(u, Symbol) and isinstance(v, Symbol):
  703. pt = self.arbitrary_point(u, v)
  704. sol = solve(pt - other, (u, v), dict=True)
  705. else:
  706. raise ValueError('expecting 1 or 2 symbols')
  707. if not sol:
  708. raise ValueError("Given point is not on %s" % func_name(self))
  709. return sol[0] # {t: tval} or {u: uval, v: vval}
  710. @property
  711. def ambient_dimension(self):
  712. return self.p1.ambient_dimension