You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2804 lines
76 KiB
2804 lines
76 KiB
"""Line-like geometrical entities.
|
|
|
|
Contains
|
|
========
|
|
LinearEntity
|
|
Line
|
|
Ray
|
|
Segment
|
|
LinearEntity2D
|
|
Line2D
|
|
Ray2D
|
|
Segment2D
|
|
LinearEntity3D
|
|
Line3D
|
|
Ray3D
|
|
Segment3D
|
|
|
|
"""
|
|
|
|
from sympy.core.containers import Tuple
|
|
from sympy.core.evalf import N
|
|
from sympy.core.expr import Expr
|
|
from sympy.core.numbers import Rational, oo, Float
|
|
from sympy.core.relational import Eq
|
|
from sympy.core.singleton import S
|
|
from sympy.core.sorting import ordered
|
|
from sympy.core.symbol import _symbol, Dummy, uniquely_named_symbol
|
|
from sympy.core.sympify import sympify
|
|
from sympy.functions.elementary.piecewise import Piecewise
|
|
from sympy.functions.elementary.trigonometric import (_pi_coeff, acos, tan, atan2)
|
|
from .entity import GeometryEntity, GeometrySet
|
|
from .exceptions import GeometryError
|
|
from .point import Point, Point3D
|
|
from .util import find, intersection
|
|
from sympy.logic.boolalg import And
|
|
from sympy.matrices import Matrix
|
|
from sympy.sets.sets import Intersection
|
|
from sympy.simplify.simplify import simplify
|
|
from sympy.solvers.solvers import solve
|
|
from sympy.solvers.solveset import linear_coeffs
|
|
from sympy.utilities.misc import Undecidable, filldedent
|
|
|
|
|
|
import random
|
|
|
|
|
|
t, u = [Dummy('line_dummy') for i in range(2)]
|
|
|
|
|
|
class LinearEntity(GeometrySet):
|
|
"""A base class for all linear entities (Line, Ray and Segment)
|
|
in n-dimensional Euclidean space.
|
|
|
|
Attributes
|
|
==========
|
|
|
|
ambient_dimension
|
|
direction
|
|
length
|
|
p1
|
|
p2
|
|
points
|
|
|
|
Notes
|
|
=====
|
|
|
|
This is an abstract class and is not meant to be instantiated.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.entity.GeometryEntity
|
|
|
|
"""
|
|
def __new__(cls, p1, p2=None, **kwargs):
|
|
p1, p2 = Point._normalize_dimension(p1, p2)
|
|
if p1 == p2:
|
|
# sometimes we return a single point if we are not given two unique
|
|
# points. This is done in the specific subclass
|
|
raise ValueError(
|
|
"%s.__new__ requires two unique Points." % cls.__name__)
|
|
if len(p1) != len(p2):
|
|
raise ValueError(
|
|
"%s.__new__ requires two Points of equal dimension." % cls.__name__)
|
|
|
|
return GeometryEntity.__new__(cls, p1, p2, **kwargs)
|
|
|
|
def __contains__(self, other):
|
|
"""Return a definitive answer or else raise an error if it cannot
|
|
be determined that other is on the boundaries of self."""
|
|
result = self.contains(other)
|
|
|
|
if result is not None:
|
|
return result
|
|
else:
|
|
raise Undecidable(
|
|
"Cannot decide whether '%s' contains '%s'" % (self, other))
|
|
|
|
def _span_test(self, other):
|
|
"""Test whether the point `other` lies in the positive span of `self`.
|
|
A point x is 'in front' of a point y if x.dot(y) >= 0. Return
|
|
-1 if `other` is behind `self.p1`, 0 if `other` is `self.p1` and
|
|
and 1 if `other` is in front of `self.p1`."""
|
|
if self.p1 == other:
|
|
return 0
|
|
|
|
rel_pos = other - self.p1
|
|
d = self.direction
|
|
if d.dot(rel_pos) > 0:
|
|
return 1
|
|
return -1
|
|
|
|
@property
|
|
def ambient_dimension(self):
|
|
"""A property method that returns the dimension of LinearEntity
|
|
object.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p1 : LinearEntity
|
|
|
|
Returns
|
|
=======
|
|
|
|
dimension : integer
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2 = Point(0, 0), Point(1, 1)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> l1.ambient_dimension
|
|
2
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2 = Point(0, 0, 0), Point(1, 1, 1)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> l1.ambient_dimension
|
|
3
|
|
|
|
"""
|
|
return len(self.p1)
|
|
|
|
def angle_between(l1, l2):
|
|
"""Return the non-reflex angle formed by rays emanating from
|
|
the origin with directions the same as the direction vectors
|
|
of the linear entities.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
l1 : LinearEntity
|
|
l2 : LinearEntity
|
|
|
|
Returns
|
|
=======
|
|
|
|
angle : angle in radians
|
|
|
|
Notes
|
|
=====
|
|
|
|
From the dot product of vectors v1 and v2 it is known that:
|
|
|
|
``dot(v1, v2) = |v1|*|v2|*cos(A)``
|
|
|
|
where A is the angle formed between the two vectors. We can
|
|
get the directional vectors of the two lines and readily
|
|
find the angle between the two using the above formula.
|
|
|
|
See Also
|
|
========
|
|
|
|
is_perpendicular, Ray2D.closing_angle
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Line
|
|
>>> e = Line((0, 0), (1, 0))
|
|
>>> ne = Line((0, 0), (1, 1))
|
|
>>> sw = Line((1, 1), (0, 0))
|
|
>>> ne.angle_between(e)
|
|
pi/4
|
|
>>> sw.angle_between(e)
|
|
3*pi/4
|
|
|
|
To obtain the non-obtuse angle at the intersection of lines, use
|
|
the ``smallest_angle_between`` method:
|
|
|
|
>>> sw.smallest_angle_between(e)
|
|
pi/4
|
|
|
|
>>> from sympy import Point3D, Line3D
|
|
>>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(1, 1, 1), Point3D(-1, 2, 0)
|
|
>>> l1, l2 = Line3D(p1, p2), Line3D(p2, p3)
|
|
>>> l1.angle_between(l2)
|
|
acos(-sqrt(2)/3)
|
|
>>> l1.smallest_angle_between(l2)
|
|
acos(sqrt(2)/3)
|
|
"""
|
|
if not isinstance(l1, LinearEntity) and not isinstance(l2, LinearEntity):
|
|
raise TypeError('Must pass only LinearEntity objects')
|
|
|
|
v1, v2 = l1.direction, l2.direction
|
|
return acos(v1.dot(v2)/(abs(v1)*abs(v2)))
|
|
|
|
def smallest_angle_between(l1, l2):
|
|
"""Return the smallest angle formed at the intersection of the
|
|
lines containing the linear entities.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
l1 : LinearEntity
|
|
l2 : LinearEntity
|
|
|
|
Returns
|
|
=======
|
|
|
|
angle : angle in radians
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2, p3 = Point(0, 0), Point(0, 4), Point(2, -2)
|
|
>>> l1, l2 = Line(p1, p2), Line(p1, p3)
|
|
>>> l1.smallest_angle_between(l2)
|
|
pi/4
|
|
|
|
See Also
|
|
========
|
|
|
|
angle_between, is_perpendicular, Ray2D.closing_angle
|
|
"""
|
|
if not isinstance(l1, LinearEntity) and not isinstance(l2, LinearEntity):
|
|
raise TypeError('Must pass only LinearEntity objects')
|
|
|
|
v1, v2 = l1.direction, l2.direction
|
|
return acos(abs(v1.dot(v2))/(abs(v1)*abs(v2)))
|
|
|
|
def arbitrary_point(self, parameter='t'):
|
|
"""A parameterized point on the Line.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
parameter : str, optional
|
|
The name of the parameter which will be used for the parametric
|
|
point. The default value is 't'. When this parameter is 0, the
|
|
first point used to define the line will be returned, and when
|
|
it is 1 the second point will be returned.
|
|
|
|
Returns
|
|
=======
|
|
|
|
point : Point
|
|
|
|
Raises
|
|
======
|
|
|
|
ValueError
|
|
When ``parameter`` already appears in the Line's definition.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2 = Point(1, 0), Point(5, 3)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> l1.arbitrary_point()
|
|
Point2D(4*t + 1, 3*t)
|
|
>>> from sympy import Point3D, Line3D
|
|
>>> p1, p2 = Point3D(1, 0, 0), Point3D(5, 3, 1)
|
|
>>> l1 = Line3D(p1, p2)
|
|
>>> l1.arbitrary_point()
|
|
Point3D(4*t + 1, 3*t, t)
|
|
|
|
"""
|
|
t = _symbol(parameter, real=True)
|
|
if t.name in (f.name for f in self.free_symbols):
|
|
raise ValueError(filldedent('''
|
|
Symbol %s already appears in object
|
|
and cannot be used as a parameter.
|
|
''' % t.name))
|
|
# multiply on the right so the variable gets
|
|
# combined with the coordinates of the point
|
|
return self.p1 + (self.p2 - self.p1)*t
|
|
|
|
@staticmethod
|
|
def are_concurrent(*lines):
|
|
"""Is a sequence of linear entities concurrent?
|
|
|
|
Two or more linear entities are concurrent if they all
|
|
intersect at a single point.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
lines
|
|
A sequence of linear entities.
|
|
|
|
Returns
|
|
=======
|
|
|
|
True : if the set of linear entities intersect in one point
|
|
False : otherwise.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.util.intersection
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2 = Point(0, 0), Point(3, 5)
|
|
>>> p3, p4 = Point(-2, -2), Point(0, 2)
|
|
>>> l1, l2, l3 = Line(p1, p2), Line(p1, p3), Line(p1, p4)
|
|
>>> Line.are_concurrent(l1, l2, l3)
|
|
True
|
|
>>> l4 = Line(p2, p3)
|
|
>>> Line.are_concurrent(l2, l3, l4)
|
|
False
|
|
>>> from sympy import Point3D, Line3D
|
|
>>> p1, p2 = Point3D(0, 0, 0), Point3D(3, 5, 2)
|
|
>>> p3, p4 = Point3D(-2, -2, -2), Point3D(0, 2, 1)
|
|
>>> l1, l2, l3 = Line3D(p1, p2), Line3D(p1, p3), Line3D(p1, p4)
|
|
>>> Line3D.are_concurrent(l1, l2, l3)
|
|
True
|
|
>>> l4 = Line3D(p2, p3)
|
|
>>> Line3D.are_concurrent(l2, l3, l4)
|
|
False
|
|
|
|
"""
|
|
common_points = Intersection(*lines)
|
|
if common_points.is_FiniteSet and len(common_points) == 1:
|
|
return True
|
|
return False
|
|
|
|
def contains(self, other):
|
|
"""Subclasses should implement this method and should return
|
|
True if other is on the boundaries of self;
|
|
False if not on the boundaries of self;
|
|
None if a determination cannot be made."""
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def direction(self):
|
|
"""The direction vector of the LinearEntity.
|
|
|
|
Returns
|
|
=======
|
|
|
|
p : a Point; the ray from the origin to this point is the
|
|
direction of `self`
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Line
|
|
>>> a, b = (1, 1), (1, 3)
|
|
>>> Line(a, b).direction
|
|
Point2D(0, 2)
|
|
>>> Line(b, a).direction
|
|
Point2D(0, -2)
|
|
|
|
This can be reported so the distance from the origin is 1:
|
|
|
|
>>> Line(b, a).direction.unit
|
|
Point2D(0, -1)
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point.unit
|
|
|
|
"""
|
|
return self.p2 - self.p1
|
|
|
|
def intersection(self, other):
|
|
"""The intersection with another geometrical entity.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
o : Point or LinearEntity
|
|
|
|
Returns
|
|
=======
|
|
|
|
intersection : list of geometrical entities
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line, Segment
|
|
>>> p1, p2, p3 = Point(0, 0), Point(1, 1), Point(7, 7)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> l1.intersection(p3)
|
|
[Point2D(7, 7)]
|
|
>>> p4, p5 = Point(5, 0), Point(0, 3)
|
|
>>> l2 = Line(p4, p5)
|
|
>>> l1.intersection(l2)
|
|
[Point2D(15/8, 15/8)]
|
|
>>> p6, p7 = Point(0, 5), Point(2, 6)
|
|
>>> s1 = Segment(p6, p7)
|
|
>>> l1.intersection(s1)
|
|
[]
|
|
>>> from sympy import Point3D, Line3D, Segment3D
|
|
>>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(1, 1, 1), Point3D(7, 7, 7)
|
|
>>> l1 = Line3D(p1, p2)
|
|
>>> l1.intersection(p3)
|
|
[Point3D(7, 7, 7)]
|
|
>>> l1 = Line3D(Point3D(4,19,12), Point3D(5,25,17))
|
|
>>> l2 = Line3D(Point3D(-3, -15, -19), direction_ratio=[2,8,8])
|
|
>>> l1.intersection(l2)
|
|
[Point3D(1, 1, -3)]
|
|
>>> p6, p7 = Point3D(0, 5, 2), Point3D(2, 6, 3)
|
|
>>> s1 = Segment3D(p6, p7)
|
|
>>> l1.intersection(s1)
|
|
[]
|
|
|
|
"""
|
|
def intersect_parallel_rays(ray1, ray2):
|
|
if ray1.direction.dot(ray2.direction) > 0:
|
|
# rays point in the same direction
|
|
# so return the one that is "in front"
|
|
return [ray2] if ray1._span_test(ray2.p1) >= 0 else [ray1]
|
|
else:
|
|
# rays point in opposite directions
|
|
st = ray1._span_test(ray2.p1)
|
|
if st < 0:
|
|
return []
|
|
elif st == 0:
|
|
return [ray2.p1]
|
|
return [Segment(ray1.p1, ray2.p1)]
|
|
|
|
def intersect_parallel_ray_and_segment(ray, seg):
|
|
st1, st2 = ray._span_test(seg.p1), ray._span_test(seg.p2)
|
|
if st1 < 0 and st2 < 0:
|
|
return []
|
|
elif st1 >= 0 and st2 >= 0:
|
|
return [seg]
|
|
elif st1 >= 0: # st2 < 0:
|
|
return [Segment(ray.p1, seg.p1)]
|
|
else: # st1 < 0 and st2 >= 0:
|
|
return [Segment(ray.p1, seg.p2)]
|
|
|
|
def intersect_parallel_segments(seg1, seg2):
|
|
if seg1.contains(seg2):
|
|
return [seg2]
|
|
if seg2.contains(seg1):
|
|
return [seg1]
|
|
|
|
# direct the segments so they're oriented the same way
|
|
if seg1.direction.dot(seg2.direction) < 0:
|
|
seg2 = Segment(seg2.p2, seg2.p1)
|
|
# order the segments so seg1 is "behind" seg2
|
|
if seg1._span_test(seg2.p1) < 0:
|
|
seg1, seg2 = seg2, seg1
|
|
if seg2._span_test(seg1.p2) < 0:
|
|
return []
|
|
return [Segment(seg2.p1, seg1.p2)]
|
|
|
|
if not isinstance(other, GeometryEntity):
|
|
other = Point(other, dim=self.ambient_dimension)
|
|
if other.is_Point:
|
|
if self.contains(other):
|
|
return [other]
|
|
else:
|
|
return []
|
|
elif isinstance(other, LinearEntity):
|
|
# break into cases based on whether
|
|
# the lines are parallel, non-parallel intersecting, or skew
|
|
pts = Point._normalize_dimension(self.p1, self.p2, other.p1, other.p2)
|
|
rank = Point.affine_rank(*pts)
|
|
|
|
if rank == 1:
|
|
# we're collinear
|
|
if isinstance(self, Line):
|
|
return [other]
|
|
if isinstance(other, Line):
|
|
return [self]
|
|
|
|
if isinstance(self, Ray) and isinstance(other, Ray):
|
|
return intersect_parallel_rays(self, other)
|
|
if isinstance(self, Ray) and isinstance(other, Segment):
|
|
return intersect_parallel_ray_and_segment(self, other)
|
|
if isinstance(self, Segment) and isinstance(other, Ray):
|
|
return intersect_parallel_ray_and_segment(other, self)
|
|
if isinstance(self, Segment) and isinstance(other, Segment):
|
|
return intersect_parallel_segments(self, other)
|
|
elif rank == 2:
|
|
# we're in the same plane
|
|
l1 = Line(*pts[:2])
|
|
l2 = Line(*pts[2:])
|
|
|
|
# check to see if we're parallel. If we are, we can't
|
|
# be intersecting, since the collinear case was already
|
|
# handled
|
|
if l1.direction.is_scalar_multiple(l2.direction):
|
|
return []
|
|
|
|
# find the intersection as if everything were lines
|
|
# by solving the equation t*d + p1 == s*d' + p1'
|
|
m = Matrix([l1.direction, -l2.direction]).transpose()
|
|
v = Matrix([l2.p1 - l1.p1]).transpose()
|
|
|
|
# we cannot use m.solve(v) because that only works for square matrices
|
|
m_rref, pivots = m.col_insert(2, v).rref(simplify=True)
|
|
# rank == 2 ensures we have 2 pivots, but let's check anyway
|
|
if len(pivots) != 2:
|
|
raise GeometryError("Failed when solving Mx=b when M={} and b={}".format(m, v))
|
|
coeff = m_rref[0, 2]
|
|
line_intersection = l1.direction*coeff + self.p1
|
|
|
|
# if both are lines, skip a containment check
|
|
if isinstance(self, Line) and isinstance(other, Line):
|
|
return [line_intersection]
|
|
|
|
if ((isinstance(self, Line) or
|
|
self.contains(line_intersection)) and
|
|
other.contains(line_intersection)):
|
|
return [line_intersection]
|
|
if not self.atoms(Float) and not other.atoms(Float):
|
|
# if it can fail when there are no Floats then
|
|
# maybe the following parametric check should be
|
|
# done
|
|
return []
|
|
# floats may fail exact containment so check that the
|
|
# arbitrary points, when equal, both give a
|
|
# non-negative parameter when the arbitrary point
|
|
# coordinates are equated
|
|
tu = solve(self.arbitrary_point(t) - other.arbitrary_point(u),
|
|
t, u, dict=True)[0]
|
|
def ok(p, l):
|
|
if isinstance(l, Line):
|
|
# p > -oo
|
|
return True
|
|
if isinstance(l, Ray):
|
|
# p >= 0
|
|
return p.is_nonnegative
|
|
if isinstance(l, Segment):
|
|
# 0 <= p <= 1
|
|
return p.is_nonnegative and (1 - p).is_nonnegative
|
|
raise ValueError("unexpected line type")
|
|
if ok(tu[t], self) and ok(tu[u], other):
|
|
return [line_intersection]
|
|
return []
|
|
else:
|
|
# we're skew
|
|
return []
|
|
|
|
return other.intersection(self)
|
|
|
|
def is_parallel(l1, l2):
|
|
"""Are two linear entities parallel?
|
|
|
|
Parameters
|
|
==========
|
|
|
|
l1 : LinearEntity
|
|
l2 : LinearEntity
|
|
|
|
Returns
|
|
=======
|
|
|
|
True : if l1 and l2 are parallel,
|
|
False : otherwise.
|
|
|
|
See Also
|
|
========
|
|
|
|
coefficients
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2 = Point(0, 0), Point(1, 1)
|
|
>>> p3, p4 = Point(3, 4), Point(6, 7)
|
|
>>> l1, l2 = Line(p1, p2), Line(p3, p4)
|
|
>>> Line.is_parallel(l1, l2)
|
|
True
|
|
>>> p5 = Point(6, 6)
|
|
>>> l3 = Line(p3, p5)
|
|
>>> Line.is_parallel(l1, l3)
|
|
False
|
|
>>> from sympy import Point3D, Line3D
|
|
>>> p1, p2 = Point3D(0, 0, 0), Point3D(3, 4, 5)
|
|
>>> p3, p4 = Point3D(2, 1, 1), Point3D(8, 9, 11)
|
|
>>> l1, l2 = Line3D(p1, p2), Line3D(p3, p4)
|
|
>>> Line3D.is_parallel(l1, l2)
|
|
True
|
|
>>> p5 = Point3D(6, 6, 6)
|
|
>>> l3 = Line3D(p3, p5)
|
|
>>> Line3D.is_parallel(l1, l3)
|
|
False
|
|
|
|
"""
|
|
if not isinstance(l1, LinearEntity) and not isinstance(l2, LinearEntity):
|
|
raise TypeError('Must pass only LinearEntity objects')
|
|
|
|
return l1.direction.is_scalar_multiple(l2.direction)
|
|
|
|
def is_perpendicular(l1, l2):
|
|
"""Are two linear entities perpendicular?
|
|
|
|
Parameters
|
|
==========
|
|
|
|
l1 : LinearEntity
|
|
l2 : LinearEntity
|
|
|
|
Returns
|
|
=======
|
|
|
|
True : if l1 and l2 are perpendicular,
|
|
False : otherwise.
|
|
|
|
See Also
|
|
========
|
|
|
|
coefficients
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2, p3 = Point(0, 0), Point(1, 1), Point(-1, 1)
|
|
>>> l1, l2 = Line(p1, p2), Line(p1, p3)
|
|
>>> l1.is_perpendicular(l2)
|
|
True
|
|
>>> p4 = Point(5, 3)
|
|
>>> l3 = Line(p1, p4)
|
|
>>> l1.is_perpendicular(l3)
|
|
False
|
|
>>> from sympy import Point3D, Line3D
|
|
>>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(1, 1, 1), Point3D(-1, 2, 0)
|
|
>>> l1, l2 = Line3D(p1, p2), Line3D(p2, p3)
|
|
>>> l1.is_perpendicular(l2)
|
|
False
|
|
>>> p4 = Point3D(5, 3, 7)
|
|
>>> l3 = Line3D(p1, p4)
|
|
>>> l1.is_perpendicular(l3)
|
|
False
|
|
|
|
"""
|
|
if not isinstance(l1, LinearEntity) and not isinstance(l2, LinearEntity):
|
|
raise TypeError('Must pass only LinearEntity objects')
|
|
|
|
return S.Zero.equals(l1.direction.dot(l2.direction))
|
|
|
|
def is_similar(self, other):
|
|
"""
|
|
Return True if self and other are contained in the same line.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2, p3 = Point(0, 1), Point(3, 4), Point(2, 3)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> l2 = Line(p1, p3)
|
|
>>> l1.is_similar(l2)
|
|
True
|
|
"""
|
|
l = Line(self.p1, self.p2)
|
|
return l.contains(other)
|
|
|
|
@property
|
|
def length(self):
|
|
"""
|
|
The length of the line.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2 = Point(0, 0), Point(3, 5)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> l1.length
|
|
oo
|
|
"""
|
|
return S.Infinity
|
|
|
|
@property
|
|
def p1(self):
|
|
"""The first defining point of a linear entity.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2 = Point(0, 0), Point(5, 3)
|
|
>>> l = Line(p1, p2)
|
|
>>> l.p1
|
|
Point2D(0, 0)
|
|
|
|
"""
|
|
return self.args[0]
|
|
|
|
@property
|
|
def p2(self):
|
|
"""The second defining point of a linear entity.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2 = Point(0, 0), Point(5, 3)
|
|
>>> l = Line(p1, p2)
|
|
>>> l.p2
|
|
Point2D(5, 3)
|
|
|
|
"""
|
|
return self.args[1]
|
|
|
|
def parallel_line(self, p):
|
|
"""Create a new Line parallel to this linear entity which passes
|
|
through the point `p`.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p : Point
|
|
|
|
Returns
|
|
=======
|
|
|
|
line : Line
|
|
|
|
See Also
|
|
========
|
|
|
|
is_parallel
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2, p3 = Point(0, 0), Point(2, 3), Point(-2, 2)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> l2 = l1.parallel_line(p3)
|
|
>>> p3 in l2
|
|
True
|
|
>>> l1.is_parallel(l2)
|
|
True
|
|
>>> from sympy import Point3D, Line3D
|
|
>>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(2, 3, 4), Point3D(-2, 2, 0)
|
|
>>> l1 = Line3D(p1, p2)
|
|
>>> l2 = l1.parallel_line(p3)
|
|
>>> p3 in l2
|
|
True
|
|
>>> l1.is_parallel(l2)
|
|
True
|
|
|
|
"""
|
|
p = Point(p, dim=self.ambient_dimension)
|
|
return Line(p, p + self.direction)
|
|
|
|
def perpendicular_line(self, p):
|
|
"""Create a new Line perpendicular to this linear entity which passes
|
|
through the point `p`.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p : Point
|
|
|
|
Returns
|
|
=======
|
|
|
|
line : Line
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.line.LinearEntity.is_perpendicular, perpendicular_segment
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Line3D
|
|
>>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(2, 3, 4), Point3D(-2, 2, 0)
|
|
>>> L = Line3D(p1, p2)
|
|
>>> P = L.perpendicular_line(p3); P
|
|
Line3D(Point3D(-2, 2, 0), Point3D(4/29, 6/29, 8/29))
|
|
>>> L.is_perpendicular(P)
|
|
True
|
|
|
|
In 3D the, the first point used to define the line is the point
|
|
through which the perpendicular was required to pass; the
|
|
second point is (arbitrarily) contained in the given line:
|
|
|
|
>>> P.p2 in L
|
|
True
|
|
"""
|
|
p = Point(p, dim=self.ambient_dimension)
|
|
if p in self:
|
|
p = p + self.direction.orthogonal_direction
|
|
return Line(p, self.projection(p))
|
|
|
|
def perpendicular_segment(self, p):
|
|
"""Create a perpendicular line segment from `p` to this line.
|
|
|
|
The endpoints of the segment are ``p`` and the closest point in
|
|
the line containing self. (If self is not a line, the point might
|
|
not be in self.)
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p : Point
|
|
|
|
Returns
|
|
=======
|
|
|
|
segment : Segment
|
|
|
|
Notes
|
|
=====
|
|
|
|
Returns `p` itself if `p` is on this linear entity.
|
|
|
|
See Also
|
|
========
|
|
|
|
perpendicular_line
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2, p3 = Point(0, 0), Point(1, 1), Point(0, 2)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> s1 = l1.perpendicular_segment(p3)
|
|
>>> l1.is_perpendicular(s1)
|
|
True
|
|
>>> p3 in s1
|
|
True
|
|
>>> l1.perpendicular_segment(Point(4, 0))
|
|
Segment2D(Point2D(4, 0), Point2D(2, 2))
|
|
>>> from sympy import Point3D, Line3D
|
|
>>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(1, 1, 1), Point3D(0, 2, 0)
|
|
>>> l1 = Line3D(p1, p2)
|
|
>>> s1 = l1.perpendicular_segment(p3)
|
|
>>> l1.is_perpendicular(s1)
|
|
True
|
|
>>> p3 in s1
|
|
True
|
|
>>> l1.perpendicular_segment(Point3D(4, 0, 0))
|
|
Segment3D(Point3D(4, 0, 0), Point3D(4/3, 4/3, 4/3))
|
|
|
|
"""
|
|
p = Point(p, dim=self.ambient_dimension)
|
|
if p in self:
|
|
return p
|
|
l = self.perpendicular_line(p)
|
|
# The intersection should be unique, so unpack the singleton
|
|
p2, = Intersection(Line(self.p1, self.p2), l)
|
|
|
|
return Segment(p, p2)
|
|
|
|
@property
|
|
def points(self):
|
|
"""The two points used to define this linear entity.
|
|
|
|
Returns
|
|
=======
|
|
|
|
points : tuple of Points
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2 = Point(0, 0), Point(5, 11)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> l1.points
|
|
(Point2D(0, 0), Point2D(5, 11))
|
|
|
|
"""
|
|
return (self.p1, self.p2)
|
|
|
|
def projection(self, other):
|
|
"""Project a point, line, ray, or segment onto this linear entity.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
other : Point or LinearEntity (Line, Ray, Segment)
|
|
|
|
Returns
|
|
=======
|
|
|
|
projection : Point or LinearEntity (Line, Ray, Segment)
|
|
The return type matches the type of the parameter ``other``.
|
|
|
|
Raises
|
|
======
|
|
|
|
GeometryError
|
|
When method is unable to perform projection.
|
|
|
|
Notes
|
|
=====
|
|
|
|
A projection involves taking the two points that define
|
|
the linear entity and projecting those points onto a
|
|
Line and then reforming the linear entity using these
|
|
projections.
|
|
A point P is projected onto a line L by finding the point
|
|
on L that is closest to P. This point is the intersection
|
|
of L and the line perpendicular to L that passes through P.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point, perpendicular_line
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line, Segment, Rational
|
|
>>> p1, p2, p3 = Point(0, 0), Point(1, 1), Point(Rational(1, 2), 0)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> l1.projection(p3)
|
|
Point2D(1/4, 1/4)
|
|
>>> p4, p5 = Point(10, 0), Point(12, 1)
|
|
>>> s1 = Segment(p4, p5)
|
|
>>> l1.projection(s1)
|
|
Segment2D(Point2D(5, 5), Point2D(13/2, 13/2))
|
|
>>> p1, p2, p3 = Point(0, 0, 1), Point(1, 1, 2), Point(2, 0, 1)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> l1.projection(p3)
|
|
Point3D(2/3, 2/3, 5/3)
|
|
>>> p4, p5 = Point(10, 0, 1), Point(12, 1, 3)
|
|
>>> s1 = Segment(p4, p5)
|
|
>>> l1.projection(s1)
|
|
Segment3D(Point3D(10/3, 10/3, 13/3), Point3D(5, 5, 6))
|
|
|
|
"""
|
|
if not isinstance(other, GeometryEntity):
|
|
other = Point(other, dim=self.ambient_dimension)
|
|
|
|
def proj_point(p):
|
|
return Point.project(p - self.p1, self.direction) + self.p1
|
|
|
|
if isinstance(other, Point):
|
|
return proj_point(other)
|
|
elif isinstance(other, LinearEntity):
|
|
p1, p2 = proj_point(other.p1), proj_point(other.p2)
|
|
# test to see if we're degenerate
|
|
if p1 == p2:
|
|
return p1
|
|
projected = other.__class__(p1, p2)
|
|
projected = Intersection(self, projected)
|
|
if projected.is_empty:
|
|
return projected
|
|
# if we happen to have intersected in only a point, return that
|
|
if projected.is_FiniteSet and len(projected) == 1:
|
|
# projected is a set of size 1, so unpack it in `a`
|
|
a, = projected
|
|
return a
|
|
# order args so projection is in the same direction as self
|
|
if self.direction.dot(projected.direction) < 0:
|
|
p1, p2 = projected.args
|
|
projected = projected.func(p2, p1)
|
|
return projected
|
|
|
|
raise GeometryError(
|
|
"Do not know how to project %s onto %s" % (other, self))
|
|
|
|
def random_point(self, seed=None):
|
|
"""A random point on a LinearEntity.
|
|
|
|
Returns
|
|
=======
|
|
|
|
point : Point
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line, Ray, Segment
|
|
>>> p1, p2 = Point(0, 0), Point(5, 3)
|
|
>>> line = Line(p1, p2)
|
|
>>> r = line.random_point(seed=42) # seed value is optional
|
|
>>> r.n(3)
|
|
Point2D(-0.72, -0.432)
|
|
>>> r in line
|
|
True
|
|
>>> Ray(p1, p2).random_point(seed=42).n(3)
|
|
Point2D(0.72, 0.432)
|
|
>>> Segment(p1, p2).random_point(seed=42).n(3)
|
|
Point2D(3.2, 1.92)
|
|
|
|
"""
|
|
if seed is not None:
|
|
rng = random.Random(seed)
|
|
else:
|
|
rng = random
|
|
pt = self.arbitrary_point(t)
|
|
if isinstance(self, Ray):
|
|
v = abs(rng.gauss(0, 1))
|
|
elif isinstance(self, Segment):
|
|
v = rng.random()
|
|
elif isinstance(self, Line):
|
|
v = rng.gauss(0, 1)
|
|
else:
|
|
raise NotImplementedError('unhandled line type')
|
|
return pt.subs(t, Rational(v))
|
|
|
|
def bisectors(self, other):
|
|
"""Returns the perpendicular lines which pass through the intersections
|
|
of self and other that are in the same plane.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
line : Line3D
|
|
|
|
Returns
|
|
=======
|
|
|
|
list: two Line instances
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Line3D
|
|
>>> r1 = Line3D(Point3D(0, 0, 0), Point3D(1, 0, 0))
|
|
>>> r2 = Line3D(Point3D(0, 0, 0), Point3D(0, 1, 0))
|
|
>>> r1.bisectors(r2)
|
|
[Line3D(Point3D(0, 0, 0), Point3D(1, 1, 0)), Line3D(Point3D(0, 0, 0), Point3D(1, -1, 0))]
|
|
|
|
"""
|
|
if not isinstance(other, LinearEntity):
|
|
raise GeometryError("Expecting LinearEntity, not %s" % other)
|
|
|
|
l1, l2 = self, other
|
|
|
|
# make sure dimensions match or else a warning will rise from
|
|
# intersection calculation
|
|
if l1.p1.ambient_dimension != l2.p1.ambient_dimension:
|
|
if isinstance(l1, Line2D):
|
|
l1, l2 = l2, l1
|
|
_, p1 = Point._normalize_dimension(l1.p1, l2.p1, on_morph='ignore')
|
|
_, p2 = Point._normalize_dimension(l1.p2, l2.p2, on_morph='ignore')
|
|
l2 = Line(p1, p2)
|
|
|
|
point = intersection(l1, l2)
|
|
|
|
# Three cases: Lines may intersect in a point, may be equal or may not intersect.
|
|
if not point:
|
|
raise GeometryError("The lines do not intersect")
|
|
else:
|
|
pt = point[0]
|
|
if isinstance(pt, Line):
|
|
# Intersection is a line because both lines are coincident
|
|
return [self]
|
|
|
|
|
|
d1 = l1.direction.unit
|
|
d2 = l2.direction.unit
|
|
|
|
bis1 = Line(pt, pt + d1 + d2)
|
|
bis2 = Line(pt, pt + d1 - d2)
|
|
|
|
return [bis1, bis2]
|
|
|
|
|
|
class Line(LinearEntity):
|
|
"""An infinite line in space.
|
|
|
|
A 2D line is declared with two distinct points, point and slope, or
|
|
an equation. A 3D line may be defined with a point and a direction ratio.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p1 : Point
|
|
p2 : Point
|
|
slope : SymPy expression
|
|
direction_ratio : list
|
|
equation : equation of a line
|
|
|
|
Notes
|
|
=====
|
|
|
|
`Line` will automatically subclass to `Line2D` or `Line3D` based
|
|
on the dimension of `p1`. The `slope` argument is only relevant
|
|
for `Line2D` and the `direction_ratio` argument is only relevant
|
|
for `Line3D`.
|
|
|
|
The order of the points will define the direction of the line
|
|
which is used when calculating the angle between lines.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point
|
|
sympy.geometry.line.Line2D
|
|
sympy.geometry.line.Line3D
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Line, Segment, Point, Eq
|
|
>>> from sympy.abc import x, y, a, b
|
|
|
|
>>> L = Line(Point(2,3), Point(3,5))
|
|
>>> L
|
|
Line2D(Point2D(2, 3), Point2D(3, 5))
|
|
>>> L.points
|
|
(Point2D(2, 3), Point2D(3, 5))
|
|
>>> L.equation()
|
|
-2*x + y + 1
|
|
>>> L.coefficients
|
|
(-2, 1, 1)
|
|
|
|
Instantiate with keyword ``slope``:
|
|
|
|
>>> Line(Point(0, 0), slope=0)
|
|
Line2D(Point2D(0, 0), Point2D(1, 0))
|
|
|
|
Instantiate with another linear object
|
|
|
|
>>> s = Segment((0, 0), (0, 1))
|
|
>>> Line(s).equation()
|
|
x
|
|
|
|
The line corresponding to an equation in the for `ax + by + c = 0`,
|
|
can be entered:
|
|
|
|
>>> Line(3*x + y + 18)
|
|
Line2D(Point2D(0, -18), Point2D(1, -21))
|
|
|
|
If `x` or `y` has a different name, then they can be specified, too,
|
|
as a string (to match the name) or symbol:
|
|
|
|
>>> Line(Eq(3*a + b, -18), x='a', y=b)
|
|
Line2D(Point2D(0, -18), Point2D(1, -21))
|
|
"""
|
|
def __new__(cls, *args, **kwargs):
|
|
if len(args) == 1 and isinstance(args[0], (Expr, Eq)):
|
|
missing = uniquely_named_symbol('?', args)
|
|
if not kwargs:
|
|
x = 'x'
|
|
y = 'y'
|
|
else:
|
|
x = kwargs.pop('x', missing)
|
|
y = kwargs.pop('y', missing)
|
|
if kwargs:
|
|
raise ValueError('expecting only x and y as keywords')
|
|
|
|
equation = args[0]
|
|
if isinstance(equation, Eq):
|
|
equation = equation.lhs - equation.rhs
|
|
|
|
def find_or_missing(x):
|
|
try:
|
|
return find(x, equation)
|
|
except ValueError:
|
|
return missing
|
|
x = find_or_missing(x)
|
|
y = find_or_missing(y)
|
|
|
|
a, b, c = linear_coeffs(equation, x, y)
|
|
|
|
if b:
|
|
return Line((0, -c/b), slope=-a/b)
|
|
if a:
|
|
return Line((-c/a, 0), slope=oo)
|
|
|
|
raise ValueError('not found in equation: %s' % (set('xy') - {x, y}))
|
|
|
|
else:
|
|
if len(args) > 0:
|
|
p1 = args[0]
|
|
if len(args) > 1:
|
|
p2 = args[1]
|
|
else:
|
|
p2 = None
|
|
|
|
if isinstance(p1, LinearEntity):
|
|
if p2:
|
|
raise ValueError('If p1 is a LinearEntity, p2 must be None.')
|
|
dim = len(p1.p1)
|
|
else:
|
|
p1 = Point(p1)
|
|
dim = len(p1)
|
|
if p2 is not None or isinstance(p2, Point) and p2.ambient_dimension != dim:
|
|
p2 = Point(p2)
|
|
|
|
if dim == 2:
|
|
return Line2D(p1, p2, **kwargs)
|
|
elif dim == 3:
|
|
return Line3D(p1, p2, **kwargs)
|
|
return LinearEntity.__new__(cls, p1, p2, **kwargs)
|
|
|
|
def contains(self, other):
|
|
"""
|
|
Return True if `other` is on this Line, or False otherwise.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Line,Point
|
|
>>> p1, p2 = Point(0, 1), Point(3, 4)
|
|
>>> l = Line(p1, p2)
|
|
>>> l.contains(p1)
|
|
True
|
|
>>> l.contains((0, 1))
|
|
True
|
|
>>> l.contains((0, 0))
|
|
False
|
|
>>> a = (0, 0, 0)
|
|
>>> b = (1, 1, 1)
|
|
>>> c = (2, 2, 2)
|
|
>>> l1 = Line(a, b)
|
|
>>> l2 = Line(b, a)
|
|
>>> l1 == l2
|
|
False
|
|
>>> l1 in l2
|
|
True
|
|
|
|
"""
|
|
if not isinstance(other, GeometryEntity):
|
|
other = Point(other, dim=self.ambient_dimension)
|
|
if isinstance(other, Point):
|
|
return Point.is_collinear(other, self.p1, self.p2)
|
|
if isinstance(other, LinearEntity):
|
|
return Point.is_collinear(self.p1, self.p2, other.p1, other.p2)
|
|
return False
|
|
|
|
def distance(self, other):
|
|
"""
|
|
Finds the shortest distance between a line and a point.
|
|
|
|
Raises
|
|
======
|
|
|
|
NotImplementedError is raised if `other` is not a Point
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2 = Point(0, 0), Point(1, 1)
|
|
>>> s = Line(p1, p2)
|
|
>>> s.distance(Point(-1, 1))
|
|
sqrt(2)
|
|
>>> s.distance((-1, 2))
|
|
3*sqrt(2)/2
|
|
>>> p1, p2 = Point(0, 0, 0), Point(1, 1, 1)
|
|
>>> s = Line(p1, p2)
|
|
>>> s.distance(Point(-1, 1, 1))
|
|
2*sqrt(6)/3
|
|
>>> s.distance((-1, 1, 1))
|
|
2*sqrt(6)/3
|
|
|
|
"""
|
|
if not isinstance(other, GeometryEntity):
|
|
other = Point(other, dim=self.ambient_dimension)
|
|
if self.contains(other):
|
|
return S.Zero
|
|
return self.perpendicular_segment(other).length
|
|
|
|
def equals(self, other):
|
|
"""Returns True if self and other are the same mathematical entities"""
|
|
if not isinstance(other, Line):
|
|
return False
|
|
return Point.is_collinear(self.p1, other.p1, self.p2, other.p2)
|
|
|
|
def plot_interval(self, parameter='t'):
|
|
"""The plot interval for the default geometric plot of line. Gives
|
|
values that will produce a line that is +/- 5 units long (where a
|
|
unit is the distance between the two points that define the line).
|
|
|
|
Parameters
|
|
==========
|
|
|
|
parameter : str, optional
|
|
Default value is 't'.
|
|
|
|
Returns
|
|
=======
|
|
|
|
plot_interval : list (plot interval)
|
|
[parameter, lower_bound, upper_bound]
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2 = Point(0, 0), Point(5, 3)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> l1.plot_interval()
|
|
[t, -5, 5]
|
|
|
|
"""
|
|
t = _symbol(parameter, real=True)
|
|
return [t, -5, 5]
|
|
|
|
|
|
class Ray(LinearEntity):
|
|
"""A Ray is a semi-line in the space with a source point and a direction.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p1 : Point
|
|
The source of the Ray
|
|
p2 : Point or radian value
|
|
This point determines the direction in which the Ray propagates.
|
|
If given as an angle it is interpreted in radians with the positive
|
|
direction being ccw.
|
|
|
|
Attributes
|
|
==========
|
|
|
|
source
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.line.Ray2D
|
|
sympy.geometry.line.Ray3D
|
|
sympy.geometry.point.Point
|
|
sympy.geometry.line.Line
|
|
|
|
Notes
|
|
=====
|
|
|
|
`Ray` will automatically subclass to `Ray2D` or `Ray3D` based on the
|
|
dimension of `p1`.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Ray, Point, pi
|
|
>>> r = Ray(Point(2, 3), Point(3, 5))
|
|
>>> r
|
|
Ray2D(Point2D(2, 3), Point2D(3, 5))
|
|
>>> r.points
|
|
(Point2D(2, 3), Point2D(3, 5))
|
|
>>> r.source
|
|
Point2D(2, 3)
|
|
>>> r.xdirection
|
|
oo
|
|
>>> r.ydirection
|
|
oo
|
|
>>> r.slope
|
|
2
|
|
>>> Ray(Point(0, 0), angle=pi/4).slope
|
|
1
|
|
|
|
"""
|
|
def __new__(cls, p1, p2=None, **kwargs):
|
|
p1 = Point(p1)
|
|
if p2 is not None:
|
|
p1, p2 = Point._normalize_dimension(p1, Point(p2))
|
|
dim = len(p1)
|
|
|
|
if dim == 2:
|
|
return Ray2D(p1, p2, **kwargs)
|
|
elif dim == 3:
|
|
return Ray3D(p1, p2, **kwargs)
|
|
return LinearEntity.__new__(cls, p1, p2, **kwargs)
|
|
|
|
def _svg(self, scale_factor=1., fill_color="#66cc99"):
|
|
"""Returns SVG path element for the LinearEntity.
|
|
|
|
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".
|
|
"""
|
|
verts = (N(self.p1), N(self.p2))
|
|
coords = ["{},{}".format(p.x, p.y) for p in verts]
|
|
path = "M {} L {}".format(coords[0], " L ".join(coords[1:]))
|
|
|
|
return (
|
|
'<path fill-rule="evenodd" fill="{2}" stroke="#555555" '
|
|
'stroke-width="{0}" opacity="0.6" d="{1}" '
|
|
'marker-start="url(#markerCircle)" marker-end="url(#markerArrow)"/>'
|
|
).format(2.*scale_factor, path, fill_color)
|
|
|
|
def contains(self, other):
|
|
"""
|
|
Is other GeometryEntity contained in this Ray?
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Ray,Point,Segment
|
|
>>> p1, p2 = Point(0, 0), Point(4, 4)
|
|
>>> r = Ray(p1, p2)
|
|
>>> r.contains(p1)
|
|
True
|
|
>>> r.contains((1, 1))
|
|
True
|
|
>>> r.contains((1, 3))
|
|
False
|
|
>>> s = Segment((1, 1), (2, 2))
|
|
>>> r.contains(s)
|
|
True
|
|
>>> s = Segment((1, 2), (2, 5))
|
|
>>> r.contains(s)
|
|
False
|
|
>>> r1 = Ray((2, 2), (3, 3))
|
|
>>> r.contains(r1)
|
|
True
|
|
>>> r1 = Ray((2, 2), (3, 5))
|
|
>>> r.contains(r1)
|
|
False
|
|
"""
|
|
if not isinstance(other, GeometryEntity):
|
|
other = Point(other, dim=self.ambient_dimension)
|
|
if isinstance(other, Point):
|
|
if Point.is_collinear(self.p1, self.p2, other):
|
|
# if we're in the direction of the ray, our
|
|
# direction vector dot the ray's direction vector
|
|
# should be non-negative
|
|
return bool((self.p2 - self.p1).dot(other - self.p1) >= S.Zero)
|
|
return False
|
|
elif isinstance(other, Ray):
|
|
if Point.is_collinear(self.p1, self.p2, other.p1, other.p2):
|
|
return bool((self.p2 - self.p1).dot(other.p2 - other.p1) > S.Zero)
|
|
return False
|
|
elif isinstance(other, Segment):
|
|
return other.p1 in self and other.p2 in self
|
|
|
|
# No other known entity can be contained in a Ray
|
|
return False
|
|
|
|
def distance(self, other):
|
|
"""
|
|
Finds the shortest distance between the ray and a point.
|
|
|
|
Raises
|
|
======
|
|
|
|
NotImplementedError is raised if `other` is not a Point
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Ray
|
|
>>> p1, p2 = Point(0, 0), Point(1, 1)
|
|
>>> s = Ray(p1, p2)
|
|
>>> s.distance(Point(-1, -1))
|
|
sqrt(2)
|
|
>>> s.distance((-1, 2))
|
|
3*sqrt(2)/2
|
|
>>> p1, p2 = Point(0, 0, 0), Point(1, 1, 2)
|
|
>>> s = Ray(p1, p2)
|
|
>>> s
|
|
Ray3D(Point3D(0, 0, 0), Point3D(1, 1, 2))
|
|
>>> s.distance(Point(-1, -1, 2))
|
|
4*sqrt(3)/3
|
|
>>> s.distance((-1, -1, 2))
|
|
4*sqrt(3)/3
|
|
|
|
"""
|
|
if not isinstance(other, GeometryEntity):
|
|
other = Point(other, dim=self.ambient_dimension)
|
|
if self.contains(other):
|
|
return S.Zero
|
|
|
|
proj = Line(self.p1, self.p2).projection(other)
|
|
if self.contains(proj):
|
|
return abs(other - proj)
|
|
else:
|
|
return abs(other - self.source)
|
|
|
|
def equals(self, other):
|
|
"""Returns True if self and other are the same mathematical entities"""
|
|
if not isinstance(other, Ray):
|
|
return False
|
|
return self.source == other.source and other.p2 in self
|
|
|
|
def plot_interval(self, parameter='t'):
|
|
"""The plot interval for the default geometric plot of the Ray. Gives
|
|
values that will produce a ray that is 10 units long (where a unit is
|
|
the distance between the two points that define the ray).
|
|
|
|
Parameters
|
|
==========
|
|
|
|
parameter : str, optional
|
|
Default value is 't'.
|
|
|
|
Returns
|
|
=======
|
|
|
|
plot_interval : list
|
|
[parameter, lower_bound, upper_bound]
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Ray, pi
|
|
>>> r = Ray((0, 0), angle=pi/4)
|
|
>>> r.plot_interval()
|
|
[t, 0, 10]
|
|
|
|
"""
|
|
t = _symbol(parameter, real=True)
|
|
return [t, 0, 10]
|
|
|
|
@property
|
|
def source(self):
|
|
"""The point from which the ray emanates.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Ray
|
|
>>> p1, p2 = Point(0, 0), Point(4, 1)
|
|
>>> r1 = Ray(p1, p2)
|
|
>>> r1.source
|
|
Point2D(0, 0)
|
|
>>> p1, p2 = Point(0, 0, 0), Point(4, 1, 5)
|
|
>>> r1 = Ray(p2, p1)
|
|
>>> r1.source
|
|
Point3D(4, 1, 5)
|
|
|
|
"""
|
|
return self.p1
|
|
|
|
|
|
class Segment(LinearEntity):
|
|
"""A line segment in space.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p1 : Point
|
|
p2 : Point
|
|
|
|
Attributes
|
|
==========
|
|
|
|
length : number or SymPy expression
|
|
midpoint : Point
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.line.Segment2D
|
|
sympy.geometry.line.Segment3D
|
|
sympy.geometry.point.Point
|
|
sympy.geometry.line.Line
|
|
|
|
Notes
|
|
=====
|
|
|
|
If 2D or 3D points are used to define `Segment`, it will
|
|
be automatically subclassed to `Segment2D` or `Segment3D`.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Segment
|
|
>>> Segment((1, 0), (1, 1)) # tuples are interpreted as pts
|
|
Segment2D(Point2D(1, 0), Point2D(1, 1))
|
|
>>> s = Segment(Point(4, 3), Point(1, 1))
|
|
>>> s.points
|
|
(Point2D(4, 3), Point2D(1, 1))
|
|
>>> s.slope
|
|
2/3
|
|
>>> s.length
|
|
sqrt(13)
|
|
>>> s.midpoint
|
|
Point2D(5/2, 2)
|
|
>>> Segment((1, 0, 0), (1, 1, 1)) # tuples are interpreted as pts
|
|
Segment3D(Point3D(1, 0, 0), Point3D(1, 1, 1))
|
|
>>> s = Segment(Point(4, 3, 9), Point(1, 1, 7)); s
|
|
Segment3D(Point3D(4, 3, 9), Point3D(1, 1, 7))
|
|
>>> s.points
|
|
(Point3D(4, 3, 9), Point3D(1, 1, 7))
|
|
>>> s.length
|
|
sqrt(17)
|
|
>>> s.midpoint
|
|
Point3D(5/2, 2, 8)
|
|
|
|
"""
|
|
def __new__(cls, p1, p2, **kwargs):
|
|
p1, p2 = Point._normalize_dimension(Point(p1), Point(p2))
|
|
dim = len(p1)
|
|
|
|
if dim == 2:
|
|
return Segment2D(p1, p2, **kwargs)
|
|
elif dim == 3:
|
|
return Segment3D(p1, p2, **kwargs)
|
|
return LinearEntity.__new__(cls, p1, p2, **kwargs)
|
|
|
|
def contains(self, other):
|
|
"""
|
|
Is the other GeometryEntity contained within this Segment?
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Segment
|
|
>>> p1, p2 = Point(0, 1), Point(3, 4)
|
|
>>> s = Segment(p1, p2)
|
|
>>> s2 = Segment(p2, p1)
|
|
>>> s.contains(s2)
|
|
True
|
|
>>> from sympy import Point3D, Segment3D
|
|
>>> p1, p2 = Point3D(0, 1, 1), Point3D(3, 4, 5)
|
|
>>> s = Segment3D(p1, p2)
|
|
>>> s2 = Segment3D(p2, p1)
|
|
>>> s.contains(s2)
|
|
True
|
|
>>> s.contains((p1 + p2)/2)
|
|
True
|
|
"""
|
|
if not isinstance(other, GeometryEntity):
|
|
other = Point(other, dim=self.ambient_dimension)
|
|
if isinstance(other, Point):
|
|
if Point.is_collinear(other, self.p1, self.p2):
|
|
if isinstance(self, Segment2D):
|
|
# if it is collinear and is in the bounding box of the
|
|
# segment then it must be on the segment
|
|
vert = (1/self.slope).equals(0)
|
|
if vert is False:
|
|
isin = (self.p1.x - other.x)*(self.p2.x - other.x) <= 0
|
|
if isin in (True, False):
|
|
return isin
|
|
if vert is True:
|
|
isin = (self.p1.y - other.y)*(self.p2.y - other.y) <= 0
|
|
if isin in (True, False):
|
|
return isin
|
|
# use the triangle inequality
|
|
d1, d2 = other - self.p1, other - self.p2
|
|
d = self.p2 - self.p1
|
|
# without the call to simplify, SymPy cannot tell that an expression
|
|
# like (a+b)*(a/2+b/2) is always non-negative. If it cannot be
|
|
# determined, raise an Undecidable error
|
|
try:
|
|
# the triangle inequality says that |d1|+|d2| >= |d| and is strict
|
|
# only if other lies in the line segment
|
|
return bool(simplify(Eq(abs(d1) + abs(d2) - abs(d), 0)))
|
|
except TypeError:
|
|
raise Undecidable("Cannot determine if {} is in {}".format(other, self))
|
|
if isinstance(other, Segment):
|
|
return other.p1 in self and other.p2 in self
|
|
|
|
return False
|
|
|
|
def equals(self, other):
|
|
"""Returns True if self and other are the same mathematical entities"""
|
|
return isinstance(other, self.func) and list(
|
|
ordered(self.args)) == list(ordered(other.args))
|
|
|
|
def distance(self, other):
|
|
"""
|
|
Finds the shortest distance between a line segment and a point.
|
|
|
|
Raises
|
|
======
|
|
|
|
NotImplementedError is raised if `other` is not a Point
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Segment
|
|
>>> p1, p2 = Point(0, 1), Point(3, 4)
|
|
>>> s = Segment(p1, p2)
|
|
>>> s.distance(Point(10, 15))
|
|
sqrt(170)
|
|
>>> s.distance((0, 12))
|
|
sqrt(73)
|
|
>>> from sympy import Point3D, Segment3D
|
|
>>> p1, p2 = Point3D(0, 0, 3), Point3D(1, 1, 4)
|
|
>>> s = Segment3D(p1, p2)
|
|
>>> s.distance(Point3D(10, 15, 12))
|
|
sqrt(341)
|
|
>>> s.distance((10, 15, 12))
|
|
sqrt(341)
|
|
"""
|
|
if not isinstance(other, GeometryEntity):
|
|
other = Point(other, dim=self.ambient_dimension)
|
|
if isinstance(other, Point):
|
|
vp1 = other - self.p1
|
|
vp2 = other - self.p2
|
|
|
|
dot_prod_sign_1 = self.direction.dot(vp1) >= 0
|
|
dot_prod_sign_2 = self.direction.dot(vp2) <= 0
|
|
if dot_prod_sign_1 and dot_prod_sign_2:
|
|
return Line(self.p1, self.p2).distance(other)
|
|
if dot_prod_sign_1 and not dot_prod_sign_2:
|
|
return abs(vp2)
|
|
if not dot_prod_sign_1 and dot_prod_sign_2:
|
|
return abs(vp1)
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def length(self):
|
|
"""The length of the line segment.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point.distance
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Segment
|
|
>>> p1, p2 = Point(0, 0), Point(4, 3)
|
|
>>> s1 = Segment(p1, p2)
|
|
>>> s1.length
|
|
5
|
|
>>> from sympy import Point3D, Segment3D
|
|
>>> p1, p2 = Point3D(0, 0, 0), Point3D(4, 3, 3)
|
|
>>> s1 = Segment3D(p1, p2)
|
|
>>> s1.length
|
|
sqrt(34)
|
|
|
|
"""
|
|
return Point.distance(self.p1, self.p2)
|
|
|
|
@property
|
|
def midpoint(self):
|
|
"""The midpoint of the line segment.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point.midpoint
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Segment
|
|
>>> p1, p2 = Point(0, 0), Point(4, 3)
|
|
>>> s1 = Segment(p1, p2)
|
|
>>> s1.midpoint
|
|
Point2D(2, 3/2)
|
|
>>> from sympy import Point3D, Segment3D
|
|
>>> p1, p2 = Point3D(0, 0, 0), Point3D(4, 3, 3)
|
|
>>> s1 = Segment3D(p1, p2)
|
|
>>> s1.midpoint
|
|
Point3D(2, 3/2, 3/2)
|
|
|
|
"""
|
|
return Point.midpoint(self.p1, self.p2)
|
|
|
|
def perpendicular_bisector(self, p=None):
|
|
"""The perpendicular bisector of this segment.
|
|
|
|
If no point is specified or the point specified is not on the
|
|
bisector then the bisector is returned as a Line. Otherwise a
|
|
Segment is returned that joins the point specified and the
|
|
intersection of the bisector and the segment.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p : Point
|
|
|
|
Returns
|
|
=======
|
|
|
|
bisector : Line or Segment
|
|
|
|
See Also
|
|
========
|
|
|
|
LinearEntity.perpendicular_segment
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Segment
|
|
>>> p1, p2, p3 = Point(0, 0), Point(6, 6), Point(5, 1)
|
|
>>> s1 = Segment(p1, p2)
|
|
>>> s1.perpendicular_bisector()
|
|
Line2D(Point2D(3, 3), Point2D(-3, 9))
|
|
|
|
>>> s1.perpendicular_bisector(p3)
|
|
Segment2D(Point2D(5, 1), Point2D(3, 3))
|
|
|
|
"""
|
|
l = self.perpendicular_line(self.midpoint)
|
|
if p is not None:
|
|
p2 = Point(p, dim=self.ambient_dimension)
|
|
if p2 in l:
|
|
return Segment(p2, self.midpoint)
|
|
return l
|
|
|
|
def plot_interval(self, parameter='t'):
|
|
"""The plot interval for the default geometric plot of the Segment gives
|
|
values that will produce the full segment in a plot.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
parameter : str, optional
|
|
Default value is 't'.
|
|
|
|
Returns
|
|
=======
|
|
|
|
plot_interval : list
|
|
[parameter, lower_bound, upper_bound]
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Segment
|
|
>>> p1, p2 = Point(0, 0), Point(5, 3)
|
|
>>> s1 = Segment(p1, p2)
|
|
>>> s1.plot_interval()
|
|
[t, 0, 1]
|
|
|
|
"""
|
|
t = _symbol(parameter, real=True)
|
|
return [t, 0, 1]
|
|
|
|
|
|
class LinearEntity2D(LinearEntity):
|
|
"""A base class for all linear entities (line, ray and segment)
|
|
in a 2-dimensional Euclidean space.
|
|
|
|
Attributes
|
|
==========
|
|
|
|
p1
|
|
p2
|
|
coefficients
|
|
slope
|
|
points
|
|
|
|
Notes
|
|
=====
|
|
|
|
This is an abstract class and is not meant to be instantiated.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.entity.GeometryEntity
|
|
|
|
"""
|
|
@property
|
|
def bounds(self):
|
|
"""Return a tuple (xmin, ymin, xmax, ymax) representing the bounding
|
|
rectangle for the geometric figure.
|
|
|
|
"""
|
|
verts = self.points
|
|
xs = [p.x for p in verts]
|
|
ys = [p.y for p in verts]
|
|
return (min(xs), min(ys), max(xs), max(ys))
|
|
|
|
def perpendicular_line(self, p):
|
|
"""Create a new Line perpendicular to this linear entity which passes
|
|
through the point `p`.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p : Point
|
|
|
|
Returns
|
|
=======
|
|
|
|
line : Line
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.line.LinearEntity.is_perpendicular, perpendicular_segment
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2, p3 = Point(0, 0), Point(2, 3), Point(-2, 2)
|
|
>>> L = Line(p1, p2)
|
|
>>> P = L.perpendicular_line(p3); P
|
|
Line2D(Point2D(-2, 2), Point2D(-5, 4))
|
|
>>> L.is_perpendicular(P)
|
|
True
|
|
|
|
In 2D, the first point of the perpendicular line is the
|
|
point through which was required to pass; the second
|
|
point is arbitrarily chosen. To get a line that explicitly
|
|
uses a point in the line, create a line from the perpendicular
|
|
segment from the line to the point:
|
|
|
|
>>> Line(L.perpendicular_segment(p3))
|
|
Line2D(Point2D(-2, 2), Point2D(4/13, 6/13))
|
|
"""
|
|
p = Point(p, dim=self.ambient_dimension)
|
|
# any two lines in R^2 intersect, so blindly making
|
|
# a line through p in an orthogonal direction will work
|
|
# and is faster than finding the projection point as in 3D
|
|
return Line(p, p + self.direction.orthogonal_direction)
|
|
|
|
@property
|
|
def slope(self):
|
|
"""The slope of this linear entity, or infinity if vertical.
|
|
|
|
Returns
|
|
=======
|
|
|
|
slope : number or SymPy expression
|
|
|
|
See Also
|
|
========
|
|
|
|
coefficients
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2 = Point(0, 0), Point(3, 5)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> l1.slope
|
|
5/3
|
|
|
|
>>> p3 = Point(0, 4)
|
|
>>> l2 = Line(p1, p3)
|
|
>>> l2.slope
|
|
oo
|
|
|
|
"""
|
|
d1, d2 = (self.p1 - self.p2).args
|
|
if d1 == 0:
|
|
return S.Infinity
|
|
return simplify(d2/d1)
|
|
|
|
|
|
class Line2D(LinearEntity2D, Line):
|
|
"""An infinite line in space 2D.
|
|
|
|
A line is declared with two distinct points or a point and slope
|
|
as defined using keyword `slope`.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p1 : Point
|
|
pt : Point
|
|
slope : SymPy expression
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Line, Segment, Point
|
|
>>> L = Line(Point(2,3), Point(3,5))
|
|
>>> L
|
|
Line2D(Point2D(2, 3), Point2D(3, 5))
|
|
>>> L.points
|
|
(Point2D(2, 3), Point2D(3, 5))
|
|
>>> L.equation()
|
|
-2*x + y + 1
|
|
>>> L.coefficients
|
|
(-2, 1, 1)
|
|
|
|
Instantiate with keyword ``slope``:
|
|
|
|
>>> Line(Point(0, 0), slope=0)
|
|
Line2D(Point2D(0, 0), Point2D(1, 0))
|
|
|
|
Instantiate with another linear object
|
|
|
|
>>> s = Segment((0, 0), (0, 1))
|
|
>>> Line(s).equation()
|
|
x
|
|
"""
|
|
def __new__(cls, p1, pt=None, slope=None, **kwargs):
|
|
if isinstance(p1, LinearEntity):
|
|
if pt is not None:
|
|
raise ValueError('When p1 is a LinearEntity, pt should be None')
|
|
p1, pt = Point._normalize_dimension(*p1.args, dim=2)
|
|
else:
|
|
p1 = Point(p1, dim=2)
|
|
if pt is not None and slope is None:
|
|
try:
|
|
p2 = Point(pt, dim=2)
|
|
except (NotImplementedError, TypeError, ValueError):
|
|
raise ValueError(filldedent('''
|
|
The 2nd argument was not a valid Point.
|
|
If it was a slope, enter it with keyword "slope".
|
|
'''))
|
|
elif slope is not None and pt is None:
|
|
slope = sympify(slope)
|
|
if slope.is_finite is False:
|
|
# when infinite slope, don't change x
|
|
dx = 0
|
|
dy = 1
|
|
else:
|
|
# go over 1 up slope
|
|
dx = 1
|
|
dy = slope
|
|
# XXX avoiding simplification by adding to coords directly
|
|
p2 = Point(p1.x + dx, p1.y + dy, evaluate=False)
|
|
else:
|
|
raise ValueError('A 2nd Point or keyword "slope" must be used.')
|
|
return LinearEntity2D.__new__(cls, p1, p2, **kwargs)
|
|
|
|
def _svg(self, scale_factor=1., fill_color="#66cc99"):
|
|
"""Returns SVG path element for the LinearEntity.
|
|
|
|
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".
|
|
"""
|
|
verts = (N(self.p1), N(self.p2))
|
|
coords = ["{},{}".format(p.x, p.y) for p in verts]
|
|
path = "M {} L {}".format(coords[0], " L ".join(coords[1:]))
|
|
|
|
return (
|
|
'<path fill-rule="evenodd" fill="{2}" stroke="#555555" '
|
|
'stroke-width="{0}" opacity="0.6" d="{1}" '
|
|
'marker-start="url(#markerReverseArrow)" marker-end="url(#markerArrow)"/>'
|
|
).format(2.*scale_factor, path, fill_color)
|
|
|
|
@property
|
|
def coefficients(self):
|
|
"""The coefficients (`a`, `b`, `c`) for `ax + by + c = 0`.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.line.Line2D.equation
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> from sympy.abc import x, y
|
|
>>> p1, p2 = Point(0, 0), Point(5, 3)
|
|
>>> l = Line(p1, p2)
|
|
>>> l.coefficients
|
|
(-3, 5, 0)
|
|
|
|
>>> p3 = Point(x, y)
|
|
>>> l2 = Line(p1, p3)
|
|
>>> l2.coefficients
|
|
(-y, x, 0)
|
|
|
|
"""
|
|
p1, p2 = self.points
|
|
if p1.x == p2.x:
|
|
return (S.One, S.Zero, -p1.x)
|
|
elif p1.y == p2.y:
|
|
return (S.Zero, S.One, -p1.y)
|
|
return tuple([simplify(i) for i in
|
|
(self.p1.y - self.p2.y,
|
|
self.p2.x - self.p1.x,
|
|
self.p1.x*self.p2.y - self.p1.y*self.p2.x)])
|
|
|
|
def equation(self, x='x', y='y'):
|
|
"""The equation of the line: ax + by + c.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
x : str, optional
|
|
The name to use for the x-axis, default value is 'x'.
|
|
y : str, optional
|
|
The name to use for the y-axis, default value is 'y'.
|
|
|
|
Returns
|
|
=======
|
|
|
|
equation : SymPy expression
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.line.Line2D.coefficients
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Line
|
|
>>> p1, p2 = Point(1, 0), Point(5, 3)
|
|
>>> l1 = Line(p1, p2)
|
|
>>> l1.equation()
|
|
-3*x + 4*y + 3
|
|
|
|
"""
|
|
x = _symbol(x, real=True)
|
|
y = _symbol(y, real=True)
|
|
p1, p2 = self.points
|
|
if p1.x == p2.x:
|
|
return x - p1.x
|
|
elif p1.y == p2.y:
|
|
return y - p1.y
|
|
|
|
a, b, c = self.coefficients
|
|
return a*x + b*y + c
|
|
|
|
|
|
class Ray2D(LinearEntity2D, Ray):
|
|
"""
|
|
A Ray is a semi-line in the space with a source point and a direction.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p1 : Point
|
|
The source of the Ray
|
|
p2 : Point or radian value
|
|
This point determines the direction in which the Ray propagates.
|
|
If given as an angle it is interpreted in radians with the positive
|
|
direction being ccw.
|
|
|
|
Attributes
|
|
==========
|
|
|
|
source
|
|
xdirection
|
|
ydirection
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point, Line
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, pi, Ray
|
|
>>> r = Ray(Point(2, 3), Point(3, 5))
|
|
>>> r
|
|
Ray2D(Point2D(2, 3), Point2D(3, 5))
|
|
>>> r.points
|
|
(Point2D(2, 3), Point2D(3, 5))
|
|
>>> r.source
|
|
Point2D(2, 3)
|
|
>>> r.xdirection
|
|
oo
|
|
>>> r.ydirection
|
|
oo
|
|
>>> r.slope
|
|
2
|
|
>>> Ray(Point(0, 0), angle=pi/4).slope
|
|
1
|
|
|
|
"""
|
|
def __new__(cls, p1, pt=None, angle=None, **kwargs):
|
|
p1 = Point(p1, dim=2)
|
|
if pt is not None and angle is None:
|
|
try:
|
|
p2 = Point(pt, dim=2)
|
|
except (NotImplementedError, TypeError, ValueError):
|
|
raise ValueError(filldedent('''
|
|
The 2nd argument was not a valid Point; if
|
|
it was meant to be an angle it should be
|
|
given with keyword "angle".'''))
|
|
if p1 == p2:
|
|
raise ValueError('A Ray requires two distinct points.')
|
|
elif angle is not None and pt is None:
|
|
# we need to know if the angle is an odd multiple of pi/2
|
|
angle = sympify(angle)
|
|
c = _pi_coeff(angle)
|
|
p2 = None
|
|
if c is not None:
|
|
if c.is_Rational:
|
|
if c.q == 2:
|
|
if c.p == 1:
|
|
p2 = p1 + Point(0, 1)
|
|
elif c.p == 3:
|
|
p2 = p1 + Point(0, -1)
|
|
elif c.q == 1:
|
|
if c.p == 0:
|
|
p2 = p1 + Point(1, 0)
|
|
elif c.p == 1:
|
|
p2 = p1 + Point(-1, 0)
|
|
if p2 is None:
|
|
c *= S.Pi
|
|
else:
|
|
c = angle % (2*S.Pi)
|
|
if not p2:
|
|
m = 2*c/S.Pi
|
|
left = And(1 < m, m < 3) # is it in quadrant 2 or 3?
|
|
x = Piecewise((-1, left), (Piecewise((0, Eq(m % 1, 0)), (1, True)), True))
|
|
y = Piecewise((-tan(c), left), (Piecewise((1, Eq(m, 1)), (-1, Eq(m, 3)), (tan(c), True)), True))
|
|
p2 = p1 + Point(x, y)
|
|
else:
|
|
raise ValueError('A 2nd point or keyword "angle" must be used.')
|
|
|
|
return LinearEntity2D.__new__(cls, p1, p2, **kwargs)
|
|
|
|
@property
|
|
def xdirection(self):
|
|
"""The x direction of the ray.
|
|
|
|
Positive infinity if the ray points in the positive x direction,
|
|
negative infinity if the ray points in the negative x direction,
|
|
or 0 if the ray is vertical.
|
|
|
|
See Also
|
|
========
|
|
|
|
ydirection
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Ray
|
|
>>> p1, p2, p3 = Point(0, 0), Point(1, 1), Point(0, -1)
|
|
>>> r1, r2 = Ray(p1, p2), Ray(p1, p3)
|
|
>>> r1.xdirection
|
|
oo
|
|
>>> r2.xdirection
|
|
0
|
|
|
|
"""
|
|
if self.p1.x < self.p2.x:
|
|
return S.Infinity
|
|
elif self.p1.x == self.p2.x:
|
|
return S.Zero
|
|
else:
|
|
return S.NegativeInfinity
|
|
|
|
@property
|
|
def ydirection(self):
|
|
"""The y direction of the ray.
|
|
|
|
Positive infinity if the ray points in the positive y direction,
|
|
negative infinity if the ray points in the negative y direction,
|
|
or 0 if the ray is horizontal.
|
|
|
|
See Also
|
|
========
|
|
|
|
xdirection
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Ray
|
|
>>> p1, p2, p3 = Point(0, 0), Point(-1, -1), Point(-1, 0)
|
|
>>> r1, r2 = Ray(p1, p2), Ray(p1, p3)
|
|
>>> r1.ydirection
|
|
-oo
|
|
>>> r2.ydirection
|
|
0
|
|
|
|
"""
|
|
if self.p1.y < self.p2.y:
|
|
return S.Infinity
|
|
elif self.p1.y == self.p2.y:
|
|
return S.Zero
|
|
else:
|
|
return S.NegativeInfinity
|
|
|
|
def closing_angle(r1, r2):
|
|
"""Return the angle by which r2 must be rotated so it faces the same
|
|
direction as r1.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
r1 : Ray2D
|
|
r2 : Ray2D
|
|
|
|
Returns
|
|
=======
|
|
|
|
angle : angle in radians (ccw angle is positive)
|
|
|
|
See Also
|
|
========
|
|
|
|
LinearEntity.angle_between
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Ray, pi
|
|
>>> r1 = Ray((0, 0), (1, 0))
|
|
>>> r2 = r1.rotate(-pi/2)
|
|
>>> angle = r1.closing_angle(r2); angle
|
|
pi/2
|
|
>>> r2.rotate(angle).direction.unit == r1.direction.unit
|
|
True
|
|
>>> r2.closing_angle(r1)
|
|
-pi/2
|
|
"""
|
|
if not all(isinstance(r, Ray2D) for r in (r1, r2)):
|
|
# although the direction property is defined for
|
|
# all linear entities, only the Ray is truly a
|
|
# directed object
|
|
raise TypeError('Both arguments must be Ray2D objects.')
|
|
|
|
a1 = atan2(*list(reversed(r1.direction.args)))
|
|
a2 = atan2(*list(reversed(r2.direction.args)))
|
|
if a1*a2 < 0:
|
|
a1 = 2*S.Pi + a1 if a1 < 0 else a1
|
|
a2 = 2*S.Pi + a2 if a2 < 0 else a2
|
|
return a1 - a2
|
|
|
|
|
|
class Segment2D(LinearEntity2D, Segment):
|
|
"""A line segment in 2D space.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p1 : Point
|
|
p2 : Point
|
|
|
|
Attributes
|
|
==========
|
|
|
|
length : number or SymPy expression
|
|
midpoint : Point
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point, Line
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point, Segment
|
|
>>> Segment((1, 0), (1, 1)) # tuples are interpreted as pts
|
|
Segment2D(Point2D(1, 0), Point2D(1, 1))
|
|
>>> s = Segment(Point(4, 3), Point(1, 1)); s
|
|
Segment2D(Point2D(4, 3), Point2D(1, 1))
|
|
>>> s.points
|
|
(Point2D(4, 3), Point2D(1, 1))
|
|
>>> s.slope
|
|
2/3
|
|
>>> s.length
|
|
sqrt(13)
|
|
>>> s.midpoint
|
|
Point2D(5/2, 2)
|
|
|
|
"""
|
|
def __new__(cls, p1, p2, **kwargs):
|
|
p1 = Point(p1, dim=2)
|
|
p2 = Point(p2, dim=2)
|
|
|
|
if p1 == p2:
|
|
return p1
|
|
|
|
return LinearEntity2D.__new__(cls, p1, p2, **kwargs)
|
|
|
|
def _svg(self, scale_factor=1., fill_color="#66cc99"):
|
|
"""Returns SVG path element for the LinearEntity.
|
|
|
|
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".
|
|
"""
|
|
verts = (N(self.p1), N(self.p2))
|
|
coords = ["{},{}".format(p.x, p.y) for p in verts]
|
|
path = "M {} L {}".format(coords[0], " L ".join(coords[1:]))
|
|
return (
|
|
'<path fill-rule="evenodd" fill="{2}" stroke="#555555" '
|
|
'stroke-width="{0}" opacity="0.6" d="{1}" />'
|
|
).format(2.*scale_factor, path, fill_color)
|
|
|
|
|
|
class LinearEntity3D(LinearEntity):
|
|
"""An base class for all linear entities (line, ray and segment)
|
|
in a 3-dimensional Euclidean space.
|
|
|
|
Attributes
|
|
==========
|
|
|
|
p1
|
|
p2
|
|
direction_ratio
|
|
direction_cosine
|
|
points
|
|
|
|
Notes
|
|
=====
|
|
|
|
This is a base class and is not meant to be instantiated.
|
|
"""
|
|
def __new__(cls, p1, p2, **kwargs):
|
|
p1 = Point3D(p1, dim=3)
|
|
p2 = Point3D(p2, dim=3)
|
|
if p1 == p2:
|
|
# if it makes sense to return a Point, handle in subclass
|
|
raise ValueError(
|
|
"%s.__new__ requires two unique Points." % cls.__name__)
|
|
|
|
return GeometryEntity.__new__(cls, p1, p2, **kwargs)
|
|
|
|
ambient_dimension = 3
|
|
|
|
@property
|
|
def direction_ratio(self):
|
|
"""The direction ratio of a given line in 3D.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.line.Line3D.equation
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Line3D
|
|
>>> p1, p2 = Point3D(0, 0, 0), Point3D(5, 3, 1)
|
|
>>> l = Line3D(p1, p2)
|
|
>>> l.direction_ratio
|
|
[5, 3, 1]
|
|
"""
|
|
p1, p2 = self.points
|
|
return p1.direction_ratio(p2)
|
|
|
|
@property
|
|
def direction_cosine(self):
|
|
"""The normalized direction ratio of a given line in 3D.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.line.Line3D.equation
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Line3D
|
|
>>> p1, p2 = Point3D(0, 0, 0), Point3D(5, 3, 1)
|
|
>>> l = Line3D(p1, p2)
|
|
>>> l.direction_cosine
|
|
[sqrt(35)/7, 3*sqrt(35)/35, sqrt(35)/35]
|
|
>>> sum(i**2 for i in _)
|
|
1
|
|
"""
|
|
p1, p2 = self.points
|
|
return p1.direction_cosine(p2)
|
|
|
|
|
|
class Line3D(LinearEntity3D, Line):
|
|
"""An infinite 3D line in space.
|
|
|
|
A line is declared with two distinct points or a point and direction_ratio
|
|
as defined using keyword `direction_ratio`.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p1 : Point3D
|
|
pt : Point3D
|
|
direction_ratio : list
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point3D
|
|
sympy.geometry.line.Line
|
|
sympy.geometry.line.Line2D
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Line3D, Point3D
|
|
>>> L = Line3D(Point3D(2, 3, 4), Point3D(3, 5, 1))
|
|
>>> L
|
|
Line3D(Point3D(2, 3, 4), Point3D(3, 5, 1))
|
|
>>> L.points
|
|
(Point3D(2, 3, 4), Point3D(3, 5, 1))
|
|
"""
|
|
def __new__(cls, p1, pt=None, direction_ratio=(), **kwargs):
|
|
if isinstance(p1, LinearEntity3D):
|
|
if pt is not None:
|
|
raise ValueError('if p1 is a LinearEntity, pt must be None.')
|
|
p1, pt = p1.args
|
|
else:
|
|
p1 = Point(p1, dim=3)
|
|
if pt is not None and len(direction_ratio) == 0:
|
|
pt = Point(pt, dim=3)
|
|
elif len(direction_ratio) == 3 and pt is None:
|
|
pt = Point3D(p1.x + direction_ratio[0], p1.y + direction_ratio[1],
|
|
p1.z + direction_ratio[2])
|
|
else:
|
|
raise ValueError('A 2nd Point or keyword "direction_ratio" must '
|
|
'be used.')
|
|
|
|
return LinearEntity3D.__new__(cls, p1, pt, **kwargs)
|
|
|
|
def equation(self, x='x', y='y', z='z'):
|
|
"""Return the equations that define the line in 3D.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
x : str, optional
|
|
The name to use for the x-axis, default value is 'x'.
|
|
y : str, optional
|
|
The name to use for the y-axis, default value is 'y'.
|
|
z : str, optional
|
|
The name to use for the z-axis, default value is 'z'.
|
|
|
|
Returns
|
|
=======
|
|
|
|
equation : Tuple of simultaneous equations
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Line3D, solve
|
|
>>> from sympy.abc import x, y, z
|
|
>>> p1, p2 = Point3D(1, 0, 0), Point3D(5, 3, 0)
|
|
>>> l1 = Line3D(p1, p2)
|
|
>>> eq = l1.equation(x, y, z); eq
|
|
(-3*x + 4*y + 3, z)
|
|
>>> solve(eq.subs(z, 0), (x, y, z))
|
|
{x: 4*y/3 + 1}
|
|
"""
|
|
x, y, z, k = [_symbol(i, real=True) for i in (x, y, z, 'k')]
|
|
p1, p2 = self.points
|
|
d1, d2, d3 = p1.direction_ratio(p2)
|
|
x1, y1, z1 = p1
|
|
eqs = [-d1*k + x - x1, -d2*k + y - y1, -d3*k + z - z1]
|
|
# eliminate k from equations by solving first eq with k for k
|
|
for i, e in enumerate(eqs):
|
|
if e.has(k):
|
|
kk = solve(eqs[i], k)[0]
|
|
eqs.pop(i)
|
|
break
|
|
return Tuple(*[i.subs(k, kk).as_numer_denom()[0] for i in eqs])
|
|
|
|
|
|
class Ray3D(LinearEntity3D, Ray):
|
|
"""
|
|
A Ray is a semi-line in the space with a source point and a direction.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p1 : Point3D
|
|
The source of the Ray
|
|
p2 : Point or a direction vector
|
|
direction_ratio: Determines the direction in which the Ray propagates.
|
|
|
|
|
|
Attributes
|
|
==========
|
|
|
|
source
|
|
xdirection
|
|
ydirection
|
|
zdirection
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point3D, Line3D
|
|
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Ray3D
|
|
>>> r = Ray3D(Point3D(2, 3, 4), Point3D(3, 5, 0))
|
|
>>> r
|
|
Ray3D(Point3D(2, 3, 4), Point3D(3, 5, 0))
|
|
>>> r.points
|
|
(Point3D(2, 3, 4), Point3D(3, 5, 0))
|
|
>>> r.source
|
|
Point3D(2, 3, 4)
|
|
>>> r.xdirection
|
|
oo
|
|
>>> r.ydirection
|
|
oo
|
|
>>> r.direction_ratio
|
|
[1, 2, -4]
|
|
|
|
"""
|
|
def __new__(cls, p1, pt=None, direction_ratio=(), **kwargs):
|
|
if isinstance(p1, LinearEntity3D):
|
|
if pt is not None:
|
|
raise ValueError('If p1 is a LinearEntity, pt must be None')
|
|
p1, pt = p1.args
|
|
else:
|
|
p1 = Point(p1, dim=3)
|
|
if pt is not None and len(direction_ratio) == 0:
|
|
pt = Point(pt, dim=3)
|
|
elif len(direction_ratio) == 3 and pt is None:
|
|
pt = Point3D(p1.x + direction_ratio[0], p1.y + direction_ratio[1],
|
|
p1.z + direction_ratio[2])
|
|
else:
|
|
raise ValueError(filldedent('''
|
|
A 2nd Point or keyword "direction_ratio" must be used.
|
|
'''))
|
|
|
|
return LinearEntity3D.__new__(cls, p1, pt, **kwargs)
|
|
|
|
@property
|
|
def xdirection(self):
|
|
"""The x direction of the ray.
|
|
|
|
Positive infinity if the ray points in the positive x direction,
|
|
negative infinity if the ray points in the negative x direction,
|
|
or 0 if the ray is vertical.
|
|
|
|
See Also
|
|
========
|
|
|
|
ydirection
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Ray3D
|
|
>>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(1, 1, 1), Point3D(0, -1, 0)
|
|
>>> r1, r2 = Ray3D(p1, p2), Ray3D(p1, p3)
|
|
>>> r1.xdirection
|
|
oo
|
|
>>> r2.xdirection
|
|
0
|
|
|
|
"""
|
|
if self.p1.x < self.p2.x:
|
|
return S.Infinity
|
|
elif self.p1.x == self.p2.x:
|
|
return S.Zero
|
|
else:
|
|
return S.NegativeInfinity
|
|
|
|
@property
|
|
def ydirection(self):
|
|
"""The y direction of the ray.
|
|
|
|
Positive infinity if the ray points in the positive y direction,
|
|
negative infinity if the ray points in the negative y direction,
|
|
or 0 if the ray is horizontal.
|
|
|
|
See Also
|
|
========
|
|
|
|
xdirection
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Ray3D
|
|
>>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(-1, -1, -1), Point3D(-1, 0, 0)
|
|
>>> r1, r2 = Ray3D(p1, p2), Ray3D(p1, p3)
|
|
>>> r1.ydirection
|
|
-oo
|
|
>>> r2.ydirection
|
|
0
|
|
|
|
"""
|
|
if self.p1.y < self.p2.y:
|
|
return S.Infinity
|
|
elif self.p1.y == self.p2.y:
|
|
return S.Zero
|
|
else:
|
|
return S.NegativeInfinity
|
|
|
|
@property
|
|
def zdirection(self):
|
|
"""The z direction of the ray.
|
|
|
|
Positive infinity if the ray points in the positive z direction,
|
|
negative infinity if the ray points in the negative z direction,
|
|
or 0 if the ray is horizontal.
|
|
|
|
See Also
|
|
========
|
|
|
|
xdirection
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Ray3D
|
|
>>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(-1, -1, -1), Point3D(-1, 0, 0)
|
|
>>> r1, r2 = Ray3D(p1, p2), Ray3D(p1, p3)
|
|
>>> r1.ydirection
|
|
-oo
|
|
>>> r2.ydirection
|
|
0
|
|
>>> r2.zdirection
|
|
0
|
|
|
|
"""
|
|
if self.p1.z < self.p2.z:
|
|
return S.Infinity
|
|
elif self.p1.z == self.p2.z:
|
|
return S.Zero
|
|
else:
|
|
return S.NegativeInfinity
|
|
|
|
|
|
class Segment3D(LinearEntity3D, Segment):
|
|
"""A line segment in a 3D space.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
p1 : Point3D
|
|
p2 : Point3D
|
|
|
|
Attributes
|
|
==========
|
|
|
|
length : number or SymPy expression
|
|
midpoint : Point3D
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point3D, Line3D
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Segment3D
|
|
>>> Segment3D((1, 0, 0), (1, 1, 1)) # tuples are interpreted as pts
|
|
Segment3D(Point3D(1, 0, 0), Point3D(1, 1, 1))
|
|
>>> s = Segment3D(Point3D(4, 3, 9), Point3D(1, 1, 7)); s
|
|
Segment3D(Point3D(4, 3, 9), Point3D(1, 1, 7))
|
|
>>> s.points
|
|
(Point3D(4, 3, 9), Point3D(1, 1, 7))
|
|
>>> s.length
|
|
sqrt(17)
|
|
>>> s.midpoint
|
|
Point3D(5/2, 2, 8)
|
|
|
|
"""
|
|
def __new__(cls, p1, p2, **kwargs):
|
|
p1 = Point(p1, dim=3)
|
|
p2 = Point(p2, dim=3)
|
|
|
|
if p1 == p2:
|
|
return p1
|
|
|
|
return LinearEntity3D.__new__(cls, p1, p2, **kwargs)
|