Tutorial

This page presents a short overview of PyNuSMV capabilities with a small example in the Getting started section. It then goes deeper into these capabilities and explains how to use them in the next sections.

Getting started

Let’s consider the following SMV model. This model is composed of two counters, incrementing from 0 to 3, and looping. They run asynchronously and the running one is defined at each step by the run action.

MODULE counter(run, start, stop)
    -- A modulo counter
    -- Go from start (inclusive) to stop (exclusive) by 1-increments
    -- Run only when run is true
    VAR c : start..stop;
    INIT c = start
    TRANS next(c) = case    run : case  c + 1 = stop : start;
                                        TRUE : c + 1; esac;
                            !run: c;
                    esac

MODULE main
    IVAR
        run : {rc1, rc2};
    VAR
        c1 : counter(run = rc1, start, stop);
        c2 : counter(run = rc2, start, stop);
    DEFINE
        start := 0;
        stop := 3;


SPEC AF c1.c = stop - 1

Considering that the model is saved in the counters.smv file in the current directory, we can now run Python. The following Python session shows the basics of PyNuSMV. After importing pynusmv, the function init_nusmv must be called before calling any other PyNuSMV functionality. The function deinit_nusmv must also be called after using PyNuSMV to release all resources hold by NuSMV. After initializing PyNuSMV, the model is read with the function load_from_file and the model is computed, that is, flattened and encoded into BDDs, with the function compute_model.

>>> import pynusmv
>>> pynusmv.init.init_nusmv()
>>> pynusmv.glob.load_from_file("counters.smv")
>>> pynusmv.glob.compute_model()
>>> pynusmv.init.deinit_nusmv()

Another way to initialize and release NuSMV resources is to use the result of the init_nusmv function as a context manager with the with statement. The following code is equivalent to the one above:

>>> import pynusmv
>>> with pynusmv.init.init_nusmv():
...     pynusmv.glob.load_from_file("counters.smv")
...     pynusmv.glob.compute_model()

All NuSMV resources are automatically released when the context is exited.

The next Python session shows functionalities of FSMs, access to specifications of the model, calls to CTL model checking and manipulation of BDDs. First, NuSMV is initialized and the model is read. Then the model encoded with BDDs is retrieved from the main propositions database. The first (and only) proposition is then retrieved from the same database, and the specification of this proposition is isolated.

From the BDD-encoded FSM fsm and the specification spec, we call the eval_ctl_spec function to get all the states of fsm satisfying spec. Conjuncted with the set of reachables states of the model, we get bdd, a BDD representing all the reachable states of fsm satisfying spec. Finally, from this BDD we extract all the single states and display them, that is, we display, for each of them, the value of each state variable of the model.

>>> import pynusmv
>>> pynusmv.init.init_nusmv()
>>> pynusmv.glob.load_from_file("counters.smv")
>>> pynusmv.glob.compute_model()
>>> fsm = pynusmv.glob.prop_database().master.bddFsm
>>> fsm
<pynusmv.fsm.BddFsm object at 0x1016d9e90>
>>> prop = pynusmv.glob.prop_database()[0]
>>> prop
<pynusmv.prop.Prop object at 0x101770250>
>>> spec = prop.expr
>>> print(spec)
AF c1.c = stop - 1
>>> bdd = pynusmv.mc.eval_ctl_spec(fsm, spec) & fsm.reachable_states
>>> bdd
<pynusmv.dd.BDD object at 0x101765a90>
>>> satstates = fsm.pick_all_states(bdd)
>>> for state in satstates:
...     print(state.get_str_values())
...
{'c1.c': '2', 'c2.c': '2', 'stop': '3', 'start': '0'}
{'c1.c': '2', 'c2.c': '0', 'stop': '3', 'start': '0'}
{'c1.c': '2', 'c2.c': '1', 'stop': '3', 'start': '0'}
>>> pynusmv.init.deinit_nusmv()

This short tutorial showed the main functionalities of PyNuSMV. More of them are available, such as functionalities to parse and evaluate a simple expression, to build new CTL specifications, or to perform operations on BDDs. The rest of this page gives more details on these functionalities. The full documentation of the library is also given beside this tutorial.

Defining a model

As explained above, a model can be defined in SMV format and loaded into PyNuSMV through a file. PyNuSMV also provides a set of classes in the model module to define an SMV model directly in Python. For instance, the two-counter model above can befined with

from pynusmv.model import *

