5. Explore a directed graph

Starting at node 0, which nodes can we reach by following directed edges?

A sixteen-node directed graph with node 3 pointing outward and nodes 12 through 15 in a separate component.

Use one Boolean indicator per node. A state means exactly one indicator is true:

Si(x0, …, x15) = xi ∧ ⋀j ≠ i ¬xj.

A second set of indicators describes the next state. The transition relation is

T(x, x′) = ⋁(i,j) ∈ E (Si(x) ∧ Sj(x′)).

5.1. Name the variables

The edge list describes the graph. Each current and next indicator gets its own variable in one shared vtree.

const size_t edges[][2] = {
    {0,1}, {1,2}, {3,2}, {4,5}, {5,6}, {6,7}, {8,9}, {9,10}, {10,11},
    {0,4}, {1,5}, {2,6}, {3,7}, {4,8}, {5,9}, {6,10}, {7,11},
    {4,0}, {5,1}, {10,6}, {11,7}, {12,13}, {13,15}, {15,14}, {14,12}
};
TididiVtree *vtree = NULL;
check(tididi_vtree_balanced(32, &vtree));
uint32_t current_vars[16], next_vars[16];
for (uint32_t i = 0; i < 16; ++i) {
    current_vars[i] = i + 1;
    next_vars[i] = i + 17;
}

The helper constructs one state as a conjunction of sixteen signed literals.

static TididiCircuit *state(const TididiVtree *vtree, const uint32_t vars[16], size_t node) {
    int64_t indicators[16];
    for (size_t i = 0; i < 16; ++i)
        indicators[i] = i == node ? (int64_t)vars[i] : -(int64_t)vars[i];
    TididiCircuit *result = NULL;
    check(tididi_cube(vtree, indicators, 16, &result, NULL));
    return result;
}

5.2. Compile the relation

For each edge, conjoin its source state with its destination state, then union all edges. The initial reachable set contains only node 0.

enum { EDGE_COUNT = sizeof(edges) / sizeof(edges[0]) };
TididiCircuit *terms[EDGE_COUNT] = {NULL}, *transition = NULL;
for (size_t i = 0; i < EDGE_COUNT; ++i) {
    TididiCircuit *source = state(vtree, current_vars, edges[i][0]);
    TididiCircuit *target = state(vtree, next_vars, edges[i][1]);
    check(tididi_and(source, target, &terms[i], NULL));
    release(source); release(target);
}
check(tididi_or_many(terms, EDGE_COUNT, &transition, NULL));
for (size_t i = 0; i < EDGE_COUNT; ++i) release(terms[i]);
TididiCircuit *reached = state(vtree, current_vars, 0);

5.3. Take one step

Conjoin the current set with the transition relation. Quantify the current variables to leave possible destinations, then rename next indicators to current indicators. Count only the current variables; next variables are free after renaming and should not multiply the number of states.

TididiCircuit *r = copy(reached), *t = copy(transition);
TididiCircuit *joined = NULL, *projected = NULL, *image = NULL;
check(tididi_and(r, t, &joined, NULL));
check(tididi_exists(joined, current_vars, 16, &projected, NULL));
check(tididi_rename(projected, next_vars, current_vars, 16, &image, NULL));
uint64_t states = 0;
check(tididi_projected_model_count(image, current_vars, 16, &states, NULL));
printf("States after one step: %" PRIu64 "\n", states);

Output:

States after one step: 2

5.4. Reach a fixed point

tididi_and_exists performs the AND followed by existential quantification from the preceding step in one call. It can avoid constructing the full intermediate conjunction. Union the image with the states already reached, minimize, and compare Boolean functions to detect convergence.

for (unsigned step = 1; ; ++step) {
    r = copy(reached); t = copy(transition);
    TididiCircuit *next = NULL, *renamed = NULL, *candidate = NULL, *smaller = NULL;
    check(tididi_and_exists(r, t, current_vars, 16, &next, NULL));
    check(tididi_rename(next, next_vars, current_vars, 16, &renamed, NULL));
    TididiCircuit *old_copy = copy(reached);
    check(tididi_or(old_copy, renamed, &candidate, NULL));
    check(tididi_minimize(candidate, &smaller, NULL));
    bool done = false;
    check(tididi_equivalent(reached, smaller, &done, NULL));
    size_t nodes = 0, pairs = 0;
    check(tididi_size(smaller, &nodes, &pairs));
    check(tididi_projected_model_count(smaller, current_vars, 16, &states, NULL));
    printf("Step %u: %" PRIu64 " states, %zu nodes, %zu pairs%s\n",
           step, states, nodes, pairs, done ? " (fixed point)" : "");
    release(reached); reached = smaller;
    release(r); release(t); release(next); release(renamed);
    release(old_copy); release(candidate);
    if (done) break;
}

Output:

Step 1: 3 states, 35 nodes, 37 pairs
Step 2: 6 states, 40 nodes, 45 pairs
Step 3: 8 states, 41 nodes, 48 pairs
Step 4: 10 states, 42 nodes, 51 pairs
Step 5: 11 states, 42 nodes, 52 pairs
Step 6: 11 states, 42 nodes, 52 pairs (fixed point)

5.5. Inspect the result

Node 3 has edges leading into the reachable region but no directed path from node 0 reaches it. Nodes 12 through 15 form a separate component. Test each state by intersecting it with the final reachable set.

for (size_t node = 0; node < 16; ++node) {
    TididiCircuit *at_node = state(vtree, current_vars, node);
    TididiCircuit *reached_copy = copy(reached), *overlap = NULL;
    check(tididi_and(reached_copy, at_node, &overlap, NULL));
    bool reachable = false;
    check(tididi_is_sat(overlap, &reachable, NULL));
    if (!reachable) printf("Unreachable: %zu\n", node);
    release(at_node); release(reached_copy); release(overlap);
}

Output:

Unreachable: 3
Unreachable: 12
Unreachable: 13
Unreachable: 14
Unreachable: 15

5.6. Complete program

Download reachability.c and the shared helper. The source includes cleanup for all handles. Both files are included in the source checkout.