def ltl2prefix(ltl: str): parser = LTLfParser() formula = parser( ltl.replace('1', 'true').replace('0', 'false').replace('W', 'R')) return preorder(formula).replace('true', '1').replace('false', '0').replace('R', 'W')
def test_ltlf_example_readme(): from ltlf2dfa.parser.ltlf import LTLfParser parser = LTLfParser() formula = "G(a -> X b)" parsed_formula = parser(formula) assert str(parsed_formula) == "G((a -> X(b)))" assert parsed_formula.find_labels() == [c for c in "ab"]
def test_mona(): parser = LTLfParser() a, b, c = [LTLfAtomic(c) for c in "abc"] tt = LTLfTrue() ff = LTLfFalse() assert a.to_mona(v="0") == "(0 in A)" assert b.to_mona(v="0") == "(0 in B)" assert c.to_mona(v="0") == "(0 in C)" assert tt.to_mona(v="0") == "true" assert ff.to_mona(v="0") == "false" f = parser("!(a & !b)") assert f.to_mona(v="0") == "~(((0 in A) & ~((0 in B))))" f = parser("!(!a | b)") assert f.to_mona(v="0") == "~((~((0 in A)) | (0 in B)))" f = parser("!(a <-> b)") assert (f.to_nnf().to_mona( v="0") == "((~((0 in A)) | ~((0 in B))) & ((0 in A) | (0 in B)))") # Next and Weak Next f = parser("X(a & b)") assert f.to_mona(v="0") == "(ex1 v_1: v_1=1 & ((v_1 in A) & (v_1 in B)))" f = parser("WX (a & b)") assert (f.to_mona(v="0") == "((0 = max($)) | (ex1 v_1: v_1=1 & ((v_1 in A) & (v_1 in B))))") # Until and Release f = parser("a U b") assert ( f.to_mona(v="0") == "(ex1 v_1: 0<=v_1&v_1<=max($) & (v_1 in B) & (all1 v_2: 0<=v_2&v_2<v_1" " => (v_2 in A)))") f = parser("a R b") assert ( f.to_mona(v="0") == "((ex1 v_1: 0<=v_1&v_1<=max($) & (v_1 in A) & (all1 v_2: 0<=v_2&v_2<=v_1" " => (v_2 in B))) | (all1 v_2: 0<=v_2&v_2<=max($) => (v_2 in B)))") # Eventually and Always f = parser("F(a & b)") assert ( f.to_mona(v="0") == "(ex1 v_1: 0<=v_1&v_1<=max($) & ((v_1 in A) & (v_1 in B)) & (all1 v_2: " "0<=v_2&v_2<v_1 => true))") f = parser("G(a | b)") assert ( f.to_mona(v="0") == "((ex1 v_1: 0<=v_1&v_1<=max($) & false & (all1 v_2: 0<=v_2&v_2<=v_1 => " "((v_2 in A) | (v_2 in B)))) | (all1 v_2: 0<=v_2&v_2<=max($) => ((v_2 in A) " "| (v_2 in B))))")
def dfa(): formula_string = request.form["inputFormula"] control_set = request.form["controllables"] uncontrol_set = request.form["uncontrollables"] assert formula_string automa_name = "dfa_" + str(datetime.datetime.now()).replace( " ", "_") + "_" + str(uuid.uuid4()) isLtlf = True if all(c in FUTURE_OPS for c in formula_string if c.isupper()): f_parser = LTLfParser() try: formula = f_parser(formula_string) except Exception as e: if request.form.get("exampleCheck1"): return render_template("dfa.html", error=str(e).encode("utf-8")) return render_template("index.html", error=str(e).encode("utf-8")) else: assert all(c in PAST_OPS for c in formula_string if c.isupper()) isLtlf = False p_parser = PLTLfParser() try: formula = p_parser(formula_string) except Exception as e: if request.form.get("exampleCheck1"): return render_template("dfa.html", error=str(e).encode("utf-8")) return render_template("index.html", error=str(e).encode("utf-8")) dfa = ltl2dfagame(formula_string, control_set, uncontrol_set, isLtlf) write_dot_file(str(dfa), automa_name) subprocess.call('dot -Tsvg {} -o {}'.format( "{}/static/dot/{}.dot".format(PACKAGE_DIR, automa_name), "{}/static/tmp/{}.svg".format(PACKAGE_DIR, automa_name)), shell=True) encoding = encode_svg("{}/static/tmp/{}.svg".format( PACKAGE_DIR, automa_name)).decode("utf-8") piero_encoding = encode_svg( "{}/static/tmp/piero.svg".format(PACKAGE_DIR)).decode("utf-8") os.unlink("{}/static/dot/{}.dot".format(PACKAGE_DIR, automa_name)) os.unlink("{}/static/tmp/{}.svg".format(PACKAGE_DIR, automa_name)) os.unlink("{}/static/tmp/piero.svg".format(PACKAGE_DIR)) return render_template("dfa.html", formula=formula, output=piero_encoding, output2=encoding)
def test_QuotedFormula(): from ltlf2dfa.base import QuotedFormula from ltlf2dfa.ltlf import LTLfAnd, LTLfAtomic from ltlf2dfa.parser.ltlf import LTLfParser f = LTLfParser()("!(G a)") qf = QuotedFormula(f) atomf = LTLfAnd([LTLfAtomic(f), LTLfAtomic(f)]) assert qf.wrapped is f dir_qf = dir(qf) for member in dir(f): assert member in dir_qf assert hasattr(qf, member)
def test_hash_consistency_after_pickling(): from ltlf2dfa.parser.ltlf import LTLfParser import pickle parser = LTLfParser() formula = "F (a & !b)" old_obj = parser(formula) h = hash(old_obj) pickle.dump(old_obj, open("temp", "wb")) new_obj = pickle.load(open("temp", "rb")) assert new_obj._hash is None assert h == hash(new_obj) os.remove("temp")
def ltl2dfagame(ltl, controllables, uncontrollables, isLtlf): controllables = controllables.split(' ') uncontrollables = uncontrollables.split(' ') print("controllables :") print( controllables) print("uncontrollables :") print( uncontrollables) controllables = set(controllables) uncontrollables = set(uncontrollables) ### trasformazione in automata (LTL2DFA) if(isLtlf): parser = LTLfParser() else: parser = PLTLfParser() formula = parser(ltl) print("converting to automaton....") dfa = formula.to_dfa() print(dfa) # prints the DFA in DOT format ### trasformazione in automata (PYTHOMATA) dfa = converter(dfa) ### stampo automa normale graph = dfa.to_graphviz() #graph.render(outputFileName+"_normal") ### calcolo winning dict print("\n\n############# winning_dict ################") win_dict = winning_dict(dfa, controllables, uncontrollables) #print(win_dict) ### stampo winning map graph = to_graphviz_winning_map(dfa, win_dict) #graph.render(outputFileName) return graph
def execute(planning_domain, planning_problem, goal_formula): """Execute the compilation.""" pddl_parser = PDDLParser() parsed_domain = pddl_parser(planning_domain) parsed_problem = pddl_parser(planning_problem) # parsed_domain = planning_domain # parsed_problem = planning_problem symbols = compute_symb_vars(goal_formula) if not check_symbols( symbols, parsed_domain): # TODO: it checks symbols but not objects.... raise ValueError("[ERROR]: Formula symbols not in the domain.") if all(c in FUTURE_OPS for c in goal_formula if c.isupper()): f_parser = LTLfParser() try: formula = f_parser(goal_formula) except Exception: raise ParsingError() else: assert all(c in PAST_OPS for c in goal_formula if c.isupper()) p_parser = PLTLfParser() try: formula = p_parser(goal_formula) except Exception: raise ParsingError() mona_output = formula.to_dfa(mona_dfa_out=True) dfa = parse_dfa(mona_output) operators_trans, parameters = dfa.create_operators_trans( parsed_domain.predicates, symbols) new_domain = parsed_domain.get_new_domain(parameters, dfa.states, operators_trans) new_problem = parsed_problem.get_new_problem(list(dfa.accepting_states), symbols) return new_domain, new_problem
def test_parser(): parser = LTLfParser() a, b, c = [LTLfAtomic(c) for c in "abc"] assert parser("!a | b <-> !(a & !b) <-> a->b") == LTLfEquivalence([ LTLfOr([LTLfNot(a), b]), LTLfNot(LTLfAnd([a, LTLfNot(b)])), LTLfImplies([a, b]), ]) assert parser("(X a) & (WX !b)") == LTLfAnd( [LTLfNext(a), LTLfWeakNext(LTLfNot(b))]) assert parser("(F (a&b)) <-> !(G (!a | !b) )") == LTLfEquivalence([ LTLfEventually(LTLfAnd([a, b])), LTLfNot(LTLfAlways(LTLfOr([LTLfNot(a), LTLfNot(b)]))), ]) assert parser("(a U b U c) <-> !(!a R !b R !c)") == LTLfEquivalence([ LTLfUntil([a, b, c]), LTLfNot(LTLfRelease([LTLfNot(a), LTLfNot(b), LTLfNot(c)])), ])
def test_nnf(): parser = LTLfParser() a, b, c = [LTLfAtomic(c) for c in "abc"] f = parser("!(a & !b)") assert f.to_nnf() == LTLfOr([LTLfNot(a), b]) f = parser("!(!a | b)") assert f.to_nnf() == LTLfAnd([a, LTLfNot(b)]) f = parser("!(a <-> b)") assert f.to_nnf() == LTLfAnd( [LTLfOr([LTLfNot(a), LTLfNot(b)]), LTLfOr([a, b])]) # Next and Weak Next f = parser("!(X (a & b))") assert f.to_nnf() == LTLfWeakNext(LTLfOr([LTLfNot(a), LTLfNot(b)])) f = parser("!(WX (a & b))") assert f.to_nnf() == LTLfNext(LTLfOr([LTLfNot(a), LTLfNot(b)])) # Eventually and Always f = parser("!(F (a | b))") assert f.to_nnf() == LTLfAlways(LTLfAnd([LTLfNot(a), LTLfNot(b)])).to_nnf() # Until and Release f = parser("!(a U b)") assert f.to_nnf() == LTLfRelease([LTLfNot(a), LTLfNot(b)]) f = parser("!(a R b)") assert f.to_nnf() == LTLfUntil([LTLfNot(a), LTLfNot(b)]) f = parser("!(F (a | b))") assert f.to_nnf() == LTLfAlways(LTLfAnd([LTLfNot(a), LTLfNot(b)])).to_nnf() f = parser("!(G (a | b))") assert f.to_nnf() == LTLfEventually(LTLfAnd([LTLfNot(a), LTLfNot(b)])).to_nnf()
def test_ltlf_dfa(): parser = LTLfParser() f = parser("a") dfa = f.to_dfa(mona_dfa_out=False) mona_dfa = f.to_dfa(mona_dfa_out=True) expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 3; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 2 [label="~a"]; 1 -> 3 [label="a"]; 2 -> 2 [label="true"]; 3 -> 3 [label="true"]; }""" expected_mona = """DFA for formula with free variables: A Initial state: 0 Accepting states: 3 Rejecting states: 0 1 2 Automaton has 4 states and 4 BDD-nodes Transitions: State 0: X -> state 1 State 1: 0 -> state 2 State 1: 1 -> state 3 State 2: X -> state 2 State 3: X -> state 3 A counter-example of least length (0) is: A X A = {} A satisfying example of least length (1) is: A X 1 A = {0}""" assert dfa == expected assert mona_dfa == expected_mona f = parser("true") dfa = f.to_dfa(mona_dfa_out=False) expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 1; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 1 [label="true"]; }""" assert dfa == expected f = parser("false") dfa = f.to_dfa(mona_dfa_out=False) expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 1 [label="true"]; }""" assert dfa == expected f = parser("G a") dfa = f.to_dfa(mona_dfa_out=False) expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 1; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 2 [label="~a"]; 1 -> 1 [label="a"]; 2 -> 2 [label="true"]; }""" assert dfa == expected f = parser("F(a & b)") dfa = f.to_dfa(mona_dfa_out=False) expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 2; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 1 [label="~a | ~b"]; 1 -> 2 [label="a & b"]; 2 -> 2 [label="true"]; }""" assert dfa == expected f = parser("X(a)") dfa = f.to_dfa(mona_dfa_out=False) expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 4; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 2 [label="true"]; 2 -> 3 [label="~a"]; 2 -> 4 [label="a"]; 3 -> 3 [label="true"]; 4 -> 4 [label="true"]; }""" assert dfa == expected f = parser("a U b") dfa = f.to_dfa(mona_dfa_out=False) expected1 = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 3; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 2 [label="~a & ~b"]; 1 -> 3 [label="b"]; 1 -> 4 [label="a & ~b"]; 2 -> 2 [label="true"]; 3 -> 3 [label="true"]; 4 -> 2 [label="~a & ~b"]; 4 -> 3 [label="b"]; 4 -> 4 [label="a & ~b"]; }""" expected2 = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 3; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 2 [label="~a & ~b"]; 1 -> 3 [label="a & ~b"]; 1 -> 4 [label="b"]; 2 -> 2 [label="true"]; 3 -> 2 [label="~a & ~b"]; 3 -> 3 [label="a & ~b"]; 3 -> 4 [label="b"]; 4 -> 4 [label="true"]; }""" assert dfa == expected1 or expected2 f = parser("G(a) & F(b)") dfa = f.to_dfa(mona_dfa_out=False) expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 3; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 2 [label="~a"]; 1 -> 1 [label="a & ~b"]; 1 -> 3 [label="a & b"]; 2 -> 2 [label="true"]; 3 -> 2 [label="~a"]; 3 -> 3 [label="a"]; }""" assert dfa == expected
def test_ltlf_mona_dfa(): parser = LTLfParser() f = parser("a") mona_dfa = f.to_dfa(mona_dfa_out=True) expected_mona = """DFA for formula with free variables: A Initial state: 0 Accepting states: 3 Rejecting states: 0 1 2 Automaton has 4 states and 4 BDD-nodes Transitions: State 0: X -> state 1 State 1: 0 -> state 2 State 1: 1 -> state 3 State 2: X -> state 2 State 3: X -> state 3 A counter-example of least length (0) is: A X A = {} A satisfying example of least length (1) is: A X 1 A = {0}""" assert mona_dfa == expected_mona f = parser("true") mona_dfa = f.to_dfa(mona_dfa_out=True) expected_mona = """DFA for formula with free variables: Initial state: 0 Accepting states: 1 Rejecting states: 0 Automaton has 2 states and 1 BDD-node Transitions: State 0: -> state 1 State 1: -> state 1 Formula is valid A satisfying example of least length (0) is:""" assert mona_dfa == expected_mona f = parser("false") mona_dfa = f.to_dfa(mona_dfa_out=True) expected_mona = """DFA for formula with free variables: Initial state: 0 Accepting states: Rejecting states: 0 Automaton has 1 state and 1 BDD-node Transitions: State 0: -> state 0 Formula is unsatisfiable A counter-example of least length (0) is:""" assert mona_dfa == expected_mona f = parser("G a") mona_dfa = f.to_dfa(mona_dfa_out=True) expected_mona = """DFA for formula with free variables: A Initial state: 0 Accepting states: 1 Rejecting states: 0 2 Automaton has 3 states and 3 BDD-nodes Transitions: State 0: X -> state 1 State 1: 0 -> state 2 State 1: 1 -> state 1 State 2: X -> state 2 A counter-example of least length (1) is: A X 0 A = {} A satisfying example of least length (0) is: A X A = {}""" assert mona_dfa == expected_mona f1 = parser("F(WX(false))") f2 = parser("F(!(X(!(false))))") mona_dfa_1 = f1.to_dfa(mona_dfa_out=True) mona_dfa_2 = f2.to_dfa(mona_dfa_out=True) assert mona_dfa_1 == mona_dfa_2 f1 = parser("F(b & WX false) -> F(a & (WX false | X(WX false)))") f2 = parser( "((! (F (! ((! b) || (X (! false)))))) || (F (! ((! a) || (! ((! (X (! false))) || (X (! (X (! false))))))))))" ) mona_dfa_1 = f1.to_dfa(mona_dfa_out=True) mona_dfa_2 = f2.to_dfa(mona_dfa_out=True) assert mona_dfa_1 == mona_dfa_2
def ltl2dfagame(ltl, controllables, uncontrollables, isLtlf): controllables = controllables.split(' ') uncontrollables = uncontrollables.split(' ') print("controllables :") print(controllables) print("uncontrollables :") print(uncontrollables) controllables = set(controllables) uncontrollables = set(uncontrollables) ### trasformazione in automata (LTL2DFA) if (isLtlf): parser = LTLfParser() else: parser = PLTLfParser() formula = parser(ltl) print("converting to automaton....") dfa = formula.to_dfa() print(dfa) # prints the DFA in DOT format ### trasformazione in automata (PYTHOMATA) dfa = converter(dfa) ### stampo automa normale graph = dfa.to_graphviz() #graph.render(outputFileName+"_normal") ### calcolo winning dict print("\n\n############# winning_dict ################") win_dict, strat_list = winning_dict(dfa, controllables, uncontrollables) #print(win_dict) ### stampo winning map graph, color_map = to_graphviz_winning_map(dfa, win_dict) #graph.render(outputFileName) ### stampo se e' realizzabile realizable = realizzabile(dfa, win_dict) #realizable = False ### stampo la strategy ''' if realizable: strat, strat_list = computeStrategy(dfa, win_dict, controllables) else: strat_list = [] ''' if not realizable: strat_list = [] ### plot print(color_map) plot(ltl, strat_list, realizable, controllables, uncontrollables, color_map) return graph
def ltl2turple(ltl: str): parser = LTLfParser() formula = parser( ltl.replace('1', 'true').replace('0', 'false').replace('W', 'R')) return preorder_turple(formula)
def test_ltlf_dfa(): parser = LTLfParser() f = parser("a") dfa = f.to_dfa() expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 3; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 2 [label="~a"]; 1 -> 3 [label="a"]; 2 -> 2 [label="true"]; 3 -> 3 [label="true"]; }""" assert dfa == expected f = parser("true") dfa = f.to_dfa() expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 1; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 1 [label="true"]; }""" assert dfa == expected f = parser("false") dfa = f.to_dfa() expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 1 [label="true"]; }""" assert dfa == expected f = parser("G a") dfa = f.to_dfa() expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 3; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 2 [label="~a"]; 1 -> 3 [label="a"]; 2 -> 2 [label="true"]; 3 -> 2 [label="~a"]; 3 -> 3 [label="a"]; }""" assert dfa == expected f = parser("F(a & b)") dfa = f.to_dfa() expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 3; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 2 [label="~a | ~b"]; 1 -> 3 [label="a & b"]; 2 -> 2 [label="~a | ~b"]; 2 -> 3 [label="a & b"]; 3 -> 3 [label="true"]; }""" assert dfa == expected f = parser("X(a)") dfa = f.to_dfa() expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 4; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 2 [label="true"]; 2 -> 3 [label="~a"]; 2 -> 4 [label="a"]; 3 -> 3 [label="true"]; 4 -> 4 [label="true"]; }""" assert dfa == expected f = parser("a U b") dfa = f.to_dfa() expected1 = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 3; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 2 [label="~a & ~b"]; 1 -> 3 [label="b"]; 1 -> 4 [label="a & ~b"]; 2 -> 2 [label="true"]; 3 -> 3 [label="true"]; 4 -> 2 [label="~a & ~b"]; 4 -> 3 [label="b"]; 4 -> 4 [label="a & ~b"]; }""" expected2 = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 3; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 2 [label="~a & ~b"]; 1 -> 3 [label="a & ~b"]; 1 -> 4 [label="b"]; 2 -> 2 [label="true"]; 3 -> 2 [label="~a & ~b"]; 3 -> 3 [label="a & ~b"]; 3 -> 4 [label="b"]; 4 -> 4 [label="true"]; }""" assert dfa == expected1 or expected2 f = parser("G(a) & F(b)") dfa = f.to_dfa() expected = """digraph MONA_DFA { rankdir = LR; center = true; size = "7.5,10.5"; edge [fontname = Courier]; node [height = .5, width = .5]; node [shape = doublecircle]; 4; node [shape = circle]; 1; init [shape = plaintext, label = ""]; init -> 1; 1 -> 2 [label="~a"]; 1 -> 3 [label="a & ~b"]; 1 -> 4 [label="a & b"]; 2 -> 2 [label="true"]; 3 -> 2 [label="~a"]; 3 -> 3 [label="a & ~b"]; 3 -> 4 [label="a & b"]; 4 -> 2 [label="~a"]; 4 -> 4 [label="a"]; }""" assert dfa == expected