Skip to content

Commit babfd49

Browse files
isPANNclaude
andcommitted
Remove unsound Planar3SAT->GCD rule (#377) + final-review cleanups
The Planar3Satisfiability -> MinimumGeometricConnectedDominatingSet reduction emitted a geometrically disconnected target (point bands >=1.5 apart, copies/clauses spaced 3 apart, radius 1), so the unit-disk graph admits no connected dominating set and every satisfiable source mapped to an infeasible target. Confirmed unsound via /verify-reduction (6004 checks). Removed the rule, its test, mod.rs registration, example-spec wiring, and the paper reduction-rule entry. The MinimumGeometricConnectedDominatingSet model is retained as a sound orphan node pending a correct reduction (+ a GCD->ILP rule for closed-loop tests); #377 stays open. Cleanups: dedupe PCSF feasibility into a shared forest_components() helper (removing an unreachable branch); dedupe KColoring forward_witness between rule and test; trim the oversized SCSS brute-force test; minor KISS nits (eulerianpath y_idx wrapper, OLA is_valid_solution, CMO overhead). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 301da58 commit babfd49

11 files changed

Lines changed: 35 additions & 734 deletions

docs/paper/reductions.typ

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19717,33 +19717,6 @@ The following table shows concrete variable overhead for example instances, take
1971719717
_Solution extraction._ The Max-Cut partition vector is already a valid Graph Partitioning witness, so extraction is the identity map.
1971819718
]
1971919719

19720-
#let p3sat_gcd = load-example("Planar3Satisfiability", "MinimumGeometricConnectedDominatingSet")
19721-
#let p3sat_gcd_sol = p3sat_gcd.solutions.at(0)
19722-
#reduction-rule("Planar3Satisfiability", "MinimumGeometricConnectedDominatingSet",
19723-
example: true,
19724-
example-caption: [trivial $m = 0$ corner case (vacuously satisfiable)],
19725-
extra: [
19726-
#pred-commands(
19727-
"pred create --example Planar3Satisfiability -o planar3sat.json",
19728-
"pred reduce planar3sat.json --to " + target-spec(p3sat_gcd) + " -o bundle.json",
19729-
"pred solve bundle.json",
19730-
)
19731-
Canonical fixture: the empty formula ($n = 0$, $m = 0$) is vacuously satisfiable, and the construction short-circuits to the single point $(0, 0)$ with radius $1$ and bound $K = 1$. The trivial connected dominating set is the singleton itself, witnessed by target config $(#p3sat_gcd_sol.target_config.map(str).join(", "))$ #sym.checkmark.
19732-
19733-
*Why the fixture is trivial.* The full Lichtenstein layout emits dozens of geometric points for even a one-clause input, which exceeds this codebase's $<= 16$ brute-force bound. We therefore use the $m = 0$ corner case as the canonical witness and rely on dedicated structural tests for non-trivial point emission. A full round-trip solve test will become feasible once a `MinimumGeometricConnectedDominatingSet -> ILP` rule is added.
19734-
],
19735-
)[
19736-
@lichtenstein1982 (Theorem 5, §6, combined with the bipolar refinement of Lemma 1.) The construction proceeds in two phases. _Phase A_ (Lemma 1) rewrites the Planar 3-SAT instance $phi$ into a _bipolar_ formula $phi'$ by replacing each variable $v_i$ with $m_i$ fresh copies arranged on a cycle and equating consecutive copies through chain clauses, so $phi'$ has $sum_i m_i = 3 m$ variables and $m_b = 4 m$ clauses with the property that every variable has all positive incidences on one side of the planar embedding and all negative incidences on the other. _Phase B_ (§6, Figures 12--15) embeds $phi'$ as a set of points in the plane: each copy variable becomes a "variable structure" (two parallel columns of rounds with square forcers at distance $1\/40$), a ground spine threads through the columns, and each bipolar clause becomes a tripod with three branches reaching the columns of its literal copies. With distance threshold $1$, the resulting Minimum Geometric Connected Dominating Set has solution $<= K = N_V + N_C + N_G + m_b$ if and only if $phi'$ (and hence $phi$) is satisfiable.
19737-
][
19738-
_Construction (this implementation)._ We follow Lichtenstein's Phase A exactly --- count occurrences $m_i$, form $sum_i m_i = 3 m$ copy variables, build the bipolar formula $phi'$ with $m_b = m + sum_i m_i = 4 m$ clauses --- but we make one deliberate simplification in Phase B: instead of emitting Lichtenstein's full per-copy row stack of height $mu_u$, we _collapse the stack to a single row per copy_ ($mu_u = 1$). This emits $N_V = 4 m$ variable-structure rounds (top, bottom, top-partner, bottom-partner per copy) instead of $4 mu_u m$, while retaining the exact $K = N_V + N_C + N_G + m_b$ shape that Lichtenstein's analysis prescribes. The ground spine and clause tripods follow the original layout. The trivial corner case $m = 0$ short-circuits to a single point with $K = 1$.
19739-
19740-
_Why this simplification is faithful._ Lichtenstein's $mu_u$ is a scaling factor used to argue that a satisfying assignment forces _strictly fewer_ than $K + 1$ points into the dominating set; the same bipolar witness, when read off a single-row layout, still hits each variable-column anchor exactly once and dominates the ground spine and clause tripods. The simplification trades the original layout's exponential separation for a smaller point set; planarity, the bipolar property, and the bound shape are preserved. The rule's source-code docstring records this deviation explicitly.
19741-
19742-
_Correctness sketch._ ($arrow.r.double$) Given a satisfying assignment $bold(x)$ of $phi$, lift it to a consistent assignment of $phi'$ (each copy gets the value of its source variable). For each copy variable select the anchor round on the column matching the assigned polarity; for each clause tripod, the branch whose underlying literal is true points at a selected variable round, so picking the tripod root plus that branch dominates the tripod. The ground spine is connected by construction. Total selected rounds match $K$. ($arrow.l.double$) Any connected dominating set of size $<= K$ must, by Lichtenstein's gadget argument, select exactly one of the two column anchors per copy variable (the square forcers leave no other option), and the chain clauses of Phase A propagate this choice consistently across all copies of the same source variable. Reading off the column choice for each source variable's first occurrence gives a satisfying assignment of $phi$.
19743-
19744-
_Solution extraction._ For each source variable $v_i$, look up the recorded top-anchor index in the target point list. If the top-column anchor is selected, set $v_i$ to the polarity of its first occurrence (positive occurrence $arrow.r$ true); if the bottom anchor is selected, flip the polarity; if neither (variable does not occur), default to false. This is implemented in `ReductionPlanar3SATToGCD::extract_solution` and uses the auxiliary `first_occurrence_polarity` and `top_anchor_index` tables built during the reduction.
19745-
]
19746-
1974719720
#let pcsf_st = load-example("PrizeCollectingSteinerForest", "SteinerTree")
1974819721
#let pcsf_st_sol = pcsf_st.solutions.at(0)
1974919722
#let pcsf_st_n = pcsf_st.source.instance.graph.num_vertices

src/models/graph/optimal_linear_arrangement.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ impl<G: Graph> OptimalLinearArrangement<G> {
9393

9494
/// Check if a configuration is a valid permutation.
9595
pub fn is_valid_solution(&self, config: &[usize]) -> bool {
96-
self.total_edge_length(config).is_some()
96+
self.is_valid_permutation(config)
9797
}
9898

9999
/// Check if a configuration forms a valid permutation of {0, ..., n-1}.

src/models/graph/prize_collecting_steiner_forest.rs

Lines changed: 17 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ impl<G: Graph, W: WeightElement> PrizeCollectingSteinerForest<G, W> {
186186
/// Whether this configuration is a feasible forest (selected edges only
187187
/// touch selected vertices and induce an acyclic subgraph).
188188
pub fn is_valid_solution(&self, config: &[usize]) -> bool {
189-
is_feasible_forest(&self.graph, config)
189+
forest_components(&self.graph, config).is_some()
190190
}
191191
}
192192

@@ -208,81 +208,10 @@ where
208208

209209
fn evaluate(&self, config: &[usize]) -> Min<W::Sum> {
210210
let n = self.graph.num_vertices();
211-
let m = self.graph.num_edges();
212-
if config.len() != n + m {
213-
return Min(None);
214-
}
215-
let edges = self.graph.edges();
216-
217-
// Feasibility: selected edges must be incident only to selected
218-
// vertices, and the resulting subgraph must be acyclic.
219-
let mut adj: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n];
220-
let mut selected_edge_count = 0usize;
221-
for (i, &(u, v)) in edges.iter().enumerate() {
222-
let y_e = config[n + i];
223-
if y_e == 0 {
224-
continue;
225-
}
226-
if y_e != 1 {
227-
return Min(None);
228-
}
229-
let x_u = config[u];
230-
let x_v = config[v];
231-
if x_u != 1 || x_v != 1 {
232-
return Min(None);
233-
}
234-
adj[u].push((v, i));
235-
adj[v].push((u, i));
236-
selected_edge_count += 1;
237-
}
238-
239-
// Acyclicity via BFS on the selected subgraph restricted to selected
240-
// vertices. We also count tree components: a selected vertex with no
241-
// incident selected edges is one singleton tree.
242-
let mut visited = vec![false; n];
243-
let mut kappa: usize = 0;
244-
let mut total_tree_edges: usize = 0;
245-
for start in 0..n {
246-
if config[start] != 1 || visited[start] {
247-
continue;
248-
}
249-
// Discovered a fresh component containing `start`.
250-
kappa += 1;
251-
visited[start] = true;
252-
let mut comp_edges: usize = 0;
253-
// Parent edge index per vertex inside this BFS, used to detect
254-
// back-edges (cycles).
255-
let mut parent_edge: Vec<Option<usize>> = vec![None; n];
256-
let mut queue: VecDeque<usize> = VecDeque::new();
257-
queue.push_back(start);
258-
while let Some(u) = queue.pop_front() {
259-
for &(w, edge_idx) in &adj[u] {
260-
if parent_edge[u] == Some(edge_idx) {
261-
// Skip the edge we came in on.
262-
continue;
263-
}
264-
if visited[w] {
265-
// Back-edge inside this component => cycle.
266-
return Min(None);
267-
}
268-
visited[w] = true;
269-
parent_edge[w] = Some(edge_idx);
270-
comp_edges += 1;
271-
queue.push_back(w);
272-
}
273-
}
274-
// Each discovered tree edge is counted once via BFS discovery. If
275-
// we did not pick up an extra back-edge (which would have
276-
// triggered the cycle return above), the selected-edge subgraph
277-
// restricted to this component is a tree.
278-
total_tree_edges += comp_edges;
279-
}
280-
// Sanity: every selected edge must have been visited as a tree edge.
281-
// If it was not, both endpoints would have been in distinct
282-
// components, which is impossible by construction.
283-
if total_tree_edges != selected_edge_count {
284-
return Min(None);
285-
}
211+
let kappa = match forest_components(&self.graph, config) {
212+
Some(kappa) => kappa,
213+
None => return Min(None),
214+
};
286215

287216
// Objective: beta * sum_{v notin V_F} p(v)
288217
// + sum_{e in E_F} c(e)
@@ -322,13 +251,15 @@ where
322251
}
323252
}
324253

