Using ppLTLTT to work with pure-past LTL subformulas
Table of Contents
Spot has no support for past operators at the moment. However, if you
really need to work with past operators, the tool ppLTLTT
comes in
handy. It takes a pure-past LTL formula, and produces a temporal
tester, i.e., a DFA with transition-based acceptance represented by an
atomic proposition.
ppLTLTT -f 'a S b' -m 'end$'
HOA: v1 Start: 0 acc-name: all Acceptance: 1 t AP: 3 "end$" "a" "b" --BODY-- State: 0 [!0&!1&!2] 0 [!0&1&!2] 0 [0&!1&2] 1 [0&1&2] 1 State: 1 [!0&!1&!2] 0 [0&1&!2] 1 [0&!1&2] 1 [0&1&2] 1 --END--
Another interpretation is that this is a deterministic monitor for
G("end$" <-> (a S b))
.
As shown in the ppLTLTT
tool paper, published at ATVA'23, such a
temporal tester can be combined with existing tools to add some
support for pure-past subformulas. Below, we show some integration
examples for LTL and LTLf.
Translating LTL with ppLTL subformulas
This use-case is already covered by the tool pltl2tgba
that is
distributed with ppLTLPP
. In other words, the code below shows
how to re-implement pltl2tgba
.
If you have an LTL formula f(...,g(...),...)
where g(...)
is a
pure-past LTL formula, then you can rewrite the former formula as
∃newg, f(...,newg,...) & G(newg <-> g(...))
. This makes it possible
to translate G(newg <-> g(...))
using ppLTLPP
, and translate
f(....,newg,...)
using ltl2tgba
.
For instance, consider the formula G((request S trigger)->(request U
grant))
, which states that every request that has been active
continuously since a trigger in the past must remain active until it
is granted.
We can rewrite this formula as ∃phi, G(phi -> (request U grant)) & G(phi <-> (request S trigger))
.
Shell
Here is a sequence of command-line calls that produce a Büchi automaton for the above formula.
ltl2tgba -f 'G(phi -> (request U grant))' > future.hoa ppLTLTT -f 'request S trigger' -m phi > past.hoa autfilt --merge-trans --product future.hoa past.hoa | autfilt -B --remove-ap=phi --dot
Python
Since Spot has no support for past operators, the code below generalizes
the syntax a bit by using braces to denote the subformulas that should
be passed to ppLTLTT
.
import spot import re def extract_braces(s: str): """Transform "{H a} R {b S c}" into ('"$v1$" R "$v2$"', {'$v1$': 'H a', '$v2$': 'b S c'})""" mapping = {} def replacer(match, counter=[1]): key = f"$v{counter[0]}$" mapping[key] = match.group(1) counter[0] += 1 return '"' + key + '"' s = re.sub(r"{([^}]+)}", replacer, s) return s, mapping def translate(formula: str): s, m = extract_braces(formula) aut = spot.translate(s) for k, v in m.items(): past = spot.automaton(f"ppLTLTT -f '{v}' -m '{k}'|") past.merge_edges() aut = spot.product(aut, past) rem = spot.remove_ap() rem.add_ap(k) aut = rem.strip(aut) aut = aut.postprocess('Buchi', 'SBacc') aut.set_name(formula) return aut example = translate("G({request S trigger}->(request U grant))") print(example.to_str('dot', ".n"))
C++
The C++ version of this code is similar. The main issue is to call
ppLTLTT
and read its output. The Python code for spot.automaton
,
that supports a trailing pipe, does not exist in C++: we have to
implement that by ourselves.
Here is a function that calls ppLTLTT
and parses its HOA output as a
twa_graph_ptr
.
#include <spot/parseaut/public.hh> #include <iostream> #include <vector> #include <string> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <stdarg.h> #include <errno.h> #include <spawn.h> #include <unistd.h> #include <sys/wait.h> void fatal(int errnum, const char* fmt, ...) { va_list args; va_start(args, fmt); fprintf(stderr, "error: "); vfprintf(stderr, fmt, args); va_end(args); if (errnum) fprintf(stderr, ": %s\n", strerror(errnum)); else fputc('\n', stderr); exit(2); } spot::twa_graph_ptr run_ppLTLTT(const std::string& formula, const std::string& ap, spot::bdd_dict_ptr dict) { int cout_pipe[2]; if (int err = pipe(cout_pipe)) fatal(err, "pipe() failed"); posix_spawn_file_actions_t actions; if (int err = posix_spawn_file_actions_init(&actions)) fatal(err, "posix_spawn_file_actions_init() failed"); posix_spawn_file_actions_addclose(&actions, STDIN_FILENO); posix_spawn_file_actions_addclose(&actions, cout_pipe[0]); posix_spawn_file_actions_adddup2(&actions, cout_pipe[1], STDOUT_FILENO); posix_spawn_file_actions_addclose(&actions, cout_pipe[1]); std::vector<char*> argv; argv.push_back(const_cast<char*>("ppLTLTT")); argv.push_back(const_cast<char*>("-f")); argv.push_back(const_cast<char*>(formula.c_str())); argv.push_back(const_cast<char*>("-m")); argv.push_back(const_cast<char*>(ap.c_str())); argv.push_back(nullptr); pid_t pid; if (int err = posix_spawnp(&pid, argv[0], &actions, nullptr, argv.data(), environ)) fatal(err, "failed to run '%s'", argv[0]); if (int err = posix_spawn_file_actions_destroy(&actions)) fatal(err, "posix_spawn_file_actions_destroy() failed"); if (close(cout_pipe[1]) < 0) fatal(errno, "closing write-side of pipe failed"); spot::automaton_stream_parser parser(cout_pipe[0], "ppLTLTT's output"); spot::parsed_aut_ptr paut = parser.parse(dict); if (close(cout_pipe[0]) < 0) fatal(errno, "closing read-side of pipe failed"); int exit_code = 0; if (waitpid(pid, &exit_code, 0) == -1) fatal(errno, "waitpid() failed"); if (exit_code) fatal(0, "ppLTLTT -f '%s' -m '%s': exited with status %d", formula.c_str(), ap.c_str(), exit_code); if (paut->format_errors(std::cerr)) exit(2); return paut->aut; }
With this out of the way, we can focus on our actual objective.
#include <spot/tl/parse.hh> #include <spot/twaalgos/dot.hh> #include <spot/twaalgos/product.hh> #include <spot/twaalgos/remprop.hh> #include <spot/twaalgos/translate.hh> #include <utility> spot::twa_graph_ptr translate(const std::string& s, spot::bdd_dict_ptr dict) { // Scan s to replace braced subformulas by fresh propositions. std::string out; std::vector<std::pair<std::string, std::string>> m; // [(prop,subf)...] out.reserve(s.size()); size_t pos = 0; int counter = 1; size_t ss = s.size(); while (pos < ss) { size_t open = s.find('{', pos); if (open == std::string::npos) // No {, copy the rest. { out += s.substr(pos); break; } out += s.substr(pos, open - pos); size_t close = s.find('}', open + 1); if (close != std::string::npos && close > open + 1) { // Found a valid content inside braces std::string subf = s.substr(open + 1, close - (open + 1)); std::string prop = "$v" + std::to_string(counter++) + "$"; m.emplace_back(prop, subf); out += '"'; out += prop; out += '"'; pos = close + 1; // move past the closing brace } else // No close }, or empty brace pair. { out += '{'; pos = open + 1; } } // parse and translate the main formula spot::parsed_formula pf = spot::parse_infix_psl(out); if (pf.format_errors(std::cerr)) exit(2); spot::translator trans(dict); spot::twa_graph_ptr aut = trans.run(pf.f); // call ppLTLTT for all subformulas, and combine them for (const auto& [prop, subf] : m) { spot::twa_graph_ptr past = run_ppLTLTT(subf, prop, dict); past->merge_edges(); aut = spot::product(aut, past); spot::remove_ap rem; rem.add_ap(prop.c_str()); aut = rem.strip(aut); } // Simplify the resulting automaton and turn it into a BA spot::postprocessor post; post.set_type(spot::postprocessor::Buchi); post.set_pref(spot::postprocessor::SBAcc); aut = post.run(aut); aut->set_named_prop("automaton-name", new std::string(s)); return aut; } int main() { spot::bdd_dict_ptr dict = spot::make_bdd_dict(); spot::twa_graph_ptr aut = translate("G({request S trigger}->(request U grant))", dict); spot::print_dot(std::cout, aut, ".n"); }
Translating a pair (LTLf, ppLTL)
We now consider a similar setup, this time working over finite traces. Take the problem of translating a specification of the form \(\alpha\land \mathsf{G}\mathsf{F}\beta\) where \(\alpha\) is an LTLf formula, and \(\beta\) is a ppLTL formula that should be matched from the end of the finite trace.
Spot can translate LTLf formulas into MTDFA, i.e., DFA represented by
multi-terminal BDDs. See ltlf2dfa
for some details. Translating
LTLf using MTDFA is more efficient than using explicit
automata. Therefore, for this demonstration, let's assume we want to
build an MTDFA for \(\alpha\land \mathsf{G}\mathsf{F}\beta\).
Unfortunately, we do not have a nice exchange format for MTDFA, so we cannot do operations conveniently on the command-line. We shall implement our translation directly in Python and C++.
In this case, we translate \(\alpha\) and \(\beta\) separately, but we
interpret the end$
variable of the automaton produced by ppLTLTT
as an indication of accepting transitions.
Python
Here is how to compute an MTDFA for (a U b) & GF(c S d)
. Note again
that we do not parse this formula, since Spot does not understand past
operators. Instead we pass a U b
and c S d
separately, and we let
ppLTLTT
parse the pure-past LTL formula.
import sys import spot import subprocess from buddy import bdd_implies, bdd_exist, bdd_ithvar # Call ppLTLTT to translate a ppLTL formula into an explicit temporal # tester, convert the result to mtdfa. def ppltl_to_mtdfa(formula: str): end_var = "end$" result = subprocess.run(['ppLTLTT', '-f', formula, '-m', end_var], capture_output=True, text=True) if result.returncode: raise RuntimeError(f"ppLTLTT failed: {result.stderr}") aut = spot.automaton(result.stdout) # Turn transition with positive end$ assignments into # accepting transitions. We declare the automaton as Büchi # just so that we can tag some transition as accepting, # but the function twadfa_to_mtdfa() called at the end knows # that is meant to be interpreted as a DFA. end_num = aut.register_ap(end_var) end_bdd = bdd_ithvar(end_num) accmark = aut.set_buchi(); for e in aut.edges(): if bdd_implies(e.cond, end_bdd): e.acc = accmark e.cond = bdd_exist(e.cond, end_bdd) aut.unregister_ap(end_num) # The automaton was marked as incomplete because of the end_var, # but it should now be complete. aut.prop_complete(True) # Make sure we keep transition-based acceptance if we print that # automaton, because we are storing a finite automaton in a class # for ω-automata. aut.prop_state_acc(False) return spot.twadfa_to_mtdfa(aut) def translate(α, β): ppltl_mtdfa = ppltl_to_mtdfa(β) ltlf_mtdfa = spot.ltlf_to_mtdfa(α) return spot.product(ltlf_mtdfa, ppltl_mtdfa) aut = translate("a U b", "c S d") # Print the product as an MTDFA: aut.print_dot(spot.get_cout()) # Or convert back to twa for printing as HOA: # print(aut.as_twa().to_str('hoa'))
C++
The following reuses the code for run_ppLTLTT
presented above.
#include <spot/tl/parse.hh> #include <spot/twaalgos/ltlf2dfa.hh> spot::mtdfa_ptr ppltl_to_mtdfa(std::string s, spot::bdd_dict_ptr dict) { std::string end_var = "end$"; spot::twa_graph_ptr aut = run_ppLTLTT(s, end_var, dict); // Turn "end$" transitions into accepting transitions. // We declare the automaton as Büchi just so that we can tag some // transition as accepting, but the function twadfa_to_mtdfa() // called at the end knows that is meant to be interpreted as a DFA. unsigned end_num = aut->register_ap(end_var); bdd end_bdd = bdd_ithvar(end_num); spot::acc_cond::mark_t accmark = aut->set_buchi(); for (auto& e: aut->edges()) { if (bdd_implies(e.cond, end_bdd)) e.acc = accmark; e.cond = bdd_exist(e.cond, end_bdd); } aut->unregister_ap(end_num); // The automaton was marked as incomplete because of the end_var, // but it should now be complete. aut->prop_complete(true); // Make sure we keep transition-based acceptance if we print that // automaton, because we are storing a finite automaton in a class // for ω-automata. aut->prop_state_acc(false); return spot::twadfa_to_mtdfa(aut); } spot::mtdfa_ptr translate(const std::string& alpha, const std::string& beta, spot::bdd_dict_ptr dict) { // parse and translate the main formula spot::parsed_formula pf = spot::parse_infix_psl(alpha); if (pf.format_errors(std::cerr)) exit(2); spot::mtdfa_ptr ppltl_mtdfa = ppltl_to_mtdfa(beta, dict); spot::mtdfa_ptr ltlf_mtdfa = spot::ltlf_to_mtdfa(pf.f, dict); return spot::product(ltlf_mtdfa, ppltl_mtdfa); }
int main() { spot::bdd_dict_ptr dict = spot::make_bdd_dict(); spot::mtdfa_ptr aut = translate("a U b", "c S d", dict); aut->print_dot(std::cout); }
If you prefer to get a twa_graph_ptr
, just use as_twa()
:
#include <spot/twaalgos/dot.hh> int main() { spot::bdd_dict_ptr dict = spot::make_bdd_dict(); spot::twa_graph_ptr aut = translate("a U b", "(c & !d) S d", dict)->as_twa(true); spot::print_dot(std::cout, aut, ".A"); }