class counter(Module):
    COMMENT = """
        A modulo counter
        Go from start (inclusive) to stop (exclusive) by 1-increments
        Run only when run is true
    """
    run, start, stop = (Identifier(id_) for id_ in ("run", "start", "stop"))
    ARGS = [run, start, stop]
    c = Var(Range(start, stop))
    INIT = [c == start]
    TRANS = [c.next() == (Case(((run, Case((((c + 1) == stop, start),
                                            (Trueexp(), c + 1)))),
                                (~run, c))))]

class main(Module):
    start = Def(0)
    stop = Def(3)
    run = IVar(Scalar(("rc1", "rc2")))
    c1 = Var(counter(run == "rc1", start, stop))
    c2 = Var(counter(run == "rc2", start, stop))

print(counter)
print(main)

This prints the following

-- A modulo counter
-- Go from start (inclusive) to stop (exclusive) by 1-increments
-- Run only when run is true
MODULE counter(run, start, stop)
    VAR
        c: start .. stop;
    INIT
        c = start
    TRANS
        next(c) =
        case
            run:
            case
                c + 1 = stop: start;
                TRUE: c + 1;
            esac;
            ! run: c;
        esac
MODULE main
    DEFINE
        start := 0;
        stop := 3;
    IVAR
        run: {rc1, rc2};
    VAR
        c1: counter(run = rc1, start, stop);
        c2: counter(run = rc2, start, stop);

SMV state and input variables can be declared as members of the Module sub-class defining the module by instantiating Var and IVar classes. The argument to the constructor is the type of the variable, and can be either a primitive one (Range, Boolean, etc.), or instances of another module. All these instantiated objects can then be used as identifiers everywhere in the module definition. The different sections of an SMV module are declared as members with the corresponding names such as INIT, TRANS, or ASSIGN. Some must be iterables (such as INIT and TRANS), others must be mappings (such as ASSIGN). The model module supports a large variety of classes to define all concepts in SMV modules. For instance, in the code above, we can write c1.c for the c variable of the c1 instance. Standard arithmetic operations such as additions are supported by SMV expressions, as shown with c + 1.

Another way to produce a Python-defined NuSMV model is to parse an existing SMV model (as a string) with the parser module functionalities. It contains the parseAllString function to parse a string according to a pre-defined parser. Several parsers are provided to parse identifiers (parser.identifier), expressions (parser.next_expression), modules (parser.module), etc.

Loading a model

The Python-defined modules can be loaded in PyNuSMV in a similar way to SMV files:

import pynusmv
pynusmv.init.init_nusmv()
pynusmv.glob.load(counter, main)

This load function accepts either sub-classes of Module, a single path to an SMV file, or a string containing the whole definition of the model. Once the model is loaded, the corresponding internal data structures such as the BDD-encoded finite-state machine are built with

pynusmv.glob.compute_model()

This compute_model function accepts the path to a file containing the BDD variable order to use for building the BDD FSM, and whether or not single enumerations should be kept as they are, or converted into defines. Once the model is built, the BDD-encoded FSM is accessed via

fsm = pynusmv.glob.prop_database().master.bddFsm

Manipulating BDDs

The BDD-encoded finite-state machine representing the SMV model is an instance of the BddFsm class. It gives access to the parts of the model: the BDD representing the initial states (fsm.init), its reachable states (fsm.reachable_states), etc. It also allows us to pick one particular state from a given BDD-encoded set of states with the pick_one_state method, or to count the input values contained in one BDD:

print(fsm.count_states(fsm.init))
for state in fsm.pick_all_states(fsm.init):
    print(state.get_str_values())

prints

1
{'stop': '3', 'c1.c': '0', 'start': '0', 'c2.c': '0'}

The BddFsm class also gives access to the transition relation (fsm.trans) and the pre and post methods returning the pre- and post-images of given states:

for state in fsm.pick_all_states(fsm.post(fsm.init)):
    print(state.get_str_values())

prints

{'stop': '3', 'c1.c': '0', 'start': '0', 'c2.c': '1'}
{'stop': '3', 'c1.c': '1', 'start': '0', 'c2.c': '0'}

The transition relation is an instance of the BddTrans class and can be replaced. Several transition relations can also co-exist, separately from the FSM itself. The BddTrans class provides a way to define a new separated transition relation from a TRANS expression:

from pynusmv.fsm import BddTrans
trans = BddTrans.from_string(fsm.bddEnc.symbTable,"next(c1.c) = 0")
for state in fsm.pick_all_states(trans.post(fsm.init)):
    print(state.get_str_values())

prints

