Reference
Implementation-agnostic set builders.
This module defines a tiny DSL of lazy "set builders" that describe sets and
set operations without committing to a concrete representation. A SetBuilder
is realized by an Impl[R]
(see pyspect.impls.*
), which interprets operations
(e.g., empty
, complement
, intersect
, halfspace
).
Key ideas
- Builders are composable and track requirements on the target
Impl
. - Builders can carry named free variables (ReferredSet) resolved at realization.
- AppliedSet defers a call to an
Impl
method by name until realization.
AbsurdSet
Bases: SetBuilder[R]
A builder that cannot be realized.
Used as a sentinel for impossible constructions. Realization raises.
Source code in src/pyspect/set_builder.py
class AbsurdSet[R](SetBuilder[R]):
"""A builder that cannot be realized.
Used as a sentinel for impossible constructions. Realization raises.
"""
def __call__(self, impl: Impl[R], **m: SetBuilder[R]) -> R:
raise ValueError("Cannot realize the absurd set.")
AppliedSet
Bases: SetBuilder[R]
Defer a call to Impl.<funcname>(*args)
where args
are realized builders.
- Accumulates required
Impl
methods from children and addsfuncname
. - Propagates and de-duplicates children's free variables.
- On realization, calls child builders first, then invokes the
Impl
method. - Wraps child exceptions to pinpoint which argument failed.
Source code in src/pyspect/set_builder.py
class AppliedSet[R](SetBuilder[R]):
"""Defer a call to `Impl.<funcname>(*args)` where `args` are realized builders.
- Accumulates required `Impl` methods from children and adds `funcname`.
- Propagates and de-duplicates children's free variables.
- On realization, calls child builders first, then invokes the `Impl` method.
- Wraps child exceptions to pinpoint which argument failed.
"""
def __init__(self, funcname: str, *builders: SetBuilder[R]) -> None:
self.funcname = funcname
self.builders = builders
_require = (funcname,)
for builder in self.builders:
_require += builder.__require__
self.free += tuple(name for name in builder.free if name not in self.free)
self.add_requirements(_require)
def __call__(self, impl: Impl[R], **m: SetBuilder[R]) -> R:
try:
func = getattr(impl, self.funcname)
except AttributeError as e:
raise AttributeError(f'Impl {impl.__class__.__name__} does not support "{self.funcname}".') from e
args = []
for i, sb in enumerate(self.builders):
try:
args.append(sb(impl, **m))
except Exception as e:
E = type(e)
raise E(f'When applying "{self.funcname}" on argument {i}, received: {e!s}') from e
return func(*args)
BoundedSet
Bases: SetBuilder[R]
Axis-aligned box possibly unbounded on one side per axis.
Bounds mapping: name -> (vmin, vmax)
. Use Ellipsis to denote an open side
(e.g., (0, ...) or (..., 1)). For periodic axes where vmax < vmin
, the
range wraps around.
Example:
# Left half-circle bounded in y.
A = BoundedSet(y=(-0.5, 0.5), theta=(+pi/2, -pi/2))
Requires
Impl < AxesImpl
Impl.complement(inp: R) -> R
Impl.halfspace(normal, offset, axes, ...) -> R
Impl.intersect(inp1: R, inp2: R) -> R
Source code in src/pyspect/set_builder.py
class BoundedSet[R](SetBuilder[R]):
"""Axis-aligned box possibly unbounded on one side per axis.
Bounds mapping: `name -> (vmin, vmax)`. Use Ellipsis to denote an open side
(e.g., (0, ...) or (..., 1)). For periodic axes where `vmax < vmin`, the
range wraps around.
Example:
```
# Left half-circle bounded in y.
A = BoundedSet(y=(-0.5, 0.5), theta=(+pi/2, -pi/2))
```
Requires:
- `Impl < AxesImpl`
- `Impl.complement(inp: R) -> R`
- `Impl.halfspace(normal, offset, axes, ...) -> R`
- `Impl.intersect(inp1: R, inp2: R) -> R`
"""
__require__ = ('complement', 'halfspace', 'intersect')
def __init__(self, **bounds: list[int]) -> None:
self.bounds = bounds
def __call__(self, impl: Impl[R], **m: SetBuilder[R]) -> R:
s = impl.complement(impl.empty())
for name, (vmin, vmax) in self.bounds.items():
i = impl.axis(name)
if vmin is Ellipsis:
assert vmax is not Ellipsis, f'Invalid bounds for axis {impl.axis_name(i)}, there must be either an upper or lower bound.'
upper_bound = impl.halfspace(normal=[0 if i != j else -1 for j in range(impl.ndim)],
offset=[0 if i != j else vmax for j in range(impl.ndim)])
axis_range = upper_bound
elif vmax is Ellipsis:
assert vmin is not Ellipsis, f'Invalid bounds for axis {impl.axis_name(i)}, there must be either an upper or lower bound.'
lower_bound = impl.halfspace(normal=[0 if i != j else +1 for j in range(impl.ndim)],
offset=[0 if i != j else vmin for j in range(impl.ndim)])
axis_range = lower_bound
elif impl.axis_is_periodic(i) and vmax < vmin:
upper_bound = impl.halfspace(normal=[0 if i != j else -1 for j in range(impl.ndim)],
offset=[0 if i != j else vmin for j in range(impl.ndim)])
lower_bound = impl.halfspace(normal=[0 if i != j else +1 for j in range(impl.ndim)],
offset=[0 if i != j else vmax for j in range(impl.ndim)])
axis_range = impl.complement(impl.intersect(upper_bound, lower_bound))
else:
# NOTE: See similar assertion in TVHJImpl's halfspace
amin, amax = impl.axis_bounds(i)
assert amin < vmin < amax, f'For dimension "{name}", {amin} < {vmin=} < {amax}. Use Ellipsis (...) to indicate subset stretching to the space boundary.'
assert amin < vmax < amax, f'For dimension "{name}", {amin} < {vmax=} < {amax}. Use Ellipsis (...) to indicate subset stretching to the space boundary.'
upper_bound = impl.halfspace(normal=[0 if i != j else -1 for j in range(impl.ndim)],
offset=[0 if i != j else vmax for j in range(impl.ndim)])
lower_bound = impl.halfspace(normal=[0 if i != j else +1 for j in range(impl.ndim)],
offset=[0 if i != j else vmin for j in range(impl.ndim)])
axis_range = impl.intersect(upper_bound, lower_bound)
s = impl.intersect(s, axis_range)
return s
EmptySet
Bases: SetBuilder[R]
Builder for the empty set.
Requires
Impl.empty() -> R
Source code in src/pyspect/set_builder.py
class EmptySet[R](SetBuilder[R]):
"""Builder for the empty set.
Requires:
- `Impl.empty() -> R`
"""
__require__ = ('empty',)
def __call__(self, impl: Impl[R], **m: SetBuilder[R]) -> R:
return impl.empty()
HalfSpaceSet
Bases: SetBuilder[R]
Half-space described by the normal and offset of a hyperplane.
Note: The set is in the direction of the normal.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
normal
|
list[float]
|
coefficients along each axis |
required |
offset
|
list[float]
|
offsets along each axis |
required |
axes
|
list[Axis]
|
axis indices (or str if using |
required |
kwds
|
Any
|
forwarded to |
{}
|
Requires
Impl.halfspace(normal, offset, axes, ...) -> R
Source code in src/pyspect/set_builder.py
class HalfSpaceSet[R](SetBuilder[R]):
"""Half-space described by the normal and offset of a hyperplane.
Note: The set is in the direction of the normal.
Parameters:
normal: coefficients along each axis
offset: offsets along each axis
axes: axis indices (or str if using `AxesImpl`) in the `Impl`'s coordinate system
kwds: forwarded to `Impl.halfspace`
Requires:
- `Impl.halfspace(normal, offset, axes, ...) -> R`
"""
__require__ = ('halfspace',)
def __init__(
self,
normal: list[float],
offset: list[float],
axes: list[Axis],
**kwds: Any,
) -> None:
assert len(axes) == len(normal) == len(offset)
self.normal = normal
self.offset = offset
self.axes = axes
self.kwds = kwds
def __call__(self, impl: Impl[R], **m: SetBuilder[R]) -> R:
return impl.halfspace(normal=self.normal,
offset=self.offset,
axes=[impl.axis(ax) for ax in self.axes],
**self.kwds)
ReferredSet
Bases: SetBuilder[R]
Reference a named free variable resolved from the realization mapping.
ReferredSet('X')(impl, X=some_builder)
realizes to some_builder(impl, ...)
.
This is useful in two ways:
1. We can be lazy when constructing the call tree, i.e. we allow users to
define which builder to use at a later stage.
2. This essentially allow variables to exist within the call tree which avoids
having to reconstruct an entire tree in some cases.
Source code in src/pyspect/set_builder.py
class ReferredSet[R](SetBuilder[R]):
"""Reference a named free variable resolved from the realization mapping.
`ReferredSet('X')(impl, X=some_builder)` realizes to `some_builder(impl, ...)`.
This is useful in two ways:
1. We can be lazy when constructing the call tree, i.e. we allow users to
define which builder to use at a later stage.
2. This essentially allow variables to exist within the call tree which avoids
having to reconstruct an entire tree in some cases.
"""
def __init__(self, name: str) -> None:
self.free += (name,)
def __call__(self, impl: Impl[R], **m: SetBuilder[R]) -> R:
name, = self.free
sb = m.pop(name)
return sb(impl, **m)
Set
Bases: SetBuilder[R]
Wrap a concrete set value R and return it unchanged on realization.
Source code in src/pyspect/set_builder.py
class Set[R](SetBuilder[R]):
"""Wrap a concrete set value R and return it unchanged on realization."""
def __init__(self, arg: R) -> None:
self.arg = arg
def __call__(self, impl: Impl[R], **m: SetBuilder[R]) -> R:
return self.arg
SetBuilder
Bases: ImplClient[R]
Abstract base for all set builders.
Responsibilities
- Be callable with an implementation
Impl[R]
to produce a concrete setR
. - Track required
Impl
operations throughImplClient
. - Track free variable names (see
ReferredSet
).
Subclasses should implement __call__
, which is called to realize the sets.
Source code in src/pyspect/set_builder.py
class SetBuilder[R](ImplClient[R]):
"""Abstract base for all set builders.
Responsibilities:
- Be callable with an implementation `Impl[R]` to produce a concrete set `R`.
- Track required `Impl` operations through `ImplClient`.
- Track free variable names (see `ReferredSet`).
Subclasses should implement `__call__`, which is called to realize the sets.
"""
free: tuple[str, ...] = ()
def __call__(self, impl: Impl[R], **m: SetBuilder[R]) -> R:
raise NotImplementedError(f"{type(self).__name__}.__call__ is not implemented. SetBuilders must implement __call__.")
def __repr__(self) -> str:
"""Return a compact identifier for the builder instance."""
cls = type(self)
ptr = hash(self)
return f'<{cls.__name__} at {ptr:#0{18}x}>'
@property
def uid(self) -> str:
"""Stable hexadecimal id derived from the object hash."""
# Simple way to create a unique id from a python function.
# - hash(sb) returns the function pointer (I think)
# - Convert to bytes to get capture full 64-bit value (incl. zeroes)
# - Convert to hex-string
return hash(self).to_bytes(8,"big").hex()
uid
property
Stable hexadecimal id derived from the object hash.
__repr__()
Return a compact identifier for the builder instance.
Source code in src/pyspect/set_builder.py
def __repr__(self) -> str:
"""Return a compact identifier for the builder instance."""
cls = type(self)
ptr = hash(self)
return f'<{cls.__name__} at {ptr:#0{18}x}>'
Compl(*args)
Return complement of a builder via Impl.complement.
Source code in src/pyspect/set_builder.py
def Compl[R](*args: SetBuilder[R]) -> SetBuilder[R]:
"""Return complement of a builder via Impl.complement."""
return AppliedSet('complement', *args)
Inter(*args)
Return intersection of builders via Impl.intersect.
Source code in src/pyspect/set_builder.py
def Inter[R](*args: SetBuilder[R]) -> SetBuilder[R]:
"""Return intersection of builders via Impl.intersect."""
return AppliedSet('intersect', *args)
Union(*args)
Return union of builders via Impl.union.
Source code in src/pyspect/set_builder.py
def Union[R](*args: SetBuilder[R]) -> SetBuilder[R]:
"""Return union of builders via Impl.union."""
return AppliedSet('union', *args)