Reference

Temporal Logic Trees (TLTs).

This module wires tuple-encoded temporal logic formulas (see pyspect.logics) to implementation-agnostic set builders (see pyspect.set_builder) and provides a small runtime for constructing, combining, and realizing TLTs against a concrete implementation Impl[R] (see pyspect.impls.*).

Key ideas
  • A TLT wraps:
    • _formula: a TLExpr made of tuples and proposition names
    • _builder: a SetBuilder[R] describing the set semantics
    • _approx: an APPROXDIR flag describing the approximation direction
    • _setmap: a mapping from proposition names to SetBuilder bindings
  • Primitives are registered on the class via TLT.select(...) and used by new_from_formula when building trees.
  • Realization checks requirements (operations and bound props) and then calls the builder with the selected Impl.

APPROXDIR

Bases: Enum

Approximation direction tag for a TLT node.

Values
  • INVALID: semantics cannot be guaranteed (construction error)
  • UNDER: under-approximation (subset of the exact set)
  • EXACT: exact semantics
  • OVER: over-approximation (superset of the exact set)

The enum supports + and * with either another APPROXDIR or an int with the conventional meaning used by the primitive combination rules.

Source code in src/pyspect/tlt.py
class APPROXDIR(Enum):
    """Approximation direction tag for a TLT node.

    Values:
        - INVALID: semantics cannot be guaranteed (construction error)
        - UNDER: under-approximation (subset of the exact set)
        - EXACT: exact semantics
        - OVER: over-approximation (superset of the exact set)

    The enum supports + and * with either another APPROXDIR or an int with the
    conventional meaning used by the primitive combination rules.
    """
    INVALID = None
    UNDER = -1
    EXACT = 0
    OVER = +1

    def __str__(self):
        return f'{self.name}'

    def __radd__(self, other): return self.__add__(other)

    def __add__(self, other: 'APPROXDIR | int') -> 'APPROXDIR':
        """Combine approximation directions additively, propagating INVALID."""
        lhs = self.value
        rhs = other.value if isinstance(other, APPROXDIR) else other
        return APPROXDIR(None if None in (lhs, rhs) else
                         lhs + rhs)

    def __rmul__(self, other): return self.__mul__(other)

    def __mul__(self, other: 'APPROXDIR | int') -> 'APPROXDIR':
        """Combine approximation directions multiplicatively, propagating INVALID."""
        lhs = self.value
        rhs = other.value if isinstance(other, APPROXDIR) else other
        return APPROXDIR(None if None in (lhs, rhs) else
                         lhs * rhs)

__add__(other)

Combine approximation directions additively, propagating INVALID.

Source code in src/pyspect/tlt.py
def __add__(self, other: 'APPROXDIR | int') -> 'APPROXDIR':
    """Combine approximation directions additively, propagating INVALID."""
    lhs = self.value
    rhs = other.value if isinstance(other, APPROXDIR) else other
    return APPROXDIR(None if None in (lhs, rhs) else
                     lhs + rhs)

__mul__(other)

Combine approximation directions multiplicatively, propagating INVALID.

Source code in src/pyspect/tlt.py
def __mul__(self, other: 'APPROXDIR | int') -> 'APPROXDIR':
    """Combine approximation directions multiplicatively, propagating INVALID."""
    lhs = self.value
    rhs = other.value if isinstance(other, APPROXDIR) else other
    return APPROXDIR(None if None in (lhs, rhs) else
                     lhs * rhs)

AppliedSet

Bases: 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.
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

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 AxesImpl) in the Impl's coordinate system

required
kwds Any

forwarded to Impl.halfspace

{}
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)

Impl

Marker base class for concrete implementation backends.

Source code in src/pyspect/impls/base.py
class Impl[R]:
    """Marker base class for concrete implementation backends."""

ImplClient

Client-side mixin for declaring and checking required operations.

Clients refer to objects that use an implementation backend. ImplClient ensures to list the names of operations they need the implementation to provide via the class attribute require. This mixin offers methods to: - report which operations are missing on a given Impl - test if an implementation satisfies all requirements - extend or inherit requirement sets dynamically

Source code in src/pyspect/impls/base.py
class ImplClient[R](metaclass=ImplClientMeta):
    """Client-side mixin for declaring and checking required operations.

    Clients refer to objects that use an implementation backend. ImplClient
    ensures to list the names of operations they need the implementation to
    provide via the class attribute __require__. This mixin offers methods to:
        - report which operations are missing on a given Impl
        - test if an implementation satisfies all requirements
        - extend or inherit requirement sets dynamically
    """

    @classmethod
    def missing_ops(cls, impl: Impl[R]) -> list[str]:
        """Return the names of required operations not found on impl."""
        return [
            field
            for field in cls.__require__
            if not hasattr(impl, field)
        ]

    @classmethod
    def is_supported(cls, impl: Impl[R]) -> bool:
        """Return True if impl provides all operations listed in __require__."""
        return not cls.missing_ops(impl)

    def add_requirements(self, funcnames: Sequence[str]) -> None:
        """Add function names to the set of required operations."""
        _require = set(self.__require__)
        _require = _require.union(funcnames)
        self.__require__ = tuple(_require)

    def inherit_requirements(self, *others: ImplClient[R]) -> None:
        """Merge requirement sets from other ImplClient-like classes."""
        for other in others:
            self.add_requirements(other.__require__)

add_requirements(funcnames)

Add function names to the set of required operations.

Source code in src/pyspect/impls/base.py
def add_requirements(self, funcnames: Sequence[str]) -> None:
    """Add function names to the set of required operations."""
    _require = set(self.__require__)
    _require = _require.union(funcnames)
    self.__require__ = tuple(_require)

inherit_requirements(*others)

Merge requirement sets from other ImplClient-like classes.

Source code in src/pyspect/impls/base.py
def inherit_requirements(self, *others: ImplClient[R]) -> None:
    """Merge requirement sets from other ImplClient-like classes."""
    for other in others:
        self.add_requirements(other.__require__)

is_supported(impl) classmethod

Return True if impl provides all operations listed in require.

Source code in src/pyspect/impls/base.py
@classmethod
def is_supported(cls, impl: Impl[R]) -> bool:
    """Return True if impl provides all operations listed in __require__."""
    return not cls.missing_ops(impl)

missing_ops(impl) classmethod

Return the names of required operations not found on impl.

Source code in src/pyspect/impls/base.py
@classmethod
def missing_ops(cls, impl: Impl[R]) -> list[str]:
    """Return the names of required operations not found on impl."""
    return [
        field
        for field in cls.__require__
        if not hasattr(impl, field)
    ]

ImplClientMeta

Bases: type

Metaclass that aggregates required operation names across inheritance.

Classes using this metaclass can declare a tuple[str, ...] in require. During class creation, this metaclass unions all require tuples found in the base classes (that also use ImplClientMeta) with the one declared on the subclass, storing the result back into require.

Source code in src/pyspect/impls/base.py
class ImplClientMeta(type):
    """Metaclass that aggregates required operation names across inheritance.

    Classes using this metaclass can declare a tuple[str, ...] in __require__.
    During class creation, this metaclass unions all __require__ tuples found
    in the base classes (that also use ImplClientMeta) with the one declared
    on the subclass, storing the result back into __require__.
    """

    __require__: ClassVar[tuple[str, ...]]

    def __new__(mcs, name: str, bases: tuple[type, ...], namespace: dict[str, Any]) -> None:
        _require = namespace.setdefault('__require__', ())

        require = set(_require)
        for base in bases:
            if isinstance(base, ImplClientMeta):
                require = require.union(base.__require__)

        namespace['__require__'] = tuple(require)

        return super().__new__(mcs, name, bases, namespace)                

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 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.

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}>'

TLT

Bases: ImplClient[R]

Temporal Logic Tree node parameterized by a concrete set type R.

A TLT encapsulates a tuple-based logic formula, an implementation-agnostic set builder realizing its semantics, an approximation direction, and a mapping from free proposition names to bound SetBuilders.

Construction
  • TLT.select(primitives) must be called to choose available operators.
  • TLT(arg, **where) dispatches based on the type of arg: str -> proposition SetBuilder -> constant set, assigned a unique proposition name tuple(TLExpr)-> formula; recursively constructs children via primitives TLT -> existing tree, optionally updated with new bindings
Realization
  • Call .realize(impl) to obtain an R from an implementation.
  • .assert_realizable() checks missing props/ops and approximation validity.
