reimplement evaluation using discrete-signals library

This commit is contained in:
Marcell Vazquez-Chanlatte 2018-09-24 00:55:43 -07:00
parent cfbdabf517
commit dd6bd4b6be
12 changed files with 241 additions and 447 deletions

View file

@ -1,7 +1,6 @@
# flake8: noqa # flake8: noqa
from mtl.ast import TOP, BOT from mtl.ast import TOP, BOT
from mtl.ast import (Interval, Or, And, F, G, Neg, from mtl.ast import (Interval, And, G, Neg,
AtomicPred, Until, Next) AtomicPred, WeakUntil, Next)
from mtl.parser import parse from mtl.parser import parse
from mtl.fastboolean_eval import pointwise_sat from mtl.utils import alw, env, andf, orf, until
from mtl.utils import alw, env, andf, orf

View file

@ -6,14 +6,12 @@ import attr
import funcy as fn import funcy as fn
from lenses import bind from lenses import bind
import mtl
def flatten_binary(phi, op, dropT, shortT): def flatten_binary(phi, op, dropT, shortT):
def f(x): def f(x):
return x.args if isinstance(x, op) else [x] return x.args if isinstance(x, op) else [x]
args = [arg for arg in phi.args if arg is not dropT] args = [arg for arg in phi.args if arg != dropT]
if any(arg is shortT for arg in args): if any(arg is shortT for arg in args):
return shortT return shortT
@ -26,7 +24,7 @@ def flatten_binary(phi, op, dropT, shortT):
def _or(exp1, exp2): def _or(exp1, exp2):
return flatten_binary(Or((exp1, exp2)), Or, BOT, TOP) return ~(~exp1 & ~exp2)
def _and(exp1, exp2): def _and(exp1, exp2):
@ -34,21 +32,18 @@ def _and(exp1, exp2):
def _neg(exp): def _neg(exp):
if isinstance(exp, _Bot): if isinstance(exp, Neg):
return _Top()
elif isinstance(exp, _Top):
return _Bot()
elif isinstance(exp, Neg):
return exp.arg return exp.arg
return Neg(exp) return Neg(exp)
def _eval(exp, trace, time=0): def _eval(exp, trace, time=0, *, dt=0.1, quantitative=True):
return mtl.pointwise_sat(exp)(trace, time) from mtl import evaluator
return evaluator.pointwise_sat(exp, dt)(trace, time, quantitative)
def _timeshift(exp, t): def _timeshift(exp, t):
if exp in (BOT, TOP): if exp == BOT:
return exp return exp
for _ in range(t): for _ in range(t):
@ -142,22 +137,12 @@ def _update_itvl(itvl, lookup):
return Interval(*map(_update_param, itvl)) return Interval(*map(_update_param, itvl))
@ast_class
class _Top:
def __repr__(self):
return "TRUE"
@ast_class @ast_class
class _Bot: class _Bot:
def __repr__(self): def __repr__(self):
return "FALSE" return "FALSE"
TOP = _Top()
BOT = _Bot()
@ast_class @ast_class
class AtomicPred: class AtomicPred:
id: str id: str
@ -187,10 +172,6 @@ class NaryOpMTL:
return tuple(self.args) return tuple(self.args)
class Or(NaryOpMTL):
OP = "|"
class And(NaryOpMTL): class And(NaryOpMTL):
OP = "&" OP = "&"
@ -211,21 +192,17 @@ class ModalOp:
return (self.arg,) return (self.arg,)
class F(ModalOp):
OP = "< >"
class G(ModalOp): class G(ModalOp):
OP = "[ ]" OP = "G"
@ast_class @ast_class
class Until: class WeakUntil:
arg1: "Node" arg1: "Node"
arg2: "Node" arg2: "Node"
def __repr__(self): def __repr__(self):
return f"({self.arg1} U {self.arg2})" return f"({self.arg1} W {self.arg2})"
@property @property
def children(self): def children(self):
@ -249,7 +226,7 @@ class Next:
arg: "Node" arg: "Node"
def __repr__(self): def __repr__(self):
return f"@{self.arg}" return f"X{self.arg}"
@property @property
def children(self): def children(self):
@ -259,3 +236,7 @@ class Next:
def type_pred(*args): def type_pred(*args):
ast_types = set(args) ast_types = set(args)
return lambda x: type(x) in ast_types return lambda x: type(x) in ast_types
BOT = _Bot()
TOP = ~BOT

