Skip to content

Commit a4e05bd

Browse files
GiggleLiuisPANNclaude
authored
Fix #215: [Model] EnsembleComputation (#721)
* Add plan for #215: [Model] EnsembleComputation * Implement #215: [Model] EnsembleComputation * chore: remove plan file after implementation --------- Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 77577dd commit a4e05bd

10 files changed

Lines changed: 579 additions & 7 deletions

File tree

docs/paper/reductions.typ

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
"KSatisfiability": [$k$-SAT],
9999
"CircuitSAT": [CircuitSAT],
100100
"ConjunctiveQueryFoldability": [Conjunctive Query Foldability],
101+
"EnsembleComputation": [Ensemble Computation],
101102
"Factoring": [Factoring],
102103
"KingsSubgraph": [King's Subgraph MIS],
103104
"TriangularSubgraph": [Triangular Subgraph MIS],
@@ -2627,6 +2628,56 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
26272628
) <fig:cqf-example>
26282629
]
26292630

2631+
#problem-def("EnsembleComputation")[
2632+
Given a finite set $A$, a collection $C$ of subsets of $A$, and a positive integer $J$, determine whether there exists a sequence $S = (z_1 <- x_1 union y_1, z_2 <- x_2 union y_2, dots, z_j <- x_j union y_j)$ of $j <= J$ union operations such that each operand $x_i, y_i$ is either a singleton ${a}$ for some $a in A$ or a previously computed set $z_k$ with $k < i$, the two operands are disjoint for every step, and every target subset $c in C$ is equal to some computed set $z_i$.
2633+
][
2634+
Ensemble Computation is problem PO9 in Garey and Johnson @garey1979. It can be viewed as monotone circuit synthesis over set union: each operation introduces one reusable intermediate set, and the objective is simply to realize all targets within the given budget. The implementation in this library uses $2J$ operand variables with domain size $|A| + J$ and accepts as soon as some valid prefix has produced every target set, so the original "$j <= J$" semantics are preserved under brute-force enumeration. The resulting search space yields a straightforward exact upper bound of $(|A| + J)^(2J)$. Järvisalo, Kaski, Koivisto, and Korhonen study SAT encodings for finding efficient ensemble computations in a monotone-circuit setting @jarvisalo2012.
2635+
2636+
*Example.* Let $A = {0, 1, 2, 3}$, $C = {{0, 1, 2}, {0, 1, 3}}$, and $J = 4$. A satisfying witness uses three essential unions:
2637+
$z_1 = {0} union {1} = {0, 1}$,
2638+
$z_2 = z_1 union {2} = {0, 1, 2}$, and
2639+
$z_3 = z_1 union {3} = {0, 1, 3}$.
2640+
Thus both target subsets appear among the computed $z_i$ values while staying within the budget.
2641+
2642+
#figure(
2643+
canvas(length: 1cm, {
2644+
import draw: *
2645+
let node(pos, label, name, fill) = {
2646+
rect(
2647+
(pos.at(0) - 0.45, pos.at(1) - 0.18),
2648+
(pos.at(0) + 0.45, pos.at(1) + 0.18),
2649+
radius: 0.08,
2650+
fill: fill,
2651+
stroke: 0.5pt + luma(140),
2652+
name: name,
2653+
)
2654+
content(name, text(7pt, label))
2655+
}
2656+
2657+
let base = rgb("#4e79a7").transparentize(78%)
2658+
let target = rgb("#59a14f").transparentize(72%)
2659+
let aux = rgb("#f28e2b").transparentize(74%)
2660+
2661+
node((0.0, 1.4), [\{0\}], "a0", base)
2662+
node((1.2, 1.4), [\{1\}], "a1", base)
2663+
node((2.4, 1.4), [\{2\}], "a2", base)
2664+
node((3.6, 1.4), [\{3\}], "a3", base)
2665+
2666+
node((0.6, 0.6), [$z_1 = \{0,1\}$], "z1", aux)
2667+
node((1.8, -0.2), [$z_2 = \{0,1,2\}$], "z2", target)
2668+
node((3.0, -0.2), [$z_3 = \{0,1,3\}$], "z3", target)
2669+
2670+
line("a0.south", "z1.north-west", stroke: 0.5pt + luma(120), mark: (end: "straight", scale: 0.4))
2671+
line("a1.south", "z1.north-east", stroke: 0.5pt + luma(120), mark: (end: "straight", scale: 0.4))
2672+
line("z1.south-west", "z2.north-west", stroke: 0.5pt + luma(120), mark: (end: "straight", scale: 0.4))
2673+
line("a2.south", "z2.north-east", stroke: 0.5pt + luma(120), mark: (end: "straight", scale: 0.4))
2674+
line("z1.south-east", "z3.north-west", stroke: 0.5pt + luma(120), mark: (end: "straight", scale: 0.4))
2675+
line("a3.south", "z3.north-east", stroke: 0.5pt + luma(120), mark: (end: "straight", scale: 0.4))
2676+
}),
2677+
caption: [An ensemble computation for $A = {0,1,2,3}$ and $C = {{0,1,2}, {0,1,3}}$. The intermediate set $z_1 = {0,1}$ is reused to produce both target subsets.],
2678+
) <fig:ensemble-computation>
2679+
]
2680+
26302681
#{
26312682
let x = load-model-example("Factoring")
26322683
let N = x.instance.target