Source code in src/pyspect/tlt.py
class TLT[R](ImplClient[R]):
    """Temporal Logic Tree node parameterized by a concrete set type R.

    A TLT encapsulates a tuple-based logic formula, an implementation-agnostic
    set builder realizing its semantics, an approximation direction, and a
    mapping from free proposition names to bound SetBuilders.

    Construction:
        - TLT.select(primitives) must be called to choose available operators.
        - TLT(arg, **where) dispatches based on the type of `arg`:
            str          -> proposition
            SetBuilder   -> constant set, assigned a unique proposition name
            tuple(TLExpr)-> formula; recursively constructs children via primitives
            TLT          -> existing tree, optionally updated with new bindings

    Realization:
        - Call .realize(impl) to obtain an R from an implementation.
        - .assert_realizable() checks missing props/ops and approximation validity.
    """

    __primitives__ = idict({}) # Null set of primitives

    @classmethod
    def select(cls, primitives):
        """Select the set of primitive operator implementations for this class."""
        cls.__primitives__ = primitives

    @classmethod
    def construct(cls, arg: TLTLike[R], **kwds: TLTLike[R]) -> TLT[R]:
        """Explicit constructor alias to apply usual dispatch rules."""
        return cls(arg, **kwds)

    def __new__(cls, arg: TLTLike[R], **kwds: TLTLike[R]) -> TLT[R]:
        """Construct a TLT by dispatching on the kind of `arg`."""
        if 'where' in kwds: kwds |= kwds.pop('where')
        return (cls.__new_from_tlt__(arg, **kwds)      if isinstance(arg, TLT) else
                cls.__new_from_prop__(arg, **kwds)     if isinstance(arg, str) else
                cls.__new_from_builder__(arg, **kwds)  if isinstance(arg, Callable) else
                cls.__new_from_formula__(arg, **kwds))

    @classmethod
    @TLTDebugger.wrap('tlt')
    def __new_from_tlt__(cls, tlt: TLT[R], **kwds: TLTLike[R]) -> TLT[R]:
        """Rebuild a TLT from an existing one, optionally rebinding free props."""
        # Create new TLT with updated information. With this, we can bind new 
        # TLT-like objects to free variables/propositions. We do not allow 
        # updates to existing propositions. First collect all updatable props.
        kwds = {prop: kwds[prop] if prop in kwds else sb
                for prop, sb in tlt._setmap.items()
                if prop in kwds or sb is not None}

        # It is best to reconstruct builders in the "top-level" TLT when there
        # is updates to the tree since we have modified how 
        return cls.__new_from_formula__(tlt._formula, **kwds)

    @classmethod
    @TLTDebugger.wrap('prop')
    def __new_from_prop__(cls, prop: str, **kwds: TLTLike[R]) -> TLT[R]:
        """Create a TLT from a proposition name, optionally binding it from kwds."""
        return (cls(kwds.pop(prop), **kwds) if prop in kwds else
                cls.__new_init__(prop, ReferredSet(prop), setmap={prop: None}))

    @classmethod
    @TLTDebugger.wrap('builder')
    def __new_from_builder__(cls, sb: SetBuilder, **kwds: TLTLike[R]) -> TLT[R]:
        """Create a TLT from a constant SetBuilder, assigning it a unique name."""
        # Assume a formula "_0" where the set exactly represent the prop "_0".
        # In reality, we define a unique ID `uid` instead of "_0". We add one
        # extra level of indirection with a ReferredSet for `uid` and 
        # letting `setmap` hold the actual set builder `sb`. This way, the root
        # TLT will hold a full formula that refers to even constant sets
        # (information is not lost as when internally binding constant sets to
        # builder functions).
        name = f'_{sb.uid}'
        formula = name # UID is a proposition
        self = cls.__new_init__(formula, ReferredSet(name), setmap={name: sb})

        self.inherit_requirements(sb)

        return self

    @classmethod
    @TLTDebugger.wrap('formula')
    def __new_from_formula__(cls, formula: 'TLExpr', **kwds: TLTLike[R]) -> TLT[R]:
        """Create a TLT from a TLExpr by invoking registered primitives."""
        if isinstance(formula, str):
            # If formula is a string, it is a proposition
            return cls.__new_from_prop__(formula, **kwds)

        # Otherwise, it is a non-trivial formula
        head, *tail = formula

        assert head in cls.__primitives__, \
            f'Unknown operator `{head}` in formula `{formula}`. ' \
            f'Available operators: {cls.__primitives__.keys()}'

        args = [cls(arg, **kwds) for arg in tail]  # make TLTs of formula args
        return cls.__primitives__[head](*args)

    @classmethod
    def __new_init__(cls, formula=..., builder=..., approx=..., setmap=..., times=...):
        """Initialize a bare TLT instance with the provided internal fields."""
        self = super(TLT, cls).__new__(cls)

        # Lock the selected language for this instance
        self.__primitives__ = self.__primitives__

        self._formula = formula if formula is not ... else '_0'

        # If constructed with the absurd set, then the TLT is also absurd, i.e. cannot be realized.
        self._builder = builder if builder is not ... else ABSURD

        self._approx = approx if approx is not ... else APPROXDIR.EXACT

        # Sets are associated with names using ReferredSets.
        self._setmap = setmap if setmap is not ... else idict()

        self.inherit_requirements(self._builder)

        return self

    _formula: TLExpr
    _builder: SetBuilder[R]
    _approx: APPROXDIR
    _setmap: SetMap[R]

    def __repr__(self) -> str:
        cls = type(self).__name__
        approx = str(self._approx)
        formula = str(self._formula)
        return f'{cls}({approx}, {formula})'

    def realize(self, impl: Impl[R], memoize: bool = False) -> R:
        """Realize this TLT into a concrete set R using `impl`.

        Raises if:
            - some proposition is unbound
            - required Impl operations are missing
            - approximation is INVALID

        memoize=True is reserved for a future optimization where realized sets
        may be cached on the node.
        """
        self.assert_realizable(impl)
        out = self._builder(impl, **self._setmap)
        if memoize:
            raise NotImplementedError() # TODO: builder = Set(out)
        return out

    def assert_realizable(self, impl: Optional[Impl[R]] = None) -> None:
        """Validate that this TLT can be realized, optionally against `impl`."""
        for name, sb in self._setmap.items():
            if sb is None: 
                raise Exception(f'Missing proposition `{name}`')
        if impl is not None and bool(missing := self.missing_ops(impl)):
            raise Exception(f'Missing from implementation: {", ".join(missing)}')
        if self._approx is APPROXDIR.INVALID:
            raise Exception('Invalid approximation. TLT operational semantics of formula does not hold.')

    def is_realizable(self, impl: Optional[Impl[R]] = None) -> bool:
        """Return True if assert_realizable would succeed."""
        try:
            self.assert_realizable(impl)
        except Exception:
            return False
        else:
            return True

    def iter_frml(self, formula: Optional[TLExpr] = None, **kwds):
        """Yield sub-formulas in post-order; optionally only terminals.

        Parameters:
            - formula: root to traverse (defaults to self._formula)
            - only_terminals: if True, yield only terminal sub-formulas
        """
        only_terminals = kwds.get('only_terminals', False)
        if formula is None:
            formula = self._formula
        _, *args = formula
        for arg in args:
            yield from self.iter_frml(arg, **kwds)
        if not (only_terminals and args):
            yield formula

    def iter_free(self):
        """Iterate names of free (unbound) propositions in this TLT."""
        yield from filter(lambda p: self._setmap[p] is None, self._setmap)

__new__(arg, **kwds)

Construct a TLT by dispatching on the kind of arg.

Source code in src/pyspect/tlt.py
def __new__(cls, arg: TLTLike[R], **kwds: TLTLike[R]) -> TLT[R]:
    """Construct a TLT by dispatching on the kind of `arg`."""
    if 'where' in kwds: kwds |= kwds.pop('where')
    return (cls.__new_from_tlt__(arg, **kwds)      if isinstance(arg, TLT) else
            cls.__new_from_prop__(arg, **kwds)     if isinstance(arg, str) else
            cls.__new_from_builder__(arg, **kwds)  if isinstance(arg, Callable) else
            cls.__new_from_formula__(arg, **kwds))

__new_from_builder__(sb, **kwds) classmethod

Create a TLT from a constant SetBuilder, assigning it a unique name.

Source code in src/pyspect/tlt.py
@classmethod
@TLTDebugger.wrap('builder')
def __new_from_builder__(cls, sb: SetBuilder, **kwds: TLTLike[R]) -> TLT[R]:
    """Create a TLT from a constant SetBuilder, assigning it a unique name."""
    # Assume a formula "_0" where the set exactly represent the prop "_0".
    # In reality, we define a unique ID `uid` instead of "_0". We add one
    # extra level of indirection with a ReferredSet for `uid` and 
    # letting `setmap` hold the actual set builder `sb`. This way, the root
    # TLT will hold a full formula that refers to even constant sets
    # (information is not lost as when internally binding constant sets to
    # builder functions).
    name = f'_{sb.uid}'
    formula = name # UID is a proposition
    self = cls.__new_init__(formula, ReferredSet(name), setmap={name: sb})

    self.inherit_requirements(sb)

    return self

__new_from_formula__(formula, **kwds) classmethod

Create a TLT from a TLExpr by invoking registered primitives.

Source code in src/pyspect/tlt.py
@classmethod
@TLTDebugger.wrap('formula')
def __new_from_formula__(cls, formula: 'TLExpr', **kwds: TLTLike[R]) -> TLT[R]:
    """Create a TLT from a TLExpr by invoking registered primitives."""
    if isinstance(formula, str):
        # If formula is a string, it is a proposition
        return cls.__new_from_prop__(formula, **kwds)

    # Otherwise, it is a non-trivial formula
    head, *tail = formula

    assert head in cls.__primitives__, \
        f'Unknown operator `{head}` in formula `{formula}`. ' \
        f'Available operators: {cls.__primitives__.keys()}'

    args = [cls(arg, **kwds) for arg in tail]  # make TLTs of formula args
    return cls.__primitives__[head](*args)

__new_from_prop__(prop, **kwds) classmethod

Create a TLT from a proposition name, optionally binding it from kwds.

Source code in src/pyspect/tlt.py
@classmethod
@TLTDebugger.wrap('prop')
def __new_from_prop__(cls, prop: str, **kwds: TLTLike[R]) -> TLT[R]:
    """Create a TLT from a proposition name, optionally binding it from kwds."""
    return (cls(kwds.pop(prop), **kwds) if prop in kwds else
            cls.__new_init__(prop, ReferredSet(prop), setmap={prop: None}))

__new_from_tlt__(tlt, **kwds) classmethod

Rebuild a TLT from an existing one, optionally rebinding free props.

