When states are countable, reachability becomes graph search — powerful, elegant, and combinatorially explosive.
Imagine a self-driving car navigating an intersection. The car has a finite set of decisions: go straight, turn left, turn right, stop. The traffic light has three states: green, yellow, red. Other vehicles occupy discrete lanes. At this level of abstraction, the world is not a continuous differential equation — it is a graph.
Chapters 8 and 9 handled continuous dynamics (differential equations, neural networks). But many systems are naturally discrete, or are best analyzed at a discrete level. Digital controllers execute finite state machines. Protocol specifications describe message sequences. Even continuous systems can be abstracted into discrete graphs by partitioning the state space into cells.
Discrete reachability has a major advantage: it is exact. There is no overapproximation. If a state is reachable in the graph, it is truly reachable. If it is not reachable, it is truly unreachable. The challenge is not approximation quality but scalability: a system with n binary state variables has 2n possible states. Exploring them all is infeasible for large n.
| Continuous | Discrete |
|---|---|
| States: Rn | States: finite set S |
| Dynamics: x' = f(x) | Transitions: edges in a graph |
| Reachable set: curved blob | Reachable set: subset of nodes |
| Overapproximate | Exact (but may be huge) |
A discrete system is a directed graph G = (V, E). The vertices V are the system states. An edge (u, v) ∈ E means the system can transition from state u to state v in one step. Some edges may carry weights (probabilities, costs).
How you store the graph determines the cost of reachability queries:
| Representation | Space | Outneighbor query | Inneighbor query |
|---|---|---|---|
| Adjacency matrix | O(|V|2) | O(|V|) | O(|V|) |
| Adjacency list | O(|V| + |E|) | O(degree) | O(|V| + |E|) unless stored separately |
| Both directions | O(|V| + |E|) | O(degree) | O(degree) |
For reachability analysis, we almost always want both forward and backward edges, so we store two adjacency lists: one for outneighbors, one for inneighbors. This doubles the storage but makes both directions equally fast.
In verification, the graph often represents a transition system: a tuple (S, S0, T) where S is the state set, S0 ⊆ S is the set of initial states, and T ⊆ S × S is the transition relation. States may have labels (e.g., "safe", "failure"). The reachability question becomes: starting from S0, can the system reach any state labeled "failure"?
python # Simple directed graph using adjacency lists class DiGraph: def __init__(self, n): self.n = n self.out_adj = [[] for _ in range(n)] self.in_adj = [[] for _ in range(n)] def add_edge(self, u, v): self.out_adj[u].append(v) self.in_adj[v].append(u)
The most natural reachability question: starting from a set of initial states S0, which states can the system eventually reach? This is forward reachability, and it is solved by breadth-first search (BFS).
BFS explores states layer by layer. Layer 0 is S0 itself. Layer 1 is everything reachable in one step from S0. Layer 2 is everything reachable in two steps. And so on, until no new states are discovered.
When Rk+1 = Rk, we have found the full forward reachable set. The algorithm terminates in at most |V| steps (since each step adds at least one new state, or we stop).
A 7×7 grid with walls (dark), a start cell (teal), and a goal cell (orange). Click Step to expand one BFS layer. Toggle Forward/Backward to switch direction. In Probabilistic mode, cells are colored by occupancy probability.
python from collections import deque def forward_reachable(graph, initial_states): """BFS from a set of initial states. Returns reachable set.""" visited = set(initial_states) queue = deque(initial_states) while queue: u = queue.popleft() for v in graph.out_adj[u]: if v not in visited: visited.add(v) queue.append(v) return visited
Forward reachability answers: "what can the system reach?" Backward reachability answers the dual question: "what can reach the target?" Given a set of target states T (perhaps failure states), backward reachability computes the set of all states from which T is eventually reachable.
This is BFS on the reverse graph — following edges backward (using inneighbors instead of outneighbors). The set Bk contains all states that can reach T in at most k steps.
The most powerful analysis combines both directions:
If R ∩ B is empty, the system is safe: no initial condition can lead to the failure state. If R ∩ B is non-empty, every state in the intersection lies on a path from S0 to T — a potential failure trajectory.
Try the simulation in Chapter 2: switch to "Backward" mode and watch BFS expand from the goal. The backward reachable set at layer k contains all cells that can reach the goal in exactly k steps. Compare the forward and backward wavefronts — they meet where paths connect start to goal.
python def backward_reachable(graph, target_states): """BFS on reverse graph from target states.""" visited = set(target_states) queue = deque(target_states) while queue: v = queue.popleft() for u in graph.in_adj[v]: # inneighbors! if u not in visited: visited.add(u) queue.append(u) return visited # Safety check R = forward_reachable(G, S0) B = backward_reachable(G, T_fail) if R & B: print("UNSAFE: failure is reachable") else: print("SAFE: no path from S0 to failure")
Knowing that a failure state is reachable is only half the story. We also want a counterexample: a concrete path from an initial state to the failure state. This path explains how the system can fail and is essential for debugging.
A counterexample is a sequence of states s0, s1, ..., sk where s0 ∈ S0, sk ∈ T, and each (si, si+1) ∈ E. This is simply a path in the graph from the initial set to the target set.
| Algorithm | Finds | Time | Best for |
|---|---|---|---|
| BFS | Shortest counterexample | O(|V| + |E|) | Minimum-step failures |
| DFS | Any counterexample | O(|V| + |E|) | Quick detection (any path) |
| A* | Lowest-cost counterexample | O(|E| + |V| log |V|) | Weighted graphs (most likely failure) |
BFS finds the shortest counterexample (fewest transitions). This is valuable because shorter failure paths are often more plausible and easier to understand.
DFS uses less memory (O(depth) vs O(branching factordepth)) and may find a counterexample faster if one exists deep in the graph. But the path it finds may not be the shortest.
A* is optimal for weighted graphs. If edges have costs (e.g., negative log-probabilities), A* with an admissible heuristic finds the minimum-cost path — the most likely failure trajectory.
During BFS or DFS, maintain a parent pointer for each discovered state: parent[v] = u means "v was first reached from u." Once the search reaches a target state t, trace back from t through the parent pointers to reconstruct the full path.
python def find_counterexample(graph, s0, target): """BFS shortest path from s0 to any state in target.""" parent = {s0: None} queue = deque([s0]) while queue: u = queue.popleft() if u in target: # Trace back path = [] while u is not None: path.append(u) u = parent[u] return path[::-1] for v in graph.out_adj[u]: if v not in parent: parent[v] = u queue.append(v) return None # no path exists
For small graphs, BFS and DFS work perfectly. But what about systems with 2100 states? Explicit enumeration is hopeless. SAT-based methods exploit the fact that many large systems have compact symbolic descriptions — the state can be encoded as a vector of Boolean variables, and the transition relation as a logical formula.
Suppose the system has n Boolean state variables x1, ..., xn. A state is a truth assignment to these variables (2n possible states). The transition relation T(x, x') is a Boolean formula relating the current state x to the next state x'.
The k-step reachability question becomes: is there a sequence of states x(0), x(1), ..., x(k) such that x(0) ∈ S0, x(k) ∈ T, and T(x(i), x(i+1)) holds for all i? This is a satisfiability problem:
This formula, called the bounded model checking (BMC) formula, is satisfiable if and only if there exists a failure path of length k. A SAT solver determines satisfiability and, if satisfiable, returns a satisfying assignment — which is exactly the counterexample.
We don't know the minimum failure path length in advance. So we try k = 0, 1, 2, ... increasing the bound until either the formula is satisfiable (failure found) or we reach a completeness threshold. The completeness threshold is the maximum k such that if no failure exists at depth k, none exists at any depth. For simple reachability, this is the diameter of the graph (longest shortest path).
| k | Formula size | SAT result | Meaning |
|---|---|---|---|
| 0 | n variables | UNSAT | No initial state is a failure |
| 1 | 2n variables | UNSAT | No 1-step path to failure |
| ... | ... | ... | ... |
| k* | (k*+1)n variables | SAT | Found shortest failure path! |
So far we have asked binary questions: is a state reachable or not? But if transitions have probabilities, we can ask a richer question: what is the probability of being in each state at time k?
A Markov chain over n states is described by a transition matrix P, where Pij = Pr(next state = j | current state = i). The occupancy distribution μk is a row vector giving the probability of being in each state at time k:
Starting from μ0 (which puts all probability on the initial states), each multiplication by P propagates the distribution one time step. After k steps, μk = μ0 Pk.
Consider a 3-state system with transition matrix:
Starting from state 0: μ0 = [1, 0, 0].
μ1 = [1, 0, 0] · P = [0.5, 0.5, 0.0]
μ2 = [0.5, 0.5, 0.0] · P = [0.25, 0.40, 0.35]
State 2 is an absorbing state (P22 = 1). Its probability grows monotonically. Eventually, all probability mass concentrates there: μ∞ = [0, 0, 1].
A 5-state Markov chain. Click Step to propagate the distribution one time step (μk+1 = μkP). Watch probability flow from the initial state (leftmost) toward the absorbing state (rightmost).
The reachability probability question comes in two flavors, depending on whether we impose a time limit.
Finite-horizon reachability: what is the probability of reaching a target set T within k time steps? Define:
This satisfies a recursive formula. If s ∈ T, then pk(s) = 1 for all k ≥ 0. Otherwise:
Starting from p0(s) = 1 if s ∈ T, 0 otherwise, we compute p1, p2, ... by propagating backward through the transition matrix. After k iterations, pk(s) gives the exact k-step reachability probability for each state.
Infinite-horizon reachability: what is the probability of ever reaching T? Define:
This is the limit of pk(s) as k → ∞. If T is reachable with positive probability at each step, the sequence pk(s) is non-decreasing and bounded by 1, so it converges. In practice, we iterate the recursive formula until convergence (|pk+1 − pk| < ε).
Partition the states into target states T and non-target states S \ T. The sub-matrix Q = P restricted to transitions among non-target states governs the dynamics before reaching T. The infinite-horizon reachability probability for non-target states is:
where rs = ∑t ∈ T P(s, t) is the one-step probability of transitioning into T from state s. The matrix (I − Q)−1 is the fundamental matrix of the absorbing Markov chain — its (i,j) entry counts the expected number of times the process visits state j starting from state i before being absorbed.
| Horizon | Question | Method |
|---|---|---|
| k steps | Pr(reach T in ≤ k steps) | k matrix-vector products |
| Infinite | Pr(ever reach T) | Solve (I − Q)p = r |
What if the system is continuous but we want to use discrete reachability tools? The answer: discretize. Partition the continuous state space into finitely many cells. Build a graph where each cell is a node, and there is an edge from cell A to cell B if the continuous dynamics can take any state in A to some state in B.
Step 2 is where the nonlinear reachability methods from Chapter 9 enter. For each cell, we use interval arithmetic (or Taylor models) to overapproximate the image under the dynamics. The edges are conservative: we add an edge whenever the overapproximated image might overlap a neighboring cell.
If the abstract graph says a failure state is reachable but we suspect this is spurious, we can refine: split cells in the abstract path into smaller cells and re-check. If the path survives refinement, it is likely genuine. If it disappears, it was spurious. This counterexample-guided abstraction refinement (CEGAR) loop is the backbone of many verification tools.
| Partition type | Cells | Best for |
|---|---|---|
| Uniform grid | Hyper-rectangles of equal size | Simple, low dimension |
| Octree/quadtree | Adaptive rectangles (finer near boundaries) | Non-uniform importance |
| Simplex | Triangulation of state space | Polyhedral dynamics |
Every method in this chapter lives on a spectrum: finer resolution = tighter results but more computation. This final chapter maps out the tradeoffs and points forward.
| Approach | Resolution | Cost | Tightness |
|---|---|---|---|
| Explicit BFS | Exact (each state) | O(|V| + |E|) | Exact |
| SAT/BMC | Symbolic (bit-level) | NP (but often fast) | Exact |
| Coarse abstraction | Few large cells | Low | Very loose |
| Fine abstraction | Many small cells | High | Tight |
| Adaptive (CEGAR) | Fine where needed | Variable | Good |
Small state space (|V| < 106): Use explicit BFS. It is simple, exact, and fast. Store the graph in memory and run forward/backward reachability directly.
Large structured state space (|V| > 106, compact description): Use SAT/BMC. Encode the transitions as Boolean formulas and let the SAT solver handle the search. This is the sweet spot for hardware and protocol verification.
Continuous system, low dimension (n ≤ 5): Use discrete abstraction with adaptive refinement. The cell count is manageable, and the graph tools from this chapter apply directly.
Continuous system, high dimension (n > 10): Return to Chapter 9's nonlinear methods (interval arithmetic, Taylor models) or use sampling-based falsification (Chapters 4-5). Discrete abstraction is impractical due to the curse of dimensionality.
This chapter closes the reachability trilogy (Chapters 8-10). Each chapter handles a different flavor of dynamics:
What we covered:
Ch 8: Linear systems (zonotopes, polytopes)
Ch 9: Nonlinear systems (intervals, Taylor models)
Ch 10: Discrete systems (graphs, SAT, Markov chains)
What comes next:
Ch 11: Explainability (why did the system fail?)
Ch 12: Runtime monitoring (detecting failures in real time)
The reachability methods provide pre-deployment guarantees: before the system runs, we prove it cannot reach unsafe states. Chapters 11 and 12 address what happens during deployment: monitoring the system in real time and explaining its decisions when things go wrong.