docs/paper/references.bib

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,18 @@ @article{bjorklund2009
504504
doi = {10.1137/070683933}
505505
}
506506

507+
@incollection{jarvisalo2012,
508+
author = {Matti J\"{a}rvisalo and Petteri Kaski and Mikko Koivisto and Janne H. Korhonen},
509+
title = {Finding Efficient Circuits for Ensemble Computation},
510+
booktitle = {Theory and Applications of Satisfiability Testing -- SAT 2012},
511+
series = {Lecture Notes in Computer Science},
512+
volume = {7317},
513+
pages = {369--382},
514+
year = {2012},
515+
publisher = {Springer},
516+
doi = {10.1007/978-3-642-31612-8_28}
517+
}
518+
507519
@article{aspvall1979,
508520
author = {Bengt Aspvall and Michael F. Plass and Robert Endre Tarjan},
509521
title = {A Linear-Time Algorithm for Testing the Truth of Certain Quantified Boolean Formulas},

problemreductions-cli/src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ Flags by problem type:
244244
MaximumSetPacking --sets [--weights]
245245
MinimumHittingSet --universe, --sets
246246
MinimumSetCovering --universe, --sets [--weights]
247+
EnsembleComputation --universe, --sets, --budget
247248
ComparativeContainment --universe, --r-sets, --s-sets [--r-weights] [--s-weights]
248249
X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each)
249250
SetBasis --universe, --sets, --k

problemreductions-cli/src/commands/create.rs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ use problemreductions::models::graph::{
1818
};
1919
use problemreductions::models::misc::{
2020
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery,
21-
ConsistencyOfDatabaseFrequencyTables, FlowShopScheduling, FrequencyTable, KnownValue,
22-
LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop,
23-
PartiallyOrderedKnapsack, QueryArg, RectilinearPictureCompression,
21+
ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, FlowShopScheduling, FrequencyTable,
22+
KnownValue, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling,
23+
PaintShop, PartiallyOrderedKnapsack, QueryArg, RectilinearPictureCompression,
2424
ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines,
2525
SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime,
2626
SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines,
@@ -498,6 +498,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
498498

499499
fn cli_flag_name(field_name: &str) -> String {
500500
match field_name {
501+
"universe_size" => "universe".to_string(),
501502
"vertex_weights" => "weights".to_string(),
502503
"edge_lengths" => "edge-weights".to_string(),
503504
_ => field_name.replace('_', "-"),
@@ -554,6 +555,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
554555
"SpinGlass" => "--graph 0-1,1-2 --couplings 1,1",
555556
"KColoring" => "--graph 0-1,1-2,2-0 --k 3",
556557
"HamiltonianCircuit" => "--graph 0-1,1-2,2-3,3-0",
558+
"EnsembleComputation" => "--universe 4 --sets \"0,1,2;0,1,3\" --budget 4",
557559
"MinMaxMulticenter" => {
558560
"--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2 --bound 2"
559561
}
@@ -1992,6 +1994,31 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
19921994
)
19931995
}
19941996

1997+
// EnsembleComputation
1998+
"EnsembleComputation" => {
1999+
let usage =
2000+
"Usage: pred create EnsembleComputation --universe 4 --sets \"0,1,2;0,1,3\" --budget 4";
2001+
let universe_size = args.universe.ok_or_else(|| {
2002+
anyhow::anyhow!("EnsembleComputation requires --universe\n\n{usage}")
2003+
})?;
2004+
let subsets = parse_sets(args)?;
2005+
let budget = args
2006+
.budget
2007+
.as_deref()
2008+
.ok_or_else(|| anyhow::anyhow!("EnsembleComputation requires --budget\n\n{usage}"))?
2009+
.parse::<usize>()
2010+
.map_err(|e| {
2011+
anyhow::anyhow!(
2012+
"Invalid --budget value for EnsembleComputation: {e}\n\n{usage}"
2013+
)
2014+
})?;
2015+
(
2016+
ser(EnsembleComputation::try_new(universe_size, subsets, budget)
2017+
.map_err(anyhow::Error::msg)?)?,
2018+
resolved_variant.clone(),
2019+
)
2020+
}
2021+
19952022
// ComparativeContainment
19962023
"ComparativeContainment" => {
19972024
let universe = args.universe.ok_or_else(|| {
@@ -5826,6 +5853,37 @@ mod tests {
58265853
std::fs::remove_file(output_path).ok();
58275854
}
58285855

5856+
#[test]
5857+
fn test_create_ensemble_computation_json() {
5858+
let mut args = empty_args();
5859+
args.problem = Some("EnsembleComputation".to_string());
5860+
args.universe = Some(4);
5861+
args.sets = Some("0,1,2;0,1,3".to_string());
5862+
args.budget = Some("4".to_string());
5863+
5864+
let output_path = std::env::temp_dir().join("pred_test_create_ensemble_computation.json");
5865+
let out = OutputConfig {
5866+
output: Some(output_path.clone()),
5867+
quiet: true,
5868+
json: false,
5869+
auto_json: false,
5870+
};
5871+
5872+
create(&args, &out).unwrap();
5873+
5874+
let content = std::fs::read_to_string(&output_path).unwrap();
5875+
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
5876+
assert_eq!(json["type"], "EnsembleComputation");
5877+
assert_eq!(json["data"]["universe_size"], 4);
5878+
assert_eq!(
5879+
json["data"]["subsets"],
5880+
serde_json::json!([[0, 1, 2], [0, 1, 3]])
5881+
);
5882+
assert_eq!(json["data"]["budget"], 4);
5883+
5884+
std::fs::remove_file(output_path).ok();
5885+
}
5886+
58295887
#[test]
58305888
fn test_create_balanced_complete_bipartite_subgraph() {
58315889
use crate::dispatch::ProblemJsonOutput;

problemreductions-cli/tests/cli_tests.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7335,6 +7335,97 @@ fn test_create_sequencing_within_intervals() {
73357335
std::fs::remove_file(&output_file).ok();
73367336
}
73377337

7338+
#[test]
7339+
fn test_create_ensemble_computation() {
7340+
let output_file = std::env::temp_dir().join("pred_test_create_ensemble_computation.json");
7341+
let output = pred()
7342+
.args([
7343+
"-o",
7344+
output_file.to_str().unwrap(),
7345+
"create",
7346+
"EnsembleComputation",
7347+
"--universe",
7348+
"4",
7349+
"--sets",
7350+
"0,1,2;0,1,3",
7351+
"--budget",
7352+
"4",
7353+
])
7354+
.output()
7355+
.unwrap();
7356+
assert!(
7357+
output.status.success(),
7358+
"stderr: {}",
7359+
String::from_utf8_lossy(&output.stderr)
7360+
);
7361+
let content = std::fs::read_to_string(&output_file).unwrap();
7362+
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
7363+
assert_eq!(json["type"], "EnsembleComputation");
7364+
assert_eq!(json["data"]["universe_size"], 4);
7365+
assert_eq!(
7366+
json["data"]["subsets"],
7367+
serde_json::json!([[0, 1, 2], [0, 1, 3]])
7368+
);
7369+
assert_eq!(json["data"]["budget"], 4);
7370+
std::fs::remove_file(&output_file).ok();
7371+
}
7372+
7373+
#[test]
7374+
fn test_create_ensemble_computation_no_flags_uses_cli_flag_names() {
7375+
let output = pred()
7376+
.args(["create", "EnsembleComputation"])
7377+
.output()
7378+
.unwrap();
7379+
assert!(
7380+
!output.status.success(),
7381+
"problem-specific help should exit non-zero"
7382+
);
7383+
let stderr = String::from_utf8_lossy(&output.stderr);
7384+
assert!(
7385+
stderr.contains("--universe"),
7386+
"expected --universe in help, got: {stderr}"
7387+
);
7388+
assert!(
7389+
stderr.contains("--sets"),
7390+
"expected --sets in help, got: {stderr}"
7391+
);
7392+
assert!(
7393+
stderr.contains("--budget"),
7394+
"expected --budget in help, got: {stderr}"
7395+
);
7396+
assert!(
7397+
!stderr.contains("--universe-size"),
7398+
"help should use actual CLI flags, got: {stderr}"
7399+
);
7400+
}
7401+
7402+
#[test]
7403+
fn test_create_ensemble_computation_rejects_out_of_range_elements_without_panicking() {
7404+
let output = pred()
7405+
.args([
7406+
"create",
7407+
"EnsembleComputation",
7408+
"--universe",
7409+
"4",
7410+
"--sets",
7411+
"0,1,5",
7412+
"--budget",
7413+
"4",
7414+
])
7415+
.output()
7416+
.unwrap();
7417+
assert!(!output.status.success());
7418+
let stderr = String::from_utf8_lossy(&output.stderr);
7419+
assert!(
7420+
!stderr.contains("panicked at"),
7421+
"expected graceful CLI error, got panic: {stderr}"
7422+
);
7423+
assert!(
7424+
stderr.contains("outside universe") || stderr.contains("universe of size"),
7425+
"expected out-of-range subset error, got: {stderr}"
7426+
);
7427+
}
7428+
73387429
#[test]
73397430
fn test_create_scheduling_with_individual_deadlines_with_m_alias() {
73407431
let output_file =
@@ -7483,6 +7574,22 @@ fn test_create_model_example_sequencing_within_intervals() {
74837574
assert_eq!(json["type"], "SequencingWithinIntervals");
74847575
}
74857576

7577+
#[test]
7578+
fn test_create_model_example_ensemble_computation() {
7579+
let output = pred()
7580+
.args(["create", "--example", "EnsembleComputation"])
7581+
.output()
7582+
.unwrap();
7583+
assert!(
7584+
output.status.success(),
7585+
"stderr: {}",
7586+
String::from_utf8_lossy(&output.stderr)
7587+
);
7588+
let stdout = String::from_utf8(output.stdout).unwrap();
7589+
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
7590+
assert_eq!(json["type"], "EnsembleComputation");
7591+
}
7592+
74867593
#[test]
74877594
fn test_create_minimum_multiway_cut_rejects_single_terminal() {
74887595
let output = pred()

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ pub mod prelude {
6666
pub use crate::models::misc::{
6767
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation,
6868
ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, ConsistencyOfDatabaseFrequencyTables,
69-
Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence,
69+
EnsembleComputation, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence,
7070
MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, Partition, QueryArg,
7171
RectilinearPictureCompression, ResourceConstrainedScheduling,
7272
SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost,

0 commit comments

Comments
 (0)