Source code in src/pyspect/tlt.py
@classmethod
@TLTDebugger.wrap('tlt')
def __new_from_tlt__(cls, tlt: TLT[R], **kwds: TLTLike[R]) -> TLT[R]:
    """Rebuild a TLT from an existing one, optionally rebinding free props."""
    # Create new TLT with updated information. With this, we can bind new 
    # TLT-like objects to free variables/propositions. We do not allow 
    # updates to existing propositions. First collect all updatable props.
    kwds = {prop: kwds[prop] if prop in kwds else sb
            for prop, sb in tlt._setmap.items()
            if prop in kwds or sb is not None}

    # It is best to reconstruct builders in the "top-level" TLT when there
    # is updates to the tree since we have modified how 
    return cls.__new_from_formula__(tlt._formula, **kwds)

__new_init__(formula=..., builder=..., approx=..., setmap=..., times=...) classmethod

Initialize a bare TLT instance with the provided internal fields.

Source code in src/pyspect/tlt.py
@classmethod
def __new_init__(cls, formula=..., builder=..., approx=..., setmap=..., times=...):
    """Initialize a bare TLT instance with the provided internal fields."""
    self = super(TLT, cls).__new__(cls)

    # Lock the selected language for this instance
    self.__primitives__ = self.__primitives__

    self._formula = formula if formula is not ... else '_0'

    # If constructed with the absurd set, then the TLT is also absurd, i.e. cannot be realized.
    self._builder = builder if builder is not ... else ABSURD

    self._approx = approx if approx is not ... else APPROXDIR.EXACT

    # Sets are associated with names using ReferredSets.
    self._setmap = setmap if setmap is not ... else idict()

    self.inherit_requirements(self._builder)

    return self

assert_realizable(impl=None)

Validate that this TLT can be realized, optionally against impl.

Source code in src/pyspect/tlt.py
def assert_realizable(self, impl: Optional[Impl[R]] = None) -> None:
    """Validate that this TLT can be realized, optionally against `impl`."""
    for name, sb in self._setmap.items():
        if sb is None: 
            raise Exception(f'Missing proposition `{name}`')
    if impl is not None and bool(missing := self.missing_ops(impl)):
        raise Exception(f'Missing from implementation: {", ".join(missing)}')
    if self._approx is APPROXDIR.INVALID:
        raise Exception('Invalid approximation. TLT operational semantics of formula does not hold.')

construct(arg, **kwds) classmethod

Explicit constructor alias to apply usual dispatch rules.

Source code in src/pyspect/tlt.py
@classmethod
def construct(cls, arg: TLTLike[R], **kwds: TLTLike[R]) -> TLT[R]:
    """Explicit constructor alias to apply usual dispatch rules."""
    return cls(arg, **kwds)

is_realizable(impl=None)

Return True if assert_realizable would succeed.

Source code in src/pyspect/tlt.py
def is_realizable(self, impl: Optional[Impl[R]] = None) -> bool:
    """Return True if assert_realizable would succeed."""
    try:
        self.assert_realizable(impl)
    except Exception:
        return False
    else:
        return True

iter_free()

Iterate names of free (unbound) propositions in this TLT.

Source code in src/pyspect/tlt.py
def iter_free(self):
    """Iterate names of free (unbound) propositions in this TLT."""
    yield from filter(lambda p: self._setmap[p] is None, self._setmap)

iter_frml(formula=None, **kwds)

Yield sub-formulas in post-order; optionally only terminals.

Parameters:

Name Type Description Default
- formula

root to traverse (defaults to self._formula)

required
- only_terminals

if True, yield only terminal sub-formulas

required
Source code in src/pyspect/tlt.py
def iter_frml(self, formula: Optional[TLExpr] = None, **kwds):
    """Yield sub-formulas in post-order; optionally only terminals.

    Parameters:
        - formula: root to traverse (defaults to self._formula)
        - only_terminals: if True, yield only terminal sub-formulas
    """
    only_terminals = kwds.get('only_terminals', False)
    if formula is None:
        formula = self._formula
    _, *args = formula
    for arg in args:
        yield from self.iter_frml(arg, **kwds)
    if not (only_terminals and args):
        yield formula

realize(impl, memoize=False)

Realize this TLT into a concrete set R using impl.

Raises if
  • some proposition is unbound
  • required Impl operations are missing
  • approximation is INVALID

memoize=True is reserved for a future optimization where realized sets may be cached on the node.

Source code in src/pyspect/tlt.py
def realize(self, impl: Impl[R], memoize: bool = False) -> R:
    """Realize this TLT into a concrete set R using `impl`.

    Raises if:
        - some proposition is unbound
        - required Impl operations are missing
        - approximation is INVALID

    memoize=True is reserved for a future optimization where realized sets
    may be cached on the node.
    """
    self.assert_realizable(impl)
    out = self._builder(impl, **self._setmap)
    if memoize:
        raise NotImplementedError() # TODO: builder = Set(out)
    return out

select(primitives) classmethod

Select the set of primitive operator implementations for this class.

Source code in src/pyspect/tlt.py
@classmethod
def select(cls, primitives):
    """Select the set of primitive operator implementations for this class."""
    cls.__primitives__ = primitives

TLTDebugger

Internal helper to add lightweight debugging and tracing to TLT building.

Use TLT.debug(print=True, indent=2) to enable, and TLT.nodebug() to reset. Arbitrary keyword args starting with '__debug' are collected and forwarded.

Source code in src/pyspect/tlt.py
class TLTDebugger:
    """Internal helper to add lightweight debugging and tracing to TLT building.

    Use TLT.debug(print=True, indent=2) to enable, and TLT.nodebug() to reset.
    Arbitrary keyword args starting with '__debug' are collected and forwarded.
    """

    __debug = {}
    __debug_defaults = {'print': False, 'indent': 0}

    @classmethod
    def nodebug(cls):
        """Reset debug options to defaults for subsequent constructions."""
        for k in list(cls.__debug):
            cls.__debug.pop(k, None) # remove to set default

    @classmethod
    def debug(cls, **kwds):
        """Set debug options.

        Supported options (with prefix '__debug_' accepted on wrapper):
            - print: bool, enable/disable printing
            - indent: int, indentation level increment per nested step

        Pass Ellipsis to unset a debug option.
        """
        cls.__debug.update(collect_prefix(kwds, '__debug_'))
        for k, v in cls.__debug.items():
            if v is Ellipsis:
                cls.__debug.pop(k, None) # remove to set default

    @classmethod
    def wrap(cls, msg: str):
        """Decorator to print debug information for TLT construction.

        - Enable with `TLT.debug(print=True)`.
        - Choose indentation size with `TLT.debug(indent=...)`.
        """

        def decorator(func):
            @wraps(func)
            def wrapper(*args, **kwds):

                dbg = cls.__debug_defaults | collect_prefix(kwds, '__debug', remove=True)

                if dbg['print']:
                    print('> ' * dbg['indent'] + msg.ljust(7), kwds)
                    dbg['indent'] = dbg['indent'] + 1

                kwds |= prefix_keys(dbg, '__debug_')
                return func(*args, **kwds)
            return wrapper
        return decorator

debug(**kwds) classmethod

Set debug options.

Supported options (with prefix '__debug_' accepted on wrapper): - print: bool, enable/disable printing - indent: int, indentation level increment per nested step

Pass Ellipsis to unset a debug option.

Source code in src/pyspect/tlt.py
@classmethod
def debug(cls, **kwds):
    """Set debug options.

    Supported options (with prefix '__debug_' accepted on wrapper):
        - print: bool, enable/disable printing
        - indent: int, indentation level increment per nested step

    Pass Ellipsis to unset a debug option.
    """
    cls.__debug.update(collect_prefix(kwds, '__debug_'))
    for k, v in cls.__debug.items():
        if v is Ellipsis:
            cls.__debug.pop(k, None) # remove to set default

nodebug() classmethod

Reset debug options to defaults for subsequent constructions.

Source code in src/pyspect/tlt.py
@classmethod
def nodebug(cls):
    """Reset debug options to defaults for subsequent constructions."""
    for k in list(cls.__debug):
        cls.__debug.pop(k, None) # remove to set default

wrap(msg) classmethod

Decorator to print debug information for TLT construction.

  • Enable with TLT.debug(print=True).
  • Choose indentation size with TLT.debug(indent=...).
Source code in src/pyspect/tlt.py
@classmethod
def wrap(cls, msg: str):
    """Decorator to print debug information for TLT construction.

    - Enable with `TLT.debug(print=True)`.
    - Choose indentation size with `TLT.debug(indent=...)`.
    """

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwds):

            dbg = cls.__debug_defaults | collect_prefix(kwds, '__debug', remove=True)

            if dbg['print']:
                print('> ' * dbg['indent'] + msg.ljust(7), kwds)
                dbg['indent'] = dbg['indent'] + 1

            kwds |= prefix_keys(dbg, '__debug_')
            return func(*args, **kwds)
        return wrapper
    return decorator

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)

Identity(arg)

Identity helper that constructs a TLT from any TLTLike input.

Source code in src/pyspect/tlt.py
def Identity[R](arg: TLTLike[R]) -> TLT[R]:
    """Identity helper that constructs a TLT from any TLTLike input."""
    return TLT(arg)

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)

collect_keys(d, *keys, default=...)

Collect a subset of keys from d.

Behavior: - If default is Ellipsis (the default), include only keys that exist in d. - Otherwise, include all requested keys, filling missing ones with default.

Returns: - New dict containing the selected keys.

Source code in src/pyspect/utils/__init__.py
def collect_keys(d: dict, *keys, default=...):
    """Collect a subset of keys from `d`.

    Behavior:
    - If `default` is Ellipsis (the default), include only keys that exist in `d`.
    - Otherwise, include all requested `keys`, filling missing ones with `default`.

    Returns:
    - New dict containing the selected keys.
    """
    if default is Ellipsis:
        return {k: d[k] for k in keys if k in d}
    else:
        return {k: d.get(k, default) for k in keys}

