diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 80a0a4d0..7413428e 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -88,6 +88,7 @@ "Satisfiability": [SAT], "KSatisfiability": [$k$-SAT], "CircuitSAT": [CircuitSAT], + "CosineProductIntegration": [Cosine Product Integration], "Factoring": [Factoring], "KingsSubgraph": [King's Subgraph MIS], "TriangularSubgraph": [Triangular Subgraph MIS], @@ -2253,6 +2254,17 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS *Example.* Let $A = {3, 7, 1, 8, 2, 4}$ ($n = 6$) and target $B = 11$. Selecting $A' = {3, 8}$ gives sum $3 + 8 = 11 = B$. Another solution: $A' = {7, 4}$ with sum $7 + 4 = 11 = B$. ] +#problem-def("CosineProductIntegration")[ + Given integers $a_1, dots, a_n$, determine whether + $ + integral_0^(2 pi) product_(i=1)^n cos(a_i theta) dif theta != 0. + $ +][ + Cosine Product Integration appears as problem A7 AN14 in Garey and Johnson @garey1979. Expanding each cosine as $(e^(i a_i theta) + e^(-i a_i theta)) / 2$ shows that the integral equals $(2 pi / 2^n)$ times the number of sign vectors $epsilon in {-1, 1}^n$ with $sum_i epsilon_i a_i = 0$. Therefore the integral is nonzero exactly when there exists a balanced signed sum. Equivalently, after replacing each coefficient by its absolute value, the YES instances are precisely those whose multiset can be partitioned into two equal-sum parts. Garey and Johnson note the resulting decision problem remains solvable in pseudo-polynomial time @garey1979, and the same equivalence inherits the $O^*(2^(n slash 2))$ meet-in-the-middle exact algorithm used for Partition and Subset Sum @horowitz1974. + + *Example.* For coefficients $(2, 3, 5)$ we have the balanced sign assignment $+2 + 3 - 5 = 0$, so the integral equals $(2 pi / 2^3) times 2 = pi / 2$ and the answer is YES. For coefficients $(1, 2, 6)$ the total absolute sum is $9$, which is odd, so no balanced sign assignment exists and the integral is zero; hence the answer is NO. +] + #{ let x = load-model-example("ShortestCommonSupersequence") let alpha-size = x.instance.alphabet_size diff --git a/docs/src/cli.md b/docs/src/cli.md index 11086f4a..d6bb294b 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -351,6 +351,7 @@ pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3 -o lbdp.json pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json +pred create CosineProductIntegration --coefficients 2,3,5 -o cpi.json pred create X3C --universe 9 --sets "0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8" -o x3c.json pred create MinimumCardinalityKey --num-attributes 6 --dependencies "0,1>2;0,2>3;1,3>4;2,4>5" --k 2 -o mck.json pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 --precedence-pairs "0>3,1>3,1>4,2>4" -o mts.json @@ -506,6 +507,14 @@ Solution: [1, 0, 0, 1] Evaluation: Valid(2) ``` +Some models do not yet have an ILP reduction path. For example, `CosineProductIntegration` +currently needs brute force: + +```bash +pred create CosineProductIntegration --coefficients 2,3,5 -o cpi.json +pred solve cpi.json --solver brute-force +``` + Solve a reduction bundle (from `pred reduce`): ```bash @@ -550,7 +559,7 @@ If the shell argument is omitted, `pred completions` auto-detects your current s ## JSON Output -All commands support `-o` to write JSON to a file and `--json` to print JSON to stdout: +Successful data-producing commands support `-o` to write JSON to a file and `--json` to print JSON to stdout: ```bash pred list -o problems.json # save to file @@ -560,6 +569,8 @@ pred path MIS QUBO --json pred solve problem.json --json ``` +Errors are still reported as plain text on stderr, even when `--json` is set. + This is useful for scripting and piping: ```bash diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index d3099362..ade16a3d 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -18,10 +18,12 @@ Piping (use - to read from stdin): pred create MIS --graph 0-1,1-2 | pred evaluate - --config 1,0,1 pred create MIS --graph 0-1,1-2 | pred reduce - --to QUBO -JSON output (any command): +JSON output (successful commands): pred list --json # JSON to stdout pred show MIS --json | jq '.' # pipe to jq +Errors are reported on stderr as text, even when `--json` is set. + Use `pred --help` for detailed usage of each command. Use `pred list` to see all available problem types. @@ -231,6 +233,7 @@ Flags by problem type: LengthBoundedDisjointPaths --graph, --source, --sink, --num-paths-required, --bound Factoring --target, --m, --n BinPacking --sizes, --capacity + CosineProductIntegration --coefficients SubsetSum --sizes, --target PaintShop --sequence MaximumSetPacking --sets [--weights] @@ -288,6 +291,7 @@ Examples: pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1 pred create X3C --universe 9 --sets \"0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8\" pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3 + pred create CosineProductIntegration --coefficients 2,3,5 pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2")] pub struct CreateArgs { /// Problem type (e.g., MIS, QUBO, SAT). Omit when using --example. @@ -389,6 +393,9 @@ pub struct CreateArgs { /// Item sizes for BinPacking (comma-separated, e.g., "3,3,2,2") #[arg(long)] pub sizes: Option, + /// Integer cosine frequencies for CosineProductIntegration (comma-separated, e.g., "2,3,5") + #[arg(long)] + pub coefficients: Option, /// Bin capacity for BinPacking #[arg(long)] pub capacity: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 5b96722c..bffd2d1d 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -13,9 +13,9 @@ use problemreductions::models::graph::{ MultipleChoiceBranching, SteinerTree, StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ - BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, - MultiprocessorScheduling, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, - StringToStringCorrection, SubsetSum, + BinPacking, CosineProductIntegration, FlowShopScheduling, LongestCommonSubsequence, + MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, SequencingWithinIntervals, + ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, }; use problemreductions::models::BiconnectivityAugmentation; use problemreductions::prelude::*; @@ -57,6 +57,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.requirement_1.is_none() && args.requirement_2.is_none() && args.sizes.is_none() + && args.coefficients.is_none() && args.capacity.is_none() && args.sequence.is_none() && args.sets.is_none() @@ -308,6 +309,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "MultiprocessorScheduling" => "--lengths 4,5,3,2,6 --num-processors 2 --deadline 10", "MinimumMultiwayCut" => "--graph 0-1,1-2,2-3 --terminals 0,2 --edge-weights 1,1,1", "SequencingWithinIntervals" => "--release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1", + "CosineProductIntegration" => "--coefficients 2,3,5", "StaffScheduling" => { "--schedules \"1,1,1,1,1,0,0;0,1,1,1,1,1,0;0,0,1,1,1,1,1;1,0,0,1,1,1,1;1,1,0,0,1,1,1\" --requirements 2,2,2,3,3,2,1 --num-workers 4 --k 5" } @@ -1044,6 +1046,21 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // CosineProductIntegration + "CosineProductIntegration" => { + let coefficients_str = args.coefficients.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "CosineProductIntegration requires --coefficients\n\n\ + Usage: pred create CosineProductIntegration --coefficients 2,3,5" + ) + })?; + let coefficients: Vec = util::parse_comma_list(coefficients_str)?; + ( + ser(CosineProductIntegration::new(coefficients))?, + resolved_variant.clone(), + ) + } + // PaintShop "PaintShop" => { let seq_str = args.sequence.as_deref().ok_or_else(|| { diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 1a94f07e..656a2c2f 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -4648,6 +4648,86 @@ fn test_create_factoring_missing_bits() { ); } +// ---- CosineProductIntegration create tests ---- + +#[test] +fn test_create_cosine_product_integration() { + let output = pred() + .args([ + "create", + "CosineProductIntegration", + "--coefficients", + "2,3,5", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(json["type"], "CosineProductIntegration"); + assert_eq!(json["data"]["coefficients"], serde_json::json!([2, 3, 5])); +} + +#[test] +fn test_create_cosine_product_integration_no_flags_shows_help() { + let output = pred() + .args(["create", "CosineProductIntegration"]) + .output() + .unwrap(); + assert!( + !output.status.success(), + "should exit non-zero when showing help without data flags" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("--coefficients"), + "expected '--coefficients' in help output, got: {stderr}" + ); + assert!( + stderr.contains("pred create CosineProductIntegration --coefficients 2,3,5"), + "expected cosine-product example in help output, got: {stderr}" + ); +} + +#[test] +fn test_create_cosine_product_integration_missing_coefficients() { + let output = pred() + .args(["create", "CosineProductIntegration", "--target", "15"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("CosineProductIntegration requires --coefficients"), + "expected missing-coefficients error, got: {stderr}" + ); + assert!( + stderr.contains("pred create CosineProductIntegration --coefficients 2,3,5"), + "expected usage example in error output, got: {stderr}" + ); +} + +#[test] +fn test_create_model_example_cosine_product_integration() { + let output = pred() + .args(["create", "--example", "CosineProductIntegration"]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(json["type"], "CosineProductIntegration"); + assert_eq!(json["data"]["coefficients"], serde_json::json!([2, 3, 5])); +} + #[test] fn test_evaluate_multiprocessor_scheduling_rejects_zero_processors_json() { let problem_file = diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index bc223618..31edec5d 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -8,6 +8,7 @@ {"problem":"CircuitSAT","variant":{},"instance":{"circuit":{"assignments":[{"expr":{"op":{"And":[{"op":{"Var":"x1"}},{"op":{"Var":"x2"}}]}},"outputs":["a"]},{"expr":{"op":{"Or":[{"op":{"Var":"x1"}},{"op":{"Var":"x2"}}]}},"outputs":["b"]},{"expr":{"op":{"Xor":[{"op":{"Var":"a"}},{"op":{"Var":"b"}}]}},"outputs":["c"]}]},"variables":["a","b","c","x1","x2"]},"samples":[{"config":[0,1,1,0,1],"metric":true},{"config":[0,1,1,1,0],"metric":true}],"optimal":[{"config":[0,0,0,0,0],"metric":true},{"config":[0,1,1,0,1],"metric":true},{"config":[0,1,1,1,0],"metric":true},{"config":[1,1,0,1,1],"metric":true}]}, {"problem":"ClosestVectorProblem","variant":{"weight":"i32"},"instance":{"basis":[[2,0],[1,2]],"bounds":[{"lower":-2,"upper":4},{"lower":-2,"upper":4}],"target":[2.8,1.5]},"samples":[{"config":[3,3],"metric":{"Valid":0.5385164807134505}}],"optimal":[{"config":[3,3],"metric":{"Valid":0.5385164807134505}}]}, {"problem":"ComparativeContainment","variant":{"weight":"i32"},"instance":{"r_sets":[[0,1,2,3],[0,1]],"r_weights":[2,5],"s_sets":[[0,1,2,3],[2,3]],"s_weights":[3,6],"universe_size":4},"samples":[{"config":[1,0,0,0],"metric":true}],"optimal":[{"config":[0,1,0,0],"metric":true},{"config":[1,0,0,0],"metric":true},{"config":[1,1,0,0],"metric":true}]}, + {"problem":"CosineProductIntegration","variant":{},"instance":{"coefficients":[2,3,5]},"samples":[{"config":[0,0,1],"metric":true}],"optimal":[{"config":[0,0,1],"metric":true},{"config":[1,1,0],"metric":true}]}, {"problem":"DirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,1,1,1,1,1,1],"graph":{"inner":{"edge_property":"directed","edges":[[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,4,null],[2,5,null],[3,4,null],[3,5,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":4,"sink_2":5,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true}],"optimal":[{"config":[0,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,1,0,1,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,0,0,1,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,1,0,1,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,1,0,1,1,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,0,0,1,1,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,1,0,1,1,1],"metric":true},{"config":[0,0,1,1,0,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,1,0,1,1,0,1,1,0,0,1,0,0,1],"metric":true},{"config":[0,0,1,1,1,0,0,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,1,1,0,0,1,1,1,0,0,0,1,1,0],"metric":true},{"config":[0,0,1,1,1,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,1,1,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,1,1,0,1,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,1,1,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,1,1,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,1,1,1,1,0,1],"metric":true},{"config":[0,1,0,1,0,0,1,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,1,0,1,0,0,1,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,1,0,0,1,1,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,1,0,0,1,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,1,0,0,1,1,0,1,0,0,1,1,0,0,1],"metric":true},{"config":[0,1,1,0,1,0,0,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,1,1,1,1,0,1,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,1,0,1,1,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,0,0,1,1,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,1,0,1,1,1],"metric":true},{"config":[1,0,0,1,0,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1],"metric":true},{"config":[1,0,0,1,1,0,0,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,1,1,0,0,1,0,1,1,0,0,1,1,0],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[1,0,1,1,1,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,1,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,1,1,0,0,0,1,1,1,0,0,1],"metric":true},{"config":[1,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,0,0,1,0,0,1,0,0,1,1,0,1,1,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[1,1,0,1,1,0,1,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,1,0,1,1,1,0,0,0,0,1,0,0,0,1],"metric":true}]}, {"problem":"ExactCoverBy3Sets","variant":{},"instance":{"subsets":[[0,1,2],[0,2,4],[3,4,5],[3,5,7],[6,7,8],[1,4,6],[2,5,8]],"universe_size":9},"samples":[{"config":[1,0,1,0,1,0,0],"metric":true}],"optimal":[{"config":[1,0,1,0,1,0,0],"metric":true}]}, {"problem":"Factoring","variant":{},"instance":{"m":2,"n":3,"target":15},"samples":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}],"optimal":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}]}, diff --git a/src/lib.rs b/src/lib.rs index 0c0347eb..28ef0263 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,9 +58,10 @@ pub mod prelude { UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ - BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, SequencingWithinIntervals, - ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum, + BinPacking, CosineProductIntegration, Factoring, FlowShopScheduling, Knapsack, + LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, + SequencingWithinIntervals, ShortestCommonSupersequence, StaffScheduling, + StringToStringCorrection, SubsetSum, }; pub use crate::models::set::{ ComparativeContainment, ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis, diff --git a/src/models/misc/cosine_product_integration.rs b/src/models/misc/cosine_product_integration.rs new file mode 100644 index 00000000..7efa16c5 --- /dev/null +++ b/src/models/misc/cosine_product_integration.rs @@ -0,0 +1,123 @@ +//! Cosine Product Integration problem implementation. +//! +//! Given integer frequencies `a_1, ..., a_n`, determine whether +//! `integral_0^(2 pi) product_i cos(a_i theta) d theta != 0`. +//! +//! The integral is nonzero exactly when there is a sign assignment +//! `epsilon in {-1, +1}^n` with `sum epsilon_i a_i = 0`, so the implementation +//! checks the equivalent balanced-sum condition directly. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "CosineProductIntegration", + display_name: "Cosine Product Integration", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Decide whether a product of cosine terms has nonzero integral over [0, 2pi]", + fields: &[ + FieldInfo { + name: "coefficients", + type_name: "Vec", + description: "Integer cosine frequencies a_i", + }, + ], + } +} + +/// The Cosine Product Integration problem. +/// +/// Given integer coefficients `a_1, ..., a_n`, determine whether +/// `integral_0^(2 pi) product_i cos(a_i theta) d theta != 0`. +/// +/// # Representation +/// +/// Each configuration chooses signs for the coefficients: `0` means `+a_i`, +/// and `1` means `-a_i`. A configuration satisfies the problem exactly when +/// the resulting signed sum is zero, which is equivalent to the integral being +/// nonzero. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CosineProductIntegration { + coefficients: Vec, +} + +impl CosineProductIntegration { + /// Create a new CosineProductIntegration instance. + pub fn new(coefficients: Vec) -> Self { + Self { coefficients } + } + + /// Returns the cosine coefficients. + pub fn coefficients(&self) -> &[i64] { + &self.coefficients + } + + /// Returns the number of coefficients. + pub fn num_coefficients(&self) -> usize { + self.coefficients.len() + } + + /// Returns the signed sum induced by a sign assignment. + pub fn signed_sum(&self, sign_bits: &[usize]) -> Option { + if sign_bits.len() != self.num_coefficients() || sign_bits.iter().any(|&bit| bit > 1) { + return None; + } + + Some( + self.coefficients + .iter() + .zip(sign_bits.iter().copied()) + .map(|(&coefficient, bit)| { + if bit == 0 { + coefficient as i128 + } else { + -(coefficient as i128) + } + }) + .sum(), + ) + } + +} + +impl Problem for CosineProductIntegration { + const NAME: &'static str = "CosineProductIntegration"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![2; self.num_coefficients()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.signed_sum(config) == Some(0) + } +} + +impl SatisfactionProblem for CosineProductIntegration {} + +crate::declare_variants! { + default sat CosineProductIntegration => "2^(num_coefficients / 2)", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "cosine_product_integration", + build: || { + let problem = CosineProductIntegration::new(vec![2, 3, 5]); + crate::example_db::specs::satisfaction_example(problem, vec![vec![0, 0, 1]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/cosine_product_integration.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 17ea8984..a4ddc26d 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -2,6 +2,7 @@ //! //! Problems with unique input structures that don't fit other categories: //! - [`BinPacking`]: Bin Packing (minimize bins) +//! - [`CosineProductIntegration`]: Decide whether a cosine product integral is nonzero //! - [`Factoring`]: Integer factorization //! - [`FlowShopScheduling`]: Flow Shop Scheduling (meet deadline on m processors) //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) @@ -15,6 +16,7 @@ //! - [`SubsetSum`]: Find a subset summing to exactly a target value mod bin_packing; +mod cosine_product_integration; pub(crate) mod factoring; mod flow_shop_scheduling; mod knapsack; @@ -29,6 +31,7 @@ pub(crate) mod string_to_string_correction; mod subset_sum; pub use bin_packing::BinPacking; +pub use cosine_product_integration::CosineProductIntegration; pub use factoring::Factoring; pub use flow_shop_scheduling::FlowShopScheduling; pub use knapsack::Knapsack; @@ -45,6 +48,7 @@ pub use subset_sum::SubsetSum; #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { let mut specs = Vec::new(); + specs.extend(cosine_product_integration::canonical_model_example_specs()); specs.extend(factoring::canonical_model_example_specs()); specs.extend(longest_common_subsequence::canonical_model_example_specs()); specs.extend(multiprocessor_scheduling::canonical_model_example_specs()); diff --git a/src/models/mod.rs b/src/models/mod.rs index 2d58e6a2..4acf37de 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -22,9 +22,10 @@ pub use graph::{ UndirectedTwoCommodityIntegralFlow, }; pub use misc::{ - BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, SequencingWithinIntervals, - ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum, + BinPacking, CosineProductIntegration, Factoring, FlowShopScheduling, Knapsack, + LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, + SequencingWithinIntervals, ShortestCommonSupersequence, StaffScheduling, + StringToStringCorrection, SubsetSum, }; pub use set::{ ComparativeContainment, ExactCoverBy3Sets, MaximumSetPacking, MinimumCardinalityKey, diff --git a/src/unit_tests/models/misc/cosine_product_integration.rs b/src/unit_tests/models/misc/cosine_product_integration.rs new file mode 100644 index 00000000..c9947b0c --- /dev/null +++ b/src/unit_tests/models/misc/cosine_product_integration.rs @@ -0,0 +1,99 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_cosine_product_integration_creation() { + let problem = CosineProductIntegration::new(vec![2, 3, 5]); + assert_eq!(problem.coefficients(), &[2, 3, 5]); + assert_eq!(problem.num_coefficients(), 3); + assert_eq!(problem.dims(), vec![2, 2, 2]); + assert_eq!( + ::NAME, + "CosineProductIntegration" + ); + assert!(::variant().is_empty()); +} + +#[test] +fn test_cosine_product_integration_signed_sum_and_evaluate() { + let problem = CosineProductIntegration::new(vec![2, 3, 5]); + assert_eq!(problem.signed_sum(&[0, 0, 1]), Some(0)); + assert_eq!(problem.signed_sum(&[1, 1, 0]), Some(0)); + assert_eq!(problem.signed_sum(&[0, 1, 0]), Some(4)); + assert_eq!(problem.signed_sum(&[0, 0]), None); + assert_eq!(problem.signed_sum(&[0, 2, 0]), None); + assert!(problem.evaluate(&[0, 0, 1])); + assert!(problem.evaluate(&[1, 1, 0])); + assert!(!problem.evaluate(&[0, 1, 0])); +} + +#[test] +fn test_cosine_product_integration_unsatisfiable_instances() { + let odd_total = CosineProductIntegration::new(vec![1, 2, 6]); + let even_total = CosineProductIntegration::new(vec![1, 2, 5, 10]); + let solver = BruteForce::new(); + + assert_eq!(solver.find_satisfying(&odd_total), None); + + assert_eq!(solver.find_satisfying(&even_total), None); +} + +#[test] +fn test_cosine_product_integration_solver_behavior() { + let problem = CosineProductIntegration::new(vec![2, 3, 5]); + let solver = BruteForce::new(); + + let solution = solver.find_satisfying(&problem).unwrap(); + assert!(problem.evaluate(&solution)); + assert!(solution == vec![0, 0, 1] || solution == vec![1, 1, 0]); + + let mut solutions = solver.find_all_satisfying(&problem); + solutions.sort(); + assert_eq!(solutions, vec![vec![0, 0, 1], vec![1, 1, 0]]); +} + +#[test] +fn test_cosine_product_integration_empty_instance() { + let problem = CosineProductIntegration::new(vec![]); + let solver = BruteForce::new(); + + assert_eq!(problem.dims(), Vec::::new()); + assert!(problem.evaluate(&[])); + assert_eq!( + solver.find_all_satisfying(&problem), + vec![Vec::::new()] + ); +} + +#[test] +fn test_cosine_product_integration_zero_coefficients() { + let problem = CosineProductIntegration::new(vec![0, 0, 0]); + let solver = BruteForce::new(); + + assert!(problem.evaluate(&[0, 1, 0])); + assert_eq!(solver.find_all_satisfying(&problem).len(), 8); +} + +#[test] +fn test_cosine_product_integration_negative_coefficients() { + let problem = CosineProductIntegration::new(vec![-2, 3, -5]); + + assert_eq!(problem.signed_sum(&[1, 0, 0]), Some(0)); + assert!(problem.evaluate(&[1, 0, 0])); +} + +#[test] +fn test_cosine_product_integration_serialization() { + let problem = CosineProductIntegration::new(vec![2, 3, 5]); + let json = serde_json::to_value(&problem).unwrap(); + assert_eq!( + json, + serde_json::json!({ + "coefficients": [2, 3, 5], + }) + ); + + let restored: CosineProductIntegration = serde_json::from_value(json).unwrap(); + assert_eq!(restored.coefficients(), problem.coefficients()); +}