325-
/// Decide feasibility of a `(V_F, E_F)` configuration: selected edges only
326-
/// touch selected vertices and induce an acyclic subgraph.
327-
fn is_feasible_forest<G: Graph>(graph: &G, config: &[usize]) -> bool {
254+
/// Validate a `(V_F, E_F)` configuration and, if feasible, return the number of
255+
/// tree components `kappa(F)` among the selected vertices. Feasible means every
256+
/// selected edge is incident only to selected vertices and the selected
257+
/// subgraph is acyclic. Returns `None` for any infeasible configuration.
258+
fn forest_components<G: Graph>(graph: &G, config: &[usize]) -> Option<usize> {
328259
let n = graph.num_vertices();
329260
let m = graph.num_edges();
330261
if config.len() != n + m {
331-
return false;
262+
return None;
332263
}
333264
let edges = graph.edges();
334265
let mut adj: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n];
@@ -338,19 +269,21 @@ fn is_feasible_forest<G: Graph>(graph: &G, config: &[usize]) -> bool {
338269
continue;
339270
}
340271
if y_e != 1 {
341-
return false;
272+
return None;
342273
}
343274
if config[u] != 1 || config[v] != 1 {
344-
return false;
275+
return None;
345276
}
346277
adj[u].push((v, i));
347278
adj[v].push((u, i));
348279
}
349280
let mut visited = vec![false; n];
281+
let mut kappa: usize = 0;
350282
for start in 0..n {
351283
if config[start] != 1 || visited[start] {
352284
continue;
353285
}
286+
kappa += 1;
354287
visited[start] = true;
355288
let mut parent_edge: Vec<Option<usize>> = vec![None; n];
356289
let mut queue: VecDeque<usize> = VecDeque::new();
@@ -361,15 +294,15 @@ fn is_feasible_forest<G: Graph>(graph: &G, config: &[usize]) -> bool {
361294
continue;
362295
}
363296
if visited[w] {
364-
return false; // back-edge inside the component => cycle
297+
return None; // back-edge inside the component => cycle
365298
}
366299
visited[w] = true;
367300
parent_edge[w] = Some(edge_idx);
368301
queue.push_back(w);
369302
}
370303
}
371304
}
372-
true
305+
Some(kappa)
373306
}
374307