View file

@ -1,183 +0,0 @@
# TODO: figure out how to deduplicate this with robustness
# - Abstract as working on distributive lattice
import operator as op
from functools import singledispatch
import funcy as fn
import traces
import mtl
import mtl.ast
from mtl.utils import const_trace, andf, orf
TRUE_TRACE = const_trace(True)
FALSE_TRACE = const_trace(False)
def negate_trace(x):
return x.operation(TRUE_TRACE, op.xor)
def pointwise_sat(phi, dt=0.1):
def _eval_mtl(x, t=0):
return bool(eval_mtl(phi, dt)(x)[t])
return _eval_mtl
@singledispatch
def eval_mtl(phi, dt):
raise NotImplementedError
def or_traces(xs):
return orf(*xs)
@eval_mtl.register(mtl.Or)
def eval_mtl_or(phi, dt):
fs = [eval_mtl(arg, dt) for arg in phi.args]
def _eval(x):
out = or_traces([f(x) for f in fs])
out.compact()
return out
return _eval
def and_traces(xs):
return andf(*xs)
@eval_mtl.register(mtl.And)
def eval_mtl_and(phi, dt):
fs = [eval_mtl(arg, dt) for arg in phi.args]
def _eval(x):
out = and_traces([f(x) for f in fs])
out.compact()
return out
return _eval
def apply_until(y):
if len(y) == 1:
left, right = y.first_value()
yield (0, min(left, right))
return
periods = list(y.iterperiods())
phi2_next = False
for t, _, (phi1, phi2) in periods[::-1]:
yield (t, phi2 or (phi1 and phi2_next))
phi2_next = phi2
@eval_mtl.register(mtl.Until)
def eval_mtl_until(phi, dt):
f1, f2 = eval_mtl(phi.arg1, dt), eval_mtl(phi.arg2, dt)
def _eval(x):
y1, y2 = f1(x), f2(x)
y = y1.operation(y2, lambda a, b: (a, b))
out = traces.TimeSeries(apply_until(y))
out.compact()
return out
return _eval
@eval_mtl.register(mtl.F)
def eval_mtl_f(phi, dt):
phi = ~mtl.G(phi.interval, ~phi.arg)
return eval_mtl(phi, dt)
@eval_mtl.register(mtl.G)
def eval_mtl_g(phi, dt):
f = eval_mtl(phi.arg, dt)
a, b = phi.interval
if b < a:
return lambda _: TRUE_TRACE
def process_intervals(x):
# Need to add last interval
intervals = fn.chain(x.iterintervals(), [(
x.first_item(),
(float('inf'), None),
)])
for (start, val), (end, val2) in intervals:
start2, end2 = start - b, end + a
if end2 > start2:
yield (start2, val)
if b == float('inf'):
def _eval(x):
y = f(x).slice(a, b)
y.compact()
val = len(y) == 1 and y[a]
return const_trace(val)
else:
def _eval(x):
y = f(x)
if len(y) <= 1:
return y
out = traces.TimeSeries(process_intervals(y)).slice(
y.first_key(), float('inf')
)
out.compact()
return out
return _eval
@eval_mtl.register(mtl.Neg)
def eval_mtl_neg(phi, dt):
f = eval_mtl(phi.arg, dt)
def _eval(x):
out = negate_trace(f(x))
out.compact()
return out
return _eval
@eval_mtl.register(mtl.ast.Next)
def eval_mtl_next(phi, dt):
f = eval_mtl(phi.arg, dt)
def _eval(x):
y = f(x)
out = traces.TimeSeries(((t - dt, v) for t, v in y))
out = out.slice(0, float('inf'))
out.compact()
return out
return _eval
@eval_mtl.register(mtl.AtomicPred)
def eval_mtl_ap(phi, _):
def _eval(x):
out = x[str(phi.id)]
out.compact()
return out
return _eval
@eval_mtl.register(type(mtl.TOP))
def eval_mtl_top(_, _1):
return lambda *_: TRUE_TRACE
@eval_mtl.register(type(mtl.BOT))
def eval_mtl_bot(_, _1):
return lambda *_: FALSE_TRACE

152
mtl/evaluator.py Normal file
View file

@ -0,0 +1,152 @@
# TODO: figure out how to deduplicate this with robustness
# - Abstract as working on distributive lattice
import operator as op
from collections import defaultdict
from functools import reduce, singledispatch
import funcy as fn
from discrete_signals import signal
from mtl import ast
OO = float('inf')
CONST_FALSE = signal([(0, -1)], start=-OO, end=OO, tag=ast.BOT)
CONST_TRUE = signal([(0, 1)], start=-OO, end=OO, tag=ast.TOP)
def to_signal(ts_mapping):
start = min(fn.pluck(0, fn.cat(ts_mapping.values())))
assert start >= 0
signals = (signal(v, start, OO, tag=k) for k, v in ts_mapping.items())
return reduce(op.or_, signals)
def interp(sig, t, tag=None):
# TODO: return function that interpolates the whole signal.
sig = sig.project({tag})
key = sig.data.iloc[sig.data.bisect_right(t) - 1]
return sig[key][tag]
def dense_compose(sig1, sig2, init=None):
sig12 = sig1 | sig2
tags = sig12.tags
def _dense_compose():
state = {tag: init for tag in tags}
for t, val in sig12.items():
state = {k: val.get(k, state[k]) for k in tags}
yield t, state
data = list(_dense_compose())
return sig12.evolve(data=data)
def booleanize_signal(sig):
return sig.transform(lambda mapping: defaultdict(
lambda: None, {k: 2*int(v) - 1 for k, v in mapping.items()}
))
def pointwise_sat(phi, dt=0.1):
f = eval_mtl(phi, dt)
def _eval_mtl(x, t=0, quantitative=False):
sig = to_signal(x)
if not quantitative:
sig = booleanize_signal(sig)
res = interp(f(sig), t, phi)
return res if quantitative else res > 0
return _eval_mtl
@singledispatch
def eval_mtl(phi, dt):
raise NotImplementedError
@eval_mtl.register(ast.And)
def eval_mtl_and(phi, dt):
fs = [eval_mtl(arg, dt) for arg in phi.args]
def _eval(x):
sigs = [f(x) for f in fs]
sig = reduce(lambda x, y: dense_compose(x, y, init=OO), sigs)
return sig.map(lambda v: min(v.values()), tag=phi)
return _eval
def apply_weak_until(left_key, right_key, sig):
prev, max_right = OO, -OO
for t in reversed(sig.times()):
left, right = interp(sig, t, left_key), interp(sig, t, right_key)
max_right = max(max_right, right)
prev = max(right, min(left, prev), -max_right)
yield (t, prev)
@eval_mtl.register(ast.WeakUntil)
def eval_mtl_until(phi, dt):
f1, f2 = eval_mtl(phi.arg1, dt), eval_mtl(phi.arg2, dt)
def _eval(x):
sig = dense_compose(f1(x), f2(x), init=-OO)
data = apply_weak_until(phi.arg1, phi.arg2, sig)
return signal(data, x.start, OO, tag=phi)
return _eval
@eval_mtl.register(ast.G)
def eval_mtl_g(phi, dt):
f = eval_mtl(phi.arg, dt)
a, b = phi.interval
if b < a:
return lambda x: CONST_TRUE.retag({ast.TOP: phi})
def _min(val):
return min(val[phi.arg])
def _eval(x):
return f(x).rolling(a, b).map(_min, tag=phi)
return _eval
@eval_mtl.register(ast.Neg)
def eval_mtl_neg(phi, dt):
f = eval_mtl(phi.arg, dt)
def _eval(x):
return f(x).map(lambda v: -v[phi.arg], tag=phi)
return _eval
@eval_mtl.register(ast.Next)
def eval_mtl_next(phi, dt):
f = eval_mtl(phi.arg, dt)
def _eval(x):
return (f(x) << dt).retag({phi.arg: phi})
return _eval
@eval_mtl.register(ast.AtomicPred)
def eval_mtl_ap(phi, _):
def _eval(x):
return x.project({phi.id}).retag({phi.id: phi})
return _eval
@eval_mtl.register(type(ast.BOT))
def eval_mtl_bot(_, _1):
return lambda x: CONST_FALSE

View file

@ -1,98 +0,0 @@
from functools import reduce, singledispatch
from operator import and_, or_
import funcy as fn
from bitarray import bitarray
import mtl.ast
oo = float('inf')
def get_times(x, tau, lo, hi):
end = min(v.last_key() for v in x.values())
lo, hi = map(float, (lo, hi))
hi = hi + tau if hi + tau <= end else end
lo = lo + tau if lo + tau <= end else end
if lo > hi:
return []
elif hi == lo:
return [lo]
all_times = fn.cat(v.slice(lo, hi).items() for v in x.values())
return sorted(set(fn.pluck(0, all_times)))
def pointwise_sat(mtl):
f = pointwise_satf(mtl)
return lambda x, t: bool(int(f(x, [t]).to01()))
@singledispatch
def pointwise_satf(mtl):
raise NotImplementedError
def bool_op(mtl, conjunction=False):
binop = and_ if conjunction else or_
fs = [pointwise_satf(arg) for arg in mtl.args]
return lambda x, t: reduce(binop, (f(x, t) for f in fs))
@pointwise_satf.register(mtl.Or)
def pointwise_satf_or(mtl):
return bool_op(mtl, conjunction=False)
@pointwise_satf.register(mtl.And)
def pointwise_satf_and(mtl):
return bool_op(mtl, conjunction=True)
def temporal_op(mtl, lo, hi, conjunction=False):
fold = bitarray.all if conjunction else bitarray.any
f = pointwise_satf(mtl.arg)
def sat_comp(x, t):
return bitarray(fold(f(x, get_times(x, tau, lo, hi))) for tau in t)
return sat_comp
@pointwise_satf.register(mtl.F)
def pointwise_satf_f(mtl):
lo, hi = mtl.interval
return temporal_op(mtl, lo, hi, conjunction=False)
@pointwise_satf.register(mtl.G)
def pointwise_satf_g(mtl):
lo, hi = mtl.interval
return temporal_op(mtl, lo, hi, conjunction=True)
@pointwise_satf.register(mtl.Neg)
def pointwise_satf_neg(mtl):
return lambda x, t: ~pointwise_satf(mtl.arg)(x, t)
@pointwise_satf.register(mtl.AtomicPred)
def pointwise_satf_(phi):
return lambda x, t: bitarray(x[str(phi.id)][tau] for tau in t)
@pointwise_satf.register(mtl.Until)
def pointwise_satf_until(phi):
raise NotImplementedError
@pointwise_satf.register(type(mtl.TOP))
def pointwise_satf_top(_):
return lambda _, t: bitarray([True] * len(t))
@pointwise_satf.register(type(mtl.BOT))
def pointwise_satf_bot(_):
return lambda _, t: bitarray([False] * len(t))

View file

@ -4,12 +4,13 @@ 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 from mtl.utils import iff, implies, xor, timed_until, until
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
/ xor_outer / iff_outer / implies_outer / and_outer / or_outer / xor_outer / iff_outer / implies_outer / and_outer / or_outer
/ timed_until / until / g / f / AP) / timed_until / until / weak_until / g / f / AP)
paren_phi = "(" __ phi __ ")" paren_phi = "(" __ phi __ ")"
neg = ("~" / "¬") __ phi neg = ("~" / "¬") __ phi
@ -32,6 +33,7 @@ xor_inner = (phi __ ("⊕" / "^" / "xor") __ xor_inner) / phi
f = ("< >" / "F") interval? __ phi f = ("< >" / "F") interval? __ phi
g = ("[ ]" / "G") interval? __ phi g = ("[ ]" / "G") interval? __ phi
weak_until = "(" __ phi _ "W" _ phi __ ")"
until = "(" __ phi _ "U" _ phi __ ")" until = "(" __ phi _ "U" _ phi __ ")"
timed_until = "(" __ phi _ "U" interval _ phi __ ")" timed_until = "(" __ phi _ "U" interval _ phi __ ")"
interval = "[" __ const_or_unbound __ "," __ const_or_unbound __ "]" interval = "[" __ const_or_unbound __ "," __ const_or_unbound __ "]"
@ -97,7 +99,7 @@ class MTLVisitor(NodeVisitor):
return ast.BOT return ast.BOT
def visit_top(self, *_): def visit_top(self, *_):
return ast.TOP return ~ast.BOT
def visit_interval(self, _, children): def visit_interval(self, _, children):
_, _, left, _, _, _, right, _, _ = children _, _, left, _, _, _, right, _, _ = children
@ -110,15 +112,19 @@ class MTLVisitor(NodeVisitor):
def unary_temp_op_visitor(self, _, children, op): def unary_temp_op_visitor(self, _, children, op):
_, i, _, phi = children _, i, _, phi = children
i = self.default_interval if not i else i[0] lo, hi = self.default_interval if not i else i[0]
return op(i, phi) return op(phi, lo=lo, hi=hi)
visit_f = partialmethod(unary_temp_op_visitor, op=ast.F) visit_f = partialmethod(unary_temp_op_visitor, op=env)
visit_g = partialmethod(unary_temp_op_visitor, op=ast.G) visit_g = partialmethod(unary_temp_op_visitor, op=alw)
def visit_weak_until(self, _, children):
_, _, phi1, _, _, _, phi2, _, _ = children
return ast.WeakUntil(phi1, phi2)
def visit_until(self, _, children): def visit_until(self, _, children):
_, _, phi1, _, _, _, phi2, _, _ = children _, _, phi1, _, _, _, phi2, _, _ = children
return ast.Until(phi1, phi2) return until(phi1, phi2)
def visit_timed_until(self, _, children): def visit_timed_until(self, _, children):
_, _, phi1, _, _, itvl, _, phi2, _, _ = children _, _, phi1, _, _, itvl, _, phi2, _, _ = children

View file

@ -28,4 +28,4 @@ def test_identities(phi):
def test_walk(): def test_walk():
phi = mtl.parse( phi = mtl.parse(
'(([ ][0, 1] ap1 & < >[1,2] ap2) | (@ap1 U ap2))') '(([ ][0, 1] ap1 & < >[1,2] ap2) | (@ap1 U ap2))')
assert len(list((~phi).walk())) == 11 assert len(list((~phi).walk())) == 19

View file

@ -1,121 +1,59 @@
import hypothesis.strategies as st import hypothesis.strategies as st
import traces
from hypothesis import given from hypothesis import given
from pytest import raises
import mtl import mtl
import mtl.boolean_eval
import mtl.fastboolean_eval
from mtl.hypothesis import MetricTemporalLogicStrategy from mtl.hypothesis import MetricTemporalLogicStrategy
"""
TODO: property based test that fasteval should be the same as slow
TODO: property based test that x |= phi == ~(x |= ~phi)
TODO: property based test that ~~phi == phi
TODO: property based test that ~~~phi == ~phi
TODO: property based test that ~phi => phi
TODO: property based test that phi => phi
TODO: property based test that phi <=> phi
TODO: property based test that phi & psi => phi
TODO: property based test that psi => phi | psi
TODO: property based test that (True U psi) => F(psi)
TODO: property based test that G(psi) = ~F(~psi)
TODO: Automatically generate input time series.
"""
x = { x = {
"ap1": traces.TimeSeries([(0, True), (0.1, True), (0.2, False)]), "ap1": [(0, True), (0.1, True), (0.2, False)],
"ap2": "ap2": [(0, False), (0.2, True), (0.5, False)],
traces.TimeSeries([(0, False), (0.2, True), (0.5, False)]), "ap3": [(0, True), (0.1, True), (0.3, False)],
"ap3": "ap4": [(0, False), (0.1, False), (0.3, False)],
traces.TimeSeries([(0, True), (0.1, True), (0.3, False)]), "ap5": [(0, False), (0.1, False), (0.3, True)],
"ap4": "ap6": [(0, True), (float('inf'), True)],
traces.TimeSeries([(0, False), (0.1, False), (0.3, False)]),
"ap5":
traces.TimeSeries([(0, False), (0.1, False), (0.3, True)]),
"ap6":
traces.TimeSeries([(0, True), (float('inf'), True)]),
} }
@given(st.just(mtl.ast.Next(mtl.BOT) | mtl.ast.Next(mtl.TOP))) @given(st.just(mtl.ast.Next(mtl.BOT) | mtl.ast.Next(mtl.TOP)))
def test_eval_smoke_tests(phi): def test_eval_smoke_tests(phi):
mtl_eval9 = mtl.boolean_eval.pointwise_sat(mtl.ast.Next(phi)) assert mtl.parse('~ap4')(x, 0, quantitative=False)
mtl_eval10 = mtl.boolean_eval.pointwise_sat(~mtl.ast.Next(phi)) assert mtl.parse('G[0.1, 0.03] ~ap4')(x, 0, quantitative=False)
assert mtl_eval9(x, 0) != mtl_eval10(x, 0)
phi4 = mtl.parse('~ap4')
mtl_eval11 = mtl.boolean_eval.pointwise_sat(phi4)
assert mtl_eval11(x, 0)
phi5 = mtl.parse('G[0.1, 0.03] ~ap4')
mtl_eval12 = mtl.boolean_eval.pointwise_sat(phi5)
assert mtl_eval12(x, 0)
phi6 = mtl.parse('G[0.1, 0.03] ~ap5') phi6 = mtl.parse('G[0.1, 0.03] ~ap5')
mtl_eval13 = mtl.boolean_eval.pointwise_sat(phi6) assert phi6(x, 0, quantitative=False)
assert mtl_eval13(x, 0) assert phi6(x, 0.2, quantitative=False)
assert mtl_eval13(x, 0.4)
phi7 = mtl.parse('G ~ap4') assert mtl.parse('G ~ap4')(x, 0, quantitative=False)
mtl_eval14 = mtl.boolean_eval.pointwise_sat(phi7) assert mtl.parse('F ap5')(x, 0, quantitative=False)
assert mtl_eval14(x, 0) assert mtl.parse('(ap1 U ap2)')(x, 0, quantitative=False)
assert not mtl.parse('(ap2 U ap2)')(x, 0, quantitative=False)
phi8 = mtl.parse('F ap5')
mtl_eval15 = mtl.boolean_eval.pointwise_sat(phi8)
assert mtl_eval15(x, 0)
phi9 = mtl.parse('(ap1 U ap2)')
mtl_eval16 = mtl.boolean_eval.pointwise_sat(phi9)
assert mtl_eval16(x, 0)
phi10 = mtl.parse('(ap2 U ap2)')
mtl_eval17 = mtl.boolean_eval.pointwise_sat(phi10)
assert not mtl_eval17(x, 0)
with raises(NotImplementedError):
mtl.boolean_eval.eval_mtl(None, None)
@given(MetricTemporalLogicStrategy) @given(MetricTemporalLogicStrategy)
def test_temporal_identities(phi): def test_temporal_identity1(phi):
mtl_eval = mtl.boolean_eval.pointwise_sat(phi) assert ((~phi) >> 1)(x, 0, quantitative=False) \
mtl_eval2 = mtl.boolean_eval.pointwise_sat(~phi) == (~(phi >> 1))(x, 0, quantitative=False)
assert mtl_eval2(x, 0) == (not mtl_eval(x, 0))
mtl_eval3 = mtl.boolean_eval.pointwise_sat(~~phi)
assert mtl_eval3(x, 0) == mtl_eval(x, 0)
mtl_eval4 = mtl.boolean_eval.pointwise_sat(phi & phi)
assert mtl_eval4(x, 0) == mtl_eval(x, 0)
mtl_eval5 = mtl.boolean_eval.pointwise_sat(phi & ~phi)
assert not mtl_eval5(x, 0)
mtl_eval6 = mtl.boolean_eval.pointwise_sat(phi | ~phi)
assert mtl_eval6(x, 0)
mtl_eval7 = mtl.boolean_eval.pointwise_sat(mtl.ast.Until(mtl.TOP, phi))
mtl_eval8 = mtl.boolean_eval.pointwise_sat(mtl.env(phi))
assert mtl_eval7(x, 0) == mtl_eval8(x, 0)
@given(st.just(mtl.BOT)) @given(MetricTemporalLogicStrategy)
def test_fastboolean_equiv(phi): def test_temporal_identity2(phi):
mtl_eval = mtl.fastboolean_eval.pointwise_sat(mtl.alw(phi, lo=0, hi=4)) assert phi(x, 0, quantitative=False) \
mtl_eval2 = mtl.fastboolean_eval.pointwise_sat(~mtl.env(~phi, lo=0, hi=4)) == (not (~phi)(x, 0, quantitative=False))
assert mtl_eval2(x, 0) == mtl_eval(x, 0)
mtl_eval3 = mtl.fastboolean_eval.pointwise_sat(~mtl.alw(~phi, lo=0, hi=4))
mtl_eval4 = mtl.fastboolean_eval.pointwise_sat(mtl.env(phi, lo=0, hi=4))
assert mtl_eval4(x, 0) == mtl_eval3(x, 0)
def test_fastboolean_smoketest(): @given(MetricTemporalLogicStrategy)
phi = mtl.parse( def test_temporal_identity3(phi):
'(((G[0, 4] ap6 & F[2, 1] ap1) | ap2) & G[0,0](ap2))') assert (phi & phi)(x, 0, quantitative=False) \
mtl_eval = mtl.fastboolean_eval.pointwise_sat(phi) == phi(x, 0, quantitative=False)
assert not mtl_eval(x, 0)
with raises(NotImplementedError):
mtl.fastboolean_eval.pointwise_sat(None)
def test_callable_interface(): @given(MetricTemporalLogicStrategy)
phi = mtl.parse( def test_temporal_identity4(phi):
'(((G[0, 4] ap6 & F[2, 1] ap1) | ap2) & G[0,0](ap2))') assert not (phi & ~phi)(x, 0, quantitative=False)
assert not phi(x, 0) assert (phi | ~phi)(x, 0, quantitative=False)
@given(MetricTemporalLogicStrategy)
def test_temporal_identity5(phi):
assert mtl.until(mtl.TOP, phi)(x, 0, quantitative=False) \
== mtl.env(phi)(x, 0, quantitative=False)

View file

@ -60,8 +60,6 @@ def test_discretize():
phi2 = mtl.utils.discretize(phi, dt, distribute=True) phi2 = mtl.utils.discretize(phi, dt, distribute=True)
phi3 = mtl.utils.discretize(phi2, dt, distribute=True) phi3 = mtl.utils.discretize(phi2, dt, distribute=True)
assert phi2 == phi3 assert phi2 == phi3
assert phi2 == mtl.parse(
'(~(@a | @@a) & ~(@@a | @@@a))')
phi = mtl.TOP phi = mtl.TOP
assert mtl.utils.is_discretizable(phi, dt) assert mtl.utils.is_discretizable(phi, dt)

View file

@ -2,18 +2,18 @@ import operator as op
from functools import reduce, wraps from functools import reduce, wraps
from math import isfinite from math import isfinite
import traces from discrete_signals import signal
import numpy as np import numpy as np
import mtl.ast import mtl.ast
from mtl.ast import (And, F, G, Interval, Neg, Or, Next, Until, from mtl.ast import (And, G, Interval, Neg, Next, WeakUntil,
AtomicPred, _Top, _Bot) AtomicPred, _Bot)
oo = float('inf') oo = float('inf')
def const_trace(x): def const_trace(x):
return traces.TimeSeries([(0, x), (oo, x)]) return signal([(0, x)], start=0, end=oo)
def require_discretizable(func): def require_discretizable(func):
@ -29,9 +29,9 @@ def require_discretizable(func):
def scope(phi, dt, *, _t=0, horizon=oo): def scope(phi, dt, *, _t=0, horizon=oo):
if isinstance(phi, Next): if isinstance(phi, Next):
_t += dt _t += dt
elif isinstance(phi, (G, F)): elif isinstance(phi, G):
_t += phi.interval.upper _t += phi.interval.upper
elif isinstance(phi, Until): elif isinstance(phi, WeakUntil):
_t += float('inf') _t += float('inf')
_scope = max((scope(c, dt, _t=_t) for c in phi.children), default=_t) _scope = max((scope(c, dt, _t=_t) for c in phi.children), default=_t)
@ -51,19 +51,19 @@ def discretize(phi, dt, distribute=False, *, horizon=None):
def _discretize(phi, dt, horizon): def _discretize(phi, dt, horizon):
if isinstance(phi, (AtomicPred, _Top, _Bot)): if isinstance(phi, (AtomicPred, _Bot)):
return phi return phi
if not isinstance(phi, (F, G, Until)): if not isinstance(phi, (G, WeakUntil)):
children = tuple(_discretize(arg, dt, horizon) for arg in phi.children) children = tuple(_discretize(arg, dt, horizon) for arg in phi.children)
if isinstance(phi, (And, Or)): if isinstance(phi, And):
return phi.evolve(args=children) return phi.evolve(args=children)
elif isinstance(phi, (Neg, Next)): elif isinstance(phi, (Neg, Next)):
return phi.evolve(arg=children[0]) return phi.evolve(arg=children[0])
raise NotImplementedError raise NotImplementedError
elif isinstance(phi, Until): elif isinstance(phi, WeakUntil):
raise NotImplementedError raise NotImplementedError
# Only remaining cases are G and F # Only remaining cases are G and F
@ -91,19 +91,19 @@ def _distribute_next(phi, i=0):
children = tuple(_distribute_next(c, i) for c in phi.children) children = tuple(_distribute_next(c, i) for c in phi.children)
if isinstance(phi, (And, Or)): if isinstance(phi, And):
return phi.evolve(args=children) return phi.evolve(args=children)
elif isinstance(phi, (Neg, Next)): elif isinstance(phi, (Neg, Next)):
return phi.evolve(arg=children[0]) return phi.evolve(arg=children[0])
def is_discretizable(phi, dt): def is_discretizable(phi, dt):
if any(c for c in phi.walk() if isinstance(c, Until)): if any(c for c in phi.walk() if isinstance(c, WeakUntil)):
return False return False
return all( return all(
_interval_discretizable(c.interval, dt) for c in phi.walk() _interval_discretizable(c.interval, dt) for c in phi.walk()
if isinstance(c, (F, G))) if isinstance(c, G))
# EDSL # EDSL
@ -113,7 +113,7 @@ def alw(phi, *, lo=0, hi=float('inf')):
def env(phi, *, lo=0, hi=float('inf')): def env(phi, *, lo=0, hi=float('inf')):
return F(Interval(lo, hi), phi) return ~alw(~phi, lo=lo, hi=hi)
def andf(*args): def andf(*args):
@ -140,5 +140,9 @@ def next(phi, i=1):
return phi >> i return phi >> i
def until(phi, psi):
return mtl.ast.WeakUntil(phi, psi) & env(psi)
def timed_until(phi, psi, lo, hi): def timed_until(phi, psi, lo, hi):
return env(psi, lo=lo, hi=hi) & alw(mtl.ast.Until(phi, psi), lo=0, hi=lo) return env(psi, lo=lo, hi=hi) & alw(until(phi, psi), lo=0, hi=lo)

View file

@ -1,11 +1,9 @@
-e git://github.com/mvcisback/hypothesis-cfg@master#egg=hypothesis-cfg -e git://github.com/mvcisback/hypothesis-cfg@master#egg=hypothesis-cfg
bitarray==0.8.1
funcy==1.9.1 funcy==1.9.1
lenses==0.4.0 lenses==0.4.0
parsimonious==0.7.0 parsimonious==0.7.0
traces==0.4.1 discrete-signals==0.7.1
hypothesis==3.32.1 hypothesis==3.32.1
numpy==1.15.0
pytest==3.2.3 pytest==3.2.3
pytest-bpdb==0.1.4 pytest-bpdb==0.1.4

View file

@ -12,8 +12,7 @@ setup(
'funcy', 'funcy',
'parsimonious', 'parsimonious',
'lenses', 'lenses',
'bitarray', 'discrete-signals',
'traces',
], ],
packages=find_packages(), packages=find_packages(),
) )