document utilities and python api
This commit is contained in:
parent
aa6ebbfc67
commit
7002246850
6 changed files with 139 additions and 76 deletions
92
README.md
92
README.md
|
|
@ -71,16 +71,102 @@ phi2 = mtl.parse('G(x & y)')
|
||||||
# Note that since `U` is binary, it requires parens.
|
# Note that since `U` is binary, it requires parens.
|
||||||
phi3 = mtl.parse('(x U y)')
|
phi3 = mtl.parse('(x U y)')
|
||||||
|
|
||||||
|
# Weak until (`y` never has to hold).
|
||||||
|
phi4 = mtl.parse('(x W y)')
|
||||||
|
|
||||||
# Whenever `x` holds, then `y` holds in the next two time units.
|
# Whenever `x` holds, then `y` holds in the next two time units.
|
||||||
phi4 = mtl.parse('G(x -> F[0, 2] y)')
|
phi5 = mtl.parse('G(x -> F[0, 2] y)')
|
||||||
|
|
||||||
# We also support timed until.
|
# We also support timed until.
|
||||||
phi5 = mtl.parse('(a U[0, 2] b)')
|
phi6 = mtl.parse('(a U[0, 2] b)')
|
||||||
|
|
||||||
# Finally, if time is discretized, we also support the next operator.
|
# Finally, if time is discretized, we also support the next operator.
|
||||||
# Thus, LTL can also be modeled.
|
# Thus, LTL can also be modeled.
|
||||||
# `a` holds in two time steps.
|
# `a` holds in two time steps.
|
||||||
phi6 = mtl.parse('XX a')
|
phi7 = mtl.parse('XX a')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modal Logic (using python syntax)
|
||||||
|
|
||||||
|
```python
|
||||||
|
a, b = mtl.parse('a'), mtl.parse('b')
|
||||||
|
|
||||||
|
# Eventually `a` will hold.
|
||||||
|
phi1 = a.eventually()
|
||||||
|
|
||||||
|
# `a & b` will always hold.
|
||||||
|
phi2 = (a & b).always()
|
||||||
|
|
||||||
|
# `a` until `b`
|
||||||
|
phi3 = a.until()
|
||||||
|
|
||||||
|
# `a` weak until `b`
|
||||||
|
phi4 = a.weak_until(b)
|
||||||
|
|
||||||
|
# Whenever `a` holds, then `b` holds in the next two time units.
|
||||||
|
phi5 = (a.implies(b.eventually(lo=0, hi=2))).always()
|
||||||
|
|
||||||
|
# We also support timed until.
|
||||||
|
phi6 = a.timed_until(b, lo=0, hi=2)
|
||||||
|
|
||||||
|
# `a` holds in two time steps.
|
||||||
|
phi7 = a >> 2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Boolean Evaluation
|
||||||
|
```python
|
||||||
|
# Assumes piece wise constant interpolation.
|
||||||
|
data = {
|
||||||
|
'a': [(0, True), (1, False), (3, False)]
|
||||||
|
'b': [(0, False), (0.2, True), (4, False)]
|
||||||
|
}
|
||||||
|
|
||||||
|
phi = mtl.parse('F(a | b)')
|
||||||
|
print(phi(data, quantitative=False))
|
||||||
|
# output: True
|
||||||
|
|
||||||
|
# Evaluate at t=3
|
||||||
|
print(phi(data, t=3, quantitative=False))
|
||||||
|
# output: False
|
||||||
|
|
||||||
|
# Evaluate with discrete time
|
||||||
|
phi = mtl.parse('X b')
|
||||||
|
print(phi(data, dt=0.2, quantitative=False))
|
||||||
|
# output: True
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quantitative Evaluate
|
||||||
|
```python
|
||||||
|
# Assumes piece wise constant interpolation.
|
||||||
|
data = {
|
||||||
|
'a': [(0, 100), (1, -1), (3, -2)]
|
||||||
|
'b': [(0, 20), (0.2, 2), (4, -10)]
|
||||||
|
}
|
||||||
|
|
||||||
|
phi = mtl.parse('F(a | b)')
|
||||||
|
print(phi(data))
|
||||||
|
# output: 100
|
||||||
|
|
||||||
|
# Evaluate at t=3
|
||||||
|
print(phi(data, t=3))
|
||||||
|
# output: 2
|
||||||
|
|
||||||
|
# Evaluate with discrete time
|
||||||
|
phi = mtl.parse('X b')
|
||||||
|
print(phi(data, dt=0.2))
|
||||||
|
# output: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utilities
|
||||||
|
```python
|
||||||
|
import mtl
|
||||||
|
from mtl import utils
|
||||||
|
|
||||||
|
print(utils.scope(mtl.parse('XX a'), dt=0.1))
|
||||||
|
# output: 0.2
|
||||||
|
|
||||||
|
print(utils.discretize(mtl.parse('F[0, 0.2] a'), dt=0.1))
|
||||||
|
# output: (a | X a | XX a)
|
||||||
```
|
```
|
||||||
|
|
||||||
[1]: https://link.springer.com/chapter/10.1007/BFb0031988
|
[1]: https://link.springer.com/chapter/10.1007/BFb0031988
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,3 @@ from mtl.ast import TOP, BOT
|
||||||
from mtl.ast import (Interval, And, G, Neg,
|
from mtl.ast import (Interval, And, G, Neg,
|
||||||
AtomicPred, WeakUntil, Next)
|
AtomicPred, WeakUntil, Next)
|
||||||
from mtl.parser import parse
|
from mtl.parser import parse
|
||||||
from mtl.utils import alw, env, andf, orf, until
|
|
||||||
|
|
|
||||||
11
mtl/ast.py
11
mtl/ast.py
|
|
@ -6,6 +6,8 @@ import attr
|
||||||
import funcy as fn
|
import funcy as fn
|
||||||
from lenses import bind
|
from lenses import bind
|
||||||
|
|
||||||
|
from mtl import sugar
|
||||||
|
|
||||||
|
|
||||||
def flatten_binary(phi, op, dropT, shortT):
|
def flatten_binary(phi, op, dropT, shortT):
|
||||||
def f(x):
|
def f(x):
|
||||||
|
|
@ -109,6 +111,7 @@ class Param(NamedTuple):
|
||||||
|
|
||||||
|
|
||||||
def ast_class(cls):
|
def ast_class(cls):
|
||||||
|
cls.__xor__ = sugar.xor
|
||||||
cls.__or__ = _or
|
cls.__or__ = _or
|
||||||
cls.__and__ = _and
|
cls.__and__ = _and
|
||||||
cls.__invert__ = _neg
|
cls.__invert__ = _neg
|
||||||
|
|
@ -119,6 +122,14 @@ def ast_class(cls):
|
||||||
cls.params = property(_params)
|
cls.params = property(_params)
|
||||||
cls.atomic_predicates = property(_atomic_predicates)
|
cls.atomic_predicates = property(_atomic_predicates)
|
||||||
cls.evolve = attr.evolve
|
cls.evolve = attr.evolve
|
||||||
|
cls.iff = sugar.iff
|
||||||
|
cls.implies = sugar.implies
|
||||||
|
# Avoid circular dependence.
|
||||||
|
cls.weak_until = lambda a, b: WeakUntil(a, b)
|
||||||
|
cls.until = sugar.until
|
||||||
|
cls.timed_until = sugar.timed_until
|
||||||
|
cls.always = sugar.alw
|
||||||
|
cls.eventually = sugar.env
|
||||||
|
|
||||||
if not hasattr(cls, "children"):
|
if not hasattr(cls, "children"):
|
||||||
cls.children = property(lambda _: ())
|
cls.children = property(lambda _: ())
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@ from functools import partialmethod, reduce
|
||||||
|
|
||||||
from parsimonious import Grammar, NodeVisitor
|
from parsimonious import Grammar, NodeVisitor
|
||||||
from mtl import ast
|
from mtl import ast
|
||||||
from mtl.utils import iff, implies, xor, timed_until, until
|
from mtl import sugar
|
||||||
from mtl.utils import env, alw
|
|
||||||
|
|
||||||
MTL_GRAMMAR = Grammar(u'''
|
MTL_GRAMMAR = Grammar(u'''
|
||||||
phi = (neg / paren_phi / next / bot / top
|
phi = (neg / paren_phi / next / bot / top
|
||||||
|
|
@ -81,10 +80,10 @@ class MTLVisitor(NodeVisitor):
|
||||||
visit_xor_inner = binop_inner
|
visit_xor_inner = binop_inner
|
||||||
|
|
||||||
visit_and_outer = partialmethod(binop_outer, binop=op.and_)
|
visit_and_outer = partialmethod(binop_outer, binop=op.and_)
|
||||||
visit_iff_outer = partialmethod(binop_outer, binop=iff)
|
visit_iff_outer = partialmethod(binop_outer, binop=sugar.iff)
|
||||||
visit_implies_outer = partialmethod(binop_outer, binop=implies)
|
visit_implies_outer = partialmethod(binop_outer, binop=sugar.implies)
|
||||||
visit_or_outer = partialmethod(binop_outer, binop=op.or_)
|
visit_or_outer = partialmethod(binop_outer, binop=op.or_)
|
||||||
visit_xor_outer = partialmethod(binop_outer, binop=xor)
|
visit_xor_outer = partialmethod(binop_outer, binop=sugar.xor)
|
||||||
|
|
||||||
def generic_visit(self, _, children):
|
def generic_visit(self, _, children):
|
||||||
return children
|
return children
|
||||||
|
|
@ -115,20 +114,20 @@ class MTLVisitor(NodeVisitor):
|
||||||
lo, hi = self.default_interval if not i else i[0]
|
lo, hi = self.default_interval if not i else i[0]
|
||||||
return op(phi, lo=lo, hi=hi)
|
return op(phi, lo=lo, hi=hi)
|
||||||
|
|
||||||
visit_f = partialmethod(unary_temp_op_visitor, op=env)
|
visit_f = partialmethod(unary_temp_op_visitor, op=sugar.env)
|
||||||
visit_g = partialmethod(unary_temp_op_visitor, op=alw)
|
visit_g = partialmethod(unary_temp_op_visitor, op=sugar.alw)
|
||||||
|
|
||||||
def visit_weak_until(self, _, children):
|
def visit_weak_until(self, _, children):
|
||||||
_, _, phi1, _, _, _, phi2, _, _ = children
|
_, _, phi1, _, _, _, phi2, _, _ = children
|
||||||
return ast.WeakUntil(phi1, phi2)
|
return phi1.weak_until(phi2)
|
||||||
|
|
||||||
def visit_until(self, _, children):
|
def visit_until(self, _, children):
|
||||||
_, _, phi1, _, _, _, phi2, _, _ = children
|
_, _, phi1, _, _, _, phi2, _, _ = children
|
||||||
return until(phi1, phi2)
|
return phi1.until(phi2)
|
||||||
|
|
||||||
def visit_timed_until(self, _, children):
|
def visit_timed_until(self, _, children):
|
||||||
_, _, phi1, _, _, itvl, _, phi2, _, _ = children
|
_, _, phi1, _, _, itvl, _, phi2, _, _ = children
|
||||||
return timed_until(phi1, phi2, itvl.lower, itvl.upper)
|
return phi1.timed_until(phi2, itvl.lower, itvl.upper)
|
||||||
|
|
||||||
def visit_id(self, name, _):
|
def visit_id(self, name, _):
|
||||||
return name.text
|
return name.text
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import mtl
|
import mtl
|
||||||
|
from mtl import utils
|
||||||
|
from mtl import sugar
|
||||||
from mtl.hypothesis import MetricTemporalLogicStrategy
|
from mtl.hypothesis import MetricTemporalLogicStrategy
|
||||||
|
|
||||||
from hypothesis import given
|
from hypothesis import given
|
||||||
|
|
@ -28,49 +30,49 @@ def test_inline_context(phi):
|
||||||
|
|
||||||
@given(MetricTemporalLogicStrategy, MetricTemporalLogicStrategy)
|
@given(MetricTemporalLogicStrategy, MetricTemporalLogicStrategy)
|
||||||
def test_timed_until_smoke_test(phi1, phi2):
|
def test_timed_until_smoke_test(phi1, phi2):
|
||||||
mtl.utils.timed_until(phi1, phi2, lo=2, hi=20)
|
sugar.timed_until(phi1, phi2, lo=2, hi=20)
|
||||||
|
|
||||||
|
|
||||||
def test_discretize():
|
def test_discretize():
|
||||||
dt = 0.3
|
dt = 0.3
|
||||||
|
|
||||||
phi = mtl.parse('@ ap1')
|
phi = mtl.parse('@ ap1')
|
||||||
assert mtl.utils.is_discretizable(phi, dt)
|
assert utils.is_discretizable(phi, dt)
|
||||||
phi2 = mtl.utils.discretize(phi, dt)
|
phi2 = utils.discretize(phi, dt)
|
||||||
phi3 = mtl.utils.discretize(phi2, dt)
|
phi3 = utils.discretize(phi2, dt)
|
||||||
assert phi2 == phi3
|
assert phi2 == phi3
|
||||||
|
|
||||||
phi = mtl.parse('G[0.3, 1.2] F[0.6, 1.5] ap1')
|
phi = mtl.parse('G[0.3, 1.2] F[0.6, 1.5] ap1')
|
||||||
assert mtl.utils.is_discretizable(phi, dt)
|
assert utils.is_discretizable(phi, dt)
|
||||||
phi2 = mtl.utils.discretize(phi, dt)
|
phi2 = utils.discretize(phi, dt)
|
||||||
phi3 = mtl.utils.discretize(phi2, dt)
|
phi3 = utils.discretize(phi2, dt)
|
||||||
assert phi2 == phi3
|
assert phi2 == phi3
|
||||||
|
|
||||||
phi = mtl.parse('G[0.3, 1.4] F[0.6, 1.5] ap1')
|
phi = mtl.parse('G[0.3, 1.4] F[0.6, 1.5] ap1')
|
||||||
assert not mtl.utils.is_discretizable(phi, dt)
|
assert not utils.is_discretizable(phi, dt)
|
||||||
|
|
||||||
phi = mtl.parse('G[0.3, 1.2] F ap1')
|
phi = mtl.parse('G[0.3, 1.2] F ap1')
|
||||||
assert not mtl.utils.is_discretizable(phi, dt)
|
assert not utils.is_discretizable(phi, dt)
|
||||||
|
|
||||||
phi = mtl.parse('G[0.3, 1.2] (ap1 U ap2)')
|
phi = mtl.parse('G[0.3, 1.2] (ap1 U ap2)')
|
||||||
assert not mtl.utils.is_discretizable(phi, dt)
|
assert not utils.is_discretizable(phi, dt)
|
||||||
|
|
||||||
phi = mtl.parse('G[0.3, 0.6] ~F[0, 0.3] a')
|
phi = mtl.parse('G[0.3, 0.6] ~F[0, 0.3] a')
|
||||||
assert mtl.utils.is_discretizable(phi, dt)
|
assert utils.is_discretizable(phi, dt)
|
||||||
phi2 = mtl.utils.discretize(phi, dt, distribute=True)
|
phi2 = utils.discretize(phi, dt, distribute=True)
|
||||||
phi3 = mtl.utils.discretize(phi2, dt, distribute=True)
|
phi3 = utils.discretize(phi2, dt, distribute=True)
|
||||||
assert phi2 == phi3
|
assert phi2 == phi3
|
||||||
|
|
||||||
phi = mtl.TOP
|
phi = mtl.TOP
|
||||||
assert mtl.utils.is_discretizable(phi, dt)
|
assert utils.is_discretizable(phi, dt)
|
||||||
phi2 = mtl.utils.discretize(phi, dt)
|
phi2 = utils.discretize(phi, dt)
|
||||||
phi3 = mtl.utils.discretize(phi2, dt)
|
phi3 = utils.discretize(phi2, dt)
|
||||||
assert phi2 == phi3
|
assert phi2 == phi3
|
||||||
|
|
||||||
phi = mtl.BOT
|
phi = mtl.BOT
|
||||||
assert mtl.utils.is_discretizable(phi, dt)
|
assert utils.is_discretizable(phi, dt)
|
||||||
phi2 = mtl.utils.discretize(phi, dt)
|
phi2 = utils.discretize(phi, dt)
|
||||||
phi3 = mtl.utils.discretize(phi2, dt)
|
phi3 = utils.discretize(phi2, dt)
|
||||||
assert phi2 == phi3
|
assert phi2 == phi3
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -78,16 +80,16 @@ def test_scope():
|
||||||
dt = 0.3
|
dt = 0.3
|
||||||
|
|
||||||
phi = mtl.parse('@ap1')
|
phi = mtl.parse('@ap1')
|
||||||
assert mtl.utils.scope(phi, dt) == 0.3
|
assert utils.scope(phi, dt) == 0.3
|
||||||
|
|
||||||
phi = mtl.parse('(@@ap1 | ap2)')
|
phi = mtl.parse('(@@ap1 | ap2)')
|
||||||
assert mtl.utils.scope(phi, dt) == 0.6
|
assert utils.scope(phi, dt) == 0.6
|
||||||
|
|
||||||
phi = mtl.parse('G[0.3, 1.2] F[0.6, 1.5] ap1')
|
phi = mtl.parse('G[0.3, 1.2] F[0.6, 1.5] ap1')
|
||||||
assert mtl.utils.scope(phi, dt) == 1.2 + 1.5
|
assert utils.scope(phi, dt) == 1.2 + 1.5
|
||||||
|
|
||||||
phi = mtl.parse('G[0.3, 1.2] F ap1')
|
phi = mtl.parse('G[0.3, 1.2] F ap1')
|
||||||
assert mtl.utils.scope(phi, dt) == float('inf')
|
assert utils.scope(phi, dt) == float('inf')
|
||||||
|
|
||||||
phi = mtl.parse('G[0.3, 1.2] (ap1 U ap2)')
|
phi = mtl.parse('G[0.3, 1.2] (ap1 U ap2)')
|
||||||
assert mtl.utils.scope(phi, dt) == float('inf')
|
assert utils.scope(phi, dt) == float('inf')
|
||||||
|
|
|
||||||
42
mtl/utils.py
42
mtl/utils.py
|
|
@ -70,7 +70,7 @@ def _discretize(phi, dt, horizon):
|
||||||
upper = min(phi.interval.upper, horizon)
|
upper = min(phi.interval.upper, horizon)
|
||||||
l, u = round(phi.interval.lower / dt), round(upper / dt)
|
l, u = round(phi.interval.lower / dt), round(upper / dt)
|
||||||
|
|
||||||
psis = (next(_discretize(phi.arg, dt, horizon - i), i)
|
psis = (_discretize(phi.arg, dt, horizon - i) >> i
|
||||||
for i in range(l, u + 1))
|
for i in range(l, u + 1))
|
||||||
opf = andf if isinstance(phi, G) else orf
|
opf = andf if isinstance(phi, G) else orf
|
||||||
return opf(*psis)
|
return opf(*psis)
|
||||||
|
|
@ -85,7 +85,7 @@ def _interval_discretizable(itvl, dt):
|
||||||
|
|
||||||
def _distribute_next(phi, i=0):
|
def _distribute_next(phi, i=0):
|
||||||
if isinstance(phi, AtomicPred):
|
if isinstance(phi, AtomicPred):
|
||||||
return mtl.utils.next(phi, i=i)
|
return phi >> i
|
||||||
elif isinstance(phi, Next):
|
elif isinstance(phi, Next):
|
||||||
return _distribute_next(phi.arg, i=i+1)
|
return _distribute_next(phi.arg, i=i+1)
|
||||||
|
|
||||||
|
|
@ -105,44 +105,10 @@ def is_discretizable(phi, dt):
|
||||||
_interval_discretizable(c.interval, dt) for c in phi.walk()
|
_interval_discretizable(c.interval, dt) for c in phi.walk()
|
||||||
if isinstance(c, G))
|
if isinstance(c, G))
|
||||||
|
|
||||||
# EDSL
|
|
||||||
|
|
||||||
|
|
||||||
def alw(phi, *, lo=0, hi=float('inf')):
|
|
||||||
return G(Interval(lo, hi), phi)
|
|
||||||
|
|
||||||
|
|
||||||
def env(phi, *, lo=0, hi=float('inf')):
|
|
||||||
return ~alw(~phi, lo=lo, hi=hi)
|
|
||||||
|
|
||||||
|
|
||||||
def andf(*args):
|
def andf(*args):
|
||||||
return reduce(op.and_, args) if args else mtl.TOP
|
return reduce(op.and_, args) if args else ast.TOP
|
||||||
|
|
||||||
|
|
||||||
def orf(*args):
|
def orf(*args):
|
||||||
return reduce(op.or_, args) if args else mtl.TOP
|
return reduce(op.or_, args) if args else ast.TOP
|
||||||
|
|
||||||
|
|
||||||
def implies(x, y):
|
|
||||||
return ~x | y
|
|
||||||
|
|
||||||
|
|
||||||
def xor(x, y):
|
|
||||||
return (x | y) & ~(x & y)
|
|
||||||
|
|
||||||
|
|
||||||
def iff(x, y):
|
|
||||||
return (x & y) | (~x & ~y)
|
|
||||||
|
|
||||||
|
|
||||||
def next(phi, i=1):
|
|
||||||
return phi >> i
|
|
||||||
|
|
||||||
|
|
||||||
def until(phi, psi):
|
|
||||||
return mtl.ast.WeakUntil(phi, psi) & env(psi)
|
|
||||||
|
|
||||||
|
|
||||||
def timed_until(phi, psi, lo, hi):
|
|
||||||
return env(psi, lo=lo, hi=hi) & alw(until(phi, psi), lo=0, hi=lo)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue