123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634 |
- """The definition of the base geometrical entity with attributes common to
- all derived geometrical entities.
- Contains
- ========
- GeometryEntity
- GeometricSet
- Notes
- =====
- A GeometryEntity is any object that has special geometric properties.
- A GeometrySet is a superclass of any GeometryEntity that can also
- be viewed as a sympy.sets.Set. In particular, points are the only
- GeometryEntity not considered a Set.
- Rn is a GeometrySet representing n-dimensional Euclidean space. R2 and
- R3 are currently the only ambient spaces implemented.
- """
- from sympy.core.basic import Basic
- from sympy.core.containers import Tuple
- from sympy.core.evalf import EvalfMixin, N
- from sympy.core.numbers import oo
- from sympy.core.symbol import Dummy
- from sympy.core.sympify import sympify
- from sympy.functions.elementary.trigonometric import cos, sin, atan
- from sympy.matrices import eye
- from sympy.multipledispatch import dispatch
- from sympy.sets import Set, Union, FiniteSet
- from sympy.sets.handlers.intersection import intersection_sets
- from sympy.sets.handlers.union import union_sets
- from sympy.utilities.misc import func_name
- from sympy.utilities.iterables import is_sequence
- # How entities are ordered; used by __cmp__ in GeometryEntity
- ordering_of_classes = [
- "Point2D",
- "Point3D",
- "Point",
- "Segment2D",
- "Ray2D",
- "Line2D",
- "Segment3D",
- "Line3D",
- "Ray3D",
- "Segment",
- "Ray",
- "Line",
- "Plane",
- "Triangle",
- "RegularPolygon",
- "Polygon",
- "Circle",
- "Ellipse",
- "Curve",
- "Parabola"
- ]
- class GeometryEntity(Basic, EvalfMixin):
- """The base class for all geometrical entities.
- This class doesn't represent any particular geometric entity, it only
- provides the implementation of some methods common to all subclasses.
- """
- def __cmp__(self, other):
- """Comparison of two GeometryEntities."""
- n1 = self.__class__.__name__
- n2 = other.__class__.__name__
- c = (n1 > n2) - (n1 < n2)
- if not c:
- return 0
- i1 = -1
- for cls in self.__class__.__mro__:
- try:
- i1 = ordering_of_classes.index(cls.__name__)
- break
- except ValueError:
- i1 = -1
- if i1 == -1:
- return c
- i2 = -1
- for cls in other.__class__.__mro__:
- try:
- i2 = ordering_of_classes.index(cls.__name__)
- break
- except ValueError:
- i2 = -1
- if i2 == -1:
- return c
- return (i1 > i2) - (i1 < i2)
- def __contains__(self, other):
- """Subclasses should implement this method for anything more complex than equality."""
- if type(self) is type(other):
- return self == other
- raise NotImplementedError()
- def __getnewargs__(self):
- """Returns a tuple that will be passed to __new__ on unpickling."""
- return tuple(self.args)
- def __ne__(self, o):
- """Test inequality of two geometrical entities."""
- return not self == o
- def __new__(cls, *args, **kwargs):
- # Points are sequences, but they should not
- # be converted to Tuples, so use this detection function instead.
- def is_seq_and_not_point(a):
- # we cannot use isinstance(a, Point) since we cannot import Point
- if hasattr(a, 'is_Point') and a.is_Point:
- return False
- return is_sequence(a)
- args = [Tuple(*a) if is_seq_and_not_point(a) else sympify(a) for a in args]
- return Basic.__new__(cls, *args)
- def __radd__(self, a):
- """Implementation of reverse add method."""
- return a.__add__(self)
- def __rtruediv__(self, a):
- """Implementation of reverse division method."""
- return a.__truediv__(self)
- def __repr__(self):
- """String representation of a GeometryEntity that can be evaluated
- by sympy."""
- return type(self).__name__ + repr(self.args)
- def __rmul__(self, a):
- """Implementation of reverse multiplication method."""
- return a.__mul__(self)
- def __rsub__(self, a):
- """Implementation of reverse subtraction method."""
- return a.__sub__(self)
- def __str__(self):
- """String representation of a GeometryEntity."""
- from sympy.printing import sstr
- return type(self).__name__ + sstr(self.args)
- def _eval_subs(self, old, new):
- from sympy.geometry.point import Point, Point3D
- if is_sequence(old) or is_sequence(new):
- if isinstance(self, Point3D):
- old = Point3D(old)
- new = Point3D(new)
- else:
- old = Point(old)
- new = Point(new)
- return self._subs(old, new)
- def _repr_svg_(self):
- """SVG representation of a GeometryEntity suitable for IPython"""
- try:
- bounds = self.bounds
- except (NotImplementedError, TypeError):
- # if we have no SVG representation, return None so IPython
- # will fall back to the next representation
- return None
- if not all(x.is_number and x.is_finite for x in bounds):
- return None
- svg_top = '''<svg xmlns="http://www.w3.org/2000/svg"
- xmlns:xlink="http://www.w3.org/1999/xlink"
- width="{1}" height="{2}" viewBox="{0}"
- preserveAspectRatio="xMinYMin meet">
- <defs>
- <marker id="markerCircle" markerWidth="8" markerHeight="8"
- refx="5" refy="5" markerUnits="strokeWidth">
- <circle cx="5" cy="5" r="1.5" style="stroke: none; fill:#000000;"/>
- </marker>
- <marker id="markerArrow" markerWidth="13" markerHeight="13" refx="2" refy="4"
- orient="auto" markerUnits="strokeWidth">
- <path d="M2,2 L2,6 L6,4" style="fill: #000000;" />
- </marker>
- <marker id="markerReverseArrow" markerWidth="13" markerHeight="13" refx="6" refy="4"
- orient="auto" markerUnits="strokeWidth">
- <path d="M6,2 L6,6 L2,4" style="fill: #000000;" />
- </marker>
- </defs>'''
- # Establish SVG canvas that will fit all the data + small space
- xmin, ymin, xmax, ymax = map(N, bounds)
- if xmin == xmax and ymin == ymax:
- # This is a point; buffer using an arbitrary size
- xmin, ymin, xmax, ymax = xmin - .5, ymin -.5, xmax + .5, ymax + .5
- else:
- # Expand bounds by a fraction of the data ranges
- expand = 0.1 # or 10%; this keeps arrowheads in view (R plots use 4%)
- widest_part = max([xmax - xmin, ymax - ymin])
- expand_amount = widest_part * expand
- xmin -= expand_amount
- ymin -= expand_amount
- xmax += expand_amount
- ymax += expand_amount
- dx = xmax - xmin
- dy = ymax - ymin
- width = min([max([100., dx]), 300])
- height = min([max([100., dy]), 300])
- scale_factor = 1. if max(width, height) == 0 else max(dx, dy) / max(width, height)
- try:
- svg = self._svg(scale_factor)
- except (NotImplementedError, TypeError):
- # if we have no SVG representation, return None so IPython
- # will fall back to the next representation
- return None
- view_box = "{} {} {} {}".format(xmin, ymin, dx, dy)
- transform = "matrix(1,0,0,-1,0,{})".format(ymax + ymin)
- svg_top = svg_top.format(view_box, width, height)
- return svg_top + (
- '<g transform="{}">{}</g></svg>'
- ).format(transform, svg)
- def _svg(self, scale_factor=1., fill_color="#66cc99"):
- """Returns SVG path element for the GeometryEntity.
- 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".
- """
- raise NotImplementedError()
- def _sympy_(self):
- return self
- @property
- def ambient_dimension(self):
- """What is the dimension of the space that the object is contained in?"""
- raise NotImplementedError()
- @property
- def bounds(self):
- """Return a tuple (xmin, ymin, xmax, ymax) representing the bounding
- rectangle for the geometric figure.
- """
- raise NotImplementedError()
- def encloses(self, o):
- """
- Return True if o is inside (not on or outside) the boundaries of self.
- The object will be decomposed into Points and individual Entities need
- only define an encloses_point method for their class.
- See Also
- ========
- sympy.geometry.ellipse.Ellipse.encloses_point
- sympy.geometry.polygon.Polygon.encloses_point
- Examples
- ========
- >>> from sympy import RegularPolygon, Point, Polygon
- >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
- >>> t2 = Polygon(*RegularPolygon(Point(0, 0), 2, 3).vertices)
- >>> t2.encloses(t)
- True
- >>> t.encloses(t2)
- False
- """
- from sympy.geometry.point import Point
- from sympy.geometry.line import Segment, Ray, Line
- from sympy.geometry.ellipse import Ellipse
- from sympy.geometry.polygon import Polygon, RegularPolygon
- if isinstance(o, Point):
- return self.encloses_point(o)
- elif isinstance(o, Segment):
- return all(self.encloses_point(x) for x in o.points)
- elif isinstance(o, (Ray, Line)):
- return False
- elif isinstance(o, Ellipse):
- return self.encloses_point(o.center) and \
- self.encloses_point(
- Point(o.center.x + o.hradius, o.center.y)) and \
- not self.intersection(o)
- elif isinstance(o, Polygon):
- if isinstance(o, RegularPolygon):
- if not self.encloses_point(o.center):
- return False
- return all(self.encloses_point(v) for v in o.vertices)
- raise NotImplementedError()
- def equals(self, o):
- return self == o
- def intersection(self, o):
- """
- Returns a list of all of the intersections of self with o.
- Notes
- =====
- An entity is not required to implement this method.
- If two different types of entities can intersect, the item with
- higher index in ordering_of_classes should implement
- intersections with anything having a lower index.
- See Also
- ========
- sympy.geometry.util.intersection
- """
- raise NotImplementedError()
- def is_similar(self, other):
- """Is this geometrical entity similar to another geometrical entity?
- Two entities are similar if a uniform scaling (enlarging or
- shrinking) of one of the entities will allow one to obtain the other.
- Notes
- =====
- This method is not intended to be used directly but rather
- through the `are_similar` function found in util.py.
- An entity is not required to implement this method.
- If two different types of entities can be similar, it is only
- required that one of them be able to determine this.
- See Also
- ========
- scale
- """
- raise NotImplementedError()
- def reflect(self, line):
- """
- Reflects an object across a line.
- Parameters
- ==========
- line: Line
- Examples
- ========
- >>> from sympy import pi, sqrt, Line, RegularPolygon
- >>> l = Line((0, pi), slope=sqrt(2))
- >>> pent = RegularPolygon((1, 2), 1, 5)
- >>> rpent = pent.reflect(l)
- >>> rpent
- RegularPolygon(Point2D(-2*sqrt(2)*pi/3 - 1/3 + 4*sqrt(2)/3, 2/3 + 2*sqrt(2)/3 + 2*pi/3), -1, 5, -atan(2*sqrt(2)) + 3*pi/5)
- >>> from sympy import pi, Line, Circle, Point
- >>> l = Line((0, pi), slope=1)
- >>> circ = Circle(Point(0, 0), 5)
- >>> rcirc = circ.reflect(l)
- >>> rcirc
- Circle(Point2D(-pi, pi), -5)
- """
- from sympy.geometry.point import Point
- g = self
- l = line
- o = Point(0, 0)
- if l.slope.is_zero:
- y = l.args[0].y
- if not y: # x-axis
- return g.scale(y=-1)
- reps = [(p, p.translate(y=2*(y - p.y))) for p in g.atoms(Point)]
- elif l.slope is oo:
- x = l.args[0].x
- if not x: # y-axis
- return g.scale(x=-1)
- reps = [(p, p.translate(x=2*(x - p.x))) for p in g.atoms(Point)]
- else:
- if not hasattr(g, 'reflect') and not all(
- isinstance(arg, Point) for arg in g.args):
- raise NotImplementedError(
- 'reflect undefined or non-Point args in %s' % g)
- a = atan(l.slope)
- c = l.coefficients
- d = -c[-1]/c[1] # y-intercept
- # apply the transform to a single point
- x, y = Dummy(), Dummy()
- xf = Point(x, y)
- xf = xf.translate(y=-d).rotate(-a, o).scale(y=-1
- ).rotate(a, o).translate(y=d)
- # replace every point using that transform
- reps = [(p, xf.xreplace({x: p.x, y: p.y})) for p in g.atoms(Point)]
- return g.xreplace(dict(reps))
- def rotate(self, angle, pt=None):
- """Rotate ``angle`` radians counterclockwise about Point ``pt``.
- The default pt is the origin, Point(0, 0)
- See Also
- ========
- scale, translate
- Examples
- ========
- >>> from sympy import Point, RegularPolygon, Polygon, pi
- >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
- >>> t # vertex on x axis
- Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2))
- >>> t.rotate(pi/2) # vertex on y axis now
- Triangle(Point2D(0, 1), Point2D(-sqrt(3)/2, -1/2), Point2D(sqrt(3)/2, -1/2))
- """
- newargs = []
- for a in self.args:
- if isinstance(a, GeometryEntity):
- newargs.append(a.rotate(angle, pt))
- else:
- newargs.append(a)
- return type(self)(*newargs)
- def scale(self, x=1, y=1, pt=None):
- """Scale the object by multiplying the x,y-coordinates by x and y.
- If pt is given, the scaling is done relative to that point; the
- object is shifted by -pt, scaled, and shifted by pt.
- See Also
- ========
- rotate, translate
- Examples
- ========
- >>> from sympy import RegularPolygon, Point, Polygon
- >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
- >>> t
- Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2))
- >>> t.scale(2)
- Triangle(Point2D(2, 0), Point2D(-1, sqrt(3)/2), Point2D(-1, -sqrt(3)/2))
- >>> t.scale(2, 2)
- Triangle(Point2D(2, 0), Point2D(-1, sqrt(3)), Point2D(-1, -sqrt(3)))
- """
- from sympy.geometry.point import Point
- if pt:
- pt = Point(pt, dim=2)
- return self.translate(*(-pt).args).scale(x, y).translate(*pt.args)
- return type(self)(*[a.scale(x, y) for a in self.args]) # if this fails, override this class
- def translate(self, x=0, y=0):
- """Shift the object by adding to the x,y-coordinates the values x and y.
- See Also
- ========
- rotate, scale
- Examples
- ========
- >>> from sympy import RegularPolygon, Point, Polygon
- >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
- >>> t
- Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2))
- >>> t.translate(2)
- Triangle(Point2D(3, 0), Point2D(3/2, sqrt(3)/2), Point2D(3/2, -sqrt(3)/2))
- >>> t.translate(2, 2)
- Triangle(Point2D(3, 2), Point2D(3/2, sqrt(3)/2 + 2), Point2D(3/2, 2 - sqrt(3)/2))
- """
- newargs = []
- for a in self.args:
- if isinstance(a, GeometryEntity):
- newargs.append(a.translate(x, y))
- else:
- newargs.append(a)
- return self.func(*newargs)
- def parameter_value(self, other, t):
- """Return the parameter corresponding to the given point.
- Evaluating an arbitrary point of the entity at this parameter
- value will return the given point.
- Examples
- ========
- >>> from sympy import Line, Point
- >>> from sympy.abc import t
- >>> a = Point(0, 0)
- >>> b = Point(2, 2)
- >>> Line(a, b).parameter_value((1, 1), t)
- {t: 1/2}
- >>> Line(a, b).arbitrary_point(t).subs(_)
- Point2D(1, 1)
- """
- from sympy.geometry.point import Point
- from sympy.solvers.solvers import solve
- if not isinstance(other, GeometryEntity):
- other = Point(other, dim=self.ambient_dimension)
- if not isinstance(other, Point):
- raise ValueError("other must be a point")
- T = Dummy('t', real=True)
- sol = solve(self.arbitrary_point(T) - other, T, dict=True)
- if not sol:
- raise ValueError("Given point is not on %s" % func_name(self))
- return {t: sol[0][T]}
- class GeometrySet(GeometryEntity, Set):
- """Parent class of all GeometryEntity that are also Sets
- (compatible with sympy.sets)
- """
- def _contains(self, other):
- """sympy.sets uses the _contains method, so include it for compatibility."""
- if isinstance(other, Set) and other.is_FiniteSet:
- return all(self.__contains__(i) for i in other)
- return self.__contains__(other)
- @dispatch(GeometrySet, Set) # type:ignore # noqa:F811
- def union_sets(self, o): # noqa:F811
- """ Returns the union of self and o
- for use with sympy.sets.Set, if possible. """
- # if its a FiniteSet, merge any points
- # we contain and return a union with the rest
- if o.is_FiniteSet:
- other_points = [p for p in o if not self._contains(p)]
- if len(other_points) == len(o):
- return None
- return Union(self, FiniteSet(*other_points))
- if self._contains(o):
- return self
- return None
- @dispatch(GeometrySet, Set) # type: ignore # noqa:F811
- def intersection_sets(self, o): # noqa:F811
- """ Returns a sympy.sets.Set of intersection objects,
- if possible. """
- from sympy.geometry import Point
- try:
- # if o is a FiniteSet, find the intersection directly
- # to avoid infinite recursion
- if o.is_FiniteSet:
- inter = FiniteSet(*(p for p in o if self.contains(p)))
- else:
- inter = self.intersection(o)
- except NotImplementedError:
- # sympy.sets.Set.reduce expects None if an object
- # doesn't know how to simplify
- return None
- # put the points in a FiniteSet
- points = FiniteSet(*[p for p in inter if isinstance(p, Point)])
- non_points = [p for p in inter if not isinstance(p, Point)]
- return Union(*(non_points + [points]))
- def translate(x, y):
- """Return the matrix to translate a 2-D point by x and y."""
- rv = eye(3)
- rv[2, 0] = x
- rv[2, 1] = y
- return rv
- def scale(x, y, pt=None):
- """Return the matrix to multiply a 2-D point's coordinates by x and y.
- If pt is given, the scaling is done relative to that point."""
- rv = eye(3)
- rv[0, 0] = x
- rv[1, 1] = y
- if pt:
- from sympy.geometry.point import Point
- pt = Point(pt, dim=2)
- tr1 = translate(*(-pt).args)
- tr2 = translate(*pt.args)
- return tr1*rv*tr2
- return rv
- def rotate(th):
- """Return the matrix to rotate a 2-D point about the origin by ``angle``.
- The angle is measured in radians. To Point a point about a point other
- then the origin, translate the Point, do the rotation, and
- translate it back:
- >>> from sympy.geometry.entity import rotate, translate
- >>> from sympy import Point, pi
- >>> rot_about_11 = translate(-1, -1)*rotate(pi/2)*translate(1, 1)
- >>> Point(1, 1).transform(rot_about_11)
- Point2D(1, 1)
- >>> Point(0, 0).transform(rot_about_11)
- Point2D(2, 0)
- """
- s = sin(th)
- rv = eye(3)*cos(th)
- rv[0, 1] = s
- rv[1, 0] = -s
- rv[2, 2] = 1
- return rv
|