Skip to content

Commit 8a35050

Browse files
GiggleLiuclaude
andcommitted
docs: add CountingProblem trait design spec for #P/PP-complete support
Design for extending the trait hierarchy with CountingProblem to unblock 8 issues involving #P-complete and PP-complete problems (NetworkReliability, KthLargestSubset, etc.). Adds Weight<W> type, CountingSolver trait, ReduceToCount reduction support, and registry/CLI integration plan. Related: #235, #237, #404, #405, #256, #257, #394, #395, #737, #748 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3ac6795 commit 8a35050

1 file changed

Lines changed: 244 additions & 0 deletions

File tree

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# CountingProblem Trait — Supporting #P and PP-Complete Problems
2+
3+
**Date:** 2026-03-22
4+
**Status:** Approved design, pending implementation
5+
6+
## Problem
7+
8+
The current trait hierarchy supports two problem families:
9+
10+
- `OptimizationProblem` (`Metric = SolutionSize<V>`) — find a config that maximizes/minimizes an objective
11+
- `SatisfactionProblem` (`Metric = bool`) — find a config satisfying all constraints
12+
13+
8 issues are blocked because they model problems where the answer depends on **aggregating over the entire configuration space** — counting feasible configs or summing weighted probabilities. These are #P-complete or PP-complete problems (not known to be in NP) that don't fit either existing trait.
14+
15+
### Blocked issues
16+
17+
**Models:** #235 NetworkReliability, #237 NetworkSurvivability, #404 KthLargestSubset, #405 KthLargestMTuple
18+
19+
**Rules (blocked on models above):** #256 SteinerTree → NetworkReliability, #257 VertexCover → NetworkSurvivability, #394 SubsetSum → KthLargestSubset, #395 SubsetSum → KthLargestMTuple
20+
21+
## Design
22+
23+
### New type: `Weight<W>`
24+
25+
A newtype wrapper for per-configuration weights, parallel to `SolutionSize<V>` for optimization problems. Infeasible configs have weight zero — no separate `Infeasible` variant needed (unlike `SolutionSize::Invalid`) because a zero-weight config contributes nothing to the sum.
26+
27+
```rust
28+
// src/types.rs
29+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30+
pub struct Weight<W>(pub W);
31+
```
32+
33+
### New trait: `CountingProblem`
34+
35+
A marker trait parallel to `SatisfactionProblem`, binding `Metric = Weight<Self::Value>`:
36+
37+
```rust
38+
// src/traits.rs
39+
pub trait CountingProblem: Problem<Metric = Weight<Self::Value>> {
40+
/// The inner weight type (e.g., `u64` for unweighted counting, `f64` for probabilities).
41+
type Value: Clone + AddAssign + Zero + PartialOrd + fmt::Debug + Serialize + DeserializeOwned;
42+
}
43+
```
44+
45+
The `evaluate(config) -> Weight<V>` method (inherited from `Problem`) returns the weight of a single configuration. The "answer" to the problem is the sum of weights over all configurations. This is computed by the solver, not by `evaluate`.
46+
47+
### Trait hierarchy (updated)
48+
49+
```
50+
Problem (Metric: Clone)
51+
├── OptimizationProblem (Metric = SolutionSize<V>) — existing, unchanged
52+
├── SatisfactionProblem (Metric = bool) — existing, unchanged
53+
└── CountingProblem (Metric = Weight<V>) — NEW
54+
```
55+
56+
### Solver extension
57+
58+
Add a separate `CountingSolver` trait (parallel to how problem families have distinct traits) rather than extending the existing `Solver` trait. This avoids forcing `ILPSolver` to implement a meaningless `count` method:
59+
60+
```rust
61+
// src/solvers/mod.rs (existing Solver trait unchanged)
62+
63+
/// Solver trait for counting problems.
64+
pub trait CountingSolver {
65+
/// Compute the total weight (sum of evaluate over all configs).
66+
fn count<P: CountingProblem>(&self, problem: &P) -> P::Value;
67+
}
68+
69+
// src/solvers/brute_force.rs
70+
impl CountingSolver for BruteForce {
71+
fn count<P: CountingProblem>(&self, problem: &P) -> P::Value {
72+
let mut total = P::Value::zero();
73+
for config in DimsIterator::new(problem.dims()) {
74+
total += problem.evaluate(&config).0;
75+
}
76+
total
77+
}
78+
}
79+
```
80+
81+
`BruteForce` also gets a convenience method for testing:
82+
```rust
83+
/// Return all feasible configs and their weights alongside the total count.
84+
pub fn count_with_configs<P: CountingProblem>(&self, problem: &P)
85+
-> (P::Value, Vec<(Vec<usize>, P::Value)>);
86+
```
87+
88+
### Reduction support
89+
90+
Counting reductions preserve aggregate counts, not individual solutions. New traits parallel to `ReductionResult` / `ReduceTo<T>`:
91+
92+
```rust
93+
// src/rules/traits.rs
94+
95+
pub trait CountingReductionResult {
96+
type Source: CountingProblem;
97+
type Target: CountingProblem;
98+
99+
/// Get a reference to the target problem.
100+
fn target_problem(&self) -> &Self::Target;
101+
102+
/// Transform the target's aggregate count back to the source's count.
103+
///
104+
/// For parsimonious reductions (1-to-1 config mapping), this is identity.
105+
/// For non-parsimonious reductions, this applies a correction factor
106+
/// (e.g., divide by 2 if the reduction doubles feasible configs).
107+
fn extract_count(
108+
&self,
109+
target_count: <Self::Target as CountingProblem>::Value,
110+
) -> <Self::Source as CountingProblem>::Value;
111+
}
112+
113+
pub trait ReduceToCount<T: CountingProblem>: CountingProblem {
114+
type Result: CountingReductionResult<Source = Self, Target = T>;
115+
fn reduce_to_count(&self) -> Self::Result;
116+
}
117+
```
118+
119+
### Registry and CLI integration
120+
121+
#### `declare_variants!` macro
122+
123+
Gets a new `count` keyword. The macro generates a `CountSolveFn` (instead of `SolveFn`) that calls `BruteForce::count()` and formats the result:
124+
125+
```rust
126+
crate::declare_variants! {
127+
default count NetworkReliability => "2^num_edges * num_vertices",
128+
}
129+
```
130+
131+
The `count` keyword generates:
132+
- A new `SolverKind::Count` variant in the proc macro's internal `SolverKind` enum (alongside existing `Opt` and `Sat`)
133+
- A `solver_kind` field on `VariantEntry` to distinguish problem families at runtime (enum with `Optimization`, `Satisfaction`, `Counting` variants)
134+
- A `count_fn: Option<CountSolveFn>` field on `VariantEntry` where `CountSolveFn = fn(&dyn Any) -> String`
135+
- The generated function downcasts `&dyn Any` to the concrete type, calls `BruteForce.count(&problem)`, and formats the result
136+
- The existing `ProblemType` struct (which holds problem metadata, not a classification enum) is unchanged
137+
138+
#### `LoadedDynProblem`
139+
140+
Gets a new method:
141+
```rust
142+
pub fn solve_counting(&self) -> Option<String> {
143+
(self.count_fn?)(self.inner.as_any())
144+
}
145+
```
146+
147+
The existing `solve_brute_force` remains unchanged for opt/sat problems.
148+
149+
#### `pred solve` CLI
150+
151+
The solve command checks `VariantEntry::solver_kind` to determine the dispatch path:
152+
- `SolverKind::Optimization` / `SolverKind::Satisfaction` → existing `solve_brute_force()`
153+
- `SolverKind::Counting` → new `solve_counting()`, displays `Total weight: <value>`
154+
155+
#### `#[reduction]` proc macro
156+
157+
The existing macro hardcodes `ReduceTo` trait detection. It must be extended to also recognize `ReduceToCount`:
158+
159+
- When the macro sees `impl ReduceToCount<Target> for Source`, it generates a `reduce_count_fn` field on `ReductionEntry`
160+
- The generated function returns a `Box<dyn DynCountingReductionResult>` (new type-erased trait for counting reductions)
161+
- `overhead` attribute works identically — overhead expressions are about problem size, not about solution type
162+
163+
#### `ReductionEntry` changes
164+
165+
`ReductionEntry` (in `src/rules/registry.rs`) gains new optional fields for counting reductions. A given entry has either `reduce_fn` (opt/sat) or `reduce_count_fn` (counting), never both:
166+
167+
```rust
168+
pub struct ReductionEntry {
169+
// ... existing fields unchanged ...
170+
pub reduce_fn: Option<ReduceFn>, // existing: opt/sat reductions
171+
pub reduce_count_fn: Option<CountReduceFn>, // NEW: counting reductions
172+
}
173+
```
174+
175+
Where `CountReduceFn = fn(&dyn Any) -> Box<dyn DynCountingReductionResult>`.
176+
177+
#### Reduction graph integration
178+
179+
Counting edges and opt/sat edges coexist in the same `ReductionGraph`. The graph is about problem reachability — edge type doesn't affect pathfinding. The distinction matters only at solve time:
180+
181+
- `ReductionEdgeData` gains an `edge_kind: EdgeKind` field (`enum EdgeKind { Standard, Counting }`)
182+
- `reduce_along_path` checks edge kinds: a path must be homogeneous (all-standard or all-counting); mixed paths are invalid
183+
- For all-counting paths, the runtime builds a `CountingReductionChain` instead of a `ReductionChain`
184+
185+
#### Counting reduction chains
186+
187+
For multi-hop counting paths (A →count→ B →count→ C):
188+
189+
```rust
190+
pub trait DynCountingReductionResult {
191+
fn target_problem_any(&self) -> &dyn Any;
192+
/// Transform target count to source count using serde_json::Value for type erasure.
193+
fn extract_count_dyn(&self, target_count: serde_json::Value) -> serde_json::Value;
194+
}
195+
```
196+
197+
`CountingReductionChain` composes these: reduce A→B→C, solve C to get count as `serde_json::Value`, then call `extract_count_dyn` backwards through the chain. This parallels `ReductionChain` for opt/sat reductions.
198+
199+
**Note on cross-type reductions:** When source and target have different `Value` types (e.g., `u64``f64`), the `extract_count` implementation is responsible for the type conversion. The `serde_json::Value` type erasure in `DynCountingReductionResult` handles this naturally at the runtime dispatch level.
200+
201+
#### Exports
202+
203+
Add to prelude and `lib.rs`:
204+
- `CountingProblem`, `Weight`, `CountingSolver` traits/types
205+
- `ReduceToCount`, `CountingReductionResult` traits
206+
207+
### Concrete models
208+
209+
All models store **only the counting problem data** — no decision thresholds (`k`, `q`). The threshold is part of the GJ decision formulation but not part of the counting problem we model.
210+
211+
| Model | Value type | Fields | evaluate returns |
212+
|---|---|---|---|
213+
| `NetworkReliability` | `f64` | `graph`, `terminals`, `failure_probs` | `Weight(Π p_e^{x_e} · (1-p_e)^{1-x_e})` if terminals connected, else `Weight(0.0)` |
214+
| `NetworkSurvivability` | `f64` | `graph`, `terminals`, `failure_probs` | Same pattern for survivability |
215+
| `KthLargestSubset` | `u64` | `sizes`, `bound` | `Weight(1)` if subset sum ≤ bound, else `Weight(0)` |
216+
| `KthLargestMTuple` | `u64` | `sizes`, `bound` | `Weight(1)` if m-tuple condition met, else `Weight(0)` |
217+
218+
### `Weight<W>` utility impls
219+
220+
For ergonomics, `Weight<W>` implements:
221+
- `PartialOrd` where `W: PartialOrd` — delegates to inner value
222+
- `Eq` where `W: Eq`, `Hash` where `W: Hash` — conditional impls (works for `u64`, not `f64`)
223+
- `Add<Output = Weight<W>>` and `std::iter::Sum` where `W: Add` — enables `configs.map(evaluate).sum()`
224+
- `Display` where `W: Display` — prints the inner value directly (e.g., `0.9832` not `Weight(0.9832)`)
225+
226+
### What is NOT changed
227+
228+
- `OptimizationProblem`, `SatisfactionProblem` — untouched
229+
- `ReduceTo<T>`, `ReductionResult` — untouched
230+
- All existing models and rules — untouched
231+
- Existing `Solver` trait — untouched (new `CountingSolver` is separate)
232+
233+
## Alternatives considered
234+
235+
1. **Generalized metric aggregation** (#737) — replace all three leaf traits with a single `Aggregation` enum. Elegant but large breaking refactor with no immediate payoff. Filed for future consideration.
236+
237+
2. **Two-level trait** (`is_feasible` + `weight` methods) — more explicit but adds unnecessary surface area and boilerplate for unweighted counting.
238+
239+
3. **`Metric = f64` without wrapper** — works but loses type safety. `Weight<W>` follows the `SolutionSize<V>` pattern and makes intent explicit.
240+
241+
## Related issues
242+
243+
- #737 — Generalized metric aggregation (future architecture)
244+
- #748 — DefaultSolver per problem (future architecture)

0 commit comments

Comments
 (0)