375308
crate::declare_variants! {

src/rules/eulerianpath_ilp.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,6 @@ pub struct ReductionEulerianPathToILP {
5151
}
5252

5353
impl ReductionEulerianPathToILP {
54-
fn y_idx(&self, k: usize) -> usize {
55-
k
56-
}
57-
5854
fn s_idx(&self, a: usize) -> usize {
5955
self.pairs.len() + a
6056
}
@@ -111,7 +107,7 @@ impl ReductionResult for ReductionEulerianPathToILP {
111107
.iter()
112108
.enumerate()
113109
.find(|&(k, &(a, _))| {
114-
a == current && target_solution.get(self.y_idx(k)).copied().unwrap_or(0) == 1
110+
a == current && target_solution.get(k).copied().unwrap_or(0) == 1
115111
})
116112
.map(|(_, &(_, b))| b);
117113

src/rules/kcoloring_bicliquecover.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,11 @@ impl ReduceTo<BicliqueCover> for KColoring<KN, SimpleGraph> {
223223
/// `coloring[v]` must be in `0..q`. The order of color bicliques is the
224224
/// order of first appearance of each color along `0..n`, so unused colors
225225
/// at the tail produce empty bicliques.
226-
#[cfg(feature = "example-db")]
227-
fn forward_witness(source: &KColoring<KN, SimpleGraph>, coloring: &[usize]) -> Vec<usize> {
226+
#[cfg(any(test, feature = "example-db"))]
227+
pub(crate) fn forward_witness(
228+
source: &KColoring<KN, SimpleGraph>,
229+
coloring: &[usize],
230+
) -> Vec<usize> {
228231
let n = source.graph().num_vertices();
229232
let q = source.num_colors();
230233
let k = n + q;

src/rules/maximumcontactmapoverlap_ilp.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ impl ReductionResult for ReductionCMOToILP {
6363
#[reduction(
6464
overhead = {
6565
num_vars = "num_vertices_1 * num_vertices_2 + num_contacts_1 * num_contacts_2",
66-
num_constraints = "num_vertices_1 + num_vertices_2 + num_vertices_1 * num_vertices_1 * num_vertices_2 * num_vertices_2 + 2 * num_contacts_1 * num_contacts_2",
66+
num_constraints = "num_vertices_1 + num_vertices_2 + num_vertices_1 * (num_vertices_1 - 1) / 2 * num_vertices_2 * (num_vertices_2 + 1) / 2 + 2 * num_contacts_1 * num_contacts_2",
6767
}
6868
)]
6969
impl ReduceTo<ILP<bool>> for MaximumContactMapOverlap {

src/rules/mod.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ pub(crate) mod partition_subsetsum;
124124
pub(crate) mod partition_sumofsquarespartition;
125125
pub(crate) mod partitionintocliques_minimumcoveringbycliques;
126126
pub(crate) mod partitionintopathsoflength2_boundedcomponentspanningforest;
127-
pub(crate) mod planar3satisfiability_minimumgeometricconnecteddominatingset;
128127
pub(crate) mod prizecollectingsteinerforest_steinertree;
129128
pub(crate) mod rootedtreearrangement_rootedtreestorageassignment;
130129
pub(crate) mod sat_circuitsat;
@@ -489,10 +488,6 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
489488
specs.extend(minimumdiscreteplanarinversekinematics_qubo::canonical_rule_example_specs());
490489
specs.extend(minimummultiwaycut_qubo::canonical_rule_example_specs());
491490
specs.extend(paintshop_qubo::canonical_rule_example_specs());
492-
specs.extend(
493-
planar3satisfiability_minimumgeometricconnecteddominatingset::canonical_rule_example_specs(
494-
),
495-
);
496491
specs.extend(prizecollectingsteinerforest_steinertree::canonical_rule_example_specs());
497492
specs.extend(partition_cosineproductintegration::canonical_rule_example_specs());
498493
specs.extend(partition_integralflowwithmultipliers::canonical_rule_example_specs());

0 commit comments

Comments
 (0)