Spaces:
Sleeping
Sleeping
"""Utility functions for geometrical entities. | |
Contains | |
======== | |
intersection | |
convex_hull | |
closest_points | |
farthest_points | |
are_coplanar | |
are_similar | |
""" | |
from collections import deque | |
from math import sqrt as _sqrt | |
from sympy import nsimplify | |
from .entity import GeometryEntity | |
from .exceptions import GeometryError | |
from .point import Point, Point2D, Point3D | |
from sympy.core.containers import OrderedSet | |
from sympy.core.exprtools import factor_terms | |
from sympy.core.function import Function, expand_mul | |
from sympy.core.numbers import Float | |
from sympy.core.sorting import ordered | |
from sympy.core.symbol import Symbol | |
from sympy.core.singleton import S | |
from sympy.polys.polytools import cancel | |
from sympy.functions.elementary.miscellaneous import sqrt | |
from sympy.utilities.iterables import is_sequence | |
from mpmath.libmp.libmpf import prec_to_dps | |
def find(x, equation): | |
""" | |
Checks whether a Symbol matching ``x`` is present in ``equation`` | |
or not. If present, the matching symbol is returned, else a | |
ValueError is raised. If ``x`` is a string the matching symbol | |
will have the same name; if ``x`` is a Symbol then it will be | |
returned if found. | |
Examples | |
======== | |
>>> from sympy.geometry.util import find | |
>>> from sympy import Dummy | |
>>> from sympy.abc import x | |
>>> find('x', x) | |
x | |
>>> find('x', Dummy('x')) | |
_x | |
The dummy symbol is returned since it has a matching name: | |
>>> _.name == 'x' | |
True | |
>>> find(x, Dummy('x')) | |
Traceback (most recent call last): | |
... | |
ValueError: could not find x | |
""" | |
free = equation.free_symbols | |
xs = [i for i in free if (i.name if isinstance(x, str) else i) == x] | |
if not xs: | |
raise ValueError('could not find %s' % x) | |
if len(xs) != 1: | |
raise ValueError('ambiguous %s' % x) | |
return xs[0] | |
def _ordered_points(p): | |
"""Return the tuple of points sorted numerically according to args""" | |
return tuple(sorted(p, key=lambda x: x.args)) | |
def are_coplanar(*e): | |
""" Returns True if the given entities are coplanar otherwise False | |
Parameters | |
========== | |
e: entities to be checked for being coplanar | |
Returns | |
======= | |
Boolean | |
Examples | |
======== | |
>>> from sympy import Point3D, Line3D | |
>>> from sympy.geometry.util import are_coplanar | |
>>> a = Line3D(Point3D(5, 0, 0), Point3D(1, -1, 1)) | |
>>> b = Line3D(Point3D(0, -2, 0), Point3D(3, 1, 1)) | |
>>> c = Line3D(Point3D(0, -1, 0), Point3D(5, -1, 9)) | |
>>> are_coplanar(a, b, c) | |
False | |
""" | |
from .line import LinearEntity3D | |
from .plane import Plane | |
# XXX update tests for coverage | |
e = set(e) | |
# first work with a Plane if present | |
for i in list(e): | |
if isinstance(i, Plane): | |
e.remove(i) | |
return all(p.is_coplanar(i) for p in e) | |
if all(isinstance(i, Point3D) for i in e): | |
if len(e) < 3: | |
return False | |
# remove pts that are collinear with 2 pts | |
a, b = e.pop(), e.pop() | |
for i in list(e): | |
if Point3D.are_collinear(a, b, i): | |
e.remove(i) | |
if not e: | |
return False | |
else: | |
# define a plane | |
p = Plane(a, b, e.pop()) | |
for i in e: | |
if i not in p: | |
return False | |
return True | |
else: | |
pt3d = [] | |
for i in e: | |
if isinstance(i, Point3D): | |
pt3d.append(i) | |
elif isinstance(i, LinearEntity3D): | |
pt3d.extend(i.args) | |
elif isinstance(i, GeometryEntity): # XXX we should have a GeometryEntity3D class so we can tell the difference between 2D and 3D -- here we just want to deal with 2D objects; if new 3D objects are encountered that we didn't handle above, an error should be raised | |
# all 2D objects have some Point that defines them; so convert those points to 3D pts by making z=0 | |
for p in i.args: | |
if isinstance(p, Point): | |
pt3d.append(Point3D(*(p.args + (0,)))) | |
return are_coplanar(*pt3d) | |
def are_similar(e1, e2): | |
"""Are two geometrical entities similar. | |
Can one geometrical entity be uniformly scaled to the other? | |
Parameters | |
========== | |
e1 : GeometryEntity | |
e2 : GeometryEntity | |
Returns | |
======= | |
are_similar : boolean | |
Raises | |
====== | |
GeometryError | |
When `e1` and `e2` cannot be compared. | |
Notes | |
===== | |
If the two objects are equal then they are similar. | |
See Also | |
======== | |
sympy.geometry.entity.GeometryEntity.is_similar | |
Examples | |
======== | |
>>> from sympy import Point, Circle, Triangle, are_similar | |
>>> c1, c2 = Circle(Point(0, 0), 4), Circle(Point(1, 4), 3) | |
>>> t1 = Triangle(Point(0, 0), Point(1, 0), Point(0, 1)) | |
>>> t2 = Triangle(Point(0, 0), Point(2, 0), Point(0, 2)) | |
>>> t3 = Triangle(Point(0, 0), Point(3, 0), Point(0, 1)) | |
>>> are_similar(t1, t2) | |
True | |
>>> are_similar(t1, t3) | |
False | |
""" | |
if e1 == e2: | |
return True | |
is_similar1 = getattr(e1, 'is_similar', None) | |
if is_similar1: | |
return is_similar1(e2) | |
is_similar2 = getattr(e2, 'is_similar', None) | |
if is_similar2: | |
return is_similar2(e1) | |
n1 = e1.__class__.__name__ | |
n2 = e2.__class__.__name__ | |
raise GeometryError( | |
"Cannot test similarity between %s and %s" % (n1, n2)) | |
def centroid(*args): | |
"""Find the centroid (center of mass) of the collection containing only Points, | |
Segments or Polygons. The centroid is the weighted average of the individual centroid | |
where the weights are the lengths (of segments) or areas (of polygons). | |
Overlapping regions will add to the weight of that region. | |
If there are no objects (or a mixture of objects) then None is returned. | |
See Also | |
======== | |
sympy.geometry.point.Point, sympy.geometry.line.Segment, | |
sympy.geometry.polygon.Polygon | |
Examples | |
======== | |
>>> from sympy import Point, Segment, Polygon | |
>>> from sympy.geometry.util import centroid | |
>>> p = Polygon((0, 0), (10, 0), (10, 10)) | |
>>> q = p.translate(0, 20) | |
>>> p.centroid, q.centroid | |
(Point2D(20/3, 10/3), Point2D(20/3, 70/3)) | |
>>> centroid(p, q) | |
Point2D(20/3, 40/3) | |
>>> p, q = Segment((0, 0), (2, 0)), Segment((0, 0), (2, 2)) | |
>>> centroid(p, q) | |
Point2D(1, 2 - sqrt(2)) | |
>>> centroid(Point(0, 0), Point(2, 0)) | |
Point2D(1, 0) | |
Stacking 3 polygons on top of each other effectively triples the | |
weight of that polygon: | |
>>> p = Polygon((0, 0), (1, 0), (1, 1), (0, 1)) | |
>>> q = Polygon((1, 0), (3, 0), (3, 1), (1, 1)) | |
>>> centroid(p, q) | |
Point2D(3/2, 1/2) | |
>>> centroid(p, p, p, q) # centroid x-coord shifts left | |
Point2D(11/10, 1/2) | |
Stacking the squares vertically above and below p has the same | |
effect: | |
>>> centroid(p, p.translate(0, 1), p.translate(0, -1), q) | |
Point2D(11/10, 1/2) | |
""" | |
from .line import Segment | |
from .polygon import Polygon | |
if args: | |
if all(isinstance(g, Point) for g in args): | |
c = Point(0, 0) | |
for g in args: | |
c += g | |
den = len(args) | |
elif all(isinstance(g, Segment) for g in args): | |
c = Point(0, 0) | |
L = 0 | |
for g in args: | |
l = g.length | |
c += g.midpoint*l | |
L += l | |
den = L | |
elif all(isinstance(g, Polygon) for g in args): | |
c = Point(0, 0) | |
A = 0 | |
for g in args: | |
a = g.area | |
c += g.centroid*a | |
A += a | |
den = A | |
c /= den | |
return c.func(*[i.simplify() for i in c.args]) | |
def closest_points(*args): | |
"""Return the subset of points from a set of points that were | |
the closest to each other in the 2D plane. | |
Parameters | |
========== | |
args | |
A collection of Points on 2D plane. | |
Notes | |
===== | |
This can only be performed on a set of points whose coordinates can | |
be ordered on the number line. If there are no ties then a single | |
pair of Points will be in the set. | |
Examples | |
======== | |
>>> from sympy import closest_points, Triangle | |
>>> Triangle(sss=(3, 4, 5)).args | |
(Point2D(0, 0), Point2D(3, 0), Point2D(3, 4)) | |
>>> closest_points(*_) | |
{(Point2D(0, 0), Point2D(3, 0))} | |
References | |
========== | |
.. [1] https://www.cs.mcgill.ca/~cs251/ClosestPair/ClosestPairPS.html | |
.. [2] Sweep line algorithm | |
https://en.wikipedia.org/wiki/Sweep_line_algorithm | |
""" | |
p = [Point2D(i) for i in set(args)] | |
if len(p) < 2: | |
raise ValueError('At least 2 distinct points must be given.') | |
try: | |
p.sort(key=lambda x: x.args) | |
except TypeError: | |
raise ValueError("The points could not be sorted.") | |
if not all(i.is_Rational for j in p for i in j.args): | |
def hypot(x, y): | |
arg = x*x + y*y | |
if arg.is_Rational: | |
return _sqrt(arg) | |
return sqrt(arg) | |
else: | |
from math import hypot | |
rv = [(0, 1)] | |
best_dist = hypot(p[1].x - p[0].x, p[1].y - p[0].y) | |
i = 2 | |
left = 0 | |
box = deque([0, 1]) | |
while i < len(p): | |
while left < i and p[i][0] - p[left][0] > best_dist: | |
box.popleft() | |
left += 1 | |
for j in box: | |
d = hypot(p[i].x - p[j].x, p[i].y - p[j].y) | |
if d < best_dist: | |
rv = [(j, i)] | |
elif d == best_dist: | |
rv.append((j, i)) | |
else: | |
continue | |
best_dist = d | |
box.append(i) | |
i += 1 | |
return {tuple([p[i] for i in pair]) for pair in rv} | |
def convex_hull(*args, polygon=True): | |
"""The convex hull surrounding the Points contained in the list of entities. | |
Parameters | |
========== | |
args : a collection of Points, Segments and/or Polygons | |
Optional parameters | |
=================== | |
polygon : Boolean. If True, returns a Polygon, if false a tuple, see below. | |
Default is True. | |
Returns | |
======= | |
convex_hull : Polygon if ``polygon`` is True else as a tuple `(U, L)` where | |
``L`` and ``U`` are the lower and upper hulls, respectively. | |
Notes | |
===== | |
This can only be performed on a set of points whose coordinates can | |
be ordered on the number line. | |
See Also | |
======== | |
sympy.geometry.point.Point, sympy.geometry.polygon.Polygon | |
Examples | |
======== | |
>>> from sympy import convex_hull | |
>>> points = [(1, 1), (1, 2), (3, 1), (-5, 2), (15, 4)] | |
>>> convex_hull(*points) | |
Polygon(Point2D(-5, 2), Point2D(1, 1), Point2D(3, 1), Point2D(15, 4)) | |
>>> convex_hull(*points, **dict(polygon=False)) | |
([Point2D(-5, 2), Point2D(15, 4)], | |
[Point2D(-5, 2), Point2D(1, 1), Point2D(3, 1), Point2D(15, 4)]) | |
References | |
========== | |
.. [1] https://en.wikipedia.org/wiki/Graham_scan | |
.. [2] Andrew's Monotone Chain Algorithm | |
(A.M. Andrew, | |
"Another Efficient Algorithm for Convex Hulls in Two Dimensions", 1979) | |
https://web.archive.org/web/20210511015444/http://geomalgorithms.com/a10-_hull-1.html | |
""" | |
from .line import Segment | |
from .polygon import Polygon | |
p = OrderedSet() | |
for e in args: | |
if not isinstance(e, GeometryEntity): | |
try: | |
e = Point(e) | |
except NotImplementedError: | |
raise ValueError('%s is not a GeometryEntity and cannot be made into Point' % str(e)) | |
if isinstance(e, Point): | |
p.add(e) | |
elif isinstance(e, Segment): | |
p.update(e.points) | |
elif isinstance(e, Polygon): | |
p.update(e.vertices) | |
else: | |
raise NotImplementedError( | |
'Convex hull for %s not implemented.' % type(e)) | |
# make sure all our points are of the same dimension | |
if any(len(x) != 2 for x in p): | |
raise ValueError('Can only compute the convex hull in two dimensions') | |
p = list(p) | |
if len(p) == 1: | |
return p[0] if polygon else (p[0], None) | |
elif len(p) == 2: | |
s = Segment(p[0], p[1]) | |
return s if polygon else (s, None) | |
def _orientation(p, q, r): | |
'''Return positive if p-q-r are clockwise, neg if ccw, zero if | |
collinear.''' | |
return (q.y - p.y)*(r.x - p.x) - (q.x - p.x)*(r.y - p.y) | |
# scan to find upper and lower convex hulls of a set of 2d points. | |
U = [] | |
L = [] | |
try: | |
p.sort(key=lambda x: x.args) | |
except TypeError: | |
raise ValueError("The points could not be sorted.") | |
for p_i in p: | |
while len(U) > 1 and _orientation(U[-2], U[-1], p_i) <= 0: | |
U.pop() | |
while len(L) > 1 and _orientation(L[-2], L[-1], p_i) >= 0: | |
L.pop() | |
U.append(p_i) | |
L.append(p_i) | |
U.reverse() | |
convexHull = tuple(L + U[1:-1]) | |
if len(convexHull) == 2: | |
s = Segment(convexHull[0], convexHull[1]) | |
return s if polygon else (s, None) | |
if polygon: | |
return Polygon(*convexHull) | |
else: | |
U.reverse() | |
return (U, L) | |
def farthest_points(*args): | |
"""Return the subset of points from a set of points that were | |
the furthest apart from each other in the 2D plane. | |
Parameters | |
========== | |
args | |
A collection of Points on 2D plane. | |
Notes | |
===== | |
This can only be performed on a set of points whose coordinates can | |
be ordered on the number line. If there are no ties then a single | |
pair of Points will be in the set. | |
Examples | |
======== | |
>>> from sympy.geometry import farthest_points, Triangle | |
>>> Triangle(sss=(3, 4, 5)).args | |
(Point2D(0, 0), Point2D(3, 0), Point2D(3, 4)) | |
>>> farthest_points(*_) | |
{(Point2D(0, 0), Point2D(3, 4))} | |
References | |
========== | |
.. [1] https://code.activestate.com/recipes/117225-convex-hull-and-diameter-of-2d-point-sets/ | |
.. [2] Rotating Callipers Technique | |
https://en.wikipedia.org/wiki/Rotating_calipers | |
""" | |
def rotatingCalipers(Points): | |
U, L = convex_hull(*Points, **{"polygon": False}) | |
if L is None: | |
if isinstance(U, Point): | |
raise ValueError('At least two distinct points must be given.') | |
yield U.args | |
else: | |
i = 0 | |
j = len(L) - 1 | |
while i < len(U) - 1 or j > 0: | |
yield U[i], L[j] | |
# if all the way through one side of hull, advance the other side | |
if i == len(U) - 1: | |
j -= 1 | |
elif j == 0: | |
i += 1 | |
# still points left on both lists, compare slopes of next hull edges | |
# being careful to avoid divide-by-zero in slope calculation | |
elif (U[i+1].y - U[i].y) * (L[j].x - L[j-1].x) > \ | |
(L[j].y - L[j-1].y) * (U[i+1].x - U[i].x): | |
i += 1 | |
else: | |
j -= 1 | |
p = [Point2D(i) for i in set(args)] | |
if not all(i.is_Rational for j in p for i in j.args): | |
def hypot(x, y): | |
arg = x*x + y*y | |
if arg.is_Rational: | |
return _sqrt(arg) | |
return sqrt(arg) | |
else: | |
from math import hypot | |
rv = [] | |
diam = 0 | |
for pair in rotatingCalipers(args): | |
h, q = _ordered_points(pair) | |
d = hypot(h.x - q.x, h.y - q.y) | |
if d > diam: | |
rv = [(h, q)] | |
elif d == diam: | |
rv.append((h, q)) | |
else: | |
continue | |
diam = d | |
return set(rv) | |
def idiff(eq, y, x, n=1): | |
"""Return ``dy/dx`` assuming that ``eq == 0``. | |
Parameters | |
========== | |
y : the dependent variable or a list of dependent variables (with y first) | |
x : the variable that the derivative is being taken with respect to | |
n : the order of the derivative (default is 1) | |
Examples | |
======== | |
>>> from sympy.abc import x, y, a | |
>>> from sympy.geometry.util import idiff | |
>>> circ = x**2 + y**2 - 4 | |
>>> idiff(circ, y, x) | |
-x/y | |
>>> idiff(circ, y, x, 2).simplify() | |
(-x**2 - y**2)/y**3 | |
Here, ``a`` is assumed to be independent of ``x``: | |
>>> idiff(x + a + y, y, x) | |
-1 | |
Now the x-dependence of ``a`` is made explicit by listing ``a`` after | |
``y`` in a list. | |
>>> idiff(x + a + y, [y, a], x) | |
-Derivative(a, x) - 1 | |
See Also | |
======== | |
sympy.core.function.Derivative: represents unevaluated derivatives | |
sympy.core.function.diff: explicitly differentiates wrt symbols | |
""" | |
if is_sequence(y): | |
dep = set(y) | |
y = y[0] | |
elif isinstance(y, Symbol): | |
dep = {y} | |
elif isinstance(y, Function): | |
pass | |
else: | |
raise ValueError("expecting x-dependent symbol(s) or function(s) but got: %s" % y) | |
f = {s: Function(s.name)(x) for s in eq.free_symbols | |
if s != x and s in dep} | |
if isinstance(y, Symbol): | |
dydx = Function(y.name)(x).diff(x) | |
else: | |
dydx = y.diff(x) | |
eq = eq.subs(f) | |
derivs = {} | |
for i in range(n): | |
# equation will be linear in dydx, a*dydx + b, so dydx = -b/a | |
deq = eq.diff(x) | |
b = deq.xreplace({dydx: S.Zero}) | |
a = (deq - b).xreplace({dydx: S.One}) | |
yp = factor_terms(expand_mul(cancel((-b/a).subs(derivs)), deep=False)) | |
if i == n - 1: | |
return yp.subs([(v, k) for k, v in f.items()]) | |
derivs[dydx] = yp | |
eq = dydx - yp | |
dydx = dydx.diff(x) | |
def intersection(*entities, pairwise=False, **kwargs): | |
"""The intersection of a collection of GeometryEntity instances. | |
Parameters | |
========== | |
entities : sequence of GeometryEntity | |
pairwise (keyword argument) : Can be either True or False | |
Returns | |
======= | |
intersection : list of GeometryEntity | |
Raises | |
====== | |
NotImplementedError | |
When unable to calculate intersection. | |
Notes | |
===== | |
The intersection of any geometrical entity with itself should return | |
a list with one item: the entity in question. | |
An intersection requires two or more entities. If only a single | |
entity is given then the function will return an empty list. | |
It is possible for `intersection` to miss intersections that one | |
knows exists because the required quantities were not fully | |
simplified internally. | |
Reals should be converted to Rationals, e.g. Rational(str(real_num)) | |
or else failures due to floating point issues may result. | |
Case 1: When the keyword argument 'pairwise' is False (default value): | |
In this case, the function returns a list of intersections common to | |
all entities. | |
Case 2: When the keyword argument 'pairwise' is True: | |
In this case, the functions returns a list intersections that occur | |
between any pair of entities. | |
See Also | |
======== | |
sympy.geometry.entity.GeometryEntity.intersection | |
Examples | |
======== | |
>>> from sympy import Ray, Circle, intersection | |
>>> c = Circle((0, 1), 1) | |
>>> intersection(c, c.center) | |
[] | |
>>> right = Ray((0, 0), (1, 0)) | |
>>> up = Ray((0, 0), (0, 1)) | |
>>> intersection(c, right, up) | |
[Point2D(0, 0)] | |
>>> intersection(c, right, up, pairwise=True) | |
[Point2D(0, 0), Point2D(0, 2)] | |
>>> left = Ray((1, 0), (0, 0)) | |
>>> intersection(right, left) | |
[Segment2D(Point2D(0, 0), Point2D(1, 0))] | |
""" | |
if len(entities) <= 1: | |
return [] | |
entities = list(entities) | |
prec = None | |
for i, e in enumerate(entities): | |
if not isinstance(e, GeometryEntity): | |
# entities may be an immutable tuple | |
e = Point(e) | |
# convert to exact Rationals | |
d = {} | |
for f in e.atoms(Float): | |
prec = f._prec if prec is None else min(f._prec, prec) | |
d.setdefault(f, nsimplify(f, rational=True)) | |
entities[i] = e.xreplace(d) | |
if not pairwise: | |
# find the intersection common to all objects | |
res = entities[0].intersection(entities[1]) | |
for entity in entities[2:]: | |
newres = [] | |
for x in res: | |
newres.extend(x.intersection(entity)) | |
res = newres | |
else: | |
# find all pairwise intersections | |
ans = [] | |
for j in range(len(entities)): | |
for k in range(j + 1, len(entities)): | |
ans.extend(intersection(entities[j], entities[k])) | |
res = list(ordered(set(ans))) | |
# convert back to Floats | |
if prec is not None: | |
p = prec_to_dps(prec) | |
res = [i.n(p) for i in res] | |
return res | |