Reference
Primitives for temporal logic trees (TLTs).
This module defines: - primitive: a decorator class to register TLT primitives tied to tuple-form formulas - Derived connectives built from equivalences (e.g., Minus, Implies, Eventually) - Different sets of primitive TLTs for propositional and temporal operators
Concepts
- A primitive is specified by a TLExpr (operator head and argument names). Decorating a factory function with @primitive(("OP", "x", "y")) binds the function to that operator and returns a single-element set of a primitive TLT. Union of such sets forms the underlying grammar/logic fragment.
- A decorated factory should return (SetBuilder[R], APPROXDIR) which specify how
to realize the set semantics and the approximation direction the primitive contributes.
The decorator constructs a TLT by:
- Substituting child formulas into the operator head
- Composing the children's set maps
- Propagating requirements/constraints
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
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}>'
primitive
Decorator class for declaring TLT primitives.
Usage:
- Specify the operator form and argument names:
@primitive(AND('_1', '_2'))
def And(_1: TLTLike[R], _2: TLTLike[R]) -> tuple[SetBuilder[R], APPROXDIR]:
...
The decorator registers a callable under the operator head ("AND") that,
when invoked with TLTLike arguments, returns a constructed TLT[R]. The
arguments to the decorated function are mapped by name from the provided
operands by order as they appear in the formula. The formula must consist
of only a single connective.
Notes: - The decorated function receives named arguments matching the formula's parameter names (strings in the TLExpr tail) and should return the pair (SetBuilder[R], APPROXDIR) describing the primitive's semantics and its approximation contribution.
Source code in src/pyspect/primitives.py
class primitive[R, **P]:
"""Decorator class for declaring TLT primitives.
Usage:
- Specify the operator form and argument names:
```
@primitive(AND('_1', '_2'))
def And(_1: TLTLike[R], _2: TLTLike[R]) -> tuple[SetBuilder[R], APPROXDIR]:
...
```
The decorator registers a callable under the operator head ("AND") that,
when invoked with TLTLike arguments, returns a constructed TLT[R]. The
arguments to the decorated function are mapped by name from the provided
operands by order as they appear in the formula. The formula must consist
of only a single connective.
Notes:
- The decorated function receives named arguments matching the formula's
parameter names (strings in the TLExpr tail) and should return the pair
(SetBuilder[R], APPROXDIR) describing the primitive's semantics and its
approximation contribution.
"""
type PrimitiveFunc = Callable[P, TLT[R]]
type PrimitiveFactory = Callable[P, tuple[SetBuilder[R], APPROXDIR]]
formula: TLExpr
head: str
tail: tuple[TLExpr, ...]
def __init__(self, formula: TLExpr):
"""Bind this primitive to a formula shape.
Args:
formula: a TLExpr operator tuple whose head is the operator name and
whose tail elements are unique proposition names (str)
"""
assert isinstance(formula, tuple), "Formula must be a tuple"
assert isinstance(formula[0], str), "Operator name of formula must be a string"
assert all(isinstance(arg, str) for arg in formula[1:]), "Arguments of formula must be propositions (string) for primitives"
head, *tail = formula
assert len(tail) == len(set(tail)), "Argument names must be unique"
self.formula = formula
self.head = head
self.tail = tail
def __call__(self, func: PrimitiveFactory) -> idict[str, PrimitiveFunc]:
"""Decorator entry: wrap `func` to build a TLT node for this primitive.
The wrapped function:
- Validates/assembles the operator formula with the child formulas
- Calls `func` with named arguments mapped from the provided children
- Constructs a TLT using the returned (SetBuilder, APPROXDIR)
- Merges children's set maps and inherits requirements
Returns:
idict mapping {connective: function}
"""
@wraps(func)
def wrapper(*args: P.args) -> TLT[R]:
formula = (self.head, *[arg._formula if isinstance(arg, TLT) else arg for arg in args])
builder, approx = func(**{name: arg for name, arg in zip(self.tail, args)})
setmap = idict(sum([list(arg._setmap.items()) for arg in args if isinstance(arg, TLT)], []))
tree = TLT.__new_init__(formula, builder, approx, setmap)
tree.inherit_requirements(*args)
return tree
return idict({self.head: wrapper})
def define_as(self, equiv: TLExpr) -> idict[str, Callable[..., TLT]]:
"""Define this operator via a pure logical equivalence.
This produces a thin wrapper that expands calls to this operator into
the provided equivalent TLExpr, without supplying a new SetBuilder.
Constraints:
- `equiv` must be well-formed
- `equiv` must reference exactly the same set of proposition names as
the original operator formula
Returns:
idict mapping {connective: expansion_function}
"""
assert (x := get_malformed(equiv)) is None, f"Equivalent formula must be well-formed, not {x}"
assert get_props(self.formula) == get_props(equiv), "Formula and equivalent must have the same set of propositions"
func = lambda *args: TLT(equiv, **{name: arg for name, arg in zip(self.tail, args)})
return idict({self.head: func})
__call__(func)
Decorator entry: wrap func
to build a TLT node for this primitive.
The wrapped function:
- Validates/assembles the operator formula with the child formulas
- Calls func
with named arguments mapped from the provided children
- Constructs a TLT using the returned (SetBuilder, APPROXDIR)
- Merges children's set maps and inherits requirements
Returns:
Type | Description |
---|---|
idict[str, PrimitiveFunc]
|
idict mapping {connective: function} |
Source code in src/pyspect/primitives.py
def __call__(self, func: PrimitiveFactory) -> idict[str, PrimitiveFunc]:
"""Decorator entry: wrap `func` to build a TLT node for this primitive.
The wrapped function:
- Validates/assembles the operator formula with the child formulas
- Calls `func` with named arguments mapped from the provided children
- Constructs a TLT using the returned (SetBuilder, APPROXDIR)
- Merges children's set maps and inherits requirements
Returns:
idict mapping {connective: function}
"""
@wraps(func)
def wrapper(*args: P.args) -> TLT[R]:
formula = (self.head, *[arg._formula if isinstance(arg, TLT) else arg for arg in args])
builder, approx = func(**{name: arg for name, arg in zip(self.tail, args)})
setmap = idict(sum([list(arg._setmap.items()) for arg in args if isinstance(arg, TLT)], []))
tree = TLT.__new_init__(formula, builder, approx, setmap)
tree.inherit_requirements(*args)
return tree
return idict({self.head: wrapper})
__init__(formula)
Bind this primitive to a formula shape.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
formula
|
TLExpr
|
a TLExpr operator tuple whose head is the operator name and whose tail elements are unique proposition names (str) |
required |
Source code in src/pyspect/primitives.py
def __init__(self, formula: TLExpr):
"""Bind this primitive to a formula shape.
Args:
formula: a TLExpr operator tuple whose head is the operator name and
whose tail elements are unique proposition names (str)
"""
assert isinstance(formula, tuple), "Formula must be a tuple"
assert isinstance(formula[0], str), "Operator name of formula must be a string"
assert all(isinstance(arg, str) for arg in formula[1:]), "Arguments of formula must be propositions (string) for primitives"
head, *tail = formula
assert len(tail) == len(set(tail)), "Argument names must be unique"
self.formula = formula
self.head = head
self.tail = tail
define_as(equiv)
Define this operator via a pure logical equivalence.
This produces a thin wrapper that expands calls to this operator into the provided equivalent TLExpr, without supplying a new SetBuilder.
Constraints:
- equiv
must be well-formed
- equiv
must reference exactly the same set of proposition names as
the original operator formula
Returns:
Type | Description |
---|---|
idict[str, Callable[..., TLT]]
|
idict mapping {connective: expansion_function} |
Source code in src/pyspect/primitives.py
def define_as(self, equiv: TLExpr) -> idict[str, Callable[..., TLT]]:
"""Define this operator via a pure logical equivalence.
This produces a thin wrapper that expands calls to this operator into
the provided equivalent TLExpr, without supplying a new SetBuilder.
Constraints:
- `equiv` must be well-formed
- `equiv` must reference exactly the same set of proposition names as
the original operator formula
Returns:
idict mapping {connective: expansion_function}
"""
assert (x := get_malformed(equiv)) is None, f"Equivalent formula must be well-formed, not {x}"
assert get_props(self.formula) == get_props(equiv), "Formula and equivalent must have the same set of propositions"
func = lambda *args: TLT(equiv, **{name: arg for name, arg in zip(self.tail, args)})
return idict({self.head: func})
And(_1, _2)
Conjunction (intersection).
Source code in src/pyspect/primitives.py
@primitive(AND('_1', '_2'))
def And[R](_1: TLTLike[R], _2: TLTLike[R]) -> tuple[SetBuilder[R], APPROXDIR]:
"""Conjunction (intersection)."""
b1, a1 = _1._builder, _1._approx
b2, a2 = _2._builder, _2._approx
return (
AppliedSet('intersect', b1, b2),
APPROXDIR.INVALID if APPROXDIR.INVALID in (a1, a2) else
APPROXDIR.INVALID if APPROXDIR.UNDER in (a1, a2) else
APPROXDIR.OVER,
)
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)
Falsify()
Bottom/falsehood (∅).
Source code in src/pyspect/primitives.py
@primitive(FALSIFY())
def Falsify[R]() -> tuple[SetBuilder[R], APPROXDIR]:
"""Bottom/falsehood (∅)."""
return (
EMPTY,
APPROXDIR.EXACT,
)
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)
Next(_1)
Next/predecessor set.
Source code in src/pyspect/primitives.py
@primitive(NEXT('_1'))
def Next[R](_1: TLTLike[R]) -> tuple[SetBuilder[R], APPROXDIR]:
"""Next/predecessor set."""
b1, a1 = _1._builder, _1._approx
ao = APPROXDIR.EXACT # TODO
assert ao != APPROXDIR.INVALID, 'Operator may never be inherently invalid'
return (
AppliedSet('pre', b1, Compl(EMPTY)),
a1 if a1 == APPROXDIR.INVALID else
ao + a1 if ao * a1 == APPROXDIR.EXACT else
a1 if ao == a1 else
APPROXDIR.INVALID,
)
Not(_1)
Logical negation (complement).
Source code in src/pyspect/primitives.py
@primitive(NOT('_1'))
def Not[R](_1: TLTLike[R]) -> tuple[SetBuilder[R], APPROXDIR]:
"""Logical negation (complement)."""
b1, a1 = _1._builder, _1._approx
return (
AppliedSet('complement', b1),
-1 * a1,
)
Or(_1, _2)
Disjunction (union).
Source code in src/pyspect/primitives.py
@primitive(OR('_1', '_2'))
def Or[R](_1: TLTLike[R], _2: TLTLike[R]) -> tuple[SetBuilder[R], APPROXDIR]:
"""Disjunction (union)."""
b1, a1 = _1._builder, _1._approx
b2, a2 = _2._builder, _2._approx
return (
AppliedSet('union', b1, b2),
APPROXDIR.INVALID if APPROXDIR.INVALID in (a1, a2) else
a1 if a1 == a2 else
a2 if a1 == APPROXDIR.EXACT else
a1 if a2 == APPROXDIR.EXACT else
APPROXDIR.INVALID,
)
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)
Until(_1, _2)
Until (reachability): hold _1 until _2 is reached.
Source code in src/pyspect/primitives.py
@primitive(UNTIL('_1', '_2'))
def Until[R](_1: TLTLike[R], _2: TLTLike[R]) -> tuple[SetBuilder[R], APPROXDIR]:
"""Until (reachability): hold _1 until _2 is reached."""
b1, a1 = _1._builder, _1._approx
b2, a2 = _2._builder, _2._approx
ao = APPROXDIR.UNDER # TODO: reach normally implement UNDER
assert ao != APPROXDIR.INVALID, 'Operator may never be inherently invalid'
return (
AppliedSet('reach', b2, b1),
a2 if a2 == APPROXDIR.INVALID else
ao + a2 if ao * a2 == APPROXDIR.EXACT else
a2 if ao == a2 else
APPROXDIR.INVALID,
)
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]
|
|
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)}")
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
|
|
Notes
- This function expects
formula
to be an operator tuple (not a raw str or SetBuilder). It descends recursively and replaces terminals that matchprop
. - For nullary or terminal positions, if the terminal equals
prop
, it is replaced byexpr
.
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