diff --git a/mtl/__init__.py b/mtl/__init__.py new file mode 100644 index 0000000..4b7bbeb --- /dev/null +++ b/mtl/__init__.py @@ -0,0 +1,7 @@ +# flake8: noqa +from mtl.ast import TOP, BOT +from mtl.ast import (Interval, Or, And, F, G, Neg, + AtomicPred, Until, Next) +from mtl.parser import parse +from mtl.fastboolean_eval import pointwise_sat +from mtl.utils import alw, env, andf, orf diff --git a/stl/ast.py b/mtl/ast.py similarity index 97% rename from stl/ast.py rename to mtl/ast.py index c9cfe4c..b73df21 100644 --- a/stl/ast.py +++ b/mtl/ast.py @@ -5,7 +5,7 @@ from functools import lru_cache import funcy as fn from lenses import lens, bind -import stl +import mtl def flatten_binary(phi, op, dropT, shortT): @@ -49,7 +49,7 @@ class AST(object): return phi def __call__(self, trace, time=0): - return stl.pointwise_sat(self)(trace, time) + return mtl.pointwise_sat(self)(trace, time) @property def children(self): @@ -145,7 +145,7 @@ class Interval(namedtuple('I', ['lower', 'upper'])): return f"[{self.lower},{self.upper}]" -class NaryOpSTL(namedtuple('NaryOp', ['args']), AST): +class NaryOpMTL(namedtuple('NaryOp', ['args']), AST): __slots__ = () OP = "?" @@ -158,7 +158,7 @@ class NaryOpSTL(namedtuple('NaryOp', ['args']), AST): return tuple(self.args) -class Or(NaryOpSTL): +class Or(NaryOpMTL): __slots__ = () OP = "|" @@ -168,7 +168,7 @@ class Or(NaryOpSTL): return hash(repr(self)) -class And(NaryOpSTL): +class And(NaryOpMTL): __slots__ = () OP = "&" diff --git a/stl/boolean_eval.py b/mtl/boolean_eval.py similarity index 72% rename from stl/boolean_eval.py rename to mtl/boolean_eval.py index 09f1243..159a8d4 100644 --- a/stl/boolean_eval.py +++ b/mtl/boolean_eval.py @@ -7,9 +7,9 @@ from functools import singledispatch import funcy as fn import traces -import stl -import stl.ast -from stl.utils import const_trace, andf, orf +import mtl +import mtl.ast +from mtl.utils import const_trace, andf, orf TRUE_TRACE = const_trace(True) FALSE_TRACE = const_trace(False) @@ -24,15 +24,15 @@ def negate_trace(x): def pointwise_sat(phi, dt=0.1): ap_names = [z.id for z in phi.atomic_predicates] - def _eval_stl(x, t=0): + def _eval_mtl(x, t=0): evaluated = fn.project(x, ap_names) - return bool(eval_stl(phi, dt)(evaluated)[t]) + return bool(eval_mtl(phi, dt)(evaluated)[t]) - return _eval_stl + return _eval_mtl @singledispatch -def eval_stl(phi, dt): +def eval_mtl(phi, dt): raise NotImplementedError @@ -42,9 +42,9 @@ def or_traces(xs): return out -@eval_stl.register(stl.Or) -def eval_stl_or(phi, dt): - fs = [eval_stl(arg, dt) for arg in phi.args] +@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]) @@ -60,9 +60,9 @@ def and_traces(xs): return out -@eval_stl.register(stl.And) -def eval_stl_and(phi, dt): - fs = [eval_stl(arg, dt) for arg in phi.args] +@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]) @@ -80,9 +80,9 @@ def apply_until(y): phi2_next = phi2 -@eval_stl.register(stl.Until) -def eval_stl_until(phi, dt): - f1, f2 = eval_stl(phi.arg1, dt), eval_stl(phi.arg2, dt) +@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) @@ -95,15 +95,15 @@ def eval_stl_until(phi, dt): return _eval -@eval_stl.register(stl.F) -def eval_stl_f(phi, dt): - phi = ~stl.G(phi.interval, ~phi.arg) - return eval_stl(phi, dt) +@eval_mtl.register(mtl.F) +def eval_mtl_f(phi, dt): + phi = ~mtl.G(phi.interval, ~phi.arg) + return eval_mtl(phi, dt) -@eval_stl.register(stl.G) -def eval_stl_g(phi, dt): - f = eval_stl(phi.arg, 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 @@ -139,9 +139,9 @@ def eval_stl_g(phi, dt): return _eval -@eval_stl.register(stl.Neg) -def eval_stl_neg(phi, dt): - f = eval_stl(phi.arg, dt) +@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)) @@ -151,9 +151,9 @@ def eval_stl_neg(phi, dt): return _eval -@eval_stl.register(stl.ast.Next) -def eval_stl_next(phi, dt): - f = eval_stl(phi.arg, dt) +@eval_mtl.register(mtl.ast.Next) +def eval_mtl_next(phi, dt): + f = eval_mtl(phi.arg, dt) def _eval(x): y = f(x) @@ -166,8 +166,8 @@ def eval_stl_next(phi, dt): return _eval -@eval_stl.register(stl.AtomicPred) -def eval_stl_ap(phi, _): +@eval_mtl.register(mtl.AtomicPred) +def eval_mtl_ap(phi, _): def _eval(x): out = x[str(phi.id)] out.compact() @@ -177,11 +177,11 @@ def eval_stl_ap(phi, _): return _eval -@eval_stl.register(type(stl.TOP)) -def eval_stl_top(_, _1): +@eval_mtl.register(type(mtl.TOP)) +def eval_mtl_top(_, _1): return lambda *_: TRUE_TRACE -@eval_stl.register(type(stl.BOT)) -def eval_stl_bot(_, _1): +@eval_mtl.register(type(mtl.BOT)) +def eval_mtl_bot(_, _1): return lambda *_: FALSE_TRACE diff --git a/stl/fastboolean_eval.py b/mtl/fastboolean_eval.py similarity index 55% rename from stl/fastboolean_eval.py rename to mtl/fastboolean_eval.py index a135b3a..b97bfe1 100644 --- a/stl/fastboolean_eval.py +++ b/mtl/fastboolean_eval.py @@ -4,7 +4,7 @@ from operator import and_, or_ import funcy as fn from bitarray import bitarray -import stl.ast +import mtl.ast oo = float('inf') @@ -25,35 +25,35 @@ def get_times(x, tau, lo, hi): return sorted(set(fn.pluck(0, all_times))) -def pointwise_sat(stl): - f = pointwise_satf(stl) +def pointwise_sat(mtl): + f = pointwise_satf(mtl) return lambda x, t: bool(int(f(x, [t]).to01())) @singledispatch -def pointwise_satf(stl): +def pointwise_satf(mtl): raise NotImplementedError -def bool_op(stl, conjunction=False): +def bool_op(mtl, conjunction=False): binop = and_ if conjunction else or_ - fs = [pointwise_satf(arg) for arg in stl.args] + 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(stl.Or) -def pointwise_satf_or(stl): - return bool_op(stl, conjunction=False) +@pointwise_satf.register(mtl.Or) +def pointwise_satf_or(mtl): + return bool_op(mtl, conjunction=False) -@pointwise_satf.register(stl.And) -def pointwise_satf_and(stl): - return bool_op(stl, conjunction=True) +@pointwise_satf.register(mtl.And) +def pointwise_satf_and(mtl): + return bool_op(mtl, conjunction=True) -def temporal_op(stl, lo, hi, conjunction=False): +def temporal_op(mtl, lo, hi, conjunction=False): fold = bitarray.all if conjunction else bitarray.any - f = pointwise_satf(stl.arg) + 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) @@ -61,38 +61,38 @@ def temporal_op(stl, lo, hi, conjunction=False): return sat_comp -@pointwise_satf.register(stl.F) -def pointwise_satf_f(stl): - lo, hi = stl.interval - return temporal_op(stl, lo, hi, conjunction=False) +@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(stl.G) -def pointwise_satf_g(stl): - lo, hi = stl.interval - return temporal_op(stl, lo, hi, conjunction=True) +@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(stl.Neg) -def pointwise_satf_neg(stl): - return lambda x, t: ~pointwise_satf(stl.arg)(x, t) +@pointwise_satf.register(mtl.Neg) +def pointwise_satf_neg(mtl): + return lambda x, t: ~pointwise_satf(mtl.arg)(x, t) -@pointwise_satf.register(stl.AtomicPred) +@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(stl.Until) +@pointwise_satf.register(mtl.Until) def pointwise_satf_until(phi): raise NotImplementedError -@pointwise_satf.register(type(stl.TOP)) +@pointwise_satf.register(type(mtl.TOP)) def pointwise_satf_top(_): return lambda _, t: bitarray([True] * len(t)) -@pointwise_satf.register(type(stl.BOT)) +@pointwise_satf.register(type(mtl.BOT)) def pointwise_satf_bot(_): return lambda _, t: bitarray([False] * len(t)) diff --git a/stl/hypothesis.py b/mtl/hypothesis.py similarity index 86% rename from stl/hypothesis.py rename to mtl/hypothesis.py index b49afec..07ebc35 100644 --- a/stl/hypothesis.py +++ b/mtl/hypothesis.py @@ -1,7 +1,7 @@ import hypothesis.strategies as st from hypothesis_cfg import ContextFreeGrammarStrategy -import stl +import mtl GRAMMAR = { 'phi': ( @@ -18,7 +18,7 @@ GRAMMAR = { 'AP': (('ap1', ), ('ap2', ), ('ap3', ), ('ap4', ), ('ap5', )), } -SignalTemporalLogicStrategy = st.builds( - lambda term: stl.parse(''.join(term)), +MetricTemporalLogicStrategy = st.builds( + lambda term: mtl.parse(''.join(term)), ContextFreeGrammarStrategy(GRAMMAR, max_length=14, start='phi') ) diff --git a/stl/parser.py b/mtl/parser.py similarity index 93% rename from stl/parser.py rename to mtl/parser.py index 8bec950..9523fa5 100644 --- a/stl/parser.py +++ b/mtl/parser.py @@ -3,10 +3,10 @@ import operator as op from functools import partialmethod, reduce from parsimonious import Grammar, NodeVisitor -from stl import ast -from stl.utils import iff, implies, xor, timed_until +from mtl import ast +from mtl.utils import iff, implies, xor, timed_until -STL_GRAMMAR = Grammar(u''' +MTL_GRAMMAR = Grammar(u''' phi = (neg / paren_phi / next / bot / top / xor_outer / iff_outer / implies_outer / and_outer / or_outer / timed_until / until / g / f / AP) @@ -53,7 +53,7 @@ EOL = "\\n" oo = float('inf') -class STLVisitor(NodeVisitor): +class MTLVisitor(NodeVisitor): def __init__(self, H=oo): super().__init__() self.default_interval = ast.Interval(0.0, H) @@ -137,5 +137,5 @@ class STLVisitor(NodeVisitor): return ast.Next(children[2]) -def parse(stl_str: str, rule: str = "phi", H=oo) -> "STL": - return STLVisitor(H).visit(STL_GRAMMAR[rule].parse(stl_str)) +def parse(mtl_str: str, rule: str = "phi", H=oo) -> "MTL": + return MTLVisitor(H).visit(MTL_GRAMMAR[rule].parse(mtl_str)) diff --git a/mtl/test_ast.py b/mtl/test_ast.py new file mode 100644 index 0000000..b334fb1 --- /dev/null +++ b/mtl/test_ast.py @@ -0,0 +1,31 @@ +import mtl +from mtl.hypothesis import MetricTemporalLogicStrategy + +from hypothesis import given + + +@given(MetricTemporalLogicStrategy) +def test_identities(phi): + assert mtl.TOP == mtl.TOP | phi + assert mtl.BOT == mtl.BOT & phi + assert mtl.TOP == phi | mtl.TOP + assert mtl.BOT == phi & mtl.BOT + assert phi == phi & mtl.TOP + assert phi == phi | mtl.BOT + assert mtl.TOP == mtl.TOP & mtl.TOP + assert mtl.BOT == mtl.BOT | mtl.BOT + assert mtl.TOP == mtl.TOP | mtl.BOT + assert mtl.BOT == mtl.TOP & mtl.BOT + assert ~mtl.BOT == mtl.TOP + assert ~mtl.TOP == mtl.BOT + assert ~~mtl.BOT == mtl.BOT + assert ~~mtl.TOP == mtl.TOP + assert (phi & phi) & phi == phi & (phi & phi) + assert (phi | phi) | phi == phi | (phi | phi) + assert ~~phi == phi + + +def test_walk(): + phi = mtl.parse( + '(([ ][0, 1] ap1 & < >[1,2] ap2) | (@ap1 U ap2))') + assert len(list((~phi).walk())) == 11 diff --git a/mtl/test_boolean_eval.py b/mtl/test_boolean_eval.py new file mode 100644 index 0000000..70f6ab6 --- /dev/null +++ b/mtl/test_boolean_eval.py @@ -0,0 +1,127 @@ +import hypothesis.strategies as st +import traces +from hypothesis import given +from pytest import raises + +import mtl +import mtl.boolean_eval +import mtl.fastboolean_eval +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": + traces.TimeSeries([(0, 1), (0.1, 1), (0.2, 4)], domain=(0, 10)), + "y": + traces.TimeSeries([(0, 2), (0.1, 4), (0.2, 2)], domain=(0, 10)), + "ap1": + traces.TimeSeries([(0, True), (0.1, True), (0.2, False)], domain=(0, 10)), + "ap2": + traces.TimeSeries([(0, False), (0.2, True), (0.5, False)], domain=(0, 10)), + "ap3": + traces.TimeSeries([(0, True), (0.1, True), (0.3, False)], domain=(0, 10)), + "ap4": + traces.TimeSeries( + [(0, False), (0.1, False), (0.3, False)], domain=(0, 10)), + "ap5": + traces.TimeSeries([(0, False), (0.1, False), (0.3, True)], domain=(0, 10)), + "ap6": + traces.TimeSeries([(0, True)], domain=(0, 10)), +} + + +@given(st.just(mtl.ast.Next(mtl.BOT) | mtl.ast.Next(mtl.TOP))) +def test_eval_smoke_tests(phi): + mtl_eval9 = mtl.boolean_eval.pointwise_sat(mtl.ast.Next(phi)) + mtl_eval10 = mtl.boolean_eval.pointwise_sat(~mtl.ast.Next(phi)) + 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') + mtl_eval13 = mtl.boolean_eval.pointwise_sat(phi6) + assert mtl_eval13(x, 0) + assert mtl_eval13(x, 0.4) + + phi7 = mtl.parse('G ~ap4') + mtl_eval14 = mtl.boolean_eval.pointwise_sat(phi7) + assert mtl_eval14(x, 0) + + 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) +def test_temporal_identities(phi): + mtl_eval = mtl.boolean_eval.pointwise_sat(phi) + mtl_eval2 = mtl.boolean_eval.pointwise_sat(~phi) + 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)) +def test_fastboolean_equiv(phi): + mtl_eval = mtl.fastboolean_eval.pointwise_sat(mtl.alw(phi, lo=0, hi=4)) + mtl_eval2 = mtl.fastboolean_eval.pointwise_sat(~mtl.env(~phi, lo=0, hi=4)) + 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(): + phi = mtl.parse( + '(((G[0, 4] ap6 & F[2, 1] ap1) | ap2) & G[0,0](ap2))') + mtl_eval = mtl.fastboolean_eval.pointwise_sat(phi) + assert not mtl_eval(x, 0) + + with raises(NotImplementedError): + mtl.fastboolean_eval.pointwise_sat(mtl.ast.AST()) + + +def test_callable_interface(): + phi = mtl.parse( + '(((G[0, 4] ap6 & F[2, 1] ap1) | ap2) & G[0,0](ap2))') + assert not phi(x, 0) diff --git a/stl/test_params.py b/mtl/test_params.py similarity index 56% rename from stl/test_params.py rename to mtl/test_params.py index 5117715..2d72d8b 100644 --- a/stl/test_params.py +++ b/mtl/test_params.py @@ -1,5 +1,5 @@ -import stl -from stl.hypothesis import SignalTemporalLogicStrategy +import mtl +from mtl.hypothesis import MetricTemporalLogicStrategy import hypothesis.strategies as st from hypothesis import given @@ -7,14 +7,14 @@ from hypothesis import given @given(st.integers(), st.integers(), st.integers()) def test_params1(a, b, c): - phi = stl.parse('G[a, b] x') + phi = mtl.parse('G[a, b] x') assert {x.name for x in phi.params} == {'a', 'b'} phi2 = phi.set_params({'a': a, 'b': b}) assert phi2.params == set() - assert phi2 == stl.parse(f'G[{a}, {b}](x)') + assert phi2 == mtl.parse(f'G[{a}, {b}](x)') -@given(SignalTemporalLogicStrategy) +@given(MetricTemporalLogicStrategy) def test_hash_stable(phi): - assert hash(phi) == hash(stl.parse(str(phi))) + assert hash(phi) == hash(mtl.parse(str(phi))) diff --git a/mtl/test_parser.py b/mtl/test_parser.py new file mode 100644 index 0000000..dc71eb3 --- /dev/null +++ b/mtl/test_parser.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from hypothesis import event, given + +import mtl +from mtl.hypothesis import MetricTemporalLogicStrategy + + +@given(MetricTemporalLogicStrategy) +def test_invertable_repr(phi): + event(str(phi)) + assert str(phi) == str(mtl.parse(str(phi))) + + +@given(MetricTemporalLogicStrategy) +def test_hash_inheritance(phi): + assert hash(repr(phi)) == hash(phi) + + +def test_sugar_smoke(): + mtl.parse('(x <-> x)') + mtl.parse('(x -> x)') + mtl.parse('(x ^ x)') diff --git a/mtl/test_utils.py b/mtl/test_utils.py new file mode 100644 index 0000000..77e4748 --- /dev/null +++ b/mtl/test_utils.py @@ -0,0 +1,113 @@ +import mtl +from mtl.hypothesis import MetricTemporalLogicStrategy + +from hypothesis import given +from pytest import raises + +CONTEXT = { + mtl.parse('ap1'): mtl.parse('x'), + mtl.parse('ap2'): mtl.parse('(y U z)'), + mtl.parse('ap3'): mtl.parse('x'), + mtl.parse('ap4'): mtl.parse('(x -> y -> z)'), + mtl.parse('ap5'): mtl.parse('(ap1 <-> y <-> z)'), +} +APS = set(CONTEXT.keys()) + + +@given(MetricTemporalLogicStrategy) +def test_f_neg_or_canonical_form(phi): + phi2 = mtl.utils.f_neg_or_canonical_form(phi) + phi3 = mtl.utils.f_neg_or_canonical_form(phi2) + assert phi2 == phi3 + assert not any( + isinstance(x, (mtl.ast.G, mtl.ast.And)) for x in phi2.walk()) + + +def test_f_neg_or_canonical_form_not_implemented(): + with raises(NotImplementedError): + mtl.utils.f_neg_or_canonical_form(mtl.ast.AST()) + + +def test_inline_context_rigid(): + phi = mtl.parse('G ap1') + phi2 = phi.inline_context(CONTEXT) + assert phi2 == mtl.parse('G x') + + phi = mtl.parse('G ap5') + phi2 = phi.inline_context(CONTEXT) + assert phi2 == mtl.parse('G(x <-> y <-> z)') + + +@given(MetricTemporalLogicStrategy) +def test_inline_context(phi): + phi2 = phi.inline_context(CONTEXT) + assert not (APS & phi2.atomic_predicates) + + +@given(MetricTemporalLogicStrategy, MetricTemporalLogicStrategy) +def test_timed_until_smoke_test(phi1, phi2): + mtl.utils.timed_until(phi1, phi2, lo=2, hi=20) + + +def test_discretize(): + dt = 0.3 + + phi = mtl.parse('@ ap1') + assert mtl.utils.is_discretizable(phi, dt) + phi2 = mtl.utils.discretize(phi, dt) + phi3 = mtl.utils.discretize(phi2, dt) + assert phi2 == phi3 + + phi = mtl.parse('G[0.3, 1.2] F[0.6, 1.5] ap1') + assert mtl.utils.is_discretizable(phi, dt) + phi2 = mtl.utils.discretize(phi, dt) + phi3 = mtl.utils.discretize(phi2, dt) + assert phi2 == phi3 + + phi = mtl.parse('G[0.3, 1.4] F[0.6, 1.5] ap1') + assert not mtl.utils.is_discretizable(phi, dt) + + phi = mtl.parse('G[0.3, 1.2] F ap1') + assert not mtl.utils.is_discretizable(phi, dt) + + phi = mtl.parse('G[0.3, 1.2] (ap1 U ap2)') + assert not mtl.utils.is_discretizable(phi, dt) + + phi = mtl.parse('G[0.3, 0.6] ~F[0, 0.3] a') + assert mtl.utils.is_discretizable(phi, dt) + phi2 = mtl.utils.discretize(phi, dt, distribute=True) + phi3 = mtl.utils.discretize(phi2, dt, distribute=True) + assert phi2 == phi3 + assert phi2 == mtl.parse( + '(~(@a | @@a) & ~(@@a | @@@a))') + + phi = mtl.TOP + assert mtl.utils.is_discretizable(phi, dt) + phi2 = mtl.utils.discretize(phi, dt) + phi3 = mtl.utils.discretize(phi2, dt) + assert phi2 == phi3 + + phi = mtl.BOT + assert mtl.utils.is_discretizable(phi, dt) + phi2 = mtl.utils.discretize(phi, dt) + phi3 = mtl.utils.discretize(phi2, dt) + assert phi2 == phi3 + + +def test_scope(): + dt = 0.3 + + phi = mtl.parse('@ap1') + assert mtl.utils.scope(phi, dt) == 0.3 + + phi = mtl.parse('(@@ap1 | ap2)') + assert mtl.utils.scope(phi, dt) == 0.6 + + phi = mtl.parse('G[0.3, 1.2] F[0.6, 1.5] ap1') + assert mtl.utils.scope(phi, dt) == 1.2 + 1.5 + + phi = mtl.parse('G[0.3, 1.2] F ap1') + assert mtl.utils.scope(phi, dt) == float('inf') + + phi = mtl.parse('G[0.3, 1.2] (ap1 U ap2)') + assert mtl.utils.scope(phi, dt) == float('inf') diff --git a/stl/utils.py b/mtl/utils.py similarity index 74% rename from stl/utils.py rename to mtl/utils.py index 2a75168..6d417a5 100644 --- a/stl/utils.py +++ b/mtl/utils.py @@ -6,8 +6,8 @@ import traces import numpy as np from lenses import bind -import stl.ast -from stl.ast import (And, F, G, Interval, Neg, Or, Next, Until, +import mtl.ast +from mtl.ast import (And, F, G, Interval, Neg, Or, Next, Until, AtomicPred, _Top, _Bot) oo = float('inf') @@ -40,50 +40,10 @@ def f_neg_or_canonical_form(phi): raise NotImplementedError -def _lineq_lipschitz(lineq): - return sum(map(abs, bind(lineq).terms.Each().coeff.collect())) - - -def linear_stl_lipschitz(phi): - """Infinity norm lipschitz bound of linear inequality predicate.""" - if any(isinstance(psi, (AtomicPred, _Top, _Bot)) for psi in phi.walk()): - return float('inf') - - return float(max(map(_lineq_lipschitz, phi.lineqs), default=float('inf'))) - - -op_lookup = { - ">": op.gt, - ">=": op.ge, - "<": op.lt, - "<=": op.le, - "=": op.eq, -} - - def const_trace(x, start=0): return traces.TimeSeries([(start, x)], domain=traces.Domain(start, oo)) -def eval_lineq(lineq, x, domain, compact=True): - lhs = sum(const_trace(term.coeff) * x[term.id] for term in lineq.terms) - compare = op_lookup.get(lineq.op) - output = lhs.operation(const_trace(lineq.const), compare) - output.domain = domain - - if compact: - output.compact() - return output - - -def eval_lineqs(phi, x): - lineqs = phi.lineqs - start = max(y.domain.start() for y in x.values()) - end = min(y.domain.end() for y in x.values()) - domain = traces.Domain(start, end) - return {lineq: eval_lineq(lineq, x, domain) for lineq in lineqs} - - def require_discretizable(func): @wraps(func) def _func(phi, dt, *args, **kwargs): @@ -106,7 +66,7 @@ def scope(phi, dt, *, _t=0, horizon=oo): return min(_scope, horizon) -# Code to discretize a bounded STL formula +# Code to discretize a bounded MTL formula @require_discretizable @@ -153,7 +113,7 @@ def _interval_discretizable(itvl, dt): def _distribute_next(phi, i=0): if isinstance(phi, AtomicPred): - return stl.utils.next(phi, i=i) + return mtl.utils.next(phi, i=i) elif isinstance(phi, Next): return _distribute_next(phi.arg, i=i+1) @@ -185,11 +145,11 @@ def env(phi, *, lo=0, hi=float('inf')): def andf(*args): - return reduce(op.and_, args) if args else stl.TOP + return reduce(op.and_, args) if args else mtl.TOP def orf(*args): - return reduce(op.or_, args) if args else stl.TOP + return reduce(op.or_, args) if args else mtl.TOP def implies(x, y): @@ -209,4 +169,4 @@ def next(phi, i=1): def timed_until(phi, psi, lo, hi): - return env(psi, lo=lo, hi=hi) & alw(stl.ast.Until(phi, psi), lo=0, hi=lo) + return env(psi, lo=lo, hi=hi) & alw(mtl.ast.Until(phi, psi), lo=0, hi=lo) diff --git a/requirements.txt b/requirements.txt index 263db97..8117d2d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,9 +3,7 @@ bitarray==0.8.1 funcy==1.9.1 lenses==0.4.0 -pandas==0.19.2 parsimonious==0.7.0 -sympy==1.0 traces==0.3.1 hypothesis==3.32.1 diff --git a/setup.py b/setup.py index cf300b1..bb8ad96 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,10 @@ from setuptools import find_packages, setup setup( - name='py-stl', + name='metric-temporal-logic', version='0.2', description='TODO', - url='http://github.com/mvcisback/py-stl', + url='http://github.com/mvcisback/py-mtl', author='Marcell Vazquez-Chanlatte', author_email='marcell.vc@eecs.berkeley.edu', license='MIT', @@ -12,7 +12,6 @@ setup( 'funcy', 'parsimonious', 'lenses', - 'sympy', 'bitarray', 'traces', ], diff --git a/stl/__init__.py b/stl/__init__.py deleted file mode 100644 index 5cc324a..0000000 --- a/stl/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# flake8: noqa -from stl.ast import TOP, BOT -from stl.ast import (Interval, Or, And, F, G, Neg, - AtomicPred, Until, Next) -from stl.parser import parse -from stl.fastboolean_eval import pointwise_sat -from stl.utils import alw, env, andf, orf diff --git a/stl/load.py b/stl/load.py deleted file mode 100644 index 89dc019..0000000 --- a/stl/load.py +++ /dev/null @@ -1,11 +0,0 @@ -from traces import Domain, TimeSeries - - -def from_pandas(df, compact=True): - '''TODO''' - domain = Domain(df.index[0], df.index[-1]) - data = {col: TimeSeries(df[col].T, domain=domain) for col in df.columns} - if compact: - for val in data.values(): - val.compact() - return data diff --git a/stl/test_ast.py b/stl/test_ast.py deleted file mode 100644 index 1db9dc4..0000000 --- a/stl/test_ast.py +++ /dev/null @@ -1,31 +0,0 @@ -import stl -from stl.hypothesis import SignalTemporalLogicStrategy - -from hypothesis import given - - -@given(SignalTemporalLogicStrategy) -def test_identities(phi): - assert stl.TOP == stl.TOP | phi - assert stl.BOT == stl.BOT & phi - assert stl.TOP == phi | stl.TOP - assert stl.BOT == phi & stl.BOT - assert phi == phi & stl.TOP - assert phi == phi | stl.BOT - assert stl.TOP == stl.TOP & stl.TOP - assert stl.BOT == stl.BOT | stl.BOT - assert stl.TOP == stl.TOP | stl.BOT - assert stl.BOT == stl.TOP & stl.BOT - assert ~stl.BOT == stl.TOP - assert ~stl.TOP == stl.BOT - assert ~~stl.BOT == stl.BOT - assert ~~stl.TOP == stl.TOP - assert (phi & phi) & phi == phi & (phi & phi) - assert (phi | phi) | phi == phi | (phi | phi) - assert ~~phi == phi - - -def test_walk(): - phi = stl.parse( - '(([ ][0, 1] ap1 & < >[1,2] ap2) | (@ap1 U ap2))') - assert len(list((~phi).walk())) == 11 diff --git a/stl/test_boolean_eval.py b/stl/test_boolean_eval.py deleted file mode 100644 index d285c8e..0000000 --- a/stl/test_boolean_eval.py +++ /dev/null @@ -1,127 +0,0 @@ -import hypothesis.strategies as st -import traces -from hypothesis import given -from pytest import raises - -import stl -import stl.boolean_eval -import stl.fastboolean_eval -from stl.hypothesis import SignalTemporalLogicStrategy -""" -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": - traces.TimeSeries([(0, 1), (0.1, 1), (0.2, 4)], domain=(0, 10)), - "y": - traces.TimeSeries([(0, 2), (0.1, 4), (0.2, 2)], domain=(0, 10)), - "ap1": - traces.TimeSeries([(0, True), (0.1, True), (0.2, False)], domain=(0, 10)), - "ap2": - traces.TimeSeries([(0, False), (0.2, True), (0.5, False)], domain=(0, 10)), - "ap3": - traces.TimeSeries([(0, True), (0.1, True), (0.3, False)], domain=(0, 10)), - "ap4": - traces.TimeSeries( - [(0, False), (0.1, False), (0.3, False)], domain=(0, 10)), - "ap5": - traces.TimeSeries([(0, False), (0.1, False), (0.3, True)], domain=(0, 10)), - "ap6": - traces.TimeSeries([(0, True)], domain=(0, 10)), -} - - -@given(st.just(stl.ast.Next(stl.BOT) | stl.ast.Next(stl.TOP))) -def test_eval_smoke_tests(phi): - stl_eval9 = stl.boolean_eval.pointwise_sat(stl.ast.Next(phi)) - stl_eval10 = stl.boolean_eval.pointwise_sat(~stl.ast.Next(phi)) - assert stl_eval9(x, 0) != stl_eval10(x, 0) - - phi4 = stl.parse('~ap4') - stl_eval11 = stl.boolean_eval.pointwise_sat(phi4) - assert stl_eval11(x, 0) - - phi5 = stl.parse('G[0.1, 0.03] ~ap4') - stl_eval12 = stl.boolean_eval.pointwise_sat(phi5) - assert stl_eval12(x, 0) - - phi6 = stl.parse('G[0.1, 0.03] ~ap5') - stl_eval13 = stl.boolean_eval.pointwise_sat(phi6) - assert stl_eval13(x, 0) - assert stl_eval13(x, 0.4) - - phi7 = stl.parse('G ~ap4') - stl_eval14 = stl.boolean_eval.pointwise_sat(phi7) - assert stl_eval14(x, 0) - - phi8 = stl.parse('F ap5') - stl_eval15 = stl.boolean_eval.pointwise_sat(phi8) - assert stl_eval15(x, 0) - - phi9 = stl.parse('(ap1 U ap2)') - stl_eval16 = stl.boolean_eval.pointwise_sat(phi9) - assert stl_eval16(x, 0) - - phi10 = stl.parse('(ap2 U ap2)') - stl_eval17 = stl.boolean_eval.pointwise_sat(phi10) - assert not stl_eval17(x, 0) - - with raises(NotImplementedError): - stl.boolean_eval.eval_stl(None, None) - - -@given(SignalTemporalLogicStrategy) -def test_temporal_identities(phi): - stl_eval = stl.boolean_eval.pointwise_sat(phi) - stl_eval2 = stl.boolean_eval.pointwise_sat(~phi) - assert stl_eval2(x, 0) == (not stl_eval(x, 0)) - stl_eval3 = stl.boolean_eval.pointwise_sat(~~phi) - assert stl_eval3(x, 0) == stl_eval(x, 0) - stl_eval4 = stl.boolean_eval.pointwise_sat(phi & phi) - assert stl_eval4(x, 0) == stl_eval(x, 0) - stl_eval5 = stl.boolean_eval.pointwise_sat(phi & ~phi) - assert not stl_eval5(x, 0) - stl_eval6 = stl.boolean_eval.pointwise_sat(phi | ~phi) - assert stl_eval6(x, 0) - stl_eval7 = stl.boolean_eval.pointwise_sat(stl.ast.Until(stl.TOP, phi)) - stl_eval8 = stl.boolean_eval.pointwise_sat(stl.env(phi)) - assert stl_eval7(x, 0) == stl_eval8(x, 0) - - -@given(st.just(stl.BOT)) -def test_fastboolean_equiv(phi): - stl_eval = stl.fastboolean_eval.pointwise_sat(stl.alw(phi, lo=0, hi=4)) - stl_eval2 = stl.fastboolean_eval.pointwise_sat(~stl.env(~phi, lo=0, hi=4)) - assert stl_eval2(x, 0) == stl_eval(x, 0) - - stl_eval3 = stl.fastboolean_eval.pointwise_sat(~stl.alw(~phi, lo=0, hi=4)) - stl_eval4 = stl.fastboolean_eval.pointwise_sat(stl.env(phi, lo=0, hi=4)) - assert stl_eval4(x, 0) == stl_eval3(x, 0) - - -def test_fastboolean_smoketest(): - phi = stl.parse( - '(((G[0, 4] ap6 & F[2, 1] ap1) | ap2) & G[0,0](ap2))') - stl_eval = stl.fastboolean_eval.pointwise_sat(phi) - assert not stl_eval(x, 0) - - with raises(NotImplementedError): - stl.fastboolean_eval.pointwise_sat(stl.ast.AST()) - - -def test_callable_interface(): - phi = stl.parse( - '(((G[0, 4] ap6 & F[2, 1] ap1) | ap2) & G[0,0](ap2))') - assert not phi(x, 0) diff --git a/stl/test_load.py b/stl/test_load.py deleted file mode 100644 index 433a922..0000000 --- a/stl/test_load.py +++ /dev/null @@ -1,20 +0,0 @@ -import pandas as pd - -from stl.load import from_pandas - -DATA = pd.DataFrame( - data={ - 'AP1': [True, False, True], - 'x': [0, 0, 0.1], - 'y': [-1, -1, 0], - 'z': [2, 3, 1], - }, - index=[0, 1, 2], -) - - -def test_from_pandas(): - x = from_pandas(DATA) - assert x['x'][0] == 0 - assert x['x'][0.2] == 0 - assert not x['AP1'][1.4] diff --git a/stl/test_parser.py b/stl/test_parser.py deleted file mode 100644 index e2bf6b6..0000000 --- a/stl/test_parser.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -from hypothesis import event, given - -import stl -from stl.hypothesis import SignalTemporalLogicStrategy - - -@given(SignalTemporalLogicStrategy) -def test_invertable_repr(phi): - event(str(phi)) - assert str(phi) == str(stl.parse(str(phi))) - - -@given(SignalTemporalLogicStrategy) -def test_hash_inheritance(phi): - assert hash(repr(phi)) == hash(phi) - - -def test_sugar_smoke(): - stl.parse('(x <-> x)') - stl.parse('(x -> x)') - stl.parse('(x ^ x)') diff --git a/stl/test_utils.py b/stl/test_utils.py deleted file mode 100644 index 2794652..0000000 --- a/stl/test_utils.py +++ /dev/null @@ -1,113 +0,0 @@ -import stl -from stl.hypothesis import SignalTemporalLogicStrategy - -from hypothesis import given -from pytest import raises - -CONTEXT = { - stl.parse('ap1'): stl.parse('x'), - stl.parse('ap2'): stl.parse('(y U z)'), - stl.parse('ap3'): stl.parse('x'), - stl.parse('ap4'): stl.parse('(x -> y -> z)'), - stl.parse('ap5'): stl.parse('(ap1 <-> y <-> z)'), -} -APS = set(CONTEXT.keys()) - - -@given(SignalTemporalLogicStrategy) -def test_f_neg_or_canonical_form(phi): - phi2 = stl.utils.f_neg_or_canonical_form(phi) - phi3 = stl.utils.f_neg_or_canonical_form(phi2) - assert phi2 == phi3 - assert not any( - isinstance(x, (stl.ast.G, stl.ast.And)) for x in phi2.walk()) - - -def test_f_neg_or_canonical_form_not_implemented(): - with raises(NotImplementedError): - stl.utils.f_neg_or_canonical_form(stl.ast.AST()) - - -def test_inline_context_rigid(): - phi = stl.parse('G ap1') - phi2 = phi.inline_context(CONTEXT) - assert phi2 == stl.parse('G x') - - phi = stl.parse('G ap5') - phi2 = phi.inline_context(CONTEXT) - assert phi2 == stl.parse('G(x <-> y <-> z)') - - -@given(SignalTemporalLogicStrategy) -def test_inline_context(phi): - phi2 = phi.inline_context(CONTEXT) - assert not (APS & phi2.atomic_predicates) - - -@given(SignalTemporalLogicStrategy, SignalTemporalLogicStrategy) -def test_timed_until_smoke_test(phi1, phi2): - stl.utils.timed_until(phi1, phi2, lo=2, hi=20) - - -def test_discretize(): - dt = 0.3 - - phi = stl.parse('@ ap1') - assert stl.utils.is_discretizable(phi, dt) - phi2 = stl.utils.discretize(phi, dt) - phi3 = stl.utils.discretize(phi2, dt) - assert phi2 == phi3 - - phi = stl.parse('G[0.3, 1.2] F[0.6, 1.5] ap1') - assert stl.utils.is_discretizable(phi, dt) - phi2 = stl.utils.discretize(phi, dt) - phi3 = stl.utils.discretize(phi2, dt) - assert phi2 == phi3 - - phi = stl.parse('G[0.3, 1.4] F[0.6, 1.5] ap1') - assert not stl.utils.is_discretizable(phi, dt) - - phi = stl.parse('G[0.3, 1.2] F ap1') - assert not stl.utils.is_discretizable(phi, dt) - - phi = stl.parse('G[0.3, 1.2] (ap1 U ap2)') - assert not stl.utils.is_discretizable(phi, dt) - - phi = stl.parse('G[0.3, 0.6] ~F[0, 0.3] a') - assert stl.utils.is_discretizable(phi, dt) - phi2 = stl.utils.discretize(phi, dt, distribute=True) - phi3 = stl.utils.discretize(phi2, dt, distribute=True) - assert phi2 == phi3 - assert phi2 == stl.parse( - '(~(@a | @@a) & ~(@@a | @@@a))') - - phi = stl.TOP - assert stl.utils.is_discretizable(phi, dt) - phi2 = stl.utils.discretize(phi, dt) - phi3 = stl.utils.discretize(phi2, dt) - assert phi2 == phi3 - - phi = stl.BOT - assert stl.utils.is_discretizable(phi, dt) - phi2 = stl.utils.discretize(phi, dt) - phi3 = stl.utils.discretize(phi2, dt) - assert phi2 == phi3 - - -def test_scope(): - dt = 0.3 - - phi = stl.parse('@ap1') - assert stl.utils.scope(phi, dt) == 0.3 - - phi = stl.parse('(@@ap1 | ap2)') - assert stl.utils.scope(phi, dt) == 0.6 - - phi = stl.parse('G[0.3, 1.2] F[0.6, 1.5] ap1') - assert stl.utils.scope(phi, dt) == 1.2 + 1.5 - - phi = stl.parse('G[0.3, 1.2] F ap1') - assert stl.utils.scope(phi, dt) == float('inf') - - phi = stl.parse('G[0.3, 1.2] (ap1 U ap2)') - assert stl.utils.scope(phi, dt) == float('inf')