"""Intersections and joins of geometric objects.
The join of two objects X and Y is defined as the union of all lines connecting a point
in X with a point in Y.
The functions in this module automatically choose elementary intersection/join functions
based on the types of objects and reduce intersections/joins of more than two objects as
much as possible. The following types are currently supported:
.. list-table:: Supported types for intersections
:header-rows: 1
:align: left
* - Types
- Function
* - Subspace and Subspace
- :py:func:`ddg.geometry.subspaces.intersect_subspaces`
* - Quadric and Subspace
- :py:func:`ddg.geometry.quadrics.intersect_quadric_subspace`
.. list-table:: Supported types for joins
:header-rows: 1
:align: left
* - Types
- Function
* - Subspace and Subspace
- :py:func:`ddg.geometry.subspaces.join_subspaces`
* - Quadric and Subspace
- :py:func:`ddg.geometry.quadrics.join_quadric_subspace`
Please refer to the elementary functions for further restrictions and details.
"""
from collections.abc import Iterable
# Can't import Subspace and Quadric on their own due to circular imports
import ddg.geometry.quadrics as quadrics
import ddg.geometry.subspaces as subspaces
from ddg import nonexact
# HOW TO ADD INTERSECTIONS AND JOINS:
#
# These dictionaries map combinations of types to functions that intersect/join objects
# with those types.
# The combinations are given as tuples of types and mean the following:
#
# - More than one type in the combination (T1, T2,...,Tn) means that fct takes
# exactly n objects (x1,...,xn) with those types in the correct order.
# - One type in the combination (T,) means that fct takes an arbitrary number of
# arguments of type T.
#
# The latter is currently not used anywhere, but it could be that for some type of
# object, there could be a better intersection/join algorithm than pairwise reduction.
# This allows for that kind of algorithm to exist in the future.
#
# More complex function signatures, for example a fixed sequence of types T1,...,Tk
# first and then an arbitrary number of objects of another type Tj at the end are
# currently not supported.
# For documentation purposes, it would be nice to have these constants in the sphinx
# docs. This is possible, but unfortunately the representation strings of the
# dictionaries are very hard to read, making them unsuitable for the documentation.
# Supported types should therefore be listed in the module docstring.
_INTERSECTION_COMBINATIONS = {
(subspaces.Subspace, subspaces.Subspace): subspaces.intersect_subspaces,
(quadrics.Quadric, subspaces.Subspace): quadrics.intersect_quadric_subspace,
}
_JOIN_COMBINATIONS = {
(subspaces.Subspace, subspaces.Subspace): subspaces.join_subspaces,
(quadrics.Quadric, subspaces.Subspace): quadrics.join_quadric_subspace,
}
def _typed_reduce(objects, combinations):
"""functools.reduce-like operation with multiple dispatching.
`objects` is a finite iterable of objects of different types. `combinations` is a
dictionary mapping tuples of types ``(T1,...,Tn)`` to functions
``f(x1: T1,...,xn: Tn) -> T``. This function
- searches `objects` for an object combination found in `combinations`
- applies the corresponding function, removing the found combination from `objects`
and replacing it with the function output.
These steps are repeated until no further reduction is possible. If a single object
remains, it is returned. Otherwise, the remaining irreducible list of objects is
returned.
Parameters
----------
objects : Finite iterable
combinations : dict[tuple[type, ...], Callable]
Returns
-------
list or result of final reduction step
"""
objects = list(objects)
if len(objects) == 1:
return objects[0]
for combination, fct in combinations.items():
# One type in the combination (T,) means that fct takes an arbitrary number of
# arguments of type T. If not more than one is found, move on to the next
# combination.
if len(combination) == 1:
type_ = combination[0]
found_objects = [x for x in objects if isinstance(x, type_)]
if len(found_objects) <= 1:
continue
# More than one type in the combination (T1, T2,...,Tn) means that fct takes
# exactly n objects (x1,...,xn) with those types in the correct order.
# For each type Ti, we simply iterate over objects and see if there is an object
# of type Ti that has not already been found. If there is, we append it to
# found_objects and move on to the next type T(i+1).
# If we don't end up with a list of length n at the end, the combination does
# not exist in objects and we move on to the next combination.
else:
found_objects = []
for type_ in combination:
for object_ in objects:
if isinstance(object_, type_) and object_ not in found_objects:
found_objects.append(object_)
break
if len(found_objects) != len(combination):
continue
# The combination has been found in objects.
for object_ in found_objects:
objects.remove(object_)
objects.append(fct(*found_objects))
# We have a new state and apply the function recursively.
return _typed_reduce(objects, combinations)
# None of the combinations was found in objects, we can not reduce further.
return objects
def _resolve_intersection(intersection):
result = _typed_reduce(intersection, _INTERSECTION_COMBINATIONS)
if isinstance(result, list):
return Intersection(*result)
return result
def _resolve_join(join_):
result = _typed_reduce(join_, _JOIN_COMBINATIONS)
if isinstance(result, list):
return Join(*result)
return result
[docs]def intersect(*objects, resolve=True):
"""Intersect any number of supported geometric objects.
Parameters
----------
*objects : Any
Objects to intersect
resolve : bool (default=False)
Whether or not the intersection should be resolved immediately
Returns
-------
ddg.geometry.intersection.Intersection or resolved Intersection
Instance of Intersection containing obj1 and obj2 or the intersection
of the class obtained by resolving the intersection
See Also
--------
join
"""
if resolve:
return Intersection(*objects).resolve()
return Intersection(*objects)
meet = intersect
"""Alias for intersect.
See Also
--------
intersect
"""
[docs]def join(*objects, resolve=True):
"""Join any number of supported geometric objects.
Parameters
----------
*objects : Any
Objects to join
resolve : bool (default=False)
Whether or not the join should be resolved immediately
Returns
-------
ddg.geometry.intersection.Join or resolved Intersection
Instance of Join containing obj1 and obj2 or the join of the class
obtained by resolving the join
See Also
--------
intersect
"""
if resolve:
return Join(*objects).resolve()
return Join(*objects)
class _IntersectionJoinAux:
"""Auxiliary parent class to reduce duplicate code."""
def __init__(self, *obj):
self.objects = obj
def __iter__(self):
return self.objects.__iter__()
@property
def types(self):
"""Set of types of objects.
Returns
-------
Set
"""
return set(type(o) for o in self.objects)
@property
def atol(self):
"""Maximum of absolute tolerances of objects.
Returns
-------
float
"""
return nonexact.combine_tols(*self)[0]
@property
def rtol(self):
"""Maximum of relative tolerances of objects.
Returns
-------
float
"""
return nonexact.combine_tols(*self)[1]
[docs]class Intersection(_IntersectionJoinAux, Iterable):
"""Base class for Intersections.
Parameters
----------
*obj : object
Intersecting objects
Attributes
----------
objects : tuple
Tuple of intersecting objects.
Methods
-------
resolve
Resolve the intersection.
See Also
--------
Join
Notes
-----
This class supports the "in" keyword, meaning the set element relation.
"""
def __contains__(self, p):
return all(p in o for o in self.objects)
[docs] def resolve(self):
"""Resolve the intersection.
This performs a functools.reduce-like operation on the objects contained in
`objects` until no further reduction is possible with the existing elementary
intersection functions.
Returns
-------
Intersection or resolved type
Returns an Intersection object if full reduction was not possible.
Otherwise, returns the result of the final reduction step.
"""
return _resolve_intersection(self)
[docs]class Join(_IntersectionJoinAux, Iterable):
"""Base class for joins.
Parameters
----------
*obj : object
Objects to join
Attributes
----------
objects : tuple
Tuple of joined objects.
Methods
-------
resolve
Resolve the join.
See Also
--------
Intersection
"""
[docs] def resolve(self):
"""Resolve the Join.
This performs a functools.reduce-like operation on the objects contained in
`objects` until no further reduction is possible with the existing elementary
join functions.
Returns
-------
Join or resolved type
Returns a Join object if full reduction was not possible.
Otherwise, returns the result of the final reduction step.
"""
return _resolve_join(self)