Source code for ddg.geometry.intersection

"""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)