collect_prefix(d, prefix, remove=False)

Extract and remove items whose keys start with prefix.

Side effect: - Matching items are popped from d.

Parameters: - d: source dictionary (mutated) - prefix: string to match at the start of each key - remove: if True, strip prefix from keys in the returned dict; if False, keep original keys

Returns: - New dict of the extracted items.

Source code in src/pyspect/utils/__init__.py
def collect_prefix(d: Dict[str, Any], prefix: str, remove=False) -> Dict[str, Any]:
    """Extract and remove items whose keys start with `prefix`.

    Side effect:
    - Matching items are popped from `d`.

    Parameters:
    - d: source dictionary (mutated)
    - prefix: string to match at the start of each key
    - remove: if True, strip `prefix` from keys in the returned dict;
              if False, keep original keys

    Returns:
    - New dict of the extracted items.
    """
    if remove:
        return {k.removeprefix(prefix): d.pop(k)
                for k in list(d) if k.startswith(prefix)}
    else:
        return {k: d.pop(k)
                for k in list(d) if k.startswith(prefix)}

flatten(nested, *, sep='_', inplace=False)

Flatten nested mappings into a single level by joining keys with sep.

Example: - {"a": {"b": 1}, "c": 2} -> {"a_b": 1, "c": 2} (sep="_")

Parameters: - nested: mapping to flatten; nested values that are mappings are expanded - sep: string inserted between joined key parts - inplace: if True and nested is mutable, mutate it in place; otherwise return a new dict

Returns: - A flat dict (or the mutated input when inplace=True).

Source code in src/pyspect/utils/__init__.py
def flatten(nested: Mapping[str, Any], *, sep: str = "_", inplace: bool = False) -> Dict[str, Any]:
    """Flatten nested mappings into a single level by joining keys with `sep`.

    Example:
    - {"a": {"b": 1}, "c": 2} -> {"a_b": 1, "c": 2}  (sep="_")

    Parameters:
    - nested: mapping to flatten; nested values that are mappings are expanded
    - sep: string inserted between joined key parts
    - inplace: if True and `nested` is mutable, mutate it in place; otherwise return a new dict

    Returns:
    - A flat dict (or the mutated input when `inplace=True`).
    """
    if inplace:
        for k, v in list(nested.items()):
            if isinstance(v, Mapping):
                for kk, vv in flatten(v, sep=sep).items():
                    nested[f"{k}{sep}{kk}"] = vv
                del nested[k]
        return nested
    else:
        out: Dict[str, Any] = {}
        for k, v in nested.items():
            if isinstance(v, Mapping):
                for kk, vv in flatten(v, sep=sep).items():
                    out[f"{k}{sep}{kk}"] = vv
            else:
                out[k] = v
        return out

get_malformed(formula)

Return the first malformed subexpression, or None if well-formed.

Parameters:

Name Type Description Default
- formula

TLExpr to check

required

Returns:

Type Description
Optional[TLExpr]
  • None if the expression is well-formed; otherwise the first offending subexpression
Well-formedness rules
  • Propositions: non-empty str, or any SetBuilder instance
  • Operator tuples: length 1..3, head must be str, and all arguments recursively valid
Source code in src/pyspect/logics.py
def get_malformed(formula: TLExpr) -> Optional[TLExpr]:
    """Return the first malformed subexpression, or None if well-formed.

    Parameters:
        - formula: TLExpr to check

    Returns:
        - None if the expression is well-formed; otherwise the first offending subexpression

    Well-formedness rules:
        - Propositions: non-empty str, or any SetBuilder instance
        - Operator tuples: length 1..3, head must be str, and all arguments recursively valid

    """
    if isinstance(formula, str): 
        return None if formula else formula # OK unless empty string
    elif isinstance(formula, SetBuilder):
        return None # OK, constant
    elif isinstance(formula, tuple) and len(formula) <= 3:
        head, *tail = formula
        if not isinstance(head, str): return formula # Malformed
        for arg in tail:
            if x := get_malformed(arg):
                return x # Malformed
        return None # OK
    else:
        return formula # Malformed

get_props(formula)

Collect proposition names appearing in a formula.

  • String terminals are treated as proposition names and included.
  • SetBuilder terminals are constants and contribute no proposition names.
  • Operator tuples are traversed recursively.

Parameters: - formula: TLExpr to inspect

Returns: - Set of unique proposition names found in formula

Source code in src/pyspect/logics.py
def get_props(formula: TLExpr) -> set[str]:
    """Collect proposition names appearing in a formula.

    - String terminals are treated as proposition names and included.
    - SetBuilder terminals are constants and contribute no proposition names.
    - Operator tuples are traversed recursively.

    Parameters:
    - formula: TLExpr to inspect

    Returns:
    - Set of unique proposition names found in `formula`
    """
    if isinstance(formula, str):
        return {formula}
    elif isinstance(formula, SetBuilder):
        return set()
    elif isinstance(formula, tuple):
        head, *tail = formula
        props = set()
        for arg in tail:
            props |= get_props(arg)
        return props
    else:
        raise TypeError(f"Unknown formula type: {type(formula)}")

iterwin(seq, winlen=1)

Yield fixed-size windows from an indexable sequence by striding.

Equivalent to: zip((seq[i::winlen] for i in range(winlen))). For generic iterables, prefer: zip([iter(seq)] * winlen).

Parameters: - seq: indexable sequence (supports slicing) - winlen: positive window/stride length

Yields: - Tuples of length winlen with elements at positions i mod winlen

Source code in src/pyspect/utils/__init__.py
def iterwin(seq, winlen=1):
    """Yield fixed-size windows from an indexable sequence by striding.

    Equivalent to: zip(*(seq[i::winlen] for i in range(winlen))).
    For generic iterables, prefer: zip(*[iter(seq)] * winlen).

    Parameters:
    - seq: indexable sequence (supports slicing)
    - winlen: positive window/stride length

    Yields:
    - Tuples of length `winlen` with elements at positions i mod winlen
    """
    # Works for indexables; for iterables, use: zip(*[iter(seq)]*winlen)
    slices = [seq[i::winlen] for i in range(winlen)]
    yield from zip(*slices)

prefix_keys(d, prefix)

Return a new dict with prefix added to every key in d.

Source code in src/pyspect/utils/__init__.py
def prefix_keys(d: Mapping[str, Any], prefix: str) -> Dict[str, Any]:
    """Return a new dict with `prefix` added to every key in `d`."""
    return {f"{prefix}{k}": v for k, v in d.items()}

replace_prop(formula, prop, expr)

Replace all occurrences of proposition prop with expr.

Parameters:

Name Type Description Default
- formula

operator tuple (TLOpNullary/Unary/Binary)

required
- prop

proposition name to replace

required
- expr

replacement sub-expression

required

Returns:

Type Description
TLExpr
  • New TLExpr with replacements applied
Notes
  • This function expects formula to be an operator tuple (not a raw str or SetBuilder). It descends recursively and replaces terminals that match prop.
  • For nullary or terminal positions, if the terminal equals prop, it is replaced by expr.
Source code in src/pyspect/logics.py
def replace_prop(formula: TLExpr, prop: str, expr: TLExpr) -> TLExpr:
    """Replace all occurrences of proposition `prop` with `expr`.

    Parameters:
        - formula: operator tuple (TLOpNullary/Unary/Binary)
        - prop: proposition name to replace
        - expr: replacement sub-expression

    Returns:
        - New TLExpr with replacements applied

    Notes:
        - This function expects `formula` to be an operator tuple (not a raw str or SetBuilder).
          It descends recursively and replaces terminals that match `prop`.
        - For nullary or terminal positions, if the terminal equals `prop`, it is replaced by `expr`.
    """
    head, *tail = formula
    if tail:
        # formula is an operator expression
        # => go down in arguments to replace prop
        return (head, *map(lambda arg: replace_prop(arg, prop, expr), tail))
    else:
        # formula is a terminal
        # => if terminal == prop, replace with expr
        return expr if head == prop else formula

setdefaults(d, *args, **kwds)

Set default key/value pairs on dict d without overwriting existing keys.

Calling conventions: - Keyword form: setdefaults(d, a=1, b=2) - Dict form: setdefaults(d, {'a': 1, 'b': 2}) - Variadic kv: setdefaults(d, 'a', 1, 'b', 2) # even number of args

Raises: - TypeError/ValueError if the calling convention is invalid.

Source code in src/pyspect/utils/__init__.py
def setdefaults(d: dict, *args, **kwds) -> None:
    """Set default key/value pairs on dict `d` without overwriting existing keys.

    Calling conventions:
    - Keyword form: setdefaults(d, a=1, b=2)
    - Dict form:    setdefaults(d, {'a': 1, 'b': 2})
    - Variadic kv:  setdefaults(d, 'a', 1, 'b', 2)  # even number of args

    Raises:
    - TypeError/ValueError if the calling convention is invalid.
    """
    if not args:
        if not kwds:
            raise TypeError("setdefaults expected defaults")
        defaults = kwds
    elif len(args) == 1:
        (defaults,) = args
        if not isinstance(defaults, dict):
            raise TypeError("single-arg form must be a dict")
        if kwds:
            raise TypeError("cannot mix dict arg with keyword args")
    else:
        if kwds:
            raise TypeError("cannot mix variadic kv with keyword args")
        if len(args) % 2 != 0:
            raise ValueError("variadic kv form needs even number of args")
        defaults = {k: v for k, v in iterwin(args, 2)}
    for k, v in defaults.items():
        d.setdefault(k, v)