{'start': '0', 'c1.c': '0', 'stop': '3', 'c2.c': '3'}
{'start': '0', 'c1.c': '0', 'stop': '3', 'c2.c': '2'}
{'start': '0', 'c1.c': '0', 'stop': '3', 'c2.c': '1'}
{'start': '0', 'c1.c': '0', 'stop': '3', 'c2.c': '0'}

The BDD-encoded FSM can also return the BDD encoder BddEnc (through fsm.bddEnc) that keeps track of how the model variables are encoded into BDD variables. This encoder gives access to masks, such as BddEnc.inputsMask, that represents all valid values for input variables, for instance. It also gives access to cubes (BddEnc.statesCube, for instance) and can produce cubes for particular state or input variables via BddEnc.cube_for_inputs_vars. Finally, it gives access to the current order of BDD variables used for building BDDs and the set of declared variables:

enc = fsm.bddEnc
print(enc.stateVars)
print(enc.inputsVars)

prints

frozenset({'c1.c', 'c2.c'})
frozenset({'run'})

The BDD encoder also gives access to the symbols table that is used to store the symbols of the model (bddEnc.symbTable). This SymbTable can be used to declare new variables and to encode them into BDD variables.

Most of the parts of the FSM, such as the initial and reachable states, or the masks and cubes returned by the BDD encoder, are encoded into BDDs. These BDDs are instances of the BDD class, that provides several operations on BDDs. For instance,

fsm.reachable_states & fsm.fair_states

computes the conjunct of both BDDs, getting the fair reachable states of the model. Most common BDD operations are provided as builtin operators, such as disjunction (|), conjunction (&), and negation (~). These BDDs also support comparison, and the class provides a way to build the True and False canonical BDDs with BDD.true() and BDD.false(), respectively. Finally, the dd module contains some function to enable or disable BDD variable reordering:

pynusmv.dd.enable_dynamic_reordering()

Defining properties

A NuSMV property prop is a structure containing useful information about a given specification: its type prop.type (LTL or CTL specification, etc.), its name prop.name, its actual temoral-logic formula prop.expr, its status prop.status (unchecked, true, false), etc. These properties are represented in PyNuSMV with Prop instances. They come from a property database (PropDb) built and populated by NuSMV. The property database associated to the model built by NuSMV can be obtained through the glob module:

prop_db = pynusmv.glob.prop_database()

once the model has been built with compute_model. The property database contains all properties defined beside the loaded model, such as the specification AF c1.c = stop - 1 defined in the counters.smv model at the beginning of this tutorial. It acts as a sequence of properties and particular properties can be accessed through their indices.

Property expressions spec are instances of the Spec class. They reflect NuSMV internal structures, so they have a type spec.type, a left child spec.car and a right child spec.cdr (both can be None, depending on the type of the expression). New specifications can be defined thanks to prop module functions such as atomic propositions with the atom function, Boolean operators (&, |, etc.), CTL operators (ag, ef, etc.), and LTL ones (x, u, etc.) For instance, the specification AF c1.c = stop - 1 can be built with

from pynusmv import prop
spec = prop.af(prop.atom("c1.c = stop - 1"))

Verifying properties

Once a model is loaded into PyNuSMV and a specification is defined, the latter can be checked on the former. The mc module provides all functionalities to perform this verification. It contains high-level functions as check_ltl_spec and check_ctl_spec for directly checking formulas. For instance, the specification above can be checked with

from pynusmv.mc import check_ctl_spec
print(check_ctl_spec(fsm, spec)) # Prints False

It also gives access to lower-level functions to evaluate the BDD representing the states satisfying some CTL formula (eval_ctl_spec), or to evaluate particular operators (mc.eg, etc.) It can also explain why a given specification is not satisfied by a given model:

from pynusmv.mc import explain, eval_ctl_spec
explanation = explain(fsm, fsm.init & ~eval_ctl_spec(fsm, spec), spec)

The produced explanation is a sequence of states and inputs values representing a looping path in the model that shows why the formula is violated:

for state, inputs in zip(explanation[::2], explanation[1::2]):
    if state == explanation[-1]:
        print("-- Loop starts here")
    print(state.get_str_values())
    print(inputs.get_str_values())

prints

-- Loop starts here
{'start': '0', 'c1.c': '0', 'stop': '3', 'c2.c': '0'}
{'run': 'rc2'}
{'start': '0', 'c1.c': '0', 'stop': '3', 'c2.c': '1'}
{'run': 'rc2'}
{'start': '0', 'c1.c': '0', 'stop': '3', 'c2.c': '2'}
{'run': 'rc2'}