Skip to content

close_after_widen: inverted is_stable predicate causes recovery from wrong vertices #65

@elazarg

Description

@elazarg

The following was found by Claude while working on the prevail-rust project. To the best of my understanding, it is indeed a wasted precision issue.


In split_dbm.hpp, vert_set_wrap_t::operator[] returns true for vertices in the unstable set, but close_after_widen in graph_ops.hpp interprets its third parameter as is_stable. This inverts the stability marks: unstable vertices are marked V_STABLE (skipped) and stable vertices are marked V_UNSTABLE (recovered). Dijkstra recovery runs from the wrong vertices, missing transitive edge tightenings after widening.

Details

In split_dbm.hpp:

class vert_set_wrap_t {
public:
  vert_set_wrap_t(const vert_set_t &_vs) : vs(_vs) {}
  bool operator[](vert_id v) const { return vs.find(v) != vs.end(); }
  //                                        ^^^^^^^^^^^^^^^^^^^^^^^^
  //                                        true for UNSTABLE vertices
  const vert_set_t &vs;
};

// ...
GrOps::close_after_widen(g_excl, potential, vert_set_wrap_t(unstable), delta);
//                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^
//                                          passed as `is_stable`

In graph_ops.hpp, close_after_widen:

for (vert_id v : g.verts()) {
  edge_marks[v] = is_stable[v] ? V_STABLE : V_UNSTABLE;
  //              ^^^^^^^^^^^^
  //              For unstable vertices: true  → V_STABLE (wrong)
  //              For stable vertices:   false → V_UNSTABLE (wrong)
}
for (vert_id v : g.verts()) {
  if (!edge_marks[v]) {  // V_UNSTABLE == 0
    dijkstra_recover(g, p, edge_marks, v, aux);
    // Recovery runs from stable vertices (useless), skips unstable (needed)
  }
}

Impact

After widening drops some edges, the affected vertices should have their shortest paths recomputed. Instead, the recovery operates on stable vertices (which already have correct edges) and skips the unstable vertices that actually need it. This produces non-tight results — the graph after normalize() is sound but imprecise, missing transitive edges that should be recoverable.

The bug affects all configurations since DefaultParams::widen_restabilize = 1.

Reproducer

Widening two closed graphs where the right graph has a looser edge causes that edge to be dropped, making the source vertex unstable. The transitive closure through the remaining edges should recover an equivalent tight edge, but doesn't:

  • Left graph edges (non-zero subgraph): 1→2: 5, 2→3: 5, 1→3: 10 (closed)
  • Right graph: same but 1→3: 12 (looser)
  • After widening: 1→2: 5, 2→3: 5 (edge 1→3 dropped; vertex 1 unstable)
  • Expected after close_after_widen: edge 1→3: 10 recovered via path 1→2→3
  • Actual: edge 1→3 missing (recovery ran from vertices 2, 3 instead of vertex 1)

Fix

Negate the predicate in vert_set_wrap_t:

bool operator[](vert_id v) const { return vs.find(v) == vs.end(); }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions