- Overview
- Writing specifications
- Enumeration
- Annotating graphs with formulas
- Synthesizing implementations
- Assembling implementations for simulation
- Writing your own symbolic algorithms
- Lower-level and internal details
- Temporal logic syntax
- The deprecated class
omega.symbolic.symbolic.Automaton
- Some design notes
- Copying
Using the omega
package you can write raw
TLA+ specifications,
synthesize implementations from these specifications, and write your own
symbolic algorithms that rely on binary decision diagrams. You can assemble
and simulate the synthesized implementations, and inspect the results of
symbolic computations as minimal formulas.
Writing a specification does not mean
that the specification is implementable. Implementable specifications are
called realizable. Instead of constructing an implementation by hand,
you can synthesize one by giving the specification to a synthesis algorithm
from those available in the module omega.games.gr1
.
A more precise description of what we mean by synthesis is in the TLA+
module spec/Realizability.tla
.
What is an implementation? How is it different from a specification? A specification that pins down every single step-by-step detail of how the state changes is an implementation. So the distinction between implementation and specification relates to how much freedom of behavior each one allows.
Liveness formulas can specify behavior without saying what the next step should be. In addition to liveness, temporal logic includes safety formulas. You can think of safety formulas as a description of a state machine: which states can be initial, and how the state can change.
The computational cost of synthesis depends mainly on the liveness part of the specification. Synthesis of implementations from specifications with one generalized Streett pair as liveness formula has reasonable complexity. This kind of liveness properties is also known as GR(1). A Streett pair has the form:
\/ <>[] P
\/ []<> R
A generalized Streett pair has the form:
\/ <>[]P1 \/ <>[]P2 \/ ... \/ <>[]Pk
\/ []<>R1 /\ []<>R2 /\ ... /\ []<>Rm
The difference is the disjunction between Ps and the conjunction between Rs,
which cost little extra computation.
GR(1) synthesis is available in the module omega.games.gr1
, mainly via the
functions:
solve_streett_game
andmake_streett_transducer
Specifications with one generalized Rabin pair are also supported. These have the form:
/\ <>[]P1 \/ <>[]P2 \/ ... \/ <>[]Pk
/\ []<>R1 /\ []<>R2 /\ ... /\ []<>Rm
Rabin synthesis is available via the functions:
solve_rabin_game
make_rabin_transducer
Synthesis creates a state machine for the implementation, in the form of
BDDs for the initial condition and action. These BDDs are stored in the
same automaton that was given to the *_transducer
functions mentioned
above.
You can enumerate the state machine over all states that are relevant
to the assumed environment behavior by calling the function
omega.games.enumeration.action_to_steps
.
The synthesized state machine in the literature is called also strategy or controller (irrespective of how it is represented, i.e., BDDs, formulas, or enumeration of relevant states).
Since both specifications and implementations can be described by
a combination of a state machine with some liveness property, one data
structure suffices, namely the class omega.symbolic.temporal
. Sometimes
it is easier to annotate a graph with formulas, and translate that to temporal
logic. The class omega.automata.TransitionSystem
can be used for that
purpose. Below are examples of how to use these classes.
Since specifications and implementations are both state machines, the same
data structure represents them in omega
. This data structure is
omega.symbolic.temporal.Automaton
, and has attributes that store liveness
too, to accommodate for specifications that do have a liveness part.
The interesting attributes of an Automaton
are:
init
: stores initial conditions (state predicates)action
: stores what steps the state machine can take (transition predicates)win
: stores predicates that represent the liveness part. For example, expressions from recurrence formulas.
These attributes are binary decision diagrams (BDDs). You can use formulas to set the values of these attributes.
A symbolic automaton provides a “context” for symbolic computation, in the sense of a symbol table, together with a translator from first-order formulas to BDDs. This notion of context aligns with TLA+ contexts (see Sec. 17.3 on page 324 of the TLA+ manual).
import omega.symbolic.temporal as trl
# declaration of context
aut = trl.Automaton()
aut.declare_variables(x='bool', y=(0, 5))
aut.varlist = dict(env=['x'], sys=['y'])
aut.prime_varlists()
# specification, as a stepwise implication
aut.init['env'] = ' ~ x'
aut.init['sys'] = 'y >= 2'
aut.action['env'] = " IF x THEN ~ x' ELSE x' "
aut.action['sys'] = r'''
/\ (y < 5)
/\ (y' > 0)
/\ (x' => (y = y - 1))
'''
aut.win['sys'] = aut.bdds_from('y = 3')
The method declare_variables
registers both the unprimed identifiers
x
, y
and the primed identifiers x'
, y'
in the symbol table
aut.vars
(as well as bits for representing the values designated by
the type hints using BDDs). Constants can be declared with the method
declare_constants
.
Variables are defined in the attribute vars
. Variables are untyped,
as in TLA+. The type hints provide guidance to the algorithms for
symbolically representing these variables using BDDs. Variables can take
integer and Boolean values, as demonstrated by the above example.
The dictionary varlist
stores a mapping from each component name to
the variables that represent that component (as a list
).
The method prime_varlists
adds to varlist
lists of primed variables,
which are useful during fixpoint computations.
Assigning a formula as value of aut.init['env']
stores a BDD, and
similarly for the other atributes (except for aut.win
, where we need
to do the conversion in user code). The formulas are memoized, together
with the resulting BDDs, and this relation is used to pretty print the
result, as follows:
>>> print(aut)
Automaton:
Variables:
{'x': {'type': 'bool'},
"x'": {'type': 'bool'},
'y': {'bitnames': ['y_0', 'y_1', 'y_2'],
'dom': (0, 5),
'signed': False,
'type': 'int',
'width': 3},
"y'": {'bitnames': ["y_0'", "y_1'", "y_2'"],
'dom': (0, 5),
'signed': False,
'type': 'int',
'width': 3}}
init[env] = ~ x
init[sys] = y >= 2
action[env] = IF x THEN ~ x' ELSE x'
action[sys] =
/\ (y < 5)
/\ (y' > 0)
/\ (x' => (y = y - 1))
win =
{'sys': [Function (DdNode) with var index: 1, ref count: 1, int repr: 44909187]}
The methods vars_of_players
and vars_of_all_players
are convenient for
collecting variables of interest when needed. The methods replace_with_primed
and replace_with_unprimed
are useful for priming and unpriming selected
variables that a BDD depends on.
By indexing formulas by player, an Automaton
can serve also as
a multi-player game structure.
The class omega.symbolic.temporal.Automaton
subclasses the class
omega.symbolic.fol.Context
. This class provides a way of declaring
first-order variables, together with relevant type hints, representing
predicates as BDDs, and performing operations on them. The following
methods are useful:
declare
andadd_vars
for declaring (rigid) identifierssupport
for finding those variables that a BDD depends onto_bdd
andto_expr
for converting between BDDs and formulas (strings)bdds_from
is the multi-argument counterpart ofto_bdd
true
andfalse
are properties that return BDDs for the Boolean constantslet
for substitution of variables by other variables or by valuesforall
andexist
for (bounded) quantification over the type hintsapply
for operating on BDDs (using syntax is slower but more convenient)count
for finding how many assignments (relevant states) satisfy a BDDpick
andpick_iter
for enumerating satisfying assignmentsdefine
for defining operators (currently nullary operators)assign_from
is the integer analog of "cube" in BDD packages.
One of the most useful methods is to_bdd
. It takes a first-order formula
and returns a BDD node:
>>> u = aut.to_bdd(r"x' /\ (y = 5)")
Function (DdNode) with var index: 1, ref count: 1, int repr: 44909347
A BDD is a graph, sometimes with hundreds or thousands of nodes. This form is not human-friendly. For BDDs that you create directly from expressions, this may be irrelevant. But for BDDs that result from your computations (not directly from expressions you give as input), being able to understand what they represent counts.
Converting a BDD to a readable formula introduces structure and increases
information density, so it costs computation. Structuring arises due to
expressing the formula over integer-valued variables (of fol.Context
or
temporal.Automaton
), instead of BDD bits. Information is condensed due to
the formula being (typically) a construct smaller than the BDD itself.
omega
solves a minimal covering problem in order to find the minimal
formula in disjunctive-normal form that corresponds to a given BDD.
It uses an algorithm that was originally proposed for
two-level logic minimization,
i.e., for Boolean-valued variables, but formulated for arbitrary complete
lattices. This general form is implemented for the lattice of integers in
the module omega.symbolic.cover
. For those familiar with circuit design,
this algorithm addresses a problem similar to ESPRESSO.
How do you use this conversion from BDDs to formulas? Via the method
fol.Context.to_expr
. There are details (care predicate, type hints, etc)
that need an understanding of minimal covering and untyped logic to
configure conveniently, but basic usage is simple:
import omega.symbolic.fol as _fol
ctx = _fol.Context()
ctx.declare(x=(0, 3), y=(0, 7))
u = ctx.to_bdd(r'x \in 1..3 /\ y \in 4..6')
v = ctx.to_bdd(r'x \in 2..3 /\ y \in 2..5')
w = u | v
expr = ctx.to_expr(w)
print(expr)
gives the output:
\/ (x \in 1 .. 3) /\ (y \in 4 .. 6)
\/ (x \in 2 .. 3) /\ (y \in 2 .. 6)
This example demonstrates that we didn't know what formula corresponds to w
until calling the method to_expr
, because the input was expressions for
u
and v
, not for w
. The above example is simple, but when more
operations occur between input (here u, v
) and output (w
), then being
able to read the result as a formula is more useful.
A BDD represents some assignments of values to variables. Which assignments depends on the relevant variables. Any variables that the BDD does not depend on can be assigned arbitrary values. Enumeration is the process of creating such satisfying assignments from a given BDD.
The module omega.symbolic.enumeration
includes facilities for enumerating
satisfying assignments of integer and Boolean values to variables of
a fol.Context
, from a BDD. For example:
import omega.symbolic.fol as _fol
ctx = _fol.Context()
ctx.declare(x=(1, 5), y='bool')
u = ctx.to_bdd(r'x = 2 /\ ~ y')
# select a single satisfying assignment, if any
values = ctx.pick(u)
>>> values
{'x': 2, 'y': False}
The method pick
returns a single assignment, or None
if the BDD is FALSE
.
To enumerate all satisfying assignments from those assignments allowed by the
type hints, use the method pick_iter
:
u = ctx.to_bdd(r'x < 2 /\ ~ y')
assignments = list(ctx.pick_iter(u))
>>> assignments
[{'x': 0, 'y': False}, {'x': 1, 'y': False}]
By default, the pick_*
methods return assignments to all variables in the
support of a BDD (the support includes those variables that the BDD depends
on, and is returned by ctx.support(u)
). You can reduce or increase the set
of variables that occur in all assignments, via the argument care_vars
.
For example:
u = ctx.to_bdd(r'x < 2 \/ ~ y')
assignments = list(ctx.pick_iter(u))
>>> assignments
[{'x': 0, 'y': False},
{'x': 1, 'y': False},
{'x': 4, 'y': False},
{'x': 5, 'y': False},
{'x': 2, 'y': False},
{'x': 3, 'y': False},
{'x': 6, 'y': False},
{'x': 7, 'y': False},
{'x': 0, 'y': True},
{'x': 1, 'y': True}]
assignments = list(ctx.pick_iter(u, care_vars=['y']))
>>> assignments
[{'y': False}, {'x': 0, 'y': True}, {'x': 1, 'y': True}]
The first call to pick_iter
did a full enumeration of satisfying assignments
over the records [x: 0..7, y: BOOLEAN]
(TLA+ records)
whereas the second call exhaustively enumerated only over the type hints
of the variable y
.
Under the hood, the picking methods call BDD.pick_iter
,
and the function omega.symbolic.enumeration._bitfields_to_int_iter
.
In addition to writing formulas directly, sometimes it is convenient to create a graph (an enumerated data structure with nodes and edges), and label nodes and edges with formulas or assignments to variables.
These are enumerated graphs with node and edge labels. A label can be a formula (as string). A formula describes a set, thus this representation is called “semi-symbolic”. For convenience, there are two flavors:
-
automata.Automaton
: describes a language of (in)finite trees (sequences). This represents an alternating automaton with the desired acceptance condition (Buchi, Rabin, Streett, Muller, parity, etc.), variables as alphabet, and formulas as guards labeling edges. A set ofuniversal_nodes
marks those nodes where path quantification is universal. -
automata.TransitionSystem
: describes a transition relation of a discrete system. Nodes can be labeled with state predicates (formulas over unprimed variables), and edges with actions (formulas over primed variables). Some variables can be universally quantified (inputs), the rest existentially (outputs). A transition system can be viewed as an automaton with trivial acceptance condition (e.g., Buchi with all nodes accepting). It can also be viewed as a Kripke structure, or -when an environment is present- as a contracted description of a bipartite game graph.
Let's create a small transition system:
import omega.automata as _automata
g = _automata.TransitionSystem()
g.owner = 'env'
g.vars = dict(x='bool', y=(0, 5))
g.env_vars.add('x')
g.add_nodes_from(range(5))
g.add_edge(0, 1, formula=r"x /\ (y' = 4)")
g.add_edge(0, 0, formula=" ~ x")
g.add_edge(1, 2, formula="(y' = 3)")
g.add_edge(2, 3, formula="(y' = y - 1)")
g.add_edge(3, 4, formula="y' < y")
g.add_edge(4, 0, formula="y' = 5")
g.initial_nodes.add(0)
Primed identifiers refer to values of variables at the target node of an edge,
whereas unprimed ones to values at the source node. Most attributes are of
networkx.MultiDiGraph
, with additional ones in the same spirit.
Partial assignments leave the unassigned variables free
(open world semantics).
A TransitionSystem
can be converted to a symbolic.Automaton
by invoking
the function symbolic.logicizer.graph_to_logic
. The result is a description
of the same labeled graph, but entirely with formulas. An integer variable
(named as desired) is added to represent the nodes that were enumerated in
the TransitionSystem
.
Optional:
- initial nodes can be ignored in the initial condition
- self-loops added,
- assumptions for receptivity generated.
Using the transition system g
defined earlier:
import omega.symbolic.logicizer as _lgc
aut = _lgc.graph_to_logic(g, 'nd', ignore_initial=False)
Having written a specification as described above, we can use a synthesis algorithm to create an implementation. If the specification is:
AssumeGuarantee(
EnvInit, EnvNext, EnvLive
SysInit, SysNext, SysLive) ==
/\ SysInit
/\ EnvInit => /\ [](Earlier(EnvNext) => SysNext)
/\ (EnvLive /\ []EnvNext) => SysLive
Spec == AssumeGuarantee(EnvInit, EnvNext, EnvLive,
SysInit, SysNext, SysLive)
then an implementation has the form of a state machine:
Component == Init /\ [] ImplNext
where the initial condition Init
and action ImplNext
are synthesized
to ensure that this state machine implements the specification:
THEOREM Component => Spec
As noted above, the complexity of synthesis depends on what kind of liveness
formulas are in EnvLive
and SysLive
. GR(1) synthesis
handles conjunctions of recurrence formulas in these operators.
As an example:
import omega.games.gr1 as _gr1
import omega.symbolic.temporal as trl
aut = trl.Automaton()
aut.declare_variables(foo=(1, 15), bar=(-15, -1))
aut.varlist.update(env=['foo'], sys=['bar'])
aut.prime_varlists()
# specify an inverter
aut.init['env'] = r' foo \in 1..15 '
aut.init['sys'] = r' bar \in -15..-1 '
aut.action['env'] = r'''
(* type invariant *)
/\ foo \in 1..15
/\ foo' \in 1..15
'''
aut.action['sys'] = r'''
(* type invariant *)
/\ (bar \in -15..-1)
/\ (bar' \in -15..-1)
(* inversion with one-step delay *)
/\ (bar' + foo <= 0)
/\ (bar' < -2)
'''
aut.win['<>[]'] = aut.bdds_from(' ~ (foo = 3) ')
aut.win['[]<>'] = aut.bdds_from(' bar = - 3 ')
aut.plus_one = True # strictly causal stepwise implication
aut.moore = True # implementation reads current state; not foo'
aut.qinit = r'\E \A' # disjoint-state throughout
fixpoint_iterates = _gr1.solve_streett_game(aut)
_gr1.make_streett_transducer(*fixpoint_iterates, aut)
The above example shows how to:
-
instantiate an
Automaton
-
declare some variables with integer type hints (by calling the method
Automaton.declare_variables
) -
specify which variables represent each component (via the attribute
Automaton.varlist
) -
specify the initial condition, action, and liveness for each component (via the attributes
init, action, win
ofAutomaton
), see also the operatorAssumeGuarantee
above. -
specify that we are using
AssumeGuarantee
(via the attributeAutomaton.plus_one
) -
define that the implementation's action can depend on the current state, but not on the next values of any environment variables, here
foo'
(via the attributeAutomaton.moore
) -
define that this is a disjoint-state specification, so the implementation picks the initial value of (visible) component variables (here
bar
) (via the attributeAutomaton.qinit
) -
perform the fixpoint computation of the winning collection of states (by calling the function
solve_streett_game
) -
construct a symbolic implementation from the fixpoint iterates (by calling the function
make_streett_transducer
).
Synthesis is the search problem that corresponds to the decision problem
called realizability. The latter is defined in the module
spec/Realizability.tla
. From that definition, the answer should allow
unique changes of the component's state, i.e., at each state, the action
ImplNext
should allow the component to change in only one way.
This isn't the case for the action that gr1.make_streett_transducer
creates.
This action is less restrictive (in general), though it still ensures the
refinement Component => Spec
. A deterministic state machine (functional)
can be created from the synthesized action by one of the techniques described
below:
- symbolic function synthesis (
omega.symbolic.functions
), or - state machine enumeration (
omega.games.enumeration
).
A BDD implicitly represents a constraint between variables. Suppose that
the BDD u
depends on the variables x
and y
. In order to compute the
value of y
given the value of x
, we have to "solve an equation".
Moreover, for any given value of x
, multiple values of y
may satisfy u
.
Finding those values requires extra machinery, for example the availability
of a BDD package. At deployment, we usually need only one value of y
,
and want to simplify dependencies. We can achieve this by "solving" ahead
of time, to obtain an explicit answer of how to compute y
from x
.
This approach is called functional synthesis, because we are constructing a function that maps input assignments to output assignments. Functional synthesis can be viewed as a discrete realization of the implicit function theorem.
The example below shows how to use the module omega.symbolic.functions
to synthesize a function that maps an assignment to x
to an assignment
to y'
. This function is represented as a collection of BDDs, one BDD
for each bit of y'
, namely the bits y_0'
, y_1'
, and y_2'
.
import omega.symbolic.functions as fcn
import omega.symbolic.temporal as trl
aut = trl.Automaton()
aut.declare_variables(x=(1, 6), y=(1, 6))
u = aut.to_bdd("y' = x")
out_bits = aut.vars["y'"]['bitnames']
outputs = fcn.make_functions(u, out_bits, aut.bdd)
print(outputs)
An action can allow multiple steps from a specific state. This is the case for actions that result from GR(1) synthesis, as mentioned earlier. Such a "permissive" action can be compactly represented using a BDD. Looking at the BDD communicates little about what the action means.
One possibility for inspecting what an action means is to enumerate the
steps that it allows, and construct with them a graph. This is what the
module omega.games.enumeration
does, though at each state it enumerates
only one step for each environment input allowed by the assumption
(specifically by the action of the antecedent in a stepwise implication).
The example below shows how to define symbolic actions (as BDDs), and then
enumerate this representation, creating a networkx.DiGraph
.
import omega.symbolic.temporal as trl
import omega.games.enumeration as enum
import omega.symbolic.enumeration as sym_enum
# declarations
aut = trl.Automaton()
aut.declare_variables(x=(1, 6), y=(1, 6))
aut.varlist['env'] = ['x']
aut.varlist['sys'] = ['y']
aut.prime_varlists()
aut.moore = True
# specification using stepwise implication
aut.init['env'] = r'x = 1 /\ y = 2'
aut.init['sys'] = aut.true
aut.action['sys'] = r" x < 5 /\ y' = IF x = 4 THEN 1 ELSE x "
aut.action['env'] = r" x \in 1..4 /\ x' \in 1..4 "
# enumerate and plot
g = enum.action_to_steps(aut, 'env', 'sys', qinit=r'\A \A')
sym_enum._dump_graph_as_figure(g, 'foo.pdf')
The example below shows how to synthesize a symbolic implementation (with an action that may allow multiple steps from each state), and then enumerate this implementation.
import omega.games.gr1 as _gr1
import omega.games.enumeration as enum
import omega.symbolic.temporal as trl
import omega.symbolic.enumeration as sym_enum
aut = trl.Automaton()
aut.declare_variables(x=(1, 3), y=(-3, 3))
aut.varlist.update(env=['x'], sys=['y'])
aut.init['env'] = r'x = 1 /\ y = 2'
aut.init['sys'] = 'TRUE'
aut.action['env'] = r'''
/\ x \in 1..2
/\ x' \in 1..2
'''
aut.action['sys'] = r'''
/\ y \in -3..3
/\ y' = x - 3
'''
aut.win['<>[]'] = aut.bdds_from('x = 2')
aut.win['[]<>'] = aut.bdds_from('y # -1')
aut.qinit = r'\A \A' # should work from all states that
# satisfy `aut.init['env']`
aut.moore = True
aut.plus_one = True
#
# synthesize
z, yij, xijk = _gr1.solve_streett_game(aut)
_gr1.make_streett_transducer(z, yij, xijk, aut)
#
# enumerate
g = enum.action_to_steps(aut, 'env', 'impl', qinit=aut.qinit)
# plot
sym_enum._dump_graph_as_figure(g, 'foo.pdf')
The enumeration algorithm also attempts to reduce the total number of nodes in the resulting graph. To do so, it prefers steps that lead from the current node to a node that has already been enumerated, when such a step exists.
Writing simulation code is straightforward. However, unstructured simulation
code quickly becomes unreadable. To simplify the code that users write for
simulation, omega
includes utilities for instantiating state machines.
These utilities are available in the module omega.steps
.
What usually makes simulation code confusing is tracking of state. There is a tendency to treat classes as material objects, and sprinkle state among classes that represent components of a system. This approach leads to spaghetti code and confusion.
Separating state tracking from reasoning about changes of state yields
code structure that is easier to understand, and tidier to use.
This approach is taken in omega.steps
, which includes classes for:
-
wrapping synthesized components (both symbolic and enumerated ones) (classes
AutomatonStepper
,EnumStrategyStepper
). All components are Moore, in that their state changes are independent of the next state of other components. -
assembling these components into a system (the class
Assembly
) -
storing state history "in the background", and using it at the end (e.g., for plotting state changes) (the class
History
).
Moreover, you can write your own components as classes that implement the
same (simple) interface. These can then be assembled together with
synthesized components. Doing so is useful for quickly mocking parts of the
system, for debugging, and for filling in simple components, with the
Scheduler
class being such an example.
The following example demonstrates how to use these facilities:
import omega.steps as simu
import foobar
# synthesize
foo_spec = foorbar.specify_component_foo()
bar_spec = foorbar.specify_component_bar()
foorbar.synthesize_some_controller(foo_spec, sys='foo', env='bar')
foorbar.synthesize_some_controller(bar_spec, sys='bar', env='foo')
# assemble
foo = simu.AutomatonStepper(foo_spec)
bar = simu.AutomatonStepper(bar_spec)
sch = simu.Scheduler(2)
asm = simu.Assembly()
asm.machines = dict(scheduler=sch, foo=foo, bar=bar)
# run, printing the state as it changes
n_steps = 15
asm.init()
for _ in range(n_steps):
print(asm.state)
asm.step()
print(asm.state)
# plot
# ...
The interface of a component class is elementary:
-
a method
init
that returns the initial values of variables that represent the component. This method takes no arguments. -
a method
step
that returns the next values of variables that represent the component, and takes as argument the current state.
The class Assembly
takes care of tracking state changes, including
hidden state. By convention, a component variable is hidden if its name
starts with an underscore (_
). The assembly applies name-mangling to
these variables, by prepending the component's name to avoid conflicts in
the namespace shared by all components (the "global" namespace).
Other variable names remain unchanged, and are thus visible to any components
where the names of those variables are declared.
You can use the class omega.symbolic.temporal.Automaton
to write symbolic
algorithms. Useful utilities for this purpose are available in the modules
omega.symbolic.fixpoint
and omega.symbolic.prime
.
For example, a standard practice for catching errors in a BDD-based algorithm is to check that intermediate results depend on the expected variables. The simplest case is making sure that a BDD represents a state predicate, meaning that the BDD doesn't depend on any primed variables:
import omega.symbolic.prime as prm
import omega.symbolic.temporal as trl
aut = trl.Automaton()
aut.declare_variables(x=(1, 5), y=(-2, 17))
# a state predicate depends only on unprimed identifiers
u = aut.to_bdd(r'x \in 1..3 /\ y = -2')
assert prm.is_state_predicate(u), aut.support(u)
# an action may depend on primed identifiers
u = aut.to_bdd(r"x' \in 2..4")
assert not prm.is_state_predicate(u)
Another example is inspecting the support of a predicate:
aut.declare_constants(c=(1, 5))
u = aut.to_bdd(r"c = 3 /\ x \in 1..3 /\ x' \in 2..4 /\ y = 0")
print(u.support) # bits
print(aut.support(u)) # integer-valued variables
print(prm.rigid_support(u, aut)) # identifiers of constants
print(prm.flexible_support(u, aut)) # identifiers of unprimed variables
print(prm.unprimed_support(u, aut)) # all unprimed identifiers
print(prm.primed_support(u, aut)) # all primed identifiers
This is a dict
that defines variables and their type hint.
There are three flavors of tables, at different levels of abstraction:
- simple (human friendly)
- detailed (ready for bitblasting)
- bitblasted (a refinement, used for most internal operations)
The simple format is used in the attributes
omega.automata.Automaton.vars
and omega.automata.TransitionSystem.vars
.
The detailed format is used in the attribute
omega.symbolic.temporal.Automaton.vars
.
The simple format is a dict
that maps each variable name (str
) to
either 'bool'
or a tuple
of (min, max)
. For example,
dict(x='bool', y=(0, 3))
.
The detailed format is a dict
that maps each variable name (as str
) to
a dict
with key: value
pairs:
'type': 'bool' or 'modwrap' or 'saturating'
'dom': tuple(min, max)
(only for integer variables)'owner': 'env' or 'sys'
and, optionally:
"init": initial value
A comparison of detailed and bitblasted tables:
symbolic.Automaton.vars |
detailed | bitblasted |
---|---|---|
'type' |
bool or int |
same |
'dom' |
same | same |
'owner' |
same | same |
'init' |
same | same |
'bitnames' |
- | list of bit names as str |
'signed' |
- | bool |
'width' |
- | = len(bitnames) |
The function logic.bitvector.bitblast
translates first-order
logic formulas to propositional logic. The table of variables is refined
from detailed to bitblasted by logic.bitvector.bitblast_table
.
The bitblaster can also compute safety constraints, using the function
omega.logic.bitvector.type_invariants
:
-
formulas that constrain integers (derived from
'type'
and'dom'
) -
initial conditions (derived from
'init'
)
For example:
import omega.logic.bitvector as bv
t = dict(
x=dict(type='bool'),
y=dict(type='int', dom=(0, 4)))
bt = bv.bitblast_table(t)
f = r"x' /\ (y <= 5)"
s = bv.bitblast(f, bt)
gives:
>>> import pprint
>>> pprint.pprint(bt)
{'x': {'type': 'bool'},
'y': {'bitnames': ['y_0', 'y_1', 'y_2'],
'dom': (0, 4),
'signed': False,
'type': 'int',
'width': 3}}
>>> pprint.pprint(s)
(" & x' $ 11 ^ ^ 1 ! y_0 1 | & 1 ! y_0 & ^ 1 ! y_0 1 ^ ^ 0 ! y_1 ? 1 | & 0 ! "
'y_1 & ^ 0 ! y_1 ? 1 ^ ^ 1 ! y_2 ? 3 | & 1 ! y_2 & ^ 1 ! y_2 ? 3 ^ ^ 0 ! 0 ? '
'5 | & 0 ! 0 & ^ 0 ! 0 ? 5 ^ ^ 0 ! 0 ? 7 | & 0 ! 0 & ^ 0 ! 0 ? 7 ! ^ ! ^ 0 0 '
'? 9 ')
The bitblaster recognizes the action operator '
(prime symbol),
so actions are handled as first-order expressions with primed identifiers
(of flexible variables) becoming rigid variables. In other words, each
flexible variable gives rise to two rigid variables:
- one for the unprimed value (for example
x
), and - another for the primed value (for example
x'
).
Besides integer and Boolean variables defined in the bitblasted symbol table,
the bitvector.bitblast
recognizes also bits of bitfields associated with
integers in the symbol table. This allows mixing first-order formulas with
propositional formulas that have been bitblasted earlier and translated to
infix syntax.
The workflow for compiling a symbol table is shown below.
The functions that prime and order variables are described in the BDD section. The resulting bitvector formulas are in SlugsIn syntax, which uses prefix notation. Parsing and syntax are discussed in the parsing section.
Formulas are strings, and BDDs data structures that allow for efficient
reasoning during synthesis and model checking. BDDs can be regarded as
acyclic graphs. The module omega.symbolic.bdd
is responsible for
converting bitblasted formulas to BDDs.
A bitblasted formula is expressed in terms of Boolean-valued variables
("bits"). Those variables represent the integer-valued or Boolean-valued
variables that are declared at the first-order level, i.e., those
variables declared in the classes omega.symbolic.temporal.Automaton
and
omega.symbolic.fol.Context
.
The mapping between bits and the integer-valued variables that they represent
is handled within omega.symbolic.fol.Context
. By the time a formula has
been bitblasted, this refinement mapping has been taken into account.
Primes within formulas are also handled during bitblasting.
The appropriate bits (and primed bits, for variables) are declared in the
BDD manager (dd.autoref.BDD
or dd.cudd.BDD
) at the time that you call
any of the methods:
Automaton.declare_variables
Automaton.declare_constants
Context.declare
If you want to reorder the bits declared in the BDD manager to a different
order (e.g., because you have information about a good variable order),
then call the function reorder
of the appropriate module (dd.autoref
or
dd.cudd
).
There are several parsers and abstract syntax trees (AST) in the omega
package. An effort has been made to reuse the same parsers as much as
possible, and change only the backend. The main mechanism for matching one
frontend with multiple backends is by replacing the AST nodes used.
By subclassing a prototype AST, and overriding the flattening methods, any
desired backend can be created for each parser. This is further supported by
astutils
.
Linear temporal logic (LTL) and a fragment of the raw temporal logic of
actions (TLA+) are supported. The past fragment of LTL is translated to
the future fragment using temporal testers
(see omega.logic.past.translate
).
If a formula is a conjunction of an initial predicate, safety formulas of
the form [](...)
and recurrence formulas []<>(...)
, then
omega.gr1.split_gr1
can be used to split the temporal formula into
an initial condition, an action, a conjunction of recurrence formulas, and
a disjunction of persistence formulas.
These are stored in an omega.symbolic.temporal.Automaton
when calling the
function omega.gr1.closed_system_to_automaton
.
The resulting automaton stores transition-level formulas (in TLA+ these are
formulas that include primes ('
)). These formulas can be bitblasted using
the module omega.logic.bitvector
. Bitblasting generates formulas in
propositional logic, expressed using SlugsIn syntax. The bitblasted formulas
can be:
- parsed with the class
omega.symbolic.bdd.Parser
and:- converted to
BDD
s using the classomega.symbolic.bdd.BDDNodes
- pretty-printed in infix syntax for debugging purposes,
- converted to
- fed to a synthesizer that admits input in this syntax.
Integer division has C99 semantics (6.5.5, p.82).
The parser BNF is given below. The parser admits modules or expressions as input, to make it easier to work at both levels of granularity.
-------- MODULE omega_parser_grammar -------
(* Grammar of expressions and definitions parsed by
the function `omega.logic.lexyacc.Parser.parse()`.
*)
EXTENDS
BNFGrammars
maybe(x) ==
| Nil
| x
is_lexer_grammar(L) ==
/\ L.LETTER =
| OneOf("abcdefghijklmnopqrstuvwxyz")
| OneOf("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
/\ L.UNDERSCORE = tok("_")
/\ L.NUMERAL = OneOf("0123456789")
/\ L.NAME =
LET
start ==
| L.LETTER
| L.UNDERSCORE
symbol ==
| start
| L.NUMERAL
tail == symbol^*
IN
start & tail
/\ L.NUMBER = L.NUMERAL^+
(* integer *)
(* first-order expressions *)
is_parser_grammar(L, G) ==
/\ \A symbol \in DOMAIN L:
G[symbol] = L[symbol]
/\ G.module = G.unit^+
/\ G.unit =
| G.variable_declaration
| G.constant_declaration
| G.operator_def
/\ G.variable_declaration =
| tok("VARIABLE") & G.names
| tok("VARIABLES") & G.names
/\ G.constant_declaration =
| tok("CONSTANT") & G.names
| tok("CONSTANTS") & G.names
/\ G.expr =
| G.expr & tok("*") & G.expr
| G.expr & tok("/") & G.expr
(* quotient of C99 integer division *)
| G.expr & tok("%") & G.expr
(* remainder of C99 integer division *)
| G.expr & tok("+") & G.expr
| G.expr & tok("-") & G.expr
(* predicates of arithmetic *)
| G.expr & tok("=") & G.expr
| G.expr & tok("#") & G.expr
(* different *)
| G.expr & tok("!=") & G.expr
(* different *)
| G.expr & tok("/=") & G.expr
(* different *)
| G.expr & tok("<=") & G.expr
| G.expr & tok("=<") & G.expr
(* same meaning as `<=` *)
| G.expr & tok(">=") & G.expr
(* quantifiers *)
| tok("\\A") & G.qvars & tok(":") & G.expr
(* "forall" *)
| tok("\\E") & G.qvars & tok(":") & G.expr
(* "exists" *)
(* substitution *)
| tok("LET") & G.defs & tok("IN") & G.expr
(* a little set theory *)
| LET
expr == G.expr
in == tok("\\in")
DOUBLE_COLON == tok("..")
num == G.number
range == num & DOUBLE_COLON & num
IN
expr & in & range
(* in range of integers *)
(* propositional *)
(* TLA+ syntax *)
| tok("~") & G.expr
| G.expr & tok("/\\") & G.expr
| G.expr & tok("\\/") & G.expr
| G.expr & tok("=>") & G.expr
| G.expr & tok("<=>") & G.expr
(* Promela syntax *)
| tok("!") & G.expr
| G.expr & tok("&") & G.expr
| G.expr & tok("&&") & G.expr
| G.expr & tok("|") & G.expr
| G.expr & tok("||") & G.expr
| G.expr & tok("->") & G.expr
| G.expr & tok("<->") & G.expr
(* other *)
| G.expr & tok("^") & G.expr (* xor *)
| tok("IF") & G.expr &
tok("THEN") & G.expr &
tok("ELSE") & G.expr
(* TLA+ ternary conditional *)
| tok("ite") & tok("(") &
G.expr & tok(",") &
G.expr & tok(",") &
G.expr &
tok(")")
(* ternary conditional *)
(* temporal modal *)
| tok("X") & G.expr
(* "next" *)
| G.expr & tok("'")
(* "next" *)
| tok("[]") & G.expr
(* "always" *)
| tok("<>") & G.expr
(* "eventually" *)
| tok("-X") & G.expr
(* weak "previous" *)
| tok("--X") & G.expr
(* strong "previous" *)
| tok("-[]") & G.expr
(* "historically" (dual of `[]`) *)
| tok("-<>") & G.expr
(* "once" (dual of `<>`) *)
| G.expr & tok("U") & G.expr
(* strong "until" *)
| G.expr & tok("W") & G.expr
(* weak "until" *)
| G.expr & tok("R") & G.expr
(* "release" *)
| G.expr & tok("S") & G.expr
(* "since" *)
| G.expr & tok("T") & G.expr
(* "trigger" *)
(* constants / misc *)
| tok("(") & G.expr & tok(")")
| tok("TRUE")
| tok("FALSE")
| G.number
| G.variable
| G.string
/\ G.defs =
G.operator_def & maybe(G.defs)
/\ G.operator_def =
L.NAME & tok("==") & G.expr
(* operator definition *)
/\ G.qvars =
L.NAME & maybe(tok("'")) &
maybe(tok(",") & G.qvars)
(* list of quantified variables *)
/\ G.names =
L.NAME &
maybe(tok(",") & G.names)
(* list of variable or constant names *)
/\ G.variable =
L.NAME
/\ G.string =
tok("\"") & L.NAME & tok("\"")
/\ G.number =
maybe(tok("-")) & L.NUMBER
============================================
The token precedence (lowest to highest) and associativity (r = right, l = left, n = none) is:
:
(l)<=>
,<->
(l)=>
,->
(l)^
(l)\/
,|
(l)/\
,&
(l)[]
,<>
,-[]
,-<>
(l)U
,W
,R
,S
,T
(l)=
,#
,/=
,!=
(l)<=
,=<
,>=
,>
(l)+
,-
(l)*
,/
,%
(l)~
,!
(r)X
,-X
,--X
(r)'
,.
(l)
Comments start at (*
and close at *)
, which can appear on different lines.
One-line comments start at \*
and extend to the end of the line.
The class omega.symbolic.symbolic.Automaton
has been deprecated in favor
of the class omega.symbolic.temporal.Automaton
. The deprecated class
provided a less direct route from formulas to BDDs, because the user had
to first populate an Automaton
instance, and then call the build
method
to generate primed variables, bitblast formulas and the symbol table,
and populate auxiliary attributes. Building returned a second Automaton
instance, which was less practical than working with a single instance.
An assignment to variables is represented as a dict
.
At the interface with humans, a formula is represented as a string. Deeper, formulas are converted to binary decision diagrams (BDDs) for certain operations.
There are two approaches to ensuring a data structure is well-formed: during
creation, and at check points. The latter is simple, flat, and easy to maintain.
Most classes have a method assert_consistent
that performs these checks.
This collects all checks at one place, to override and modify when subclassing.
Synthesis specifications include assumptions about the world outside
the component, called "environment". The strings "env"
and "sys"
are
used to distinguish assumptions from guarantees, in the context of a single
component. When an Automaton
represents multiple components, the names
of those components are used instead of "env"
and "sys"
.
The classes omega.symbolic.temporal.Automaton
and
omega.symbolic.symbolic.Automaton
are inspired by the design of the class
tulip.spec.form.GRSpec
.
This document is copyright 2015-2020 by California Institute of Technology. All rights reserved. Licensed under 3-clause BSD.