diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 27ce2026c..126c83f63 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -172,9 +172,11 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: ### Extension Points - New models register dynamic load/serialize/brute-force dispatch through `declare_variants!` in the model file, not by adding manual match arms in the CLI +- **CLI creation is schema-driven:** `pred create` automatically maps `ProblemSchemaEntry` fields to CLI flags via `snake_case → kebab-case` convention. New models need only: (1) matching CLI flags in `CreateArgs` + `flag_map()`, and (2) type parser support in `parse_field_value()` if using a new field type. No match arm in `create.rs` is needed. +- **CLI flag names must match schema field names.** The canonical name for a CLI flag is the schema field name in kebab-case (e.g., schema field `universe_size` → `--universe-size`, field `subsets` → `--subsets`). Old aliases (e.g., `--universe`, `--sets`) may exist as clap `alias` for backward compatibility at the clap level, but `flag_map()`, help text, error messages, and documentation must use the schema-derived name. Do not add new backward-compat aliases; if a field is renamed in the schema, update the CLI flag name to match. - Aggregate-only models are first-class in `declare_variants!`; aggregate-only reduction edges still need manual `ReductionEntry` wiring because `#[reduction]` only registers witness/config reductions today - Exact registry dispatch lives in `src/registry/`; alias resolution and partial/default variant resolution live in `problemreductions-cli/src/problem_name.rs` -- `pred create` UX lives in `problemreductions-cli/src/commands/create.rs` +- `pred create` schema-driven dispatch lives in `problemreductions-cli/src/commands/create.rs` (`create_schema_driven()`) - Canonical paper and CLI examples live in `src/example_db/model_builders.rs` and `src/example_db/rule_builders.rs` ## Conventions diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index 0f00e738c..95d89b0bf 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -175,18 +175,15 @@ The CLI now loads, serializes, and brute-force solves problems through the core ## Step 4.5: Add CLI creation support -Update `problemreductions-cli/src/commands/create.rs` so `pred create ` works: +CLI creation is **schema-driven** — `pred create ` automatically maps `ProblemSchemaEntry` fields to CLI flags via `snake_case → kebab-case` convention. No match arm in `create.rs` is needed. -1. **Add a match arm** in the `create()` function's main `match canonical.as_str()` block. Parse CLI flags and construct the problem: - - Graph-based problems with vertex weights: add to the `"MaximumIndependentSet" | ... | "MaximalIS"` arm - - Problems with unique fields: add a new arm that parses the required flags and calls the constructor - - See existing arms for patterns (e.g., `"BinPacking"` for simple fields, `"MaximumSetPacking"` for set-based) +1. **Ensure CLI flags exist** in `problemreductions-cli/src/cli.rs` (`CreateArgs` struct) for each field in your `ProblemSchemaEntry`. The flag name must match the field name via `snake_case → kebab-case` (e.g., field `edge_weights` → flag `--edge-weights`). If a flag already exists with the right name, you're done. -2. **Add CLI flags** in `problemreductions-cli/src/cli.rs` (`CreateArgs` struct) if the problem needs flags not already present. Update `all_data_flags_empty()` accordingly. +2. **Add new CLI flags** only if the problem needs flags not already present. Add them to `CreateArgs` and update `all_data_flags_empty()` accordingly. Also add entries to the `flag_map()` method on `CreateArgs`. -3. **Update help text** in `CreateArgs`'s `after_help` — add the new problem to the "Flags by problem type" table in `problemreductions-cli/src/cli.rs` (search for `Flags by problem type`). +3. **Add type parser support** if the field uses a type not yet handled by `parse_field_value()` in `create.rs`. Check the existing type dispatch table — most standard types (`Vec`, `Vec`, `Vec<(usize, usize)>`, graph types, etc.) are already covered. Only add a new parser for genuinely new types. -4. **Schema alignment**: The `ProblemSchemaEntry` fields should list **constructor parameters** (what the user provides), not internal derived fields. For example, if `m` and `n` are derived from a matrix, only list `matrix` and `k` in the schema. +4. **Schema alignment**: The `ProblemSchemaEntry` fields should list **constructor parameters** (what the user provides), not internal derived fields. For example, if `m` and `n` are derived from a matrix, only list `matrix` and `k` in the schema. Field names must match the struct field names exactly (used for JSON serialization and CLI flag mapping). ## Step 4.6: Add canonical model example to example_db @@ -315,8 +312,8 @@ Structural and quality review is handled by the `review-pipeline` stage, not her | Wrong `declare_variants!` syntax | Entries no longer use `opt` / `sat`; one entry per problem may be marked `default` | | Forgetting CLI alias | Must add lowercase entry in `problem_name.rs` `resolve_alias()` | | Inventing short aliases | Only use well-established literature abbreviations (MIS, SAT, TSP); do NOT invent new ones | -| Forgetting CLI create | Must add creation handler in `commands/create.rs` and flags in `cli.rs` | -| Missing from CLI help table | Must add entry to "Flags by problem type" table in `cli.rs` `after_help` | +| Forgetting CLI flags | Schema-driven create needs matching CLI flags in `CreateArgs` for each `ProblemSchemaEntry` field (snake_case → kebab-case). Also add to `flag_map()`. | +| Missing type parser | If the problem uses a new field type, add a handler in `parse_field_value()` in `create.rs` | | Schema lists derived fields | Schema should list constructor params, not internal fields (e.g., `matrix, k` not `matrix, m, n, k`) | | Missing canonical model example | Add a builder in `src/example_db/model_builders.rs` and keep it aligned with paper/example workflows | | Paper example not tested | Must include `test__paper_example` that verifies the exact instance, solution, and solution count shown in the paper | diff --git a/.claude/skills/review-structural/SKILL.md b/.claude/skills/review-structural/SKILL.md index c169ebcbd..5c30e15b6 100644 --- a/.claude/skills/review-structural/SKILL.md +++ b/.claude/skills/review-structural/SKILL.md @@ -62,7 +62,7 @@ Only run if review type includes "model". Given: problem name `P`, category `C`, | 10 | Re-exported in `models/mod.rs` | `Grep("{P}", "src/models/mod.rs")` | | 11 | Variant registration exists | `Grep("declare_variants!|VariantEntry", file)` | | 12 | CLI `resolve_alias` entry | `Grep("{P}", "problemreductions-cli/src/problem_name.rs")` | -| 13 | CLI `create` support | `Grep('"{P}"', "problemreductions-cli/src/commands/create.rs")` | +| 13 | CLI `create` support | Schema-driven: verify each `ProblemSchemaEntry` field has a matching CLI flag in `CreateArgs` (field `snake_case` → flag `kebab-case`). Check `flag_map()` includes the flag. If the field type is unusual, verify `parse_field_value()` handles it. | | 14 | Canonical model example registered | `Grep("{P}", "src/example_db/model_builders.rs")` | | 15 | Paper `display-name` entry | `Grep('"{P}"', "docs/paper/reductions.typ")` | | 16 | Paper `problem-def` block | `Grep('problem-def.*"{P}"', "docs/paper/reductions.typ")` | diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index f1b54917e..542f7f465 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -1,4 +1,5 @@ use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; +use std::collections::HashMap; use std::path::PathBuf; #[derive(Parser)] @@ -240,7 +241,7 @@ Flags by problem type: HamiltonianCircuit, HC --graph MaximumLeafSpanningTree --graph LongestCircuit --graph, --edge-weights - BoundedComponentSpanningForest --graph, --weights, --k, --bound + BoundedComponentSpanningForest --graph, --weights, --k, --max-weight UndirectedFlowLowerBounds --graph, --capacities, --lower-bounds, --source, --sink, --requirement IntegralFlowBundles --arcs, --bundles, --bundle-capacities, --source, --sink, --requirement [--num-vertices] UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 @@ -248,7 +249,7 @@ Flags by problem type: IntegralFlowHomologousArcs --arcs, --capacities, --source, --sink, --requirement, --homologous-pairs IsomorphicSpanningTree --graph, --tree KthBestSpanningTree --graph, --edge-weights, --k, --bound - LengthBoundedDisjointPaths --graph, --source, --sink, --bound + LengthBoundedDisjointPaths --graph, --source, --sink, --max-length PathConstrainedNetworkFlow --arcs, --capacities, --source, --sink, --paths, --requirement Factoring --target, --m, --n BinPacking --sizes, --capacity @@ -270,22 +271,22 @@ Flags by problem type: SumOfSquaresPartition --sizes, --num-groups ExpectedRetrievalCost --probabilities, --num-sectors PaintShop --sequence - MaximumSetPacking --sets [--weights] - MinimumHittingSet --universe, --sets - MinimumSetCovering --universe, --sets [--weights] - EnsembleComputation --universe, --sets, --budget - ComparativeContainment --universe, --r-sets, --s-sets [--r-weights] [--s-weights] - X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each) - 3DM (ThreeDimensionalMatching) --universe, --sets (triples w,x,y) - ThreeMatroidIntersection --universe, --partitions, --bound - SetBasis --universe, --sets, --k + MaximumSetPacking --subsets [--weights] + MinimumHittingSet --universe-size, --subsets + MinimumSetCovering --universe-size, --subsets [--weights] + EnsembleComputation --universe-size, --subsets, --budget + ComparativeContainment --universe-size, --r-sets, --s-sets [--r-weights] [--s-weights] + X3C (ExactCoverBy3Sets) --universe-size, --subsets (3 elements each) + 3DM (ThreeDimensionalMatching) --universe-size, --subsets (triples w,x,y) + ThreeMatroidIntersection --universe-size, --partitions, --bound + SetBasis --universe-size, --subsets, --k MinimumCardinalityKey --num-attributes, --dependencies - PrimeAttributeName --universe, --deps, --query - RootedTreeStorageAssignment --universe, --sets, --bound - TwoDimensionalConsecutiveSets --alphabet-size, --sets + PrimeAttributeName --universe-size, --dependencies, --query-attribute + RootedTreeStorageAssignment --universe-size, --subsets, --bound + TwoDimensionalConsecutiveSets --alphabet-size, --subsets BicliqueCover --left, --right, --biedges, --k BalancedCompleteBipartiteSubgraph --left, --right, --biedges, --k - BiconnectivityAugmentation --graph, --potential-edges, --budget [--num-vertices] + BiconnectivityAugmentation --graph, --potential-weights, --budget [--num-vertices] PartialFeedbackEdgeSet --graph, --budget, --max-cycle-length [--num-vertices] BMF --matrix (0/1), --rank ConsecutiveBlockMinimization --matrix (JSON 2D bool), --bound-k @@ -298,7 +299,7 @@ Flags by problem type: FeasibleBasisExtension --matrix (JSON 2D i64), --rhs, --required-columns SteinerTree --graph, --edge-weights, --terminals MultipleCopyFileAllocation --graph, --usage, --storage - AcyclicPartition --arcs [--weights] [--arc-costs] --weight-bound --cost-bound [--num-vertices] + AcyclicPartition --arcs [--weights] [--arc-weights] --weight-bound --cost-bound [--num-vertices] CVP --basis, --target-vec [--bounds] MultiprocessorScheduling --lengths, --num-processors, --deadline SchedulingToMinimizeWeightedCompletionTime --lengths, --weights, --num-processors @@ -306,10 +307,10 @@ Flags by problem type: OptimalLinearArrangement --graph RootedTreeArrangement --graph, --bound MinMaxMulticenter (pCenter) --graph, --weights, --edge-weights, --k - MixedChinesePostman (MCPP) --graph, --arcs, --edge-weights, --arc-costs [--num-vertices] + MixedChinesePostman (MCPP) --graph, --arcs, --edge-weights, --arc-weights [--num-vertices] RuralPostman (RPP) --graph, --edge-weights, --required-edges - StackerCrane --arcs, --graph, --arc-costs, --edge-lengths [--num-vertices] - MultipleChoiceBranching --arcs [--weights] --partition --bound [--num-vertices] + StackerCrane --arcs, --graph, --arc-lengths, --edge-lengths [--num-vertices] + MultipleChoiceBranching --arcs [--weights] --partition --threshold [--num-vertices] AdditionalKey --num-attributes, --dependencies, --relation-attrs [--known-keys] ConsistencyOfDatabaseFrequencyTables --num-objects, --attribute-domains, --frequency-tables [--known-values] SubgraphIsomorphism --graph (host), --pattern (pattern) @@ -325,18 +326,18 @@ Flags by problem type: PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences QAP --matrix (cost), --distance-matrix StrongConnectivityAugmentation --arcs, --candidate-arcs, --bound [--num-vertices] - JobShopScheduling --job-tasks [--num-processors] + JobShopScheduling --jobs [--num-processors] FlowShopScheduling --task-lengths, --deadline [--num-processors] StaffScheduling --schedules, --requirements, --num-workers, --k TimetableDesign --num-periods, --num-craftsmen, --num-tasks, --craftsman-avail, --task-avail, --requirements - MinimumTardinessSequencing --n, --deadlines [--precedence-pairs] + MinimumTardinessSequencing --num-tasks, --deadlines [--precedences] RectilinearPictureCompression --matrix (0/1), --k - SchedulingWithIndividualDeadlines --n, --num-processors/--m, --deadlines [--precedence-pairs] - SequencingToMinimizeMaximumCumulativeCost --costs [--precedence-pairs] - SequencingToMinimizeTardyTaskWeight --sizes, --weights, --deadlines - SequencingToMinimizeWeightedCompletionTime --lengths, --weights [--precedence-pairs] - SequencingToMinimizeWeightedTardiness --sizes, --weights, --deadlines, --bound - SequencingWithDeadlinesAndSetUpTimes --sizes, --deadlines, --compilers, --setup-times + SchedulingWithIndividualDeadlines --num-tasks, --num-processors/--m, --deadlines [--precedences] + SequencingToMinimizeMaximumCumulativeCost --costs [--precedences] + SequencingToMinimizeTardyTaskWeight --lengths, --weights, --deadlines + SequencingToMinimizeWeightedCompletionTime --lengths, --weights [--precedences] + SequencingToMinimizeWeightedTardiness --lengths, --weights, --deadlines, --bound + SequencingWithDeadlinesAndSetUpTimes --lengths, --deadlines, --compilers, --setup-times MinimumExternalMacroDataCompression --string, --pointer-cost [--alphabet-size] MinimumInternalMacroDataCompression --string, --pointer-cost [--alphabet-size] SCS --strings [--alphabet-size] @@ -388,16 +389,16 @@ Examples: pred create SchedulingToMinimizeWeightedCompletionTime --lengths 1,2,3,4,5 --weights 6,4,3,2,1 --num-processors 2 pred create UndirectedFlowLowerBounds --graph 0-1,0-2,1-3,2-3,1-4,3-5,4-5 --capacities 2,2,2,2,1,3,2 --lower-bounds 1,1,0,0,1,0,1 --source 0 --sink 5 --requirement 3 pred create ConsistencyOfDatabaseFrequencyTables --num-objects 6 --attribute-domains \"2,3,2\" --frequency-tables \"0,1:1,1,1|1,1,1;1,2:1,1|0,2|1,1\" --known-values \"0,0,0;3,0,1;1,2,1\" - pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5 + pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-weights 0-2:3,0-3:4,1-3:2 --budget 5 pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1 pred create MinimumDummyActivitiesPert --arcs \"0>2,0>3,1>3,1>4,2>5\" --num-vertices 6 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 IntegralFlowHomologousArcs --arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\" - 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 X3C --universe 9 --subsets \"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 --subsets \"0,1;1,2;0,2;0,1,2\" --k 3 pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" - pred create PrimeAttributeName --universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3 - pred create TwoDimensionalConsecutiveSets --alphabet-size 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"")] + pred create PrimeAttributeName --universe 6 --dependencies \"0,1>2,3,4,5;2,3>0,1,4,5\" --query-attribute 3 + pred create TwoDimensionalConsecutiveSets --alphabet-size 6 --subsets \"0,1,2;3,4,5;1,3;2,4;0,5\"")] pub struct CreateArgs { /// Problem type (e.g., MIS, QUBO, SAT). Omit when using --example. #[arg(value_parser = crate::problem_name::ProblemNameParser)] @@ -556,8 +557,8 @@ pub struct CreateArgs { /// Car paint sequence for PaintShop (comma-separated, each label appears exactly twice, e.g., "a,b,a,c,c,b") #[arg(long)] pub sequence: Option, - /// Sets for set-system problems such as SetPacking, MinimumHittingSet, and SetCovering (semicolon-separated, e.g., "0,1;1,2;0,2") - #[arg(long)] + /// Subsets for set-system problems such as SetPacking, MinimumHittingSet, and SetCovering (semicolon-separated, e.g., "0,1;1,2;0,2") + #[arg(long = "subsets", alias = "sets")] pub sets: Option, /// R-family sets for ComparativeContainment (semicolon-separated, e.g., "0,1;1,2") #[arg(long)] @@ -581,7 +582,7 @@ pub struct CreateArgs { #[arg(long)] pub bundles: Option, /// Universe size for set-system problems such as MinimumHittingSet, MinimumSetCovering, and ComparativeContainment - #[arg(long)] + #[arg(long = "universe-size", alias = "universe")] pub universe: Option, /// Bipartite graph edges for BicliqueCover / BalancedCompleteBipartiteSubgraph (e.g., "0-0,0-1,1-2" for left-right pairs) #[arg(long)] @@ -623,7 +624,14 @@ pub struct CreateArgs { #[arg(long)] pub required_edges: Option, /// Bound parameter (upper or length bound for BoundedComponentSpanningForest, GroupingBySwapping, LengthBoundedDisjointPaths, MultipleChoiceBranching, RootedTreeArrangement, or StringToStringCorrection) - #[arg(long, allow_hyphen_values = true)] + #[arg( + long, + alias = "max-length", + alias = "max-weight", + alias = "bound-k", + alias = "threshold", + allow_hyphen_values = true + )] pub bound: Option, /// Upper bound on expected retrieval latency for ExpectedRetrievalCost #[arg(long)] @@ -655,8 +663,8 @@ pub struct CreateArgs { /// Task costs for SequencingToMinimizeMaximumCumulativeCost (comma-separated, e.g., "2,-1,3,-2,1,-3") #[arg(long, allow_hyphen_values = true)] pub costs: Option, - /// Arc costs for directed graph problems with per-arc costs (comma-separated, e.g., "1,1,2,3") - #[arg(long)] + /// Arc weights/lengths for directed graph problems with per-arc costs (comma-separated, e.g., "1,1,2,3") + #[arg(long = "arc-weights", alias = "arc-costs", alias = "arc-lengths")] pub arc_costs: Option, /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] @@ -689,7 +697,7 @@ pub struct CreateArgs { #[arg(long)] pub distance_matrix: Option, /// Weighted potential augmentation edges (e.g., 0-2:3,1-3:5) - #[arg(long)] + #[arg(long = "potential-weights", alias = "potential-edges")] pub potential_edges: Option, /// Total budget for selected potential edges #[arg(long)] @@ -722,7 +730,7 @@ pub struct CreateArgs { #[arg(long)] pub task_lengths: Option, /// Job tasks for JobShopScheduling (semicolon-separated jobs, comma-separated processor:length tasks, e.g., "0:3,1:4;1:2,0:3,1:2") - #[arg(long)] + #[arg(long = "jobs", alias = "job-tasks")] pub job_tasks: Option, /// Deadline for FlowShopScheduling, MultiprocessorScheduling, or ResourceConstrainedScheduling #[arg(long)] @@ -795,7 +803,7 @@ pub struct CreateArgs { #[arg(long)] pub deps: Option, /// Query attribute index for PrimeAttributeName - #[arg(long)] + #[arg(long = "query-attribute", alias = "query")] pub query: Option, /// Right-hand side vector for FeasibleBasisExtension (comma-separated, e.g., "7,5,3") #[arg(long)] @@ -907,6 +915,229 @@ pub struct CreateArgs { pub num_colors: Option, } +impl CreateArgs { + #[allow(dead_code)] + pub fn flag_map(&self) -> HashMap<&'static str, Option> { + let mut flags = HashMap::new(); + + macro_rules! insert { + ($key:literal, $expr:expr) => { + flags.insert($key, ($expr).map(|value| value.to_string())); + }; + } + + insert!("example", self.example.as_deref()); + insert!("to", self.example_target.as_deref()); + insert!("graph", self.graph.as_deref()); + insert!("weights", self.weights.as_deref()); + insert!("edge-weights", self.edge_weights.as_deref()); + insert!("edge-lengths", self.edge_lengths.as_deref()); + insert!("capacities", self.capacities.as_deref()); + insert!("demands", self.demands.as_deref()); + insert!("setup-costs", self.setup_costs.as_deref()); + insert!("production-costs", self.production_costs.as_deref()); + insert!("inventory-costs", self.inventory_costs.as_deref()); + insert!("bundle-capacities", self.bundle_capacities.as_deref()); + insert!("cost-matrix", self.cost_matrix.as_deref()); + insert!("delay-matrix", self.delay_matrix.as_deref()); + insert!("lower-bounds", self.lower_bounds.as_deref()); + insert!("multipliers", self.multipliers.as_deref()); + insert!("sink", self.sink); + insert!("requirement", self.requirement); + insert!("num-paths-required", self.num_paths_required); + insert!("paths", self.paths.as_deref()); + insert!("couplings", self.couplings.as_deref()); + insert!("fields", self.fields.as_deref()); + insert!("clauses", self.clauses.as_deref()); + insert!("disjuncts", self.disjuncts.as_deref()); + insert!("num-vars", self.num_vars); + insert!("matrix", self.matrix.as_deref()); + insert!("k", self.k); + insert!("num-partitions", self.num_partitions); + flags.insert("random", self.random.then(|| "true".to_string())); + insert!("num-vertices", self.num_vertices); + insert!("source-vertex", self.source_vertex); + insert!("target-vertex", self.target_vertex); + insert!("edge-prob", self.edge_prob); + insert!("seed", self.seed); + insert!("target", self.target.as_deref()); + insert!("m", self.m); + insert!("n", self.n); + insert!("positions", self.positions.as_deref()); + insert!("radius", self.radius); + insert!("source-1", self.source_1); + insert!("sink-1", self.sink_1); + insert!("source-2", self.source_2); + insert!("sink-2", self.sink_2); + insert!("requirement-1", self.requirement_1); + insert!("requirement-2", self.requirement_2); + insert!("sizes", self.sizes.as_deref()); + insert!("probabilities", self.probabilities.as_deref()); + insert!("capacity", self.capacity.as_deref()); + insert!("sequence", self.sequence.as_deref()); + insert!("subsets", self.sets.as_deref()); + insert!("r-sets", self.r_sets.as_deref()); + insert!("s-sets", self.s_sets.as_deref()); + insert!("r-weights", self.r_weights.as_deref()); + insert!("s-weights", self.s_weights.as_deref()); + insert!("partition", self.partition.as_deref()); + insert!("partitions", self.partitions.as_deref()); + insert!("bundles", self.bundles.as_deref()); + insert!("universe-size", self.universe); + insert!("universe", self.universe); // PrimeAttributeName maps num_attributes → --universe + insert!("biedges", self.biedges.as_deref()); + insert!("left", self.left); + insert!("right", self.right); + insert!("rank", self.rank); + insert!("basis", self.basis.as_deref()); + insert!("target-vec", self.target_vec.as_deref()); + insert!("bounds", self.bounds.as_deref()); + insert!("release-times", self.release_times.as_deref()); + insert!("lengths", self.lengths.as_deref().or(self.sizes.as_deref())); + insert!("terminals", self.terminals.as_deref()); + insert!("terminal-pairs", self.terminal_pairs.as_deref()); + insert!("tree", self.tree.as_deref()); + insert!("required-edges", self.required_edges.as_deref()); + insert!("bound", self.bound); + insert!("max-length", self.bound); + insert!("max-weight", self.bound); + insert!("bound-k", self.bound); + insert!("threshold", self.bound); + insert!("latency-bound", self.latency_bound); + insert!("length-bound", self.length_bound); + insert!("weight-bound", self.weight_bound); + insert!("diameter-bound", self.diameter_bound); + insert!("cost-bound", self.cost_bound); + insert!("delay-budget", self.delay_budget); + insert!("pattern", self.pattern.as_deref()); + insert!("strings", self.strings.as_deref()); + insert!("string", self.string.as_deref()); + insert!("costs", self.costs.as_deref()); + insert!("arc-weights", self.arc_costs.as_deref()); + insert!("arc-costs", self.arc_costs.as_deref()); + insert!("arc-lengths", self.arc_costs.as_deref()); + insert!("arcs", self.arcs.as_deref()); + insert!("left-arcs", self.left_arcs.as_deref()); + insert!("right-arcs", self.right_arcs.as_deref()); + insert!("homologous-pairs", self.homologous_pairs.as_deref()); + insert!("quantifiers", self.quantifiers.as_deref()); + insert!("size-bound", self.size_bound); + insert!("cut-bound", self.cut_bound); + insert!("values", self.values.as_deref()); + insert!( + "precedences", + self.precedences + .as_deref() + .or(self.precedence_pairs.as_deref()) + ); + insert!( + "precedence-pairs", + self.precedences + .as_deref() + .or(self.precedence_pairs.as_deref()) + ); + insert!("distance-matrix", self.distance_matrix.as_deref()); + insert!("potential-weights", self.potential_edges.as_deref()); + insert!("potential-edges", self.potential_edges.as_deref()); + insert!("budget", self.budget.as_deref()); + insert!("max-cycle-length", self.max_cycle_length); + insert!("candidate-arcs", self.candidate_arcs.as_deref()); + insert!("usage", self.usage.as_deref()); + insert!("storage", self.storage.as_deref()); + insert!("deadlines", self.deadlines.as_deref()); + insert!("resource-bounds", self.resource_bounds.as_deref()); + insert!( + "resource-requirements", + self.resource_requirements.as_deref() + ); + insert!("task-lengths", self.task_lengths.as_deref()); + insert!("jobs", self.job_tasks.as_deref()); + insert!("job-tasks", self.job_tasks.as_deref()); + insert!("deadline", self.deadline); + insert!("num-processors", self.num_processors); + insert!("schedules", self.schedules.as_deref()); + insert!("requirements", self.requirements.as_deref()); + insert!("num-workers", self.num_workers); + insert!("num-periods", self.num_periods); + insert!("num-craftsmen", self.num_craftsmen); + insert!("num-tasks", self.num_tasks.or(self.n)); + insert!("craftsman-avail", self.craftsman_avail.as_deref()); + insert!("task-avail", self.task_avail.as_deref()); + insert!("alphabet-size", self.alphabet_size); + insert!("num-attributes", self.num_attributes); + insert!( + "dependencies", + self.dependencies.as_deref().or(self.deps.as_deref()) + ); + insert!( + "deps", + self.dependencies.as_deref().or(self.deps.as_deref()) + ); + insert!("relation-attrs", self.relation_attrs.as_deref()); + insert!("known-keys", self.known_keys.as_deref()); + insert!("num-objects", self.num_objects); + insert!("attribute-domains", self.attribute_domains.as_deref()); + insert!("frequency-tables", self.frequency_tables.as_deref()); + insert!("known-values", self.known_values.as_deref()); + insert!("domain-size", self.domain_size); + insert!("relations", self.relations.as_deref()); + insert!("conjuncts-spec", self.conjuncts_spec.as_deref()); + insert!("query-attribute", self.query); + insert!("rhs", self.rhs.as_deref()); + insert!("required-columns", self.required_columns.as_deref()); + insert!("num-groups", self.num_groups); + insert!("num-sectors", self.num_sectors); + insert!("compilers", self.compilers.as_deref()); + insert!("setup-times", self.setup_times.as_deref()); + insert!("source-string", self.source_string.as_deref()); + insert!("target-string", self.target_string.as_deref()); + insert!("pointer-cost", self.pointer_cost); + insert!("expression", self.expression.as_deref()); + insert!("equations", self.equations.as_deref()); + insert!("assignment", self.assignment.as_deref()); + insert!("coeff-a", self.coeff_a); + insert!("coeff-b", self.coeff_b); + insert!("coeff-c", self.coeff_c); + insert!("pairs", self.pairs.as_deref()); + insert!("w-sizes", self.w_sizes.as_deref()); + insert!("x-sizes", self.x_sizes.as_deref()); + insert!("y-sizes", self.y_sizes.as_deref()); + insert!("initial-marking", self.initial_marking.as_deref()); + insert!("output-arcs", self.output_arcs.as_deref()); + insert!("gate-types", self.gate_types.as_deref()); + insert!("inputs", self.inputs.as_deref()); + insert!("outputs", self.outputs.as_deref()); + insert!("true-sentences", self.true_sentences.as_deref()); + insert!("implications", self.implications.as_deref()); + insert!("loop-length", self.loop_length); + insert!("loop-variables", self.loop_variables.as_deref()); + insert!("assignments", self.assignments.as_deref()); + insert!("num-variables", self.num_variables); + insert!("truth-table", self.truth_table.as_deref()); + insert!("test-matrix", self.test_matrix.as_deref()); + insert!("num-tests", self.num_tests); + insert!("tiles", self.tiles.as_deref()); + insert!("grid-size", self.grid_size); + insert!("num-colors", self.num_colors); + + flags.insert( + "source", + self.source_string + .clone() + .or_else(|| self.source.map(|value| value.to_string())), + ); + flags.insert( + "target", + self.target_string + .clone() + .or_else(|| self.target.clone()) + .or_else(|| self.sink.map(|value| value.to_string())), + ); + + flags + } +} + #[derive(clap::Args)] #[command(after_help = "\ Examples: @@ -1064,7 +1295,7 @@ mod tests { "create help should describe --num-processors for both scheduling models" ); assert!(help.contains( - "SchedulingWithIndividualDeadlines --n, --num-processors/--m, --deadlines [--precedence-pairs]" + "SchedulingWithIndividualDeadlines --num-tasks, --num-processors/--m, --deadlines [--precedences]" )); } @@ -1076,7 +1307,7 @@ mod tests { "BiconnectivityAugmentation", "--graph", "0-1,1-2", - "--potential-edges", + "--potential-weights", "0-2:3,1-3:5", "--budget", "7", @@ -1092,6 +1323,27 @@ mod tests { assert_eq!(args.budget.as_deref(), Some("7")); } + #[test] + fn test_create_parses_biconnectivity_augmentation_legacy_flag_alias() { + let cli = Cli::parse_from([ + "pred", + "create", + "BiconnectivityAugmentation", + "--graph", + "0-1,1-2", + "--potential-edges", + "0-2:3,1-3:5", + "--budget", + "7", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + assert_eq!(args.potential_edges.as_deref(), Some("0-2:3,1-3:5")); + } + #[test] fn test_create_help_mentions_biconnectivity_augmentation_flags() { let cmd = Cli::command(); @@ -1102,10 +1354,135 @@ mod tests { .to_string(); assert!(help.contains("BiconnectivityAugmentation")); - assert!(help.contains("--potential-edges")); + assert!(help.contains("--potential-weights")); assert!(help.contains("--budget")); } + #[test] + fn test_create_parses_job_shop_scheduling_jobs_flag() { + let cli = Cli::parse_from([ + "pred", + "create", + "JobShopScheduling", + "--jobs", + "0:3,1:4;1:2,0:3,1:2", + "--num-processors", + "2", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + assert_eq!(args.problem.as_deref(), Some("JobShopScheduling")); + assert_eq!(args.job_tasks.as_deref(), Some("0:3,1:4;1:2,0:3,1:2")); + assert_eq!(args.num_processors, Some(2)); + } + + #[test] + fn test_create_parses_prime_attribute_name_canonical_flags() { + let cli = Cli::parse_from([ + "pred", + "create", + "PrimeAttributeName", + "--universe", + "6", + "--dependencies", + "0,1>2,3,4,5;2,3>0,1,4,5", + "--query-attribute", + "3", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + assert_eq!(args.problem.as_deref(), Some("PrimeAttributeName")); + assert_eq!(args.universe, Some(6)); + assert_eq!( + args.dependencies.as_deref(), + Some("0,1>2,3,4,5;2,3>0,1,4,5") + ); + assert_eq!(args.query, Some(3)); + } + + #[test] + fn test_create_args_flag_map_prefers_canonical_prime_attribute_keys() { + let cli = Cli::parse_from([ + "pred", + "create", + "PrimeAttributeName", + "--universe", + "6", + "--dependencies", + "0,1>2,3,4,5;2,3>0,1,4,5", + "--query-attribute", + "3", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + let flags = args.flag_map(); + assert_eq!(flags.get("universe-size"), Some(&Some("6".to_string()))); + assert_eq!( + flags.get("dependencies"), + Some(&Some("0,1>2,3,4,5;2,3>0,1,4,5".to_string())) + ); + assert_eq!(flags.get("query-attribute"), Some(&Some("3".to_string()))); + } + + #[test] + fn test_create_args_flag_map_converts_numeric_and_alias_backed_values() { + let cli = Cli::parse_from([ + "pred", + "create", + "LengthBoundedDisjointPaths", + "--graph", + "0-1,1-2,2-3", + "--source", + "0", + "--sink", + "3", + "--max-length", + "4", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + let flags = args.flag_map(); + assert_eq!(flags.get("source"), Some(&Some("0".to_string()))); + assert_eq!(flags.get("sink"), Some(&Some("3".to_string()))); + assert_eq!(flags.get("max-length"), Some(&Some("4".to_string()))); + assert_eq!(flags.get("bound"), Some(&Some("4".to_string()))); + } + + #[test] + fn test_create_args_flag_map_promotes_legacy_jobs_alias_to_canonical_key() { + let cli = Cli::parse_from([ + "pred", + "create", + "JobShopScheduling", + "--job-tasks", + "0:3,1:4;1:2,0:3,1:2", + "--num-processors", + "2", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + let flags = args.flag_map(); + assert_eq!( + flags.get("jobs"), + Some(&Some("0:3,1:4;1:2,0:3,1:2".to_string())) + ); + } + #[test] fn test_create_parses_partial_feedback_edge_set_flags() { let cli = Cli::parse_from([ @@ -1156,7 +1533,7 @@ mod tests { assert!(help.contains("StackerCrane")); assert!(help.contains("--arcs")); assert!(help.contains("--graph")); - assert!(help.contains("--arc-costs")); + assert!(help.contains("--arc-lengths")); assert!(help.contains("--edge-lengths")); assert!(help.contains("--bound")); assert!(help.contains("--num-vertices")); diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 5cbb0e948..85b13712c 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -6,65 +6,43 @@ use crate::problem_name::{ }; use crate::util; use anyhow::{bail, Context, Result}; +use num_bigint::BigUint; use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample}; use problemreductions::models::algebraic::{ - AlgebraicEquationsOverGF2, ClosestVectorProblem, ConsecutiveBlockMinimization, - ConsecutiveOnesMatrixAugmentation, ConsecutiveOnesSubmatrix, FeasibleBasisExtension, - MinimumMatrixCover, MinimumMatrixDomination, MinimumWeightDecoding, - MinimumWeightSolutionToLinearEquations, QuadraticCongruences, QuadraticDiophantineEquations, - SimultaneousIncongruences, SparseMatrixCompression, BMF, + ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesMatrixAugmentation, + SparseMatrixCompression, }; use problemreductions::models::formula::Quantifier; use problemreductions::models::graph::{ - DirectedHamiltonianPath, DisjointConnectingPaths, GeneralizedHex, GraphPartitioning, - HamiltonianCircuit, HamiltonianPath, HamiltonianPathBetweenTwoVertices, IntegralFlowBundles, - Kernel, LengthBoundedDisjointPaths, LongestCircuit, LongestPath, MinimumCutIntoBoundedSets, - MinimumDummyActivitiesPert, MinimumGeometricConnectedDominatingSet, MinimumMaximalMatching, - MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching, PathConstrainedNetworkFlow, - RootedTreeArrangement, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, - VertexCover, + GeneralizedHex, HamiltonianCircuit, HamiltonianPath, HamiltonianPathBetweenTwoVertices, + LengthBoundedDisjointPaths, LongestCircuit, MinimumCutIntoBoundedSets, + MinimumDummyActivitiesPert, MinimumMaximalMatching, RootedTreeArrangement, SteinerTree, + SteinerTreeInGraphs, VertexCover, }; use problemreductions::models::misc::{ - AdditionalKey, Betweenness, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, - CbqRelation, Clustering, ConjunctiveBooleanQuery, ConsistencyOfDatabaseFrequencyTables, - CyclicOrdering, DynamicStorageAllocation, EnsembleComputation, ExpectedRetrievalCost, - FeasibleRegisterAssignment, FlowShopScheduling, FrequencyTable, GroupingBySwapping, IntExpr, - IntegerExpressionMembership, JobShopScheduling, KnownValue, KthLargestMTuple, - LongestCommonSubsequence, MaximumLikelihoodRanking, MinimumAxiomSet, - MinimumCodeGenerationOneRegister, MinimumCodeGenerationParallelAssignments, - MinimumCodeGenerationUnlimitedRegisters, MinimumDecisionTree, MinimumDisjunctiveNormalForm, - MinimumExternalMacroDataCompression, MinimumFaultDetectionTestSet, - MinimumInternalMacroDataCompression, MinimumRegisterSufficiencyForLoops, - MinimumTardinessSequencing, MinimumWeightAndOrGraph, MultiprocessorScheduling, - NonLivenessFreePetriNet, Numerical3DimensionalMatching, OpenShopScheduling, PaintShop, - PartiallyOrderedKnapsack, PreemptiveScheduling, ProductionPlanning, QueryArg, - RectilinearPictureCompression, RegisterSufficiency, ResourceConstrainedScheduling, - SchedulingToMinimizeWeightedCompletionTime, SchedulingWithIndividualDeadlines, - SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeTardyTaskWeight, - SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, - SequencingWithDeadlinesAndSetUpTimes, SequencingWithReleaseTimesAndDeadlines, - SequencingWithinIntervals, ShortestCommonSupersequence, SquareTiling, StringToStringCorrection, - SubsetProduct, SubsetSum, SumOfSquaresPartition, ThreePartition, TimetableDesign, + CbqRelation, FrequencyTable, KnownValue, QueryArg, SchedulingWithIndividualDeadlines, + ThreePartition, }; -use problemreductions::models::BiconnectivityAugmentation; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; use problemreductions::topology::{ BipartiteGraph, DirectedGraph, Graph, KingsSubgraph, MixedGraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph, }; -use problemreductions::types::One; use serde::Serialize; use std::collections::{BTreeMap, BTreeSet}; +mod schema_semantics; +use self::schema_semantics::validate_schema_driven_semantics; +mod schema_support; +use self::schema_support::*; + const MULTIPLE_COPY_FILE_ALLOCATION_EXAMPLE_ARGS: &str = "--graph 0-1,1-2,2-3 --usage 5,4,3,2 --storage 1,1,1,1"; const MULTIPLE_COPY_FILE_ALLOCATION_USAGE: &str = "Usage: pred create MultipleCopyFileAllocation --graph 0-1,1-2,2-3 --usage 5,4,3,2 --storage 1,1,1,1"; const EXPECTED_RETRIEVAL_COST_EXAMPLE_ARGS: &str = "--probabilities 0.2,0.15,0.15,0.2,0.1,0.2 --num-sectors 3"; -const EXPECTED_RETRIEVAL_COST_USAGE: &str = - "Usage: pred create ExpectedRetrievalCost --probabilities 0.2,0.15,0.15,0.2,0.1,0.2 --num-sectors 3"; /// Check if all data flags are None (no problem-specific input provided). fn all_data_flags_empty(args: &CreateArgs) -> bool { @@ -111,7 +89,6 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.sink_2.is_none() && args.requirement_1.is_none() && args.requirement_2.is_none() - && args.requirement.is_none() && args.sizes.is_none() && args.probabilities.is_none() && args.capacity.is_none() @@ -158,8 +135,6 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.quantifiers.is_none() && args.usage.is_none() && args.storage.is_none() - && args.source.is_none() - && args.sink.is_none() && args.size_bound.is_none() && args.cut_bound.is_none() && args.values.is_none() @@ -169,8 +144,6 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.potential_edges.is_none() && args.budget.is_none() && args.max_cycle_length.is_none() - && args.deadlines.is_none() - && args.lengths.is_none() && args.precedence_pairs.is_none() && args.resource_bounds.is_none() && args.resource_requirements.is_none() @@ -194,17 +167,6 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.source_string.is_none() && args.target_string.is_none() && args.pointer_cost.is_none() - && args.capacities.is_none() - && args.source_1.is_none() - && args.sink_1.is_none() - && args.source_2.is_none() - && args.sink_2.is_none() - && args.requirement_1.is_none() - && args.requirement_2.is_none() - && args.requirement.is_none() - && args.homologous_pairs.is_none() - && args.num_attributes.is_none() - && args.dependencies.is_none() && args.relation_attrs.is_none() && args.known_keys.is_none() && args.num_objects.is_none() @@ -484,19 +446,19 @@ fn parse_precedence_pairs(raw: Option<&str>) -> Result> { let pair = pair.trim(); let (pred, succ) = pair.split_once('>').ok_or_else(|| { anyhow::anyhow!( - "Invalid --precedence-pairs value '{}': expected 'u>v'", + "Invalid --precedences value '{}': expected 'u>v'", pair ) })?; let pred = pred.trim().parse::().map_err(|_| { anyhow::anyhow!( - "Invalid --precedence-pairs value '{}': expected 'u>v' with nonnegative integer indices", + "Invalid --precedences value '{}': expected 'u>v' with nonnegative integer indices", pair ) })?; let succ = succ.trim().parse::().map_err(|_| { anyhow::anyhow!( - "Invalid --precedence-pairs value '{}': expected 'u>v' with nonnegative integer indices", + "Invalid --precedences value '{}': expected 'u>v' with nonnegative integer indices", pair ) })?; @@ -519,7 +481,7 @@ fn parse_job_shop_jobs(raw: &str) -> Result>> { let job_str = job_str.trim(); anyhow::ensure!( !job_str.is_empty(), - "Invalid --job-tasks value: empty job at position {}", + "Invalid --jobs value: empty job at position {}", job_index ); @@ -529,19 +491,19 @@ fn parse_job_shop_jobs(raw: &str) -> Result>> { let task_str = task_str.trim(); let (processor, length) = task_str.split_once(':').ok_or_else(|| { anyhow::anyhow!( - "Invalid --job-tasks operation '{}': expected 'processor:length'", + "Invalid --jobs operation '{}': expected 'processor:length'", task_str ) })?; let processor = processor.trim().parse::().map_err(|_| { anyhow::anyhow!( - "Invalid --job-tasks operation '{}': processor must be a nonnegative integer", + "Invalid --jobs operation '{}': processor must be a nonnegative integer", task_str ) })?; let length = length.trim().parse::().map_err(|_| { anyhow::anyhow!( - "Invalid --job-tasks operation '{}': length must be a nonnegative integer", + "Invalid --jobs operation '{}': length must be a nonnegative integer", task_str ) })?; @@ -598,720 +560,6 @@ fn create_from_example(args: &CreateArgs, out: &OutputConfig) -> Result<()> { emit_problem_output(&output, out) } -fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { - match type_name { - "SimpleGraph" => "edge list: 0-1,1-2,2-3", - "G" => match graph_type { - Some("KingsSubgraph" | "TriangularSubgraph") => "integer positions: \"0,0;1,0;1,1\"", - Some("UnitDiskGraph") => "float positions: \"0.0,0.0;1.0,0.0\"", - _ => "edge list: 0-1,1-2,2-3", - }, - "Vec<(Vec, Vec)>" => "semicolon-separated dependencies: \"0,1>2;0,2>3\"", - "Vec" => "comma-separated integers: 4,5,3,2,6", - "Vec" => "comma-separated: 1,2,3", - "W" | "N" | "W::Sum" | "N::Sum" => "numeric value: 10", - "Vec" => "comma-separated indices: 0,2,4", - "Vec<(usize, usize, W)>" | "Vec<(usize,usize,W)>" => { - "comma-separated weighted edges: 0-2:3,1-3:5" - } - "Vec>" => "semicolon-separated sets: \"0,1;1,2;0,2\"", - "Vec" => "semicolon-separated clauses: \"1,2;-1,3\"", - "Vec>" => "JSON 2D bool array: '[[true,false],[false,true]]'", - "Vec>" => "semicolon-separated rows: \"1,0.5;0.5,2\"", - "usize" => "integer", - "u64" => "integer", - "i64" => "integer", - "BigUint" => "nonnegative decimal integer", - "Vec" => "comma-separated nonnegative decimal integers: 3,7,1,8", - "Vec" => "comma-separated integers: 3,7,1,8", - "DirectedGraph" => "directed arcs: 0>1,1>2,2>0", - _ => "value", - } -} - -fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { - match canonical { - "MaximumIndependentSet" - | "MinimumVertexCover" - | "MaximumClique" - | "MinimumDominatingSet" => match graph_type { - Some("KingsSubgraph") => "--positions \"0,0;1,0;1,1;0,1\"", - Some("TriangularSubgraph") => "--positions \"0,0;0,1;1,0;1,1\"", - Some("UnitDiskGraph") => "--positions \"0,0;1,0;0.5,0.8\" --radius 1.5", - _ => "--graph 0-1,1-2,2-3 --weights 1,1,1,1", - }, - "KClique" => "--graph 0-1,0-2,1-3,2-3,2-4,3-4 --k 3", - "VertexCover" => "--graph 0-1,1-2,0-2,2-3 --k 2", - "GeneralizedHex" => "--graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5", - "IntegralFlowBundles" => { - "--arcs \"0>1,0>2,1>3,2>3,1>2,2>1\" --bundles \"0,1;2,5;3,4\" --bundle-capacities 1,1,1 --source 0 --sink 3 --requirement 1 --num-vertices 4" - } - "IntegralFlowWithMultipliers" => { - "--arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2" - } - "MinimumCutIntoBoundedSets" => { - "--graph 0-1,1-2,2-3 --edge-weights 1,1,1 --source 0 --sink 3 --size-bound 3" - } - "BoundedComponentSpanningForest" => { - "--graph 0-1,1-2,2-3,3-4,4-5,5-6,6-7,0-7,1-5,2-6 --weights 2,3,1,2,3,1,2,1 --k 3 --bound 6" - } - "HamiltonianPath" => "--graph 0-1,1-2,2-3", - "HamiltonianPathBetweenTwoVertices" => { - "--graph 0-1,0-3,1-2,1-4,2-5,3-4,4-5,2-3 --source-vertex 0 --target-vertex 5" - } - "GraphPartitioning" => "--graph 0-1,1-2,2-3,3-0 --num-partitions 2", - "LongestPath" => { - "--graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,4-6,5-6,1-6 --edge-lengths 3,2,4,1,5,2,3,2,4,1 --source-vertex 0 --target-vertex 6" - } - "UndirectedFlowLowerBounds" => { - "--graph 0-1,0-2,1-3,2-3,1-4,3-5,4-5 --capacities 2,2,2,2,1,3,2 --lower-bounds 1,1,0,0,1,0,1 --source 0 --sink 5 --requirement 3" - } - "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" - }, - "DisjointConnectingPaths" => { - "--graph 0-1,1-3,0-2,1-4,2-4,3-5,4-5 --terminal-pairs 0-3,2-5" - } - "IntegralFlowHomologousArcs" => { - "--arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\"" - } - "LengthBoundedDisjointPaths" => { - "--graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --bound 4" - } - "PathConstrainedNetworkFlow" => { - "--arcs \"0>1,0>2,1>3,1>4,2>4,3>5,4>5,4>6,5>7,6>7\" --capacities 2,1,1,1,1,1,1,1,2,1 --source 0 --sink 7 --paths \"0,2,5,8;0,3,6,8;0,3,7,9;1,4,6,8;1,4,7,9\" --requirement 3" - } - "IsomorphicSpanningTree" => "--graph 0-1,1-2,0-2 --tree 0-1,1-2", - "BoundedDiameterSpanningTree" => { - "--graph 0-1,0-2,0-3,1-2,1-4,2-3,3-4 --edge-weights 1,2,1,1,2,1,1 --weight-bound 5 --diameter-bound 3" - } - "KthBestSpanningTree" => "--graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3", - "LongestCircuit" => { - "--graph 0-1,1-2,2-3,3-4,4-5,5-0,0-3,1-4,2-5,3-5 --edge-weights 3,2,4,1,5,2,3,2,1,2" - } - "BottleneckTravelingSalesman" | "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { - "--graph 0-1,1-2,2-3 --edge-weights 1,1,1" - } - "ShortestWeightConstrainedPath" => { - "--graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,1-4 --edge-lengths 2,4,3,1,5,4,2,6 --edge-weights 5,1,2,3,2,3,1,1 --source-vertex 0 --target-vertex 5 --weight-bound 8" - } - "SteinerTreeInGraphs" => "--graph 0-1,1-2,2-3 --edge-weights 1,1,1 --terminals 0,3", - "BiconnectivityAugmentation" => { - "--graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5" - } - "PartialFeedbackEdgeSet" => { - "--graph 0-1,1-2,2-0,2-3,3-4,4-2,3-5,5-4,0-3 --budget 3 --max-cycle-length 4" - } - "Satisfiability" => "--num-vars 3 --clauses \"1,2;-1,3\"", - "NAESatisfiability" => "--num-vars 3 --clauses \"1,2,-3;-1,2,3\"", - "QuantifiedBooleanFormulas" => { - "--num-vars 3 --clauses \"1,2;-1,3\" --quantifiers \"E,A,E\"" - } - "KSatisfiability" => "--num-vars 3 --clauses \"1,2,3;-1,2,-3\" --k 3", - "Maximum2Satisfiability" => "--num-vars 4 --clauses \"1,2;1,-2;-1,3;-1,-3;2,4;-3,-4;3,4\"", - "NonTautology" => { - "--num-vars 3 --disjuncts \"1,2,3;-1,-2,-3\"" - } - "OneInThreeSatisfiability" => { - "--num-vars 4 --clauses \"1,2,3;-1,3,4;2,-3,-4\"" - } - "Planar3Satisfiability" => { - "--num-vars 4 --clauses \"1,2,3;-1,2,4;1,-3,4;-2,3,-4\"" - } - "QUBO" => "--matrix \"1,0.5;0.5,2\"", - "QuadraticAssignment" => "--matrix \"0,5;5,0\" --distance-matrix \"0,1;1,0\"", - "SpinGlass" => "--graph 0-1,1-2 --couplings 1,1", - "KColoring" => "--graph 0-1,1-2,2-0 --k 3", - "HamiltonianCircuit" => "--graph 0-1,1-2,2-3,3-0", - "MaximumLeafSpanningTree" => "--graph 0-1,0-2,0-3,1-4,2-4,2-5,3-5,4-5,1-3", - "EnsembleComputation" => "--universe 4 --sets \"0,1,2;0,1,3\"", - "RootedTreeStorageAssignment" => "--universe 5 --sets \"0,2;1,3;0,4;2,4\" --bound 1", - "MinMaxMulticenter" => { - "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2" - } - "MinimumSumMulticenter" => { - "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2" - } - "BalancedCompleteBipartiteSubgraph" => { - "--left 4 --right 4 --biedges 0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3 --k 3" - } - "MaximumAchromaticNumber" => "--graph 0-1,1-2,2-3,3-4,4-5,5-0", - "MaximumDomaticNumber" => "--graph 0-1,1-2,0-2", - "MinimumCoveringByCliques" => "--graph 0-1,1-2,0-2,2-3", - "MinimumIntersectionGraphBasis" => "--graph 0-1,1-2", - "MinimumMaximalMatching" => "--graph 0-1,1-2,2-3,3-4,4-5", - "DegreeConstrainedSpanningTree" => "--graph 0-1,0-2,0-3,1-2,1-4,2-3,3-4 --k 2", - "MonochromaticTriangle" => "--graph 0-1,0-2,0-3,1-2,1-3,2-3", - "PartitionIntoTriangles" => "--graph 0-1,1-2,0-2", - "PartitionIntoCliques" => "--graph 0-1,0-2,1-2,3-4,3-5,4-5 --k 3", - "PartitionIntoForests" => "--graph 0-1,1-2,2-0,3-4,4-5,5-3 --k 2", - "PartitionIntoPerfectMatchings" => "--graph 0-1,2-3,0-2,1-3 --k 2", - "Factoring" => "--target 15 --m 4 --n 4", - "CapacityAssignment" => { - "--capacities 1,2,3 --cost-matrix \"1,3,6;2,4,7;1,2,5\" --delay-matrix \"8,4,1;7,3,1;6,3,1\" --delay-budget 12" - } - "ProductionPlanning" => { - "--num-periods 6 --demands 5,3,7,2,8,5 --capacities 12,12,12,12,12,12 --setup-costs 10,10,10,10,10,10 --production-costs 1,1,1,1,1,1 --inventory-costs 1,1,1,1,1,1 --cost-bound 80" - } - "MultiprocessorScheduling" => "--lengths 4,5,3,2,6 --num-processors 2 --deadline 10", - "PreemptiveScheduling" => { - "--sizes 2,1,3,2,1 --num-processors 2 --precedence-pairs \"0>2,1>3\"" - } - "SchedulingToMinimizeWeightedCompletionTime" => { - "--lengths 1,2,3,4,5 --weights 6,4,3,2,1 --num-processors 2" - } - "JobShopScheduling" => { - "--job-tasks \"0:3,1:4;1:2,0:3,1:2;0:4,1:3;1:5,0:2;0:2,1:3,0:1\" --num-processors 2" - } - "MinimumMultiwayCut" => "--graph 0-1,1-2,2-3 --terminals 0,2 --edge-weights 1,1,1", - "ExpectedRetrievalCost" => EXPECTED_RETRIEVAL_COST_EXAMPLE_ARGS, - "SequencingWithinIntervals" => "--release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1", - "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" - } - "TimetableDesign" => { - "--num-periods 3 --num-craftsmen 5 --num-tasks 5 --craftsman-avail \"1,1,1;1,1,0;0,1,1;1,0,1;1,1,1\" --task-avail \"1,1,0;0,1,1;1,0,1;1,1,1;1,1,1\" --requirements \"1,0,1,0,0;0,1,0,0,1;0,0,0,1,0;0,0,0,0,1;0,1,0,0,0\"" - } - "SteinerTree" => "--graph 0-1,1-2,1-3,3-4 --edge-weights 2,2,1,1 --terminals 0,2,4", - "MultipleCopyFileAllocation" => { - MULTIPLE_COPY_FILE_ALLOCATION_EXAMPLE_ARGS - } - "AcyclicPartition" => { - "--arcs \"0>1,0>2,1>3,1>4,2>4,2>5,3>5,4>5\" --weights 2,3,2,1,3,1 --arc-costs 1,1,1,1,1,1,1,1 --weight-bound 5 --cost-bound 5" - } - "OptimalLinearArrangement" => "--graph 0-1,1-2,2-3", - "RootedTreeArrangement" => "--graph 0-1,0-2,1-2,2-3,3-4 --bound 7", - "DirectedTwoCommodityIntegralFlow" => { - "--arcs \"0>2,0>3,1>2,1>3,2>4,2>5,3>4,3>5\" --capacities 1,1,1,1,1,1,1,1 --source-1 0 --sink-1 4 --source-2 1 --sink-2 5 --requirement-1 1 --requirement-2 1" - } - "MinimumEdgeCostFlow" => { - "--arcs \"0>1,0>2,0>3,1>4,2>4,3>4\" --edge-weights 3,1,2,0,0,0 --capacities 2,2,2,2,2,2 --source 0 --sink 4 --requirement 3" - } - "MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"", - "DirectedHamiltonianPath" => { - "--arcs \"0>1,0>3,1>3,1>4,2>0,2>4,3>2,3>5,4>5,5>1\" --num-vertices 6" - } - "Kernel" => "--arcs \"0>1,0>2,1>3,2>3,3>4,4>0,4>1\"", - "MinimumGeometricConnectedDominatingSet" => { - "--positions \"0,0;3,0;6,0;9,0;0,3;3,3;6,3;9,3\" --radius 3.5" - } - "MinimumDummyActivitiesPert" => "--arcs \"0>2,0>3,1>3,1>4,2>5\" --num-vertices 6", - "FeasibleRegisterAssignment" => { - "--arcs \"0>1,0>2,1>3\" --assignment 0,1,0,0 --k 2 --num-vertices 4" - } - "MinimumFaultDetectionTestSet" => { - "--arcs \"0>2,0>3,1>3,1>4,2>5,3>5,3>6,4>6\" --inputs 0,1 --outputs 5,6 --num-vertices 7" - } - "MinimumWeightAndOrGraph" => { - "--arcs \"0>1,0>2,1>3,1>4,2>5,2>6\" --source 0 --gate-types \"AND,OR,OR,L,L,L,L\" --weights 1,2,3,1,4,2 --num-vertices 7" - } - "MinimumRegisterSufficiencyForLoops" => { - "--loop-length 6 --loop-variables \"0,3;2,3;4,3\"" - } - "RegisterSufficiency" => { - "--arcs \"2>0,2>1,3>1,4>2,4>3,5>0,6>4,6>5\" --bound 3 --num-vertices 7" - } - "StrongConnectivityAugmentation" => { - "--arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1" - } - "MixedChinesePostman" => { - "--graph 0-2,1-3,0-4,4-2 --arcs \"0>1,1>2,2>3,3>0\" --edge-weights 2,3,1,2 --arc-costs 2,3,1,4" - } - "RuralPostman" => { - "--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2" - } - "StackerCrane" => { - "--arcs \"0>4,2>5,5>1,3>0,4>3\" --graph \"0-1,1-2,2-3,3-5,4-5,0-3,1-5\" --arc-costs 3,4,2,5,3 --edge-lengths 2,1,3,2,1,4,3 --num-vertices 6" - } - "MultipleChoiceBranching" => { - "--arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10" - } - "AdditionalKey" => "--num-attributes 6 --dependencies \"0,1:2,3;2,3:4,5;4,5:0,1\" --relation-attrs 0,1,2,3,4,5 --known-keys \"0,1;2,3;4,5\"", - "ConsistencyOfDatabaseFrequencyTables" => { - "--num-objects 6 --attribute-domains \"2,3,2\" --frequency-tables \"0,1:1,1,1|1,1,1;1,2:1,1|0,2|1,1\" --known-values \"0,0,0;3,0,1;1,2,1\"" - } - "SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1", - "RectilinearPictureCompression" => { - "--matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --bound 2" - } - "SequencingToMinimizeWeightedTardiness" => { - "--sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" - } - "IntegerKnapsack" => "--sizes 3,4,5,2,7 --values 4,5,7,3,9 --capacity 15", - "SubsetProduct" => "--sizes 2,3,5,7,6,10 --target 210", - "SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11", - "MinimumAxiomSet" => { - "--n 8 --true-sentences 0,1,2,3,4,5,6,7 --implications \"0>2;0>3;1>4;1>5;2,4>6;3,5>7;6,7>0;6,7>1\"" - } - "IntegerExpressionMembership" => { - "--expression '{\"Sum\":[{\"Sum\":[{\"Union\":[{\"Atom\":1},{\"Atom\":4}]},{\"Union\":[{\"Atom\":3},{\"Atom\":6}]}]},{\"Union\":[{\"Atom\":2},{\"Atom\":5}]}]}' --target 12" - } - "NonLivenessFreePetriNet" => { - "--n 4 --m 3 --arcs \"0>0,1>1,2>2\" --output-arcs \"0>1,1>2,2>3\" --initial-marking 1,0,0,0" - } - "Betweenness" => "--n 5 --sets \"0,1,2;2,3,4;0,2,4;1,3,4\"", - "CyclicOrdering" => "--n 5 --sets \"0,1,2;2,3,0;1,3,4\"", - "Numerical3DimensionalMatching" => "--w-sizes 4,5 --x-sizes 4,5 --y-sizes 5,7 --bound 15", - "ThreePartition" => "--sizes 4,5,6,4,6,5 --bound 15", - "DynamicStorageAllocation" => "--release-times 0,0,1,2,3 --deadlines 3,2,4,5,5 --sizes 2,3,1,3,2 --capacity 6", - "KthLargestMTuple" => "--sets \"2,5,8;3,6;1,4,7\" --k 14 --bound 12", - "AlgebraicEquationsOverGF2" => "--num-vars 3 --equations \"0,1:2;1,2:0:;0:1:2:\"", - "QuadraticCongruences" => "--coeff-a 4 --coeff-b 15 --coeff-c 10", - "QuadraticDiophantineEquations" => "--coeff-a 3 --coeff-b 5 --coeff-c 53", - "SimultaneousIncongruences" => "--pairs \"2,2;1,3;2,5;3,7\"", - "BoyceCoddNormalFormViolation" => { - "--n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" - } - "Clustering" => { - "--distance-matrix \"0,1,1,3;1,0,1,3;1,1,0,3;3,3,3,0\" --k 2 --diameter-bound 1" - } - "SumOfSquaresPartition" => "--sizes 5,3,8,2,7,1 --num-groups 3", - "ComparativeContainment" => { - "--universe 4 --r-sets \"0,1,2,3;0,1\" --s-sets \"0,1,2,3;2,3\" --r-weights 2,5 --s-weights 3,6" - } - "SetBasis" => "--universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3", - "SetSplitting" => "--universe 6 --sets \"0,1,2;2,3,4;0,4,5;1,3,5\"", - "LongestCommonSubsequence" => { - "--strings \"010110;100101;001011\" --alphabet-size 2" - } - "GroupingBySwapping" => "--string \"0,1,2,0,1,2\" --bound 5", - "MinimumExternalMacroDataCompression" | "MinimumInternalMacroDataCompression" => { - "--string \"0,1,0,1\" --pointer-cost 2 --alphabet-size 2" - } - "MinimumCardinalityKey" => { - "--num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\"" - } - "PrimeAttributeName" => { - "--universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3" - } - "TwoDimensionalConsecutiveSets" => { - "--alphabet-size 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"" - } - "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\"", - "ConsecutiveBlockMinimization" => { - "--matrix '[[true,false,true],[false,true,true]]' --bound 2" - } - "ConsecutiveOnesMatrixAugmentation" => { - "--matrix \"1,0,0,1,1;1,1,0,0,0;0,1,1,0,1;0,0,1,1,0\" --bound 2" - } - "SparseMatrixCompression" => { - "--matrix \"1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0\" --bound 2" - } - "MaximumLikelihoodRanking" => "--matrix \"0,4,3,5;1,0,4,3;2,1,0,4;0,2,1,0\"", - "MinimumMatrixCover" => "--matrix \"0,3,1,0;3,0,0,2;1,0,0,4;0,2,4,0\"", - "MinimumMatrixDomination" => "--matrix \"0,1,0;1,0,1;0,1,0\"", - "MinimumWeightDecoding" => { - "--matrix '[[true,false,true,true],[false,true,true,false],[true,true,false,true]]' --rhs 'true,true,false'" - } - "MinimumWeightSolutionToLinearEquations" => { - "--matrix '[[1,2,3,1],[2,1,1,3]]' --rhs '5,4'" - } - "ConjunctiveBooleanQuery" => { - "--domain-size 6 --relations \"2:0,3|1,3|2,4;3:0,1,5|1,2,5\" --conjuncts-spec \"0:v0,c3;0:v1,c3;1:v0,v1,c5\"" - } - "ConjunctiveQueryFoldability" => "(use --example ConjunctiveQueryFoldability)", - "EquilibriumPoint" => "(use --example EquilibriumPoint)", - "SequencingToMinimizeMaximumCumulativeCost" => { - "--costs 2,-1,3,-2,1,-3 --precedence-pairs \"0>2,1>2,1>3,2>4,3>5,4>5\"" - } - "StringToStringCorrection" => { - "--source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2" - } - "FeasibleBasisExtension" => { - "--matrix '[[1,0,1,2,-1,0],[0,1,0,1,1,2],[0,0,1,1,0,1]]' --rhs '7,5,3' --required-columns '0,1'" - } - "MinimumCodeGenerationParallelAssignments" => { - "--num-variables 4 --assignments \"0:1,2;1:0;2:3;3:1,2\"" - } - "MinimumDecisionTree" => { - "--test-matrix '[[true,true,false,false],[true,false,false,false],[false,true,false,true]]' --num-objects 4 --num-tests 3" - } - "MinimumDisjunctiveNormalForm" => { - "--num-vars 3 --truth-table 0,1,1,1,1,1,1,0" - } - "SquareTiling" => { - "--num-colors 3 --tiles \"0,1,2,0;0,0,2,1;2,1,0,0;2,0,0,1\" --grid-size 2" - } - _ => "", - } -} - -fn uses_edge_weights_flag(canonical: &str) -> bool { - matches!( - canonical, - "BottleneckTravelingSalesman" - | "BoundedDiameterSpanningTree" - | "KthBestSpanningTree" - | "LongestCircuit" - | "MaxCut" - | "MaximumMatching" - | "MixedChinesePostman" - | "RuralPostman" - | "TravelingSalesman" - ) -} - -fn help_flag_name(canonical: &str, field_name: &str) -> String { - // Problem-specific overrides first - match (canonical, field_name) { - ("BoundedComponentSpanningForest", "max_components") => return "k".to_string(), - ("BoundedComponentSpanningForest", "max_weight") => return "bound".to_string(), - ("FlowShopScheduling", "num_processors") - | ("JobShopScheduling", "num_processors") - | ("OpenShopScheduling", "num_machines") - | ("SchedulingWithIndividualDeadlines", "num_processors") => { - return "num-processors/--m".to_string(); - } - ("JobShopScheduling", "jobs") => return "job-tasks".to_string(), - ("LengthBoundedDisjointPaths", "max_length") => return "bound".to_string(), - ("RectilinearPictureCompression", "bound") => return "bound".to_string(), - ("PrimeAttributeName", "num_attributes") => return "universe".to_string(), - ("PrimeAttributeName", "dependencies") => return "deps".to_string(), - ("PrimeAttributeName", "query_attribute") => return "query".to_string(), - ("MixedChinesePostman", "arc_weights") => return "arc-costs".to_string(), - ("ConsecutiveOnesMatrixAugmentation", "bound") => return "bound".to_string(), - ("ConsecutiveOnesSubmatrix", "bound") => return "bound".to_string(), - ("SparseMatrixCompression", "bound_k") => return "bound".to_string(), - ("MinimumCodeGenerationParallelAssignments", "num_variables") => { - return "num-variables".to_string(); - } - ("MinimumCodeGenerationParallelAssignments", "assignments") => { - return "assignments".to_string(); - } - ("StackerCrane", "edges") => return "graph".to_string(), - ("StackerCrane", "arc_lengths") => return "arc-costs".to_string(), - ("StackerCrane", "edge_lengths") => return "edge-lengths".to_string(), - ("StaffScheduling", "shifts_per_schedule") => return "k".to_string(), - ("TimetableDesign", "num_tasks") => return "num-tasks".to_string(), - _ => {} - } - // Edge-weight problems use --edge-weights instead of --weights - if field_name == "weights" && uses_edge_weights_flag(canonical) { - return "edge-weights".to_string(); - } - // General field-name overrides (previously in cli_flag_name) - match field_name { - "universe_size" => "universe".to_string(), - "collection" | "subsets" => "sets".to_string(), - "left_size" => "left".to_string(), - "right_size" => "right".to_string(), - "edges" => "biedges".to_string(), - "vertex_weights" => "weights".to_string(), - "potential_weights" => "potential-edges".to_string(), - "edge_lengths" => "edge-weights".to_string(), - "num_tasks" => "n".to_string(), - "precedences" => "precedence-pairs".to_string(), - "threshold" => "bound".to_string(), - "lengths" => "sizes".to_string(), - _ => field_name.replace('_', "-"), - } -} - -fn reject_vertex_weights_for_edge_weight_problem( - args: &CreateArgs, - canonical: &str, - graph_type: Option<&str>, -) -> Result<()> { - if args.weights.is_some() && uses_edge_weights_flag(canonical) { - bail!( - "{canonical} uses --edge-weights, not --weights.\n\n\ - Usage: pred create {} {}", - match graph_type { - Some(g) => format!("{canonical}/{g}"), - None => canonical.to_string(), - }, - example_for(canonical, graph_type) - ); - } - Ok(()) -} - -fn help_flag_hint( - canonical: &str, - field_name: &str, - type_name: &str, - graph_type: Option<&str>, -) -> &'static str { - match (canonical, field_name) { - ("BoundedComponentSpanningForest", "max_weight") => "integer", - ("SequencingWithinIntervals", "release_times") => "comma-separated integers: 0,0,5", - ("DynamicStorageAllocation", "release_times") => "comma-separated arrival times: 0,0,1,2,3", - ("DynamicStorageAllocation", "deadlines") => "comma-separated departure times: 3,2,4,5,5", - ("DynamicStorageAllocation", "sizes") => "comma-separated item sizes: 2,3,1,3,2", - ("DynamicStorageAllocation", "capacity") => "memory size D: 6", - ("DisjointConnectingPaths", "terminal_pairs") => "comma-separated pairs: 0-3,2-5", - ("PrimeAttributeName", "dependencies") => { - "semicolon-separated dependencies: \"0,1>2,3;2,3>0,1\"" - } - ("LongestCommonSubsequence", "strings") => { - "raw strings: \"ABAC;BACA\" or symbol lists: \"0,1,0;1,0,1\"" - } - ("GroupingBySwapping", "string") => "symbol list: \"0,1,2,0,1,2\"", - ("MinimumExternalMacroDataCompression", "string") - | ("MinimumInternalMacroDataCompression", "string") => "symbol list: \"0,1,0,1\"", - ("MinimumExternalMacroDataCompression", "pointer_cost") - | ("MinimumInternalMacroDataCompression", "pointer_cost") => "positive integer: 2", - ("MinimumAxiomSet", "num_sentences") => "total number of sentences: 8", - ("MinimumAxiomSet", "true_sentences") => "comma-separated indices: 0,1,2,3,4,5,6,7", - ("MinimumAxiomSet", "implications") => "semicolon-separated rules: \"0>2;0>3;1>4;2,4>6\"", - ("ShortestCommonSupersequence", "strings") => "symbol lists: \"0,1,2;1,2,0\"", - ("MultipleChoiceBranching", "partition") => "semicolon-separated groups: \"0,1;2,3\"", - ("IntegralFlowHomologousArcs", "homologous_pairs") => { - "semicolon-separated arc-index equalities: \"2=5;4=3\"" - } - ("ConsistencyOfDatabaseFrequencyTables", "attribute_domains") => { - "comma-separated domain sizes: 2,3,2" - } - ("ConsistencyOfDatabaseFrequencyTables", "frequency_tables") => { - "semicolon-separated tables: \"0,1:1,1,1|1,1,1;1,2:1,1|0,2|1,1\"" - } - ("ConsistencyOfDatabaseFrequencyTables", "known_values") => { - "semicolon-separated triples: \"0,0,0;3,0,1;1,2,1\"" - } - ("IntegralFlowBundles", "bundles") => "semicolon-separated groups: \"0,1;2,5;3,4\"", - ("IntegralFlowBundles", "bundle_capacities") => "comma-separated capacities: 1,1,1", - ("PathConstrainedNetworkFlow", "paths") => { - "semicolon-separated arc-index paths: \"0,2,5,8;1,4,7,9\"" - } - ("ConsecutiveOnesMatrixAugmentation", "matrix") => { - "semicolon-separated 0/1 rows: \"1,0;0,1\"" - } - ("ConsecutiveOnesSubmatrix", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"", - ("SparseMatrixCompression", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"", - ("MaximumLikelihoodRanking", "matrix") => { - "semicolon-separated i32 rows: \"0,4,3,5;1,0,4,3;2,1,0,4;0,2,1,0\"" - } - ("MinimumMatrixCover", "matrix") => "semicolon-separated i64 rows: \"0,3,1;3,0,2;1,2,0\"", - ("MinimumMatrixDomination", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"", - ("MinimumWeightDecoding", "matrix") => "JSON 2D bool array: '[[true,false],[false,true]]'", - ("MinimumWeightDecoding", "target") => "comma-separated booleans: \"true,true,false\"", - ("MinimumWeightSolutionToLinearEquations", "matrix") => { - "JSON 2D integer array: '[[1,2,3],[4,5,6]]'" - } - ("MinimumWeightSolutionToLinearEquations", "rhs") => "comma-separated integers: \"5,4\"", - ("FeasibleBasisExtension", "matrix") => "JSON 2D integer array: '[[1,0,1],[0,1,0]]'", - ("FeasibleBasisExtension", "rhs") => "comma-separated integers: \"7,5,3\"", - ("FeasibleBasisExtension", "required_columns") => "comma-separated column indices: \"0,1\"", - ("MinimumCodeGenerationParallelAssignments", "assignments") => { - "semicolon-separated target:reads entries: \"0:1,2;1:0;2:3;3:1,2\"" - } - ("NonTautology", "disjuncts") => "semicolon-separated disjuncts: \"1,2,3;-1,-2,-3\"", - ("TimetableDesign", "craftsman_avail") | ("TimetableDesign", "task_avail") => { - "semicolon-separated 0/1 rows: \"1,1,0;0,1,1\"" - } - ("TimetableDesign", "requirements") => "semicolon-separated rows: \"1,0,1;0,1,0\"", - _ => type_format_hint(type_name, graph_type), - } -} - -fn parse_nonnegative_usize_bound(bound: i64, problem_name: &str, usage: &str) -> Result { - usize::try_from(bound) - .map_err(|_| anyhow::anyhow!("{problem_name} requires nonnegative --bound\n\n{usage}")) -} - -fn resolve_processor_count_flags( - problem_name: &str, - usage: &str, - num_processors: Option, - m_alias: Option, -) -> Result> { - match (num_processors, m_alias) { - (Some(num_processors), Some(m_alias)) => { - anyhow::ensure!( - num_processors == m_alias, - "{problem_name} received conflicting processor counts: --num-processors={num_processors} but --m={m_alias}\n\n{usage}" - ); - Ok(Some(num_processors)) - } - (Some(num_processors), None) => Ok(Some(num_processors)), - (None, Some(m_alias)) => Ok(Some(m_alias)), - (None, None) => Ok(None), - } -} - -fn validate_sequencing_within_intervals_inputs( - release_times: &[u64], - deadlines: &[u64], - lengths: &[u64], - usage: &str, -) -> Result<()> { - if release_times.len() != deadlines.len() { - bail!("release_times and deadlines must have the same length\n\n{usage}"); - } - if release_times.len() != lengths.len() { - bail!("release_times and lengths must have the same length\n\n{usage}"); - } - - for (i, ((&release_time, &deadline), &length)) in release_times - .iter() - .zip(deadlines.iter()) - .zip(lengths.iter()) - .enumerate() - { - let end = release_time.checked_add(length).ok_or_else(|| { - anyhow::anyhow!("Task {i}: overflow computing r(i) + l(i)\n\n{usage}") - })?; - if end > deadline { - bail!( - "Task {i}: r({}) + l({}) > d({}), time window is empty\n\n{usage}", - release_time, - length, - deadline - ); - } - } - - Ok(()) -} - -fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> { - let is_geometry = matches!( - graph_type, - Some("KingsSubgraph" | "TriangularSubgraph" | "UnitDiskGraph") - ); - let schemas = collect_schemas(); - let schema = schemas.iter().find(|s| s.name == canonical); - - if let Some(s) = schema { - eprintln!("{}\n {}\n", canonical, s.description); - eprintln!("Parameters:"); - for field in &s.fields { - let flag_name = - problem_help_flag_name(canonical, &field.name, &field.type_name, is_geometry); - // For geometry variants, show --positions instead of --graph - if field.type_name == "G" && is_geometry { - let hint = type_format_hint(&field.type_name, graph_type); - eprintln!(" --{:<16} {} ({hint})", flag_name, field.description); - if graph_type == Some("UnitDiskGraph") { - eprintln!(" --{:<16} Distance threshold [default: 1.0]", "radius"); - } - } else if field.type_name == "DirectedGraph" { - // DirectedGraph fields use --arcs, not --graph - let hint = type_format_hint(&field.type_name, graph_type); - eprintln!(" --{:<16} {} ({})", "arcs", field.description, hint); - } else if field.type_name == "MixedGraph" { - eprintln!( - " --{:<16} Undirected edges E of the mixed graph (edge list: 0-1,1-2,2-3)", - "graph" - ); - eprintln!( - " --{:<16} Directed arcs A of the mixed graph (directed arcs: 0>1,1>2,2>0)", - "arcs" - ); - } else if field.type_name == "BipartiteGraph" { - eprintln!( - " --{:<16} Vertices in the left partition (integer)", - "left" - ); - eprintln!( - " --{:<16} Vertices in the right partition (integer)", - "right" - ); - eprintln!( - " --{:<16} Bipartite edges as left-right pairs (edge list: 0-0,0-1,1-2)", - "biedges" - ); - } else { - let hint = help_flag_hint(canonical, &field.name, &field.type_name, graph_type); - eprintln!(" --{:<16} {} ({})", flag_name, field.description, hint); - } - } - if canonical == "GraphPartitioning" { - eprintln!( - " --{:<16} Number of partitions in the balanced partitioning model (must be 2) (integer)", - "num-partitions" - ); - } - } else { - bail!("{}", crate::problem_name::unknown_problem_error(canonical)); - } - - let example = example_for(canonical, graph_type); - if !example.is_empty() { - eprintln!("\nExample:"); - eprintln!( - " pred create {} {}", - match graph_type { - Some(g) => format!("{canonical}/{g}"), - None => canonical.to_string(), - }, - example - ); - } - Ok(()) -} - -fn problem_help_flag_name( - canonical: &str, - field_name: &str, - field_type: &str, - is_geometry: bool, -) -> String { - if field_type == "G" && is_geometry { - return "positions".to_string(); - } - if field_type == "DirectedGraph" { - return "arcs".to_string(); - } - if field_type == "MixedGraph" { - return "graph".to_string(); - } - if canonical == "LengthBoundedDisjointPaths" && field_name == "max_length" { - return "bound".to_string(); - } - if canonical == "GeneralizedHex" && field_name == "target" { - return "sink".to_string(); - } - if canonical == "StringToStringCorrection" { - return match field_name { - "source" => "source-string".to_string(), - "target" => "target-string".to_string(), - "bound" => "bound".to_string(), - _ => help_flag_name(canonical, field_name), - }; - } - help_flag_name(canonical, field_name) -} - -fn lbdp_validation_error(message: &str, usage: Option<&str>) -> anyhow::Error { - match usage { - Some(usage) => anyhow::anyhow!("{message}\n\n{usage}"), - None => anyhow::anyhow!("{message}"), - } -} - -fn validate_length_bounded_disjoint_paths_args( - num_vertices: usize, - source: usize, - sink: usize, - bound: i64, - usage: Option<&str>, -) -> Result { - let max_length = usize::try_from(bound).map_err(|_| { - lbdp_validation_error( - "--bound must be a nonnegative integer for LengthBoundedDisjointPaths", - usage, - ) - })?; - if source >= num_vertices || sink >= num_vertices { - return Err(lbdp_validation_error( - "--source and --sink must be valid graph vertices", - usage, - )); - } - if source == sink { - return Err(lbdp_validation_error( - "--source and --sink must be distinct", - usage, - )); - } - if max_length == 0 { - return Err(lbdp_validation_error("--bound must be positive", usage)); - } - Ok(max_length) -} - -/// Resolve the graph type from the variant map (e.g., "KingsSubgraph", "UnitDiskGraph", or "SimpleGraph"). fn resolved_graph_type(variant: &BTreeMap) -> &str { variant .get("graph") @@ -1352,7 +600,6 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { }; let canonical = resolved.name.as_str(); let resolved_variant = resolved.variant.clone(); - let graph_type = resolved_graph_type(&resolved_variant); if args.random { return create_random(args, canonical, &resolved_variant, out); @@ -1371,5413 +618,22 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // Show schema-driven help when no data flags are provided if all_data_flags_empty(args) { - let gt = if graph_type != "SimpleGraph" { - Some(graph_type) - } else { - None - }; - print_problem_help(canonical, gt)?; + print_problem_help(canonical, &resolved_variant)?; std::process::exit(2); } - let (data, variant) = match canonical { - // Graph problems with vertex weights - "MaximumIndependentSet" - | "MinimumVertexCover" - | "MaximumClique" - | "MinimumDominatingSet" => { - create_vertex_weight_problem(args, canonical, graph_type, &resolved_variant)? - } - - // SteinerTree (graph + edge weights + terminals) - "SteinerTree" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create SteinerTree --graph 0-1,1-2,1-3,3-4 --edge-weights 2,2,1,1 --terminals 0,2,4" - ) - })?; - let edge_weights = parse_edge_weights(args, graph.num_edges())?; - let terminals = parse_terminals(args, graph.num_vertices())?; - let data = ser(SteinerTree::new(graph, edge_weights, terminals))?; - (data, resolved_variant.clone()) - } - - // Generalized Hex (graph + source + sink) - "GeneralizedHex" => { - let usage = - "Usage: pred create GeneralizedHex --graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5"; - let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let num_vertices = graph.num_vertices(); - let source = args - .source - .ok_or_else(|| anyhow::anyhow!("GeneralizedHex requires --source\n\n{usage}"))?; - let sink = args - .sink - .ok_or_else(|| anyhow::anyhow!("GeneralizedHex requires --sink\n\n{usage}"))?; - validate_vertex_index("source", source, num_vertices, usage)?; - validate_vertex_index("sink", sink, num_vertices, usage)?; - if source == sink { - bail!("GeneralizedHex requires distinct --source and --sink\n\n{usage}"); - } - ( - ser(GeneralizedHex::new(graph, source, sink))?, - resolved_variant.clone(), + let (data, variant) = create_schema_driven(args, canonical, &resolved_variant)? + .ok_or_else(|| { + anyhow::anyhow!( + "Schema-driven creation unexpectedly returned no instance for {canonical}. This indicates a missing parser, flag mapping, derived field, or schema/factory mismatch in create.rs." ) - } + })?; - // DisjointConnectingPaths (graph + terminal pairs) - "DisjointConnectingPaths" => { - let usage = - "Usage: pred create DisjointConnectingPaths --graph 0-1,1-3,0-2,1-4,2-4,3-5,4-5 --terminal-pairs 0-3,2-5"; - let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let terminal_pairs = parse_terminal_pairs(args, graph.num_vertices()) - .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - ( - ser(DisjointConnectingPaths::new(graph, terminal_pairs))?, - resolved_variant.clone(), - ) - } - - // IntegralFlowWithMultipliers (directed arcs + capacities + source/sink + multipliers + requirement) - "IntegralFlowWithMultipliers" => { - let usage = "Usage: pred create IntegralFlowWithMultipliers --arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2"; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!("IntegralFlowWithMultipliers requires --arcs\n\n{usage}") - })?; - let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices) - .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let capacities_str = args.capacities.as_deref().ok_or_else(|| { - anyhow::anyhow!("IntegralFlowWithMultipliers requires --capacities\n\n{usage}") - })?; - let capacities: Vec = util::parse_comma_list(capacities_str) - .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - if capacities.len() != num_arcs { - bail!( - "Expected {} capacities but got {}\n\n{}", - num_arcs, - capacities.len(), - usage - ); - } - for (arc_index, &capacity) in capacities.iter().enumerate() { - let fits = usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .is_some(); - if !fits { - bail!( - "capacity {} at arc index {} is too large for this platform\n\n{}", - capacity, - arc_index, - usage - ); - } - } - - let num_vertices = graph.num_vertices(); - let source = args.source.ok_or_else(|| { - anyhow::anyhow!("IntegralFlowWithMultipliers requires --source\n\n{usage}") - })?; - let sink = args.sink.ok_or_else(|| { - anyhow::anyhow!("IntegralFlowWithMultipliers requires --sink\n\n{usage}") - })?; - validate_vertex_index("source", source, num_vertices, usage)?; - validate_vertex_index("sink", sink, num_vertices, usage)?; - if source == sink { - bail!( - "IntegralFlowWithMultipliers requires distinct --source and --sink\n\n{}", - usage - ); - } - - let multipliers_str = args.multipliers.as_deref().ok_or_else(|| { - anyhow::anyhow!("IntegralFlowWithMultipliers requires --multipliers\n\n{usage}") - })?; - let multipliers: Vec = util::parse_comma_list(multipliers_str) - .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - if multipliers.len() != num_vertices { - bail!( - "Expected {} multipliers but got {}\n\n{}", - num_vertices, - multipliers.len(), - usage - ); - } - if multipliers - .iter() - .enumerate() - .any(|(vertex, &multiplier)| vertex != source && vertex != sink && multiplier == 0) - { - bail!("non-terminal multipliers must be positive\n\n{usage}"); - } - - let requirement = args.requirement.ok_or_else(|| { - anyhow::anyhow!("IntegralFlowWithMultipliers requires --requirement\n\n{usage}") - })?; - ( - ser(IntegralFlowWithMultipliers::new( - graph, - source, - sink, - multipliers, - capacities, - requirement, - ))?, - resolved_variant.clone(), - ) - } - - // Minimum cut into bounded sets (graph + edge weights + s/t/B/K) - "MinimumCutIntoBoundedSets" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create MinimumCutIntoBoundedSets --graph 0-1,1-2,2-3 --edge-weights 1,1,1 --source 0 --sink 2 --size-bound 2" - ) - })?; - let edge_weights = parse_edge_weights(args, graph.num_edges())?; - let source = args - .source - .context("--source is required for MinimumCutIntoBoundedSets")?; - let sink = args - .sink - .context("--sink is required for MinimumCutIntoBoundedSets")?; - let size_bound = args - .size_bound - .context("--size-bound is required for MinimumCutIntoBoundedSets")?; - ( - ser(MinimumCutIntoBoundedSets::new( - graph, - edge_weights, - source, - sink, - size_bound, - ))?, - resolved_variant.clone(), - ) - } - - // MaximumAchromaticNumber (graph only, no weights) - "MaximumAchromaticNumber" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create MaximumAchromaticNumber --graph 0-1,1-2,2-3,3-4,4-5,5-0" - ) - })?; - ( - ser(problemreductions::models::graph::MaximumAchromaticNumber::new(graph))?, - variant_map(&[("graph", "SimpleGraph")]), - ) - } - - // MaximumDomaticNumber (graph only, no weights) - "MaximumDomaticNumber" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create MaximumDomaticNumber --graph 0-1,1-2,0-2" - ) - })?; - ( - ser(problemreductions::models::graph::MaximumDomaticNumber::new( - graph, - ))?, - variant_map(&[("graph", "SimpleGraph")]), - ) - } - - // MinimumCoveringByCliques (graph only, no weights) - "MinimumCoveringByCliques" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create MinimumCoveringByCliques --graph 0-1,1-2,0-2,2-3" - ) - })?; - ( - ser(problemreductions::models::graph::MinimumCoveringByCliques::new(graph))?, - variant_map(&[("graph", "SimpleGraph")]), - ) - } - - // MinimumIntersectionGraphBasis (graph only, no weights) - "MinimumIntersectionGraphBasis" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create MinimumIntersectionGraphBasis --graph 0-1,1-2" - ) - })?; - ( - ser(problemreductions::models::graph::MinimumIntersectionGraphBasis::new(graph))?, - variant_map(&[("graph", "SimpleGraph")]), - ) - } - - // MinimumMaximalMatching (graph only, no weights) - "MinimumMaximalMatching" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create MinimumMaximalMatching --graph 0-1,1-2,2-3,3-4,4-5" - ) - })?; - ( - ser(MinimumMaximalMatching::new(graph))?, - variant_map(&[("graph", "SimpleGraph")]), - ) - } - - // Hamiltonian Circuit (graph only, no weights) - "HamiltonianCircuit" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create HamiltonianCircuit --graph 0-1,1-2,2-3,3-0" - ) - })?; - ( - ser(HamiltonianCircuit::new(graph))?, - resolved_variant.clone(), - ) - } - - // Maximum Leaf Spanning Tree (graph only, no weights) - "MaximumLeafSpanningTree" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create MaximumLeafSpanningTree --graph 0-1,0-2,0-3,1-4,2-4,2-5,3-5,4-5,1-3" - ) - })?; - ( - ser(problemreductions::models::graph::MaximumLeafSpanningTree::new(graph))?, - resolved_variant.clone(), - ) - } - - // Biconnectivity augmentation - "BiconnectivityAugmentation" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5" - ) - })?; - let potential_edges = parse_potential_edges(args)?; - validate_potential_edges(&graph, &potential_edges)?; - let budget = parse_budget(args)?; - ( - ser(BiconnectivityAugmentation::new( - graph, - potential_edges, - budget, - ))?, - resolved_variant.clone(), - ) - } - - // Partial Feedback Edge Set - "PartialFeedbackEdgeSet" => { - let usage = "Usage: pred create PartialFeedbackEdgeSet --graph 0-1,1-2,2-0,2-3,3-4,4-2,3-5,5-4,0-3 --budget 3 --max-cycle-length 4"; - let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let budget = args - .budget - .as_deref() - .ok_or_else(|| { - anyhow::anyhow!("PartialFeedbackEdgeSet requires --budget\n\n{usage}") - })? - .parse::() - .map_err(|e| { - anyhow::anyhow!( - "Invalid --budget value for PartialFeedbackEdgeSet: {e}\n\n{usage}" - ) - })?; - let max_cycle_length = args.max_cycle_length.ok_or_else(|| { - anyhow::anyhow!("PartialFeedbackEdgeSet requires --max-cycle-length\n\n{usage}") - })?; - ( - ser(PartialFeedbackEdgeSet::new(graph, budget, max_cycle_length))?, - resolved_variant.clone(), - ) - } - - // Bounded Component Spanning Forest - "BoundedComponentSpanningForest" => { - let usage = "Usage: pred create BoundedComponentSpanningForest --graph 0-1,1-2,2-3,3-4,4-5,5-6,6-7,0-7,1-5,2-6 --weights 2,3,1,2,3,1,2,1 --k 3 --bound 6"; - let (graph, n) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - args.weights.as_deref().ok_or_else(|| { - anyhow::anyhow!("BoundedComponentSpanningForest requires --weights\n\n{usage}") - })?; - let weights = parse_vertex_weights(args, n)?; - if weights.iter().any(|&weight| weight < 0) { - bail!("BoundedComponentSpanningForest requires nonnegative --weights\n\n{usage}"); - } - let max_components = args.k.ok_or_else(|| { - anyhow::anyhow!("BoundedComponentSpanningForest requires --k\n\n{usage}") - })?; - if max_components == 0 { - bail!("BoundedComponentSpanningForest requires --k >= 1\n\n{usage}"); - } - let bound_raw = args.bound.ok_or_else(|| { - anyhow::anyhow!("BoundedComponentSpanningForest requires --bound\n\n{usage}") - })?; - if bound_raw <= 0 { - bail!("BoundedComponentSpanningForest requires positive --bound\n\n{usage}"); - } - let max_weight = i32::try_from(bound_raw).map_err(|_| { - anyhow::anyhow!( - "BoundedComponentSpanningForest requires --bound within i32 range\n\n{usage}" - ) - })?; - ( - ser(BoundedComponentSpanningForest::new( - graph, - weights, - max_components, - max_weight, - ))?, - resolved_variant.clone(), - ) - } - - // Hamiltonian path (graph only, no weights) - "HamiltonianPath" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!("{e}\n\nUsage: pred create HamiltonianPath --graph 0-1,1-2,2-3") - })?; - (ser(HamiltonianPath::new(graph))?, resolved_variant.clone()) - } - - // Hamiltonian path between two specified vertices - "HamiltonianPathBetweenTwoVertices" => { - let usage = "pred create HamiltonianPathBetweenTwoVertices --graph 0-1,0-3,1-2,1-4,2-5,3-4,4-5,2-3 --source-vertex 0 --target-vertex 5"; - let (graph, _) = - parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\nUsage: {usage}"))?; - let source_vertex = args.source_vertex.ok_or_else(|| { - anyhow::anyhow!( - "HamiltonianPathBetweenTwoVertices requires --source-vertex\n\nUsage: {usage}" - ) - })?; - let target_vertex = args.target_vertex.ok_or_else(|| { - anyhow::anyhow!( - "HamiltonianPathBetweenTwoVertices requires --target-vertex\n\nUsage: {usage}" - ) - })?; - ensure_vertex_in_bounds(source_vertex, graph.num_vertices(), "source_vertex")?; - ensure_vertex_in_bounds(target_vertex, graph.num_vertices(), "target_vertex")?; - anyhow::ensure!( - source_vertex != target_vertex, - "source_vertex and target_vertex must be distinct" - ); - ( - ser(HamiltonianPathBetweenTwoVertices::new( - graph, - source_vertex, - target_vertex, - ))?, - resolved_variant.clone(), - ) - } - - "GraphPartitioning" => { - let usage = "pred create GraphPartitioning --graph 0-1,1-2,2-3,3-0 --num-partitions 2"; - let (graph, _) = - parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\nUsage: {usage}"))?; - let num_partitions = args.num_partitions.ok_or_else(|| { - anyhow::anyhow!("GraphPartitioning requires --num-partitions\n\nUsage: {usage}") - })?; - anyhow::ensure!( - num_partitions == 2, - "GraphPartitioning currently models balanced bipartition only, so --num-partitions must be 2 (got {num_partitions})" - ); - ( - ser(GraphPartitioning::new(graph))?, - resolved_variant.clone(), - ) - } - - // LongestPath - "LongestPath" => { - let usage = "pred create LongestPath --graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,4-6,5-6,1-6 --edge-lengths 3,2,4,1,5,2,3,2,4,1 --source-vertex 0 --target-vertex 6"; - let (graph, _) = - parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\nUsage: {usage}"))?; - if args.weights.is_some() { - bail!("LongestPath uses --edge-lengths, not --weights\n\nUsage: {usage}"); - } - let edge_lengths_raw = args.edge_lengths.as_ref().ok_or_else(|| { - anyhow::anyhow!("LongestPath requires --edge-lengths\n\nUsage: {usage}") - })?; - let edge_lengths = - parse_i32_edge_values(Some(edge_lengths_raw), graph.num_edges(), "edge length")?; - ensure_positive_i32_values(&edge_lengths, "edge lengths")?; - let source_vertex = args.source_vertex.ok_or_else(|| { - anyhow::anyhow!("LongestPath requires --source-vertex\n\nUsage: {usage}") - })?; - let target_vertex = args.target_vertex.ok_or_else(|| { - anyhow::anyhow!("LongestPath requires --target-vertex\n\nUsage: {usage}") - })?; - ensure_vertex_in_bounds(source_vertex, graph.num_vertices(), "source_vertex")?; - ensure_vertex_in_bounds(target_vertex, graph.num_vertices(), "target_vertex")?; - ( - ser(LongestPath::new( - graph, - edge_lengths, - source_vertex, - target_vertex, - ))?, - resolved_variant.clone(), - ) - } - - // ShortestWeightConstrainedPath - "ShortestWeightConstrainedPath" => { - let usage = "pred create ShortestWeightConstrainedPath --graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,1-4 --edge-lengths 2,4,3,1,5,4,2,6 --edge-weights 5,1,2,3,2,3,1,1 --source-vertex 0 --target-vertex 5 --weight-bound 8"; - let (graph, _) = - parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\nUsage: {usage}"))?; - if args.weights.is_some() { - bail!( - "ShortestWeightConstrainedPath uses --edge-weights, not --weights\n\nUsage: {usage}" - ); - } - let edge_lengths_raw = args.edge_lengths.as_ref().ok_or_else(|| { - anyhow::anyhow!( - "ShortestWeightConstrainedPath requires --edge-lengths\n\nUsage: {usage}" - ) - })?; - let edge_weights_raw = args.edge_weights.as_ref().ok_or_else(|| { - anyhow::anyhow!( - "ShortestWeightConstrainedPath requires --edge-weights\n\nUsage: {usage}" - ) - })?; - let edge_lengths = - parse_i32_edge_values(Some(edge_lengths_raw), graph.num_edges(), "edge length")?; - let edge_weights = - parse_i32_edge_values(Some(edge_weights_raw), graph.num_edges(), "edge weight")?; - ensure_positive_i32_values(&edge_lengths, "edge lengths")?; - ensure_positive_i32_values(&edge_weights, "edge weights")?; - let source_vertex = args.source_vertex.ok_or_else(|| { - anyhow::anyhow!( - "ShortestWeightConstrainedPath requires --source-vertex\n\nUsage: {usage}" - ) - })?; - let target_vertex = args.target_vertex.ok_or_else(|| { - anyhow::anyhow!( - "ShortestWeightConstrainedPath requires --target-vertex\n\nUsage: {usage}" - ) - })?; - let weight_bound = args.weight_bound.ok_or_else(|| { - anyhow::anyhow!( - "ShortestWeightConstrainedPath requires --weight-bound\n\nUsage: {usage}" - ) - })?; - ensure_vertex_in_bounds(source_vertex, graph.num_vertices(), "source_vertex")?; - ensure_vertex_in_bounds(target_vertex, graph.num_vertices(), "target_vertex")?; - ensure_positive_i32(weight_bound, "weight_bound")?; - ( - ser(ShortestWeightConstrainedPath::new( - graph, - edge_lengths, - edge_weights, - source_vertex, - target_vertex, - weight_bound, - ))?, - resolved_variant.clone(), - ) - } - - // MultipleCopyFileAllocation (graph + usage + storage) - "MultipleCopyFileAllocation" => { - let (graph, num_vertices) = parse_graph(args) - .map_err(|e| anyhow::anyhow!("{e}\n\n{MULTIPLE_COPY_FILE_ALLOCATION_USAGE}"))?; - let usage = parse_vertex_i64_values( - args.usage.as_deref(), - "usage", - num_vertices, - "MultipleCopyFileAllocation", - MULTIPLE_COPY_FILE_ALLOCATION_USAGE, - )?; - let storage = parse_vertex_i64_values( - args.storage.as_deref(), - "storage", - num_vertices, - "MultipleCopyFileAllocation", - MULTIPLE_COPY_FILE_ALLOCATION_USAGE, - )?; - ( - ser(MultipleCopyFileAllocation::new(graph, usage, storage))?, - resolved_variant.clone(), - ) - } - - // ExpectedRetrievalCost (probabilities + sectors) - "ExpectedRetrievalCost" => { - let probabilities_str = args.probabilities.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "ExpectedRetrievalCost requires --probabilities\n\n{EXPECTED_RETRIEVAL_COST_USAGE}" - ) - })?; - let probabilities: Vec = util::parse_comma_list(probabilities_str) - .map_err(|e| anyhow::anyhow!("{e}\n\n{EXPECTED_RETRIEVAL_COST_USAGE}"))?; - anyhow::ensure!( - !probabilities.is_empty(), - "ExpectedRetrievalCost requires at least one probability\n\n{EXPECTED_RETRIEVAL_COST_USAGE}" - ); - anyhow::ensure!( - probabilities.iter().all(|p| p.is_finite() && (0.0..=1.0).contains(p)), - "ExpectedRetrievalCost probabilities must be finite values in [0, 1]\n\n{EXPECTED_RETRIEVAL_COST_USAGE}" - ); - let total_probability: f64 = probabilities.iter().sum(); - anyhow::ensure!( - (total_probability - 1.0).abs() <= 1e-9, - "ExpectedRetrievalCost probabilities must sum to 1.0\n\n{EXPECTED_RETRIEVAL_COST_USAGE}" - ); - - let num_sectors = args.num_sectors.ok_or_else(|| { - anyhow::anyhow!( - "ExpectedRetrievalCost requires --num-sectors\n\n{EXPECTED_RETRIEVAL_COST_USAGE}" - ) - })?; - anyhow::ensure!( - num_sectors >= 2, - "ExpectedRetrievalCost requires at least two sectors\n\n{EXPECTED_RETRIEVAL_COST_USAGE}" - ); - - ( - ser(ExpectedRetrievalCost::new(probabilities, num_sectors))?, - resolved_variant.clone(), - ) - } - - // UndirectedFlowLowerBounds (graph + capacities + lower bounds + terminals + requirement) - "UndirectedFlowLowerBounds" => { - let usage = "Usage: pred create UndirectedFlowLowerBounds --graph 0-1,0-2,1-3,2-3,1-4,3-5,4-5 --capacities 2,2,2,2,1,3,2 --lower-bounds 1,1,0,0,1,0,1 --source 0 --sink 5 --requirement 3"; - let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let capacities = parse_capacities(args, graph.num_edges(), usage)?; - let lower_bounds = parse_lower_bounds(args, graph.num_edges(), usage)?; - let num_vertices = graph.num_vertices(); - let source = args.source.ok_or_else(|| { - anyhow::anyhow!("UndirectedFlowLowerBounds requires --source\n\n{usage}") - })?; - let sink = args.sink.ok_or_else(|| { - anyhow::anyhow!("UndirectedFlowLowerBounds requires --sink\n\n{usage}") - })?; - let requirement = args.requirement.ok_or_else(|| { - anyhow::anyhow!("UndirectedFlowLowerBounds requires --requirement\n\n{usage}") - })?; - validate_vertex_index("source", source, num_vertices, usage)?; - validate_vertex_index("sink", sink, num_vertices, usage)?; - ( - ser(UndirectedFlowLowerBounds::new( - graph, - capacities, - lower_bounds, - source, - sink, - requirement, - ))?, - resolved_variant.clone(), - ) - } - - // UndirectedTwoCommodityIntegralFlow (graph + capacities + terminals + requirements) - "UndirectedTwoCommodityIntegralFlow" => { - let usage = "Usage: 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"; - let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let capacities = parse_capacities(args, graph.num_edges(), usage)?; - for (edge_index, &capacity) in capacities.iter().enumerate() { - let fits = usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .is_some(); - if !fits { - bail!( - "capacity {} at edge index {} is too large for this platform\n\n{}", - capacity, - edge_index, - usage - ); - } - } - let num_vertices = graph.num_vertices(); - let source_1 = args.source_1.ok_or_else(|| { - anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --source-1\n\n{usage}") - })?; - let sink_1 = args.sink_1.ok_or_else(|| { - anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --sink-1\n\n{usage}") - })?; - let source_2 = args.source_2.ok_or_else(|| { - anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --source-2\n\n{usage}") - })?; - let sink_2 = args.sink_2.ok_or_else(|| { - anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --sink-2\n\n{usage}") - })?; - let requirement_1 = args.requirement_1.ok_or_else(|| { - anyhow::anyhow!( - "UndirectedTwoCommodityIntegralFlow requires --requirement-1\n\n{usage}" - ) - })?; - let requirement_2 = args.requirement_2.ok_or_else(|| { - anyhow::anyhow!( - "UndirectedTwoCommodityIntegralFlow requires --requirement-2\n\n{usage}" - ) - })?; - for (label, vertex) in [ - ("source-1", source_1), - ("sink-1", sink_1), - ("source-2", source_2), - ("sink-2", sink_2), - ] { - validate_vertex_index(label, vertex, num_vertices, usage)?; - } - ( - ser(UndirectedTwoCommodityIntegralFlow::new( - graph, - capacities, - source_1, - sink_1, - source_2, - sink_2, - requirement_1, - requirement_2, - ))?, - resolved_variant.clone(), - ) - } - - // IntegralFlowBundles (directed graph + bundles + source/sink + requirement) - "IntegralFlowBundles" => { - let usage = "Usage: pred create IntegralFlowBundles --arcs \"0>1,0>2,1>3,2>3,1>2,2>1\" --bundles \"0,1;2,5;3,4\" --bundle-capacities 1,1,1 --source 0 --sink 3 --requirement 1 --num-vertices 4"; - let arcs_str = args - .arcs - .as_deref() - .ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles requires --arcs\n\n{usage}"))?; - let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices) - .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let bundles = parse_bundles(args, num_arcs, usage)?; - let bundle_capacities = parse_bundle_capacities(args, bundles.len(), usage)?; - let source = args.source.ok_or_else(|| { - anyhow::anyhow!("IntegralFlowBundles requires --source\n\n{usage}") - })?; - let sink = args - .sink - .ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles requires --sink\n\n{usage}"))?; - let requirement = args.requirement.ok_or_else(|| { - anyhow::anyhow!("IntegralFlowBundles requires --requirement\n\n{usage}") - })?; - validate_vertex_index("source", source, graph.num_vertices(), usage)?; - validate_vertex_index("sink", sink, graph.num_vertices(), usage)?; - anyhow::ensure!( - source != sink, - "IntegralFlowBundles requires distinct --source and --sink\n\n{usage}" - ); - - ( - ser(IntegralFlowBundles::new( - graph, - source, - sink, - bundles, - bundle_capacities, - requirement, - ))?, - resolved_variant.clone(), - ) - } - - // LengthBoundedDisjointPaths (graph + source + sink + bound) - "LengthBoundedDisjointPaths" => { - let usage = "Usage: pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --bound 3"; - let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let source = args.source.ok_or_else(|| { - anyhow::anyhow!("LengthBoundedDisjointPaths requires --source\n\n{usage}") - })?; - let sink = args.sink.ok_or_else(|| { - anyhow::anyhow!("LengthBoundedDisjointPaths requires --sink\n\n{usage}") - })?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!("LengthBoundedDisjointPaths requires --bound\n\n{usage}") - })?; - let max_length = validate_length_bounded_disjoint_paths_args( - graph.num_vertices(), - source, - sink, - bound, - Some(usage), - )?; - - ( - ser(LengthBoundedDisjointPaths::new( - graph, source, sink, max_length, - ))?, - resolved_variant.clone(), - ) - } - - // IsomorphicSpanningTree (graph + tree) - "IsomorphicSpanningTree" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create IsomorphicSpanningTree --graph 0-1,1-2,0-2 --tree 0-1,1-2" - ) - })?; - let tree_str = args.tree.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "IsomorphicSpanningTree requires --tree\n\n\ - Usage: pred create IsomorphicSpanningTree --graph 0-1,1-2,0-2 --tree 0-1,1-2" - ) - })?; - let tree_edges: Vec<(usize, usize)> = tree_str - .split(',') - .map(|pair| { - let parts: Vec<&str> = pair.trim().split('-').collect(); - if parts.len() != 2 { - bail!("Invalid tree edge '{}': expected format u-v", pair.trim()); - } - let u: usize = parts[0].parse()?; - let v: usize = parts[1].parse()?; - Ok((u, v)) - }) - .collect::>>()?; - let tree_num_vertices = tree_edges - .iter() - .flat_map(|(u, v)| [*u, *v]) - .max() - .map(|m| m + 1) - .unwrap_or(0) - .max(graph.num_vertices()); - let tree = SimpleGraph::new(tree_num_vertices, tree_edges); - ( - ser(problemreductions::models::graph::IsomorphicSpanningTree::new(graph, tree))?, - resolved_variant.clone(), - ) - } - - // Bounded Diameter Spanning Tree (graph + edge weights + weight bound + diameter bound) - "BoundedDiameterSpanningTree" => { - reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; - let usage = "Usage: pred create BoundedDiameterSpanningTree --graph 0-1,0-2,0-3,1-2,1-4,2-3,3-4 --edge-weights 1,2,1,1,2,1,1 --weight-bound 5 --diameter-bound 3"; - let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let edge_weights = parse_edge_weights(args, graph.num_edges())?; - ensure_positive_i32_values(&edge_weights, "edge weights")?; - let weight_bound = args.weight_bound.ok_or_else(|| { - anyhow::anyhow!("BoundedDiameterSpanningTree requires --weight-bound\n\n{usage}") - })?; - ensure_positive_i32(weight_bound, "weight_bound")?; - let diameter_bound = args.diameter_bound.ok_or_else(|| { - anyhow::anyhow!("BoundedDiameterSpanningTree requires --diameter-bound\n\n{usage}") - })?; - if diameter_bound == 0 { - bail!("BoundedDiameterSpanningTree requires --diameter-bound >= 1\n\n{usage}"); - } - ( - ser( - problemreductions::models::graph::BoundedDiameterSpanningTree::new( - graph, - edge_weights, - weight_bound, - diameter_bound, - ), - )?, - resolved_variant.clone(), - ) - } - - // KthBestSpanningTree (weighted graph + k + bound) - "KthBestSpanningTree" => { - reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3" - ) - })?; - let edge_weights = parse_edge_weights(args, graph.num_edges())?; - let (k, _variant) = - util::validate_k_param(&resolved_variant, args.k, None, "KthBestSpanningTree")?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "KthBestSpanningTree requires --bound\n\n\ - Usage: pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3" - ) - })? as i32; - ( - ser(problemreductions::models::graph::KthBestSpanningTree::new( - graph, - edge_weights, - k, - bound, - ))?, - resolved_variant.clone(), - ) - } - - // Graph problems with edge weights - "BottleneckTravelingSalesman" | "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { - reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create {} --graph 0-1,1-2,2-3 [--edge-weights 1,1,1]", - problem - ) - })?; - let edge_weights = parse_edge_weights(args, graph.num_edges())?; - let data = match canonical { - "BottleneckTravelingSalesman" => { - ser(BottleneckTravelingSalesman::new(graph, edge_weights))? - } - "MaxCut" => ser(MaxCut::new(graph, edge_weights))?, - "MaximumMatching" => ser(MaximumMatching::new(graph, edge_weights))?, - "TravelingSalesman" => ser(TravelingSalesman::new(graph, edge_weights))?, - _ => unreachable!(), - }; - (data, resolved_variant.clone()) - } - - // SteinerTreeInGraphs (graph + edge weights + terminals) - "SteinerTreeInGraphs" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create SteinerTreeInGraphs --graph 0-1,1-2,2-3 --terminals 0,3 [--edge-weights 1,1,1]" - ) - })?; - let edge_weights = parse_edge_weights(args, graph.num_edges())?; - let terminals = parse_terminals(args, graph.num_vertices())?; - ( - ser(SteinerTreeInGraphs::new(graph, terminals, edge_weights))?, - resolved_variant.clone(), - ) - } - - // RuralPostman - "RuralPostman" => { - reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create RuralPostman --graph 0-1,1-2,2-3 --edge-weights 1,1,1 --required-edges 0,2" - ) - })?; - let edge_weights = parse_edge_weights(args, graph.num_edges())?; - let required_edges_str = args.required_edges.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "RuralPostman requires --required-edges\n\n\ - Usage: pred create RuralPostman --graph 0-1,1-2,2-3 --edge-weights 1,1,1 --required-edges 0,2" - ) - })?; - let required_edges: Vec = util::parse_comma_list(required_edges_str)?; - ( - ser(RuralPostman::new(graph, edge_weights, required_edges))?, - resolved_variant.clone(), - ) - } - - // LongestCircuit - "LongestCircuit" => { - reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; - let usage = "pred create LongestCircuit --graph 0-1,1-2,2-3,3-4,4-5,5-0,0-3,1-4,2-5,3-5 --edge-weights 3,2,4,1,5,2,3,2,1,2"; - let (graph, _) = - parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\nUsage: {usage}"))?; - let edge_lengths = parse_edge_weights(args, graph.num_edges())?; - if edge_lengths.iter().any(|&length| length <= 0) { - bail!("LongestCircuit --edge-weights must be positive (> 0)"); - } - ( - ser(LongestCircuit::new(graph, edge_lengths))?, - resolved_variant.clone(), - ) - } - - // StackerCrane - "StackerCrane" => { - let usage = "Usage: pred create StackerCrane --arcs \"0>4,2>5,5>1,3>0,4>3\" --graph \"0-1,1-2,2-3,3-5,4-5,0-3,1-5\" --arc-costs 3,4,2,5,3 --edge-lengths 2,1,3,2,1,4,3 --num-vertices 6"; - let arcs_str = args - .arcs - .as_deref() - .ok_or_else(|| anyhow::anyhow!("StackerCrane requires --arcs\n\n{usage}"))?; - let (arcs_graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?; - let (edges_graph, num_vertices) = - parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - anyhow::ensure!( - edges_graph.num_vertices() == num_vertices, - "internal error: inconsistent graph vertex count" - ); - anyhow::ensure!( - num_vertices == arcs_graph.num_vertices(), - "StackerCrane requires the directed and undirected inputs to agree on --num-vertices\n\n{usage}" - ); - let arc_lengths = parse_arc_costs(args, num_arcs)?; - let edge_lengths = parse_i32_edge_values( - args.edge_lengths.as_ref(), - edges_graph.num_edges(), - "edge length", - )?; - ( - ser(StackerCrane::try_new( - num_vertices, - arcs_graph.arcs(), - edges_graph.edges(), - arc_lengths, - edge_lengths, - ) - .map_err(|e| anyhow::anyhow!(e))?)?, - resolved_variant.clone(), - ) - } - - // MultipleChoiceBranching - "MultipleChoiceBranching" => { - let usage = "Usage: pred create MultipleChoiceBranching/i32 --arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10"; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!("MultipleChoiceBranching requires --arcs\n\n{usage}") - })?; - let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?; - let weights = parse_arc_weights(args, num_arcs)?; - let partition = parse_partition_groups(args, num_arcs)?; - let threshold = parse_multiple_choice_branching_threshold(args, usage)?; - ( - ser(MultipleChoiceBranching::new( - graph, weights, partition, threshold, - ))?, - resolved_variant.clone(), - ) - } - - // KColoring - "KColoring" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!("{e}\n\nUsage: pred create KColoring --graph 0-1,1-2,2-0 --k 3") - })?; - let (k, _variant) = - util::validate_k_param(&resolved_variant, args.k, None, "KColoring")?; - util::ser_kcoloring(graph, k)? - } - - "KClique" => { - let usage = "Usage: pred create KClique --graph 0-1,0-2,1-3,2-3,2-4,3-4 --k 3"; - let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let k = parse_kclique_threshold(args.k, graph.num_vertices(), usage)?; - (ser(KClique::new(graph, k))?, resolved_variant.clone()) - } - - "VertexCover" => { - let usage = "Usage: pred create VertexCover --graph 0-1,1-2,0-2,2-3 --k 2"; - let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let k = args - .k - .ok_or_else(|| anyhow::anyhow!("VertexCover requires --k\n\n{usage}"))?; - if k == 0 { - bail!("VertexCover: --k must be positive"); - } - if k > graph.num_vertices() { - bail!("VertexCover: k must be <= graph num_vertices"); - } - (ser(VertexCover::new(graph, k))?, resolved_variant.clone()) - } - - // SAT - "Satisfiability" => { - let num_vars = args.num_vars.ok_or_else(|| { - anyhow::anyhow!( - "Satisfiability requires --num-vars\n\n\ - Usage: pred create SAT --num-vars 3 --clauses \"1,2;-1,3\"" - ) - })?; - let clauses = parse_clauses(args)?; - ( - ser(Satisfiability::new(num_vars, clauses))?, - resolved_variant.clone(), - ) - } - "NAESatisfiability" => { - let num_vars = args.num_vars.ok_or_else(|| { - anyhow::anyhow!( - "NAESatisfiability requires --num-vars\n\n\ - Usage: pred create NAESAT --num-vars 3 --clauses \"1,2,-3;-1,2,3\"" - ) - })?; - let clauses = parse_clauses(args)?; - ( - ser(NAESatisfiability::try_new(num_vars, clauses).map_err(anyhow::Error::msg)?)?, - resolved_variant.clone(), - ) - } - "KSatisfiability" => { - let num_vars = args.num_vars.ok_or_else(|| { - anyhow::anyhow!( - "KSatisfiability requires --num-vars\n\n\ - Usage: pred create KSAT --num-vars 3 --clauses \"1,2,3;-1,2,-3\"" - ) - })?; - let clauses = parse_clauses(args)?; - let (k, _variant) = - util::validate_k_param(&resolved_variant, args.k, Some(3), "KSatisfiability")?; - util::ser_ksat(num_vars, clauses, k)? - } - - "Maximum2Satisfiability" => { - let num_vars = args.num_vars.ok_or_else(|| { - anyhow::anyhow!( - "Maximum2Satisfiability requires --num-vars\n\n\ - Usage: pred create MAX2SAT --num-vars 4 --clauses \"1,2;1,-2;-1,3;-1,-3\"" - ) - })?; - let clauses = parse_clauses(args)?; - ( - ser(Maximum2Satisfiability::new(num_vars, clauses))?, - resolved_variant.clone(), - ) - } - - "NonTautology" => { - let num_vars = args.num_vars.ok_or_else(|| { - anyhow::anyhow!( - "NonTautology requires --num-vars\n\n\ - Usage: pred create NonTautology --num-vars 3 --disjuncts \"1,2,3;-1,-2,-3\"" - ) - })?; - ( - ser(NonTautology::new(num_vars, parse_disjuncts(args)?))?, - resolved_variant.clone(), - ) - } - - "OneInThreeSatisfiability" => { - let num_vars = args.num_vars.ok_or_else(|| { - anyhow::anyhow!( - "OneInThreeSatisfiability requires --num-vars\n\n\ - Usage: pred create OneInThreeSatisfiability --num-vars 4 --clauses \"1,2,3;-1,3,4;2,-3,-4\"" - ) - })?; - let clauses = parse_clauses(args)?; - ( - ser(OneInThreeSatisfiability::new(num_vars, clauses))?, - resolved_variant.clone(), - ) - } - - "Planar3Satisfiability" => { - let num_vars = args.num_vars.ok_or_else(|| { - anyhow::anyhow!( - "Planar3Satisfiability requires --num-vars\n\n\ - Usage: pred create Planar3Satisfiability --num-vars 4 --clauses \"1,2,3;-1,2,4;1,-3,4;-2,3,-4\"" - ) - })?; - let clauses = parse_clauses(args)?; - ( - ser(Planar3Satisfiability::new(num_vars, clauses))?, - resolved_variant.clone(), - ) - } - - // QBF - "QuantifiedBooleanFormulas" => { - let num_vars = args.num_vars.ok_or_else(|| { - anyhow::anyhow!( - "QuantifiedBooleanFormulas requires --num-vars, --clauses, and --quantifiers\n\n\ - Usage: pred create QBF --num-vars 3 --clauses \"1,2;-1,3\" --quantifiers \"E,A,E\"" - ) - })?; - let clauses = parse_clauses(args)?; - let quantifiers = parse_quantifiers(args, num_vars)?; - ( - ser(QuantifiedBooleanFormulas::new( - num_vars, - quantifiers, - clauses, - ))?, - resolved_variant.clone(), - ) - } - - // QuadraticAssignment - "QuadraticAssignment" => { - let cost_str = args.matrix.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "QuadraticAssignment requires --matrix (cost) and --distance-matrix\n\n\ - Usage: pred create QAP --matrix \"0,5;5,0\" --distance-matrix \"0,1;1,0\"" - ) - })?; - let dist_str = args.distance_matrix.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "QuadraticAssignment requires --distance-matrix\n\n\ - Usage: pred create QAP --matrix \"0,5;5,0\" --distance-matrix \"0,1;1,0\"" - ) - })?; - let cost_matrix = parse_i64_matrix(cost_str).context("Invalid cost matrix")?; - let distance_matrix = parse_i64_matrix(dist_str).context("Invalid distance matrix")?; - let n = cost_matrix.len(); - for (i, row) in cost_matrix.iter().enumerate() { - if row.len() != n { - bail!( - "cost matrix must be square: row {i} has {} columns, expected {n}", - row.len() - ); - } - } - let m = distance_matrix.len(); - for (i, row) in distance_matrix.iter().enumerate() { - if row.len() != m { - bail!( - "distance matrix must be square: row {i} has {} columns, expected {m}", - row.len() - ); - } - } - if n > m { - bail!("num_facilities ({n}) must be <= num_locations ({m})"); - } - ( - ser( - problemreductions::models::algebraic::QuadraticAssignment::new( - cost_matrix, - distance_matrix, - ), - )?, - resolved_variant.clone(), - ) - } - - // QUBO - "QUBO" => { - let matrix = parse_matrix(args)?; - (ser(QUBO::from_matrix(matrix))?, resolved_variant.clone()) - } - - // SpinGlass - "SpinGlass" => { - let (graph, n) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create SpinGlass --graph 0-1,1-2 [--couplings 1,1] [--fields 0,0,0]" - ) - })?; - let use_f64 = resolved_variant.get("weight").is_some_and(|w| w == "f64") - || has_float_syntax(&args.couplings) - || has_float_syntax(&args.fields); - if use_f64 { - let couplings = parse_couplings_f64(args, graph.num_edges())?; - let fields = parse_fields_f64(args, n)?; - let mut variant = resolved_variant.clone(); - variant.insert("weight".to_string(), "f64".to_string()); - ( - ser(SpinGlass::from_graph(graph, couplings, fields))?, - variant, - ) - } else { - let couplings = parse_couplings(args, graph.num_edges())?; - let fields = parse_fields(args, n)?; - ( - ser(SpinGlass::from_graph(graph, couplings, fields))?, - resolved_variant.clone(), - ) - } - } - - // Factoring - "Factoring" => { - let usage = "Usage: pred create Factoring --target 15 --m 4 --n 4"; - let target = args - .target - .as_deref() - .ok_or_else(|| anyhow::anyhow!("Factoring requires --target\n\n{usage}"))?; - let target: u64 = target - .parse() - .context("Factoring --target must fit in u64")?; - let m = args - .m - .ok_or_else(|| anyhow::anyhow!("Factoring requires --m\n\n{usage}"))?; - let n = args - .n - .ok_or_else(|| anyhow::anyhow!("Factoring requires --n\n\n{usage}"))?; - (ser(Factoring::new(m, n, target))?, resolved_variant.clone()) - } - - // MaximalIS — same as MIS (graph + vertex weights) - "MaximalIS" => { - create_vertex_weight_problem(args, canonical, graph_type, &resolved_variant)? - } - - // BoyceCoddNormalFormViolation - "BoyceCoddNormalFormViolation" => { - let n = args.n.ok_or_else(|| { - anyhow::anyhow!( - "BoyceCoddNormalFormViolation requires --n, --sets, and --target\n\n\ - Usage: pred create BoyceCoddNormalFormViolation --n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" - ) - })?; - let sets_str = args.sets.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "BoyceCoddNormalFormViolation requires --sets (functional deps as lhs:rhs;...)\n\n\ - Usage: pred create BoyceCoddNormalFormViolation --n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" - ) - })?; - let target_str = args.target.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "BoyceCoddNormalFormViolation requires --target (comma-separated attribute indices)\n\n\ - Usage: pred create BoyceCoddNormalFormViolation --n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" - ) - })?; - let fds: Vec<(Vec, Vec)> = sets_str - .split(';') - .map(|fd_str| { - let parts: Vec<&str> = fd_str.split(':').collect(); - anyhow::ensure!( - parts.len() == 2, - "Each FD must be lhs:rhs, got '{}'", - fd_str - ); - let lhs: Vec = util::parse_comma_list(parts[0])?; - let rhs: Vec = util::parse_comma_list(parts[1])?; - ensure_attribute_indices_in_range( - &lhs, - n, - &format!("Functional dependency '{fd_str}' lhs"), - )?; - ensure_attribute_indices_in_range( - &rhs, - n, - &format!("Functional dependency '{fd_str}' rhs"), - )?; - Ok((lhs, rhs)) - }) - .collect::>()?; - let target: Vec = util::parse_comma_list(target_str)?; - ensure_attribute_indices_in_range(&target, n, "Target subset")?; - ( - ser(BoyceCoddNormalFormViolation::new(n, fds, target))?, - resolved_variant.clone(), - ) - } - - // BinPacking - "BinPacking" => { - let sizes_str = args.sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "BinPacking requires --sizes and --capacity\n\n\ - Usage: pred create BinPacking --sizes 3,3,2,2 --capacity 5" - ) - })?; - let cap_str = args.capacity.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "BinPacking requires --capacity\n\n\ - Usage: pred create BinPacking --sizes 3,3,2,2 --capacity 5" - ) - })?; - let use_f64 = sizes_str.contains('.') || cap_str.contains('.'); - if use_f64 { - let sizes: Vec = util::parse_comma_list(sizes_str)?; - let capacity: f64 = cap_str.parse()?; - let mut variant = resolved_variant.clone(); - variant.insert("weight".to_string(), "f64".to_string()); - (ser(BinPacking::new(sizes, capacity))?, variant) - } else { - let sizes: Vec = util::parse_comma_list(sizes_str)?; - let capacity: i32 = cap_str.parse()?; - ( - ser(BinPacking::new(sizes, capacity))?, - resolved_variant.clone(), - ) - } - } - - // AdditionalKey - "AdditionalKey" => { - let usage = "Usage: pred create AdditionalKey --num-attributes 6 --dependencies \"0,1:2,3;2,3:4,5\" --relation-attrs \"0,1,2,3,4,5\" --known-keys \"0,1;2,3\""; - let num_attributes = args.num_attributes.ok_or_else(|| { - anyhow::anyhow!("AdditionalKey requires --num-attributes\n\n{usage}") - })?; - let deps_str = args.dependencies.as_deref().ok_or_else(|| { - anyhow::anyhow!("AdditionalKey requires --dependencies\n\n{usage}") - })?; - let ra_str = args.relation_attrs.as_deref().ok_or_else(|| { - anyhow::anyhow!("AdditionalKey requires --relation-attrs\n\n{usage}") - })?; - let dependencies: Vec<(Vec, Vec)> = deps_str - .split(';') - .map(|dep| { - let parts: Vec<&str> = dep.trim().split(':').collect(); - anyhow::ensure!( - parts.len() == 2, - "Invalid dependency format '{}', expected 'lhs:rhs' (e.g., '0,1:2,3')", - dep.trim() - ); - let lhs: Vec = util::parse_comma_list(parts[0].trim())?; - let rhs: Vec = util::parse_comma_list(parts[1].trim())?; - Ok((lhs, rhs)) - }) - .collect::>>()?; - let relation_attrs: Vec = util::parse_comma_list(ra_str)?; - let known_keys: Vec> = match args.known_keys.as_deref() { - Some(s) if !s.is_empty() => s - .split(';') - .map(|k| util::parse_comma_list(k.trim())) - .collect::>>()?, - _ => vec![], - }; - ( - ser(AdditionalKey::new( - num_attributes, - dependencies, - relation_attrs, - known_keys, - ))?, - resolved_variant.clone(), - ) - } - - "ConsistencyOfDatabaseFrequencyTables" => { - let usage = "Usage: pred create ConsistencyOfDatabaseFrequencyTables --num-objects 6 --attribute-domains \"2,3,2\" --frequency-tables \"0,1:1,1,1|1,1,1;1,2:1,1|0,2|1,1\" --known-values \"0,0,0;3,0,1;1,2,1\""; - let num_objects = args.num_objects.ok_or_else(|| { - anyhow::anyhow!( - "ConsistencyOfDatabaseFrequencyTables requires --num-objects\n\n{usage}" - ) - })?; - let attribute_domains_str = args.attribute_domains.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "ConsistencyOfDatabaseFrequencyTables requires --attribute-domains\n\n{usage}" - ) - })?; - let frequency_tables_str = args.frequency_tables.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "ConsistencyOfDatabaseFrequencyTables requires --frequency-tables\n\n{usage}" - ) - })?; - - let attribute_domains: Vec = util::parse_comma_list(attribute_domains_str)?; - for (index, &domain_size) in attribute_domains.iter().enumerate() { - anyhow::ensure!( - domain_size > 0, - "attribute domain at index {index} must be positive\n\n{usage}" - ); - } - let frequency_tables = - parse_cdft_frequency_tables(frequency_tables_str, &attribute_domains, num_objects) - .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let known_values = parse_cdft_known_values( - args.known_values.as_deref(), - num_objects, - &attribute_domains, - ) - .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - - ( - ser(ConsistencyOfDatabaseFrequencyTables::new( - num_objects, - attribute_domains, - frequency_tables, - known_values, - ))?, - resolved_variant.clone(), - ) - } - - // IntegerKnapsack - "IntegerKnapsack" => { - let sizes_str = args.sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "IntegerKnapsack requires --sizes, --values, and --capacity\n\n\ - Usage: pred create IntegerKnapsack --sizes 3,4,5,2,7 --values 4,5,7,3,9 --capacity 15" - ) - })?; - let values_str = args.values.as_deref().ok_or_else(|| { - anyhow::anyhow!("IntegerKnapsack requires --values (e.g., 4,5,7,3,9)") - })?; - let cap_str = args - .capacity - .as_deref() - .ok_or_else(|| anyhow::anyhow!("IntegerKnapsack requires --capacity (e.g., 15)"))?; - let sizes: Vec = util::parse_comma_list(sizes_str)?; - let values: Vec = util::parse_comma_list(values_str)?; - let capacity: i64 = cap_str.parse()?; - anyhow::ensure!( - sizes.len() == values.len(), - "sizes and values must have the same length, got {} and {}", - sizes.len(), - values.len() - ); - anyhow::ensure!(sizes.iter().all(|&s| s > 0), "all sizes must be positive"); - anyhow::ensure!(values.iter().all(|&v| v > 0), "all values must be positive"); - anyhow::ensure!(capacity >= 0, "capacity must be nonnegative"); - ( - ser(problemreductions::models::set::IntegerKnapsack::new( - sizes, values, capacity, - ))?, - resolved_variant.clone(), - ) - } - - // SubsetSum - "SubsetSum" => { - let sizes_str = args.sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SubsetSum requires --sizes and --target\n\n\ - Usage: pred create SubsetSum --sizes 3,7,1,8,2,4 --target 11" - ) - })?; - let target = args.target.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SubsetSum requires --target\n\n\ - Usage: pred create SubsetSum --sizes 3,7,1,8,2,4 --target 11" - ) - })?; - let sizes = util::parse_biguint_list(sizes_str)?; - let target = util::parse_decimal_biguint(target)?; - ( - ser(SubsetSum::new(sizes, target))?, - resolved_variant.clone(), - ) - } - - // SubsetProduct - "SubsetProduct" => { - let sizes_str = args.sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SubsetProduct requires --sizes and --target\n\n\ - Usage: pred create SubsetProduct --sizes 2,3,5,7,6,10 --target 210" - ) - })?; - let target = args.target.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SubsetProduct requires --target\n\n\ - Usage: pred create SubsetProduct --sizes 2,3,5,7,6,10 --target 210" - ) - })?; - let sizes = util::parse_biguint_list(sizes_str)?; - let target = util::parse_decimal_biguint(target)?; - ( - ser(SubsetProduct::new(sizes, target))?, - resolved_variant.clone(), - ) - } - - // MinimumAxiomSet - "MinimumAxiomSet" => { - let usage = "Usage: pred create MinimumAxiomSet --n 8 --true-sentences 0,1,2,3,4,5,6,7 --implications \"0>2;0>3;1>4;1>5;2,4>6;3,5>7;6,7>0;6,7>1\""; - let num_sentences = args.n.ok_or_else(|| { - anyhow::anyhow!( - "MinimumAxiomSet requires --n, --true-sentences, and --implications\n\n{usage}" - ) - })?; - let ts_str = args.true_sentences.as_deref().ok_or_else(|| { - anyhow::anyhow!("MinimumAxiomSet requires --true-sentences\n\n{usage}") - })?; - let imp_str = args.implications.as_deref().ok_or_else(|| { - anyhow::anyhow!("MinimumAxiomSet requires --implications\n\n{usage}") - })?; - let true_sentences: Vec = ts_str - .split(',') - .map(|s| s.trim().parse::()) - .collect::>() - .context("--true-sentences must be comma-separated usize values")?; - let implications = parse_implications(imp_str).context( - "--implications must be semicolon-separated \"antecedents>consequent\" pairs", - )?; - ( - ser(MinimumAxiomSet::new( - num_sentences, - true_sentences, - implications, - ))?, - resolved_variant.clone(), - ) - } - - // IntegerExpressionMembership - "IntegerExpressionMembership" => { - let usage = "Usage: pred create IntegerExpressionMembership --expression '{\"Sum\":[{\"Atom\":1},{\"Atom\":2}]}' --target 3"; - let expr_str = args.expression.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "IntegerExpressionMembership requires --expression and --target\n\n{usage}" - ) - })?; - let target = args.target.as_deref().ok_or_else(|| { - anyhow::anyhow!("IntegerExpressionMembership requires --target\n\n{usage}") - })?; - let target: u64 = target - .parse() - .context("IntegerExpressionMembership --target must be a positive integer")?; - if target == 0 { - anyhow::bail!("IntegerExpressionMembership --target must be > 0"); - } - let expr: IntExpr = serde_json::from_str(expr_str) - .context("IntegerExpressionMembership --expression must be valid JSON representing an IntExpr tree")?; - if !expr.all_atoms_positive() { - anyhow::bail!("IntegerExpressionMembership --expression must contain only positive integers (all Atom values > 0)"); - } - ( - ser(IntegerExpressionMembership::new(expr, target))?, - resolved_variant.clone(), - ) - } - - // Numerical3DimensionalMatching - "Numerical3DimensionalMatching" => { - let w_sizes_str = args.w_sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "Numerical3DimensionalMatching requires --w-sizes, --x-sizes, --y-sizes, and --bound\n\n\ - Usage: pred create Numerical3DimensionalMatching --w-sizes 4,5 --x-sizes 4,5 --y-sizes 5,7 --bound 15" - ) - })?; - let x_sizes_str = args.x_sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "Numerical3DimensionalMatching requires --x-sizes\n\n\ - Usage: pred create Numerical3DimensionalMatching --w-sizes 4,5 --x-sizes 4,5 --y-sizes 5,7 --bound 15" - ) - })?; - let y_sizes_str = args.y_sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "Numerical3DimensionalMatching requires --y-sizes\n\n\ - Usage: pred create Numerical3DimensionalMatching --w-sizes 4,5 --x-sizes 4,5 --y-sizes 5,7 --bound 15" - ) - })?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "Numerical3DimensionalMatching requires --bound\n\n\ - Usage: pred create Numerical3DimensionalMatching --w-sizes 4,5 --x-sizes 4,5 --y-sizes 5,7 --bound 15" - ) - })?; - let bound = u64::try_from(bound).map_err(|_| { - anyhow::anyhow!( - "Numerical3DimensionalMatching requires a positive integer --bound\n\n\ - Usage: pred create Numerical3DimensionalMatching --w-sizes 4,5 --x-sizes 4,5 --y-sizes 5,7 --bound 15" - ) - })?; - let sizes_w: Vec = util::parse_comma_list(w_sizes_str)?; - let sizes_x: Vec = util::parse_comma_list(x_sizes_str)?; - let sizes_y: Vec = util::parse_comma_list(y_sizes_str)?; - ( - ser( - Numerical3DimensionalMatching::try_new(sizes_w, sizes_x, sizes_y, bound) - .map_err(anyhow::Error::msg)?, - )?, - resolved_variant.clone(), - ) - } - - // NonLivenessFreePetriNet - "NonLivenessFreePetriNet" => { - let usage = "Usage: pred create NonLivenessFreePetriNet --n 4 --m 3 --arcs \"0>0,1>1,2>2\" --output-arcs \"0>1,1>2,2>3\" --initial-marking 1,0,0,0"; - let num_places = args.n.ok_or_else(|| { - anyhow::anyhow!("NonLivenessFreePetriNet requires --n (num_places)\n\n{usage}") - })?; - let num_transitions = args.m.ok_or_else(|| { - anyhow::anyhow!("NonLivenessFreePetriNet requires --m (num_transitions)\n\n{usage}") - })?; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "NonLivenessFreePetriNet requires --arcs (place>transition arcs)\n\n{usage}" - ) - })?; - let output_arcs_str = args.output_arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "NonLivenessFreePetriNet requires --output-arcs (transition>place arcs)\n\n{usage}" - ) - })?; - let marking_str = args.initial_marking.as_deref().ok_or_else(|| { - anyhow::anyhow!("NonLivenessFreePetriNet requires --initial-marking\n\n{usage}") - })?; - - let place_to_transition: Vec<(usize, usize)> = arcs_str - .split(',') - .filter(|s| !s.trim().is_empty()) - .map(|s| { - let parts: Vec<&str> = s.trim().split('>').collect(); - if parts.len() != 2 { - bail!("Invalid arc '{s}', expected 'place>transition'"); - } - let p: usize = parts[0] - .parse() - .with_context(|| format!("Invalid place index in arc '{s}'"))?; - let t: usize = parts[1] - .parse() - .with_context(|| format!("Invalid transition index in arc '{s}'"))?; - Ok((p, t)) - }) - .collect::>()?; - - let transition_to_place: Vec<(usize, usize)> = output_arcs_str - .split(',') - .filter(|s| !s.trim().is_empty()) - .map(|s| { - let parts: Vec<&str> = s.trim().split('>').collect(); - if parts.len() != 2 { - bail!("Invalid output arc '{s}', expected 'transition>place'"); - } - let t: usize = parts[0] - .parse() - .with_context(|| format!("Invalid transition index in output arc '{s}'"))?; - let p: usize = parts[1] - .parse() - .with_context(|| format!("Invalid place index in output arc '{s}'"))?; - Ok((t, p)) - }) - .collect::>()?; - - let initial_marking: Vec = marking_str - .split(',') - .map(|s| { - s.trim() - .parse::() - .with_context(|| format!("Invalid marking value: {s}")) - }) - .collect::>()?; - - ( - ser(NonLivenessFreePetriNet::try_new( - num_places, - num_transitions, - place_to_transition, - transition_to_place, - initial_marking, - ) - .map_err(anyhow::Error::msg)?)?, - resolved_variant.clone(), - ) - } - - // Betweenness - "Betweenness" => { - let n = args.n.ok_or_else(|| { - anyhow::anyhow!( - "Betweenness requires --n and --sets\n\n\ - Usage: pred create Betweenness --n 5 --sets \"0,1,2;2,3,4;0,2,4;1,3,4\"" - ) - })?; - let sets = parse_sets(args)?; - for (i, set) in sets.iter().enumerate() { - if set.len() != 3 { - bail!( - "Triple {} has {} elements, expected 3 (a,b,c)", - i, - set.len() - ); - } - } - let triples: Vec<(usize, usize, usize)> = - sets.iter().map(|s| (s[0], s[1], s[2])).collect(); - ( - ser(Betweenness::try_new(n, triples).map_err(anyhow::Error::msg)?)?, - resolved_variant.clone(), - ) - } - - // CyclicOrdering - "CyclicOrdering" => { - let n = args.n.ok_or_else(|| { - anyhow::anyhow!( - "CyclicOrdering requires --n and --sets\n\n\ - Usage: pred create CyclicOrdering --n 5 --sets \"0,1,2;2,3,0;1,3,4\"" - ) - })?; - let sets = parse_sets(args)?; - for (i, set) in sets.iter().enumerate() { - if set.len() != 3 { - bail!( - "Triple {} has {} elements, expected 3 (a,b,c)", - i, - set.len() - ); - } - } - let triples: Vec<(usize, usize, usize)> = - sets.iter().map(|s| (s[0], s[1], s[2])).collect(); - ( - ser(CyclicOrdering::try_new(n, triples).map_err(anyhow::Error::msg)?)?, - resolved_variant.clone(), - ) - } - - // ThreePartition - "ThreePartition" => { - let sizes_str = args.sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "ThreePartition requires --sizes and --bound\n\n\ - Usage: pred create ThreePartition --sizes 4,5,6,4,6,5 --bound 15" - ) - })?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "ThreePartition requires --bound\n\n\ - Usage: pred create ThreePartition --sizes 4,5,6,4,6,5 --bound 15" - ) - })?; - let bound = u64::try_from(bound).map_err(|_| { - anyhow::anyhow!( - "ThreePartition requires a positive integer --bound\n\n\ - Usage: pred create ThreePartition --sizes 4,5,6,4,6,5 --bound 15" - ) - })?; - let sizes: Vec = util::parse_comma_list(sizes_str)?; - ( - ser(ThreePartition::try_new(sizes, bound).map_err(anyhow::Error::msg)?)?, - resolved_variant.clone(), - ) - } - - // DynamicStorageAllocation - "DynamicStorageAllocation" => { - let usage = "Usage: pred create DynamicStorageAllocation --release-times 0,0,1,2,3 --deadlines 3,2,4,5,5 --sizes 2,3,1,3,2 --capacity 6"; - let rt_str = args.release_times.as_deref().ok_or_else(|| { - anyhow::anyhow!("DynamicStorageAllocation requires --release-times\n\n{usage}") - })?; - let dl_str = args.deadlines.as_deref().ok_or_else(|| { - anyhow::anyhow!("DynamicStorageAllocation requires --deadlines\n\n{usage}") - })?; - let sizes_str = args.sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!("DynamicStorageAllocation requires --sizes\n\n{usage}") - })?; - let cap_str = args.capacity.as_deref().ok_or_else(|| { - anyhow::anyhow!("DynamicStorageAllocation requires --capacity\n\n{usage}") - })?; - let release_times: Vec = util::parse_comma_list(rt_str)?; - let deadlines: Vec = util::parse_comma_list(dl_str)?; - let sizes: Vec = util::parse_comma_list(sizes_str)?; - let memory_size: usize = cap_str.parse()?; - if release_times.len() != deadlines.len() || release_times.len() != sizes.len() { - bail!("--release-times, --deadlines, and --sizes must have the same length\n\n{usage}"); - } - let items: Vec<(usize, usize, usize)> = release_times - .into_iter() - .zip(deadlines) - .zip(sizes) - .map(|((r, d), s)| (r, d, s)) - .collect(); - ( - ser(DynamicStorageAllocation::try_new(items, memory_size) - .map_err(anyhow::Error::msg)?)?, - resolved_variant.clone(), - ) - } - - // KthLargestMTuple - "KthLargestMTuple" => { - let sets_str = args.sets.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "KthLargestMTuple requires --sets, --k, and --bound\n\n\ - Usage: pred create KthLargestMTuple --sets \"2,5,8;3,6;1,4,7\" --k 14 --bound 12" - ) - })?; - let k_val = args.k.ok_or_else(|| { - anyhow::anyhow!( - "KthLargestMTuple requires --k\n\n\ - Usage: pred create KthLargestMTuple --sets \"2,5,8;3,6;1,4,7\" --k 14 --bound 12" - ) - })?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "KthLargestMTuple requires --bound\n\n\ - Usage: pred create KthLargestMTuple --sets \"2,5,8;3,6;1,4,7\" --k 14 --bound 12" - ) - })?; - let bound = u64::try_from(bound).map_err(|_| { - anyhow::anyhow!("KthLargestMTuple requires a positive integer --bound") - })?; - let sets: Vec> = sets_str - .split(';') - .map(util::parse_comma_list) - .collect::>()?; - ( - ser(KthLargestMTuple::try_new(sets, k_val as u64, bound) - .map_err(anyhow::Error::msg)?)?, - resolved_variant.clone(), - ) - } - - // AlgebraicEquationsOverGF2 - "AlgebraicEquationsOverGF2" => { - let n = args.num_vars.ok_or_else(|| { - anyhow::anyhow!( - "AlgebraicEquationsOverGF2 requires --num-vars and --equations\n\n\ - Usage: pred create AlgebraicEquationsOverGF2 --num-vars 3 --equations \"0,1:2;1,2:0:;0:1:2:\"\n\n\ - Format: semicolons separate equations, colons separate monomials within an equation,\n\ - commas separate variable indices within a monomial, empty monomial = constant 1" - ) - })?; - let eq_str = args.equations.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "AlgebraicEquationsOverGF2 requires --equations\n\n\ - Usage: pred create AlgebraicEquationsOverGF2 --num-vars 3 --equations \"0,1:2;1,2:0:;0:1:2:\"" - ) - })?; - // Parse equations: "0,1:2;1,2:0:;0:1:2:" - // ';' separates equations, ':' separates monomials, ',' separates variables - let equations: Vec>> = eq_str - .split(';') - .map(|eq_s| { - eq_s.split(':') - .map(|mono_s| { - let mono_s = mono_s.trim(); - if mono_s.is_empty() { - Ok(vec![]) // constant 1 - } else { - mono_s - .split(',') - .map(|v| { - v.trim().parse::().map_err(|e| { - anyhow::anyhow!("Invalid variable index '{v}': {e}") - }) - }) - .collect::>>() - } - }) - .collect::>>>() - }) - .collect::>>>>()?; - ( - ser(AlgebraicEquationsOverGF2::new(n, equations).map_err(anyhow::Error::msg)?)?, - resolved_variant.clone(), - ) - } - - // QuadraticCongruences - "QuadraticCongruences" => { - let a = args.coeff_a.ok_or_else(|| { - anyhow::anyhow!( - "QuadraticCongruences requires --coeff-a, --coeff-b, and --coeff-c\n\n\ - Usage: pred create QuadraticCongruences --coeff-a 4 --coeff-b 15 --coeff-c 10" - ) - })?; - let b = args.coeff_b.ok_or_else(|| { - anyhow::anyhow!( - "QuadraticCongruences requires --coeff-b\n\n\ - Usage: pred create QuadraticCongruences --coeff-a 4 --coeff-b 15 --coeff-c 10" - ) - })?; - let c = args.coeff_c.ok_or_else(|| { - anyhow::anyhow!( - "QuadraticCongruences requires --coeff-c\n\n\ - Usage: pred create QuadraticCongruences --coeff-a 4 --coeff-b 15 --coeff-c 10" - ) - })?; - ( - ser(QuadraticCongruences::try_new(a, b, c).map_err(anyhow::Error::msg)?)?, - resolved_variant.clone(), - ) - } - - // QuadraticDiophantineEquations - "QuadraticDiophantineEquations" => { - let a = args.coeff_a.ok_or_else(|| { - anyhow::anyhow!( - "QuadraticDiophantineEquations requires --coeff-a, --coeff-b, and --coeff-c\n\n\ - Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --coeff-c 53" - ) - })?; - let b = args.coeff_b.ok_or_else(|| { - anyhow::anyhow!( - "QuadraticDiophantineEquations requires --coeff-b\n\n\ - Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --coeff-c 53" - ) - })?; - let c = args.coeff_c.ok_or_else(|| { - anyhow::anyhow!( - "QuadraticDiophantineEquations requires --coeff-c\n\n\ - Usage: pred create QuadraticDiophantineEquations --coeff-a 3 --coeff-b 5 --coeff-c 53" - ) - })?; - ( - ser(QuadraticDiophantineEquations::try_new(a, b, c).map_err(anyhow::Error::msg)?)?, - resolved_variant.clone(), - ) - } - - // SimultaneousIncongruences - "SimultaneousIncongruences" => { - let pairs_str = args.pairs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SimultaneousIncongruences requires --pairs\n\n\ - Usage: pred create SimultaneousIncongruences --pairs \"2,2;1,3;2,5;3,7\"" - ) - })?; - let pairs: Vec<(u64, u64)> = pairs_str - .split(';') - .map(|s| { - let parts: Vec<&str> = s.split(',').collect(); - if parts.len() != 2 { - return Err(anyhow::anyhow!( - "Each pair must be in \"a,b\" format, got: {s}" - )); - } - let a: u64 = parts[0] - .trim() - .parse() - .with_context(|| format!("Invalid integer in pair: {s}"))?; - let b: u64 = parts[1] - .trim() - .parse() - .with_context(|| format!("Invalid integer in pair: {s}"))?; - Ok((a, b)) - }) - .collect::>()?; - ( - ser(SimultaneousIncongruences::new(pairs).map_err(anyhow::Error::msg)?)?, - resolved_variant.clone(), - ) - } - - // SumOfSquaresPartition - "SumOfSquaresPartition" => { - let sizes_str = args.sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SumOfSquaresPartition requires --sizes and --num-groups\n\n\ - Usage: pred create SumOfSquaresPartition --sizes 5,3,8,2,7,1 --num-groups 3" - ) - })?; - let num_groups = args.num_groups.ok_or_else(|| { - anyhow::anyhow!( - "SumOfSquaresPartition requires --num-groups\n\n\ - Usage: pred create SumOfSquaresPartition --sizes 5,3,8,2,7,1 --num-groups 3" - ) - })?; - let sizes: Vec = util::parse_comma_list(sizes_str)?; - ( - ser(SumOfSquaresPartition::try_new(sizes, num_groups) - .map_err(anyhow::Error::msg)?)?, - resolved_variant.clone(), - ) - } - - // PaintShop - "PaintShop" => { - let seq_str = args.sequence.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "PaintShop requires --sequence\n\n\ - Usage: pred create PaintShop --sequence a,b,a,c,c,b" - ) - })?; - let sequence: Vec = seq_str.split(',').map(|s| s.trim().to_string()).collect(); - (ser(PaintShop::new(sequence))?, resolved_variant.clone()) - } - - // MaximumSetPacking - "MaximumSetPacking" => { - let sets = parse_sets(args)?; - let num_sets = sets.len(); - let weights = parse_set_weights(args, num_sets)?; - ( - ser(MaximumSetPacking::with_weights(sets, weights))?, - resolved_variant.clone(), - ) - } - - // SetSplitting - "SetSplitting" => { - let universe = args.universe.ok_or_else(|| { - anyhow::anyhow!( - "SetSplitting requires --universe and --sets\n\n\ - Usage: pred create SetSplitting --universe 6 --sets \"0,1,2;2,3,4;0,4,5;1,3,5\"" - ) - })?; - let subsets = parse_sets(args)?; - ( - ser(SetSplitting::try_new(universe, subsets).map_err(anyhow::Error::msg)?)?, - resolved_variant.clone(), - ) - } - - // MinimumHittingSet - "MinimumHittingSet" => { - let universe = args.universe.ok_or_else(|| { - anyhow::anyhow!( - "MinimumHittingSet requires --universe and --sets\n\n\ - Usage: pred create MinimumHittingSet --universe 6 --sets \"0,1,2;0,3,4;1,3,5;2,4,5;0,1,5;2,3;1,4\"" - ) - })?; - let sets = parse_sets(args)?; - for (i, set) in sets.iter().enumerate() { - for &element in set { - if element >= universe { - bail!( - "Set {} contains element {} which is outside universe of size {}", - i, - element, - universe - ); - } - } - } - ( - ser(MinimumHittingSet::new(universe, sets))?, - resolved_variant.clone(), - ) - } - - // MinimumSetCovering - "MinimumSetCovering" => { - let universe = args.universe.ok_or_else(|| { - anyhow::anyhow!( - "MinimumSetCovering requires --universe and --sets\n\n\ - Usage: pred create MinimumSetCovering --universe 4 --sets \"0,1;1,2;2,3;0,3\"" - ) - })?; - let sets = parse_sets(args)?; - let num_sets = sets.len(); - let weights = parse_set_weights(args, num_sets)?; - ( - ser(MinimumSetCovering::with_weights(universe, sets, weights))?, - resolved_variant.clone(), - ) - } - - // EnsembleComputation - "EnsembleComputation" => { - let usage = - "Usage: pred create EnsembleComputation --universe 4 --sets \"0,1,2;0,1,3\" [--budget 4]"; - let universe_size = args.universe.ok_or_else(|| { - anyhow::anyhow!("EnsembleComputation requires --universe\n\n{usage}") - })?; - let subsets = parse_sets(args)?; - let instance = if let Some(budget_str) = args.budget.as_deref() { - let budget = budget_str.parse::().map_err(|e| { - anyhow::anyhow!( - "Invalid --budget value for EnsembleComputation: {e}\n\n{usage}" - ) - })?; - EnsembleComputation::try_new(universe_size, subsets, budget) - .map_err(anyhow::Error::msg)? - } else { - EnsembleComputation::with_default_budget(universe_size, subsets) - }; - (ser(instance)?, resolved_variant.clone()) - } - - // ComparativeContainment - "ComparativeContainment" => { - let universe = args.universe.ok_or_else(|| { - anyhow::anyhow!( - "ComparativeContainment requires --universe, --r-sets, and --s-sets\n\n\ - Usage: pred create ComparativeContainment --universe 4 --r-sets \"0,1,2,3;0,1\" --s-sets \"0,1,2,3;2,3\" [--r-weights 2,5] [--s-weights 3,6]" - ) - })?; - let r_sets = parse_named_sets(args.r_sets.as_deref(), "--r-sets")?; - let s_sets = parse_named_sets(args.s_sets.as_deref(), "--s-sets")?; - validate_comparative_containment_sets("R", "--r-sets", universe, &r_sets)?; - validate_comparative_containment_sets("S", "--s-sets", universe, &s_sets)?; - let data = match resolved_variant.get("weight").map(|value| value.as_str()) { - Some("One") => { - let r_weights = parse_named_set_weights( - args.r_weights.as_deref(), - r_sets.len(), - "--r-weights", - )?; - let s_weights = parse_named_set_weights( - args.s_weights.as_deref(), - s_sets.len(), - "--s-weights", - )?; - if r_weights.iter().any(|&w| w != 1) || s_weights.iter().any(|&w| w != 1) { - bail!( - "Non-unit weights are not supported for ComparativeContainment/One.\n\n\ - Use `pred create ComparativeContainment/i32 ... --r-weights ... --s-weights ...` for weighted instances." - ); - } - ser(ComparativeContainment::::new(universe, r_sets, s_sets))? - } - Some("f64") => { - let r_weights = parse_named_set_weights_f64( - args.r_weights.as_deref(), - r_sets.len(), - "--r-weights", - )?; - validate_comparative_containment_f64_weights("R", "--r-weights", &r_weights)?; - let s_weights = parse_named_set_weights_f64( - args.s_weights.as_deref(), - s_sets.len(), - "--s-weights", - )?; - validate_comparative_containment_f64_weights("S", "--s-weights", &s_weights)?; - ser(ComparativeContainment::::with_weights( - universe, r_sets, s_sets, r_weights, s_weights, - ))? - } - Some("i32") | None => { - let r_weights = parse_named_set_weights( - args.r_weights.as_deref(), - r_sets.len(), - "--r-weights", - )?; - validate_comparative_containment_i32_weights("R", "--r-weights", &r_weights)?; - let s_weights = parse_named_set_weights( - args.s_weights.as_deref(), - s_sets.len(), - "--s-weights", - )?; - validate_comparative_containment_i32_weights("S", "--s-weights", &s_weights)?; - ser(ComparativeContainment::with_weights( - universe, r_sets, s_sets, r_weights, s_weights, - ))? - } - Some(other) => bail!( - "Unsupported ComparativeContainment weight variant: {}", - other - ), - }; - (data, resolved_variant.clone()) - } - - // ExactCoverBy3Sets - "ExactCoverBy3Sets" => { - let universe = args.universe.ok_or_else(|| { - anyhow::anyhow!( - "ExactCoverBy3Sets requires --universe and --sets\n\n\ - Usage: pred create X3C --universe 6 --sets \"0,1,2;3,4,5\"" - ) - })?; - if universe % 3 != 0 { - bail!("Universe size must be divisible by 3, got {}", universe); - } - let sets = parse_sets(args)?; - // Validate each set has exactly 3 distinct elements within the universe - for (i, set) in sets.iter().enumerate() { - if set.len() != 3 { - bail!( - "Subset {} has {} elements, but X3C requires exactly 3 elements per subset", - i, - set.len() - ); - } - if set[0] == set[1] || set[0] == set[2] || set[1] == set[2] { - bail!("Subset {} contains duplicate elements: {:?}", i, set); - } - for &elem in set { - if elem >= universe { - bail!( - "Subset {} contains element {} which is outside universe of size {}", - i, - elem, - universe - ); - } - } - } - let subsets: Vec<[usize; 3]> = sets.into_iter().map(|s| [s[0], s[1], s[2]]).collect(); - ( - ser(problemreductions::models::set::ExactCoverBy3Sets::new( - universe, subsets, - ))?, - resolved_variant.clone(), - ) - } - - // ThreeDimensionalMatching - "ThreeDimensionalMatching" => { - let universe = args.universe.ok_or_else(|| { - anyhow::anyhow!( - "ThreeDimensionalMatching requires --universe and --sets\n\n\ - Usage: pred create 3DM --universe 3 --sets \"0,1,2;1,0,1;2,2,0\"" - ) - })?; - let sets = parse_sets(args)?; - // Validate each set has exactly 3 elements representing (w, x, y) coordinates - for (i, set) in sets.iter().enumerate() { - if set.len() != 3 { - bail!( - "Triple {} has {} elements, expected 3 (w,x,y)", - i, - set.len() - ); - } - for (coord_idx, &elem) in set.iter().enumerate() { - let coord_name = ["w", "x", "y"][coord_idx]; - if elem >= universe { - bail!( - "Triple {} has {}-coordinate {} which is outside 0..{}", - i, - coord_name, - elem, - universe - ); - } - } - } - let triples: Vec<(usize, usize, usize)> = - sets.into_iter().map(|s| (s[0], s[1], s[2])).collect(); - ( - ser( - problemreductions::models::set::ThreeDimensionalMatching::new( - universe, triples, - ), - )?, - resolved_variant.clone(), - ) - } - - // ThreeMatroidIntersection - "ThreeMatroidIntersection" => { - let universe = args.universe.ok_or_else(|| { - anyhow::anyhow!( - "ThreeMatroidIntersection requires --universe, --partitions, and --bound\n\n\ - Usage: pred create ThreeMatroidIntersection --universe 6 --partitions \"0,1,2;3,4,5|0,3;1,4;2,5|0,4;1,5;2,3\" --bound 2" - ) - })?; - let bound_val = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "ThreeMatroidIntersection requires --bound\n\n\ - Usage: pred create ThreeMatroidIntersection --universe 6 --partitions \"0,1,2;3,4,5|0,3;1,4;2,5|0,4;1,5;2,3\" --bound 2" - ) - })?; - let bound = usize::try_from(bound_val) - .map_err(|_| anyhow::anyhow!("--bound must be non-negative, got {}", bound_val))?; - let partitions_str = args.partitions.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "ThreeMatroidIntersection requires --partitions\n\n\ - Usage: pred create ThreeMatroidIntersection --universe 6 --partitions \"0,1,2;3,4,5|0,3;1,4;2,5|0,4;1,5;2,3\" --bound 2" - ) - })?; - let matroids: Vec>> = partitions_str - .split('|') - .map(|matroid_str| { - matroid_str - .split(';') - .map(|group_str| { - group_str - .split(',') - .map(|s| { - s.trim().parse::().map_err(|_| { - anyhow::anyhow!( - "Invalid element in partitions: '{}'", - s.trim() - ) - }) - }) - .collect::>>() - }) - .collect::>>() - }) - .collect::>>()?; - if matroids.len() != 3 { - bail!( - "Expected exactly 3 partition matroids separated by '|', got {}", - matroids.len() - ); - } - ( - ser( - problemreductions::models::set::ThreeMatroidIntersection::new( - universe, matroids, bound, - ), - )?, - resolved_variant.clone(), - ) - } - - // SetBasis - "SetBasis" => { - let universe = args.universe.ok_or_else(|| { - anyhow::anyhow!( - "SetBasis requires --universe, --sets, and --k\n\n\ - Usage: pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3" - ) - })?; - let k = args.k.ok_or_else(|| { - anyhow::anyhow!( - "SetBasis requires --k\n\n\ - Usage: pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3" - ) - })?; - let sets = parse_sets(args)?; - for (i, set) in sets.iter().enumerate() { - for &element in set { - if element >= universe { - bail!( - "Set {} contains element {} which is outside universe of size {}", - i, - element, - universe - ); - } - } - } - ( - ser(problemreductions::models::set::SetBasis::new( - universe, sets, k, - ))?, - resolved_variant.clone(), - ) - } - - // MinimumCardinalityKey - "MinimumCardinalityKey" => { - let num_attributes = args.num_attributes.ok_or_else(|| { - anyhow::anyhow!( - "MinimumCardinalityKey requires --num-attributes and --dependencies\n\n\ - Usage: pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\"" - ) - })?; - let deps_str = args.dependencies.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumCardinalityKey requires --dependencies (e.g., \"0,1>2;0,2>3\")" - ) - })?; - let dependencies = parse_dependencies(deps_str)?; - ( - ser(problemreductions::models::set::MinimumCardinalityKey::new( - num_attributes, - dependencies, - ))?, - resolved_variant.clone(), - ) - } - - // TwoDimensionalConsecutiveSets - "TwoDimensionalConsecutiveSets" => { - let alphabet_size = args.alphabet_size.or(args.universe).ok_or_else(|| { - anyhow::anyhow!( - "TwoDimensionalConsecutiveSets requires --alphabet-size (or --universe) and --sets\n\n\ - Usage: pred create TwoDimensionalConsecutiveSets --alphabet-size 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"" - ) - })?; - let sets = parse_sets(args)?; - ( - ser( - problemreductions::models::set::TwoDimensionalConsecutiveSets::try_new( - alphabet_size, - sets, - ) - .map_err(anyhow::Error::msg)?, - )?, - resolved_variant.clone(), - ) - } - - // RootedTreeStorageAssignment - "RootedTreeStorageAssignment" => { - let usage = - "Usage: pred create RootedTreeStorageAssignment --universe 5 --sets \"0,2;1,3;0,4;2,4\" --bound 1"; - let universe_size = args.universe.ok_or_else(|| { - anyhow::anyhow!("RootedTreeStorageAssignment requires --universe\n\n{usage}") - })?; - let subsets = parse_sets(args)?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!("RootedTreeStorageAssignment requires --bound\n\n{usage}") - })?; - let bound = parse_nonnegative_usize_bound(bound, "RootedTreeStorageAssignment", usage)?; - ( - ser( - problemreductions::models::set::RootedTreeStorageAssignment::try_new( - universe_size, - subsets, - bound, - ) - .map_err(anyhow::Error::msg)?, - )?, - resolved_variant.clone(), - ) - } - - // BicliqueCover - "BicliqueCover" => { - let usage = "pred create BicliqueCover --left 2 --right 2 --biedges 0-0,0-1,1-1 --k 2"; - let (graph, k) = - parse_bipartite_problem_input(args, "BicliqueCover", "number of bicliques", usage)?; - (ser(BicliqueCover::new(graph, k))?, resolved_variant.clone()) - } - - // BalancedCompleteBipartiteSubgraph - "BalancedCompleteBipartiteSubgraph" => { - let usage = "pred create BalancedCompleteBipartiteSubgraph --left 4 --right 4 --biedges 0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3 --k 3"; - let (graph, k) = parse_bipartite_problem_input( - args, - "BalancedCompleteBipartiteSubgraph", - "balanced biclique size", - usage, - )?; - ( - ser(BalancedCompleteBipartiteSubgraph::new(graph, k))?, - resolved_variant.clone(), - ) - } - - // BMF - "BMF" => { - let matrix = parse_bool_matrix(args)?; - let rank = args.rank.ok_or_else(|| { - anyhow::anyhow!( - "BMF requires --matrix and --rank\n\n\ - Usage: pred create BMF --matrix \"1,0;0,1;1,1\" --rank 2" - ) - })?; - (ser(BMF::new(matrix, rank))?, resolved_variant.clone()) - } - - // ConsecutiveBlockMinimization - "ConsecutiveBlockMinimization" => { - let usage = "Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound 2"; - let matrix_str = args.matrix.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array and --bound\n\n{usage}" - ) - })?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!("ConsecutiveBlockMinimization requires --bound\n\n{usage}") - })?; - let matrix: Vec> = serde_json::from_str(matrix_str).map_err(|err| { - anyhow::anyhow!( - "ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array (e.g., '[[true,false,true],[false,true,true]]')\n\n{usage}\n\nFailed to parse --matrix: {err}" - ) - })?; - ( - ser(ConsecutiveBlockMinimization::try_new(matrix, bound) - .map_err(|err| anyhow::anyhow!("{err}\n\n{usage}"))?)?, - resolved_variant.clone(), - ) - } - - // RectilinearPictureCompression - "RectilinearPictureCompression" => { - let matrix = parse_bool_matrix(args)?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "RectilinearPictureCompression requires --matrix and --bound\n\n\ - Usage: pred create RectilinearPictureCompression --matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --bound 2" - ) - })?; - ( - ser(RectilinearPictureCompression::new(matrix, bound))?, - resolved_variant.clone(), - ) - } - - // ConsecutiveOnesSubmatrix - "ConsecutiveOnesSubmatrix" => { - let matrix = parse_bool_matrix(args)?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "ConsecutiveOnesSubmatrix requires --matrix and --bound\n\n\ - Usage: pred create ConsecutiveOnesSubmatrix --matrix \"1,1,0,1;1,0,1,1;0,1,1,0\" --bound 3" - ) - })?; - ( - ser(ConsecutiveOnesSubmatrix::new(matrix, bound))?, - resolved_variant.clone(), - ) - } - - // ConsecutiveOnesMatrixAugmentation - "ConsecutiveOnesMatrixAugmentation" => { - let matrix = parse_bool_matrix(args)?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "ConsecutiveOnesMatrixAugmentation requires --matrix and --bound\n\n\ - Usage: pred create ConsecutiveOnesMatrixAugmentation --matrix \"1,0,0,1,1;1,1,0,0,0;0,1,1,0,1;0,0,1,1,0\" --bound 2" - ) - })?; - ( - ser(ConsecutiveOnesMatrixAugmentation::try_new(matrix, bound) - .map_err(|e| anyhow::anyhow!(e))?)?, - resolved_variant.clone(), - ) - } - - // SparseMatrixCompression - "SparseMatrixCompression" => { - let matrix = parse_bool_matrix(args)?; - let usage = "Usage: pred create SparseMatrixCompression --matrix \"1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0\" --bound 2"; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!("SparseMatrixCompression requires --matrix and --bound\n\n{usage}") - })?; - let bound = parse_nonnegative_usize_bound(bound, "SparseMatrixCompression", usage)?; - if bound == 0 { - anyhow::bail!("SparseMatrixCompression requires bound >= 1\n\n{usage}"); - } - ( - ser(SparseMatrixCompression::new(matrix, bound))?, - resolved_variant.clone(), - ) - } - - // MaximumLikelihoodRanking - "MaximumLikelihoodRanking" => { - let usage = "Usage: pred create MaximumLikelihoodRanking --matrix \"0,4,3,5;1,0,4,3;2,1,0,4;0,2,1,0\""; - let matrix_str = args.matrix.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MaximumLikelihoodRanking requires --matrix (semicolon-separated i32 rows)\n\n{usage}" - ) - })?; - let matrix_i64 = parse_i64_matrix(matrix_str).context("Invalid matrix")?; - let matrix: Vec> = matrix_i64 - .into_iter() - .map(|row| { - row.into_iter() - .map(|v| { - i32::try_from(v) - .map_err(|_| anyhow::anyhow!("matrix value {v} out of i32 range")) - }) - .collect::>>() - }) - .collect::>>()?; - ( - ser(MaximumLikelihoodRanking::new(matrix))?, - resolved_variant.clone(), - ) - } - - // MinimumMatrixCover - "MinimumMatrixCover" => { - let usage = "Usage: pred create MinimumMatrixCover --matrix \"0,3,1,0;3,0,0,2;1,0,0,4;0,2,4,0\""; - let matrix_str = args.matrix.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumMatrixCover requires --matrix (semicolon-separated i64 rows)\n\n{usage}" - ) - })?; - let matrix = parse_i64_matrix(matrix_str).context("Invalid matrix")?; - ( - ser(MinimumMatrixCover::new(matrix))?, - resolved_variant.clone(), - ) - } - - // MinimumMatrixDomination - "MinimumMatrixDomination" => { - let matrix = parse_bool_matrix(args)?; - ( - ser(MinimumMatrixDomination::new(matrix))?, - resolved_variant.clone(), - ) - } - - // MinimumWeightDecoding - "MinimumWeightDecoding" => { - let usage = "Usage: pred create MinimumWeightDecoding --matrix '[[true,false,true],[false,true,true]]' --rhs 'true,true'"; - let matrix_str = args.matrix.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumWeightDecoding requires --matrix (JSON 2D bool array) and --rhs\n\n{usage}" - ) - })?; - let matrix: Vec> = serde_json::from_str(matrix_str).map_err(|err| { - anyhow::anyhow!( - "MinimumWeightDecoding requires --matrix as a JSON 2D bool array (e.g., '[[true,false],[false,true]]')\n\n{usage}\n\nFailed to parse --matrix: {err}" - ) - })?; - let rhs_str = args.rhs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumWeightDecoding requires --rhs (comma-separated booleans)\n\n{usage}" - ) - })?; - let target: Vec = rhs_str - .split(',') - .map(|s| match s.trim() { - "true" | "1" => Ok(true), - "false" | "0" => Ok(false), - other => Err(anyhow::anyhow!("invalid boolean value: {other}")), - }) - .collect::, _>>() - .map_err(|err| { - anyhow::anyhow!( - "Failed to parse --rhs as comma-separated booleans: {err}\n\n{usage}" - ) - })?; - ( - ser(MinimumWeightDecoding::new(matrix, target))?, - resolved_variant.clone(), - ) - } - - // MinimumWeightSolutionToLinearEquations - "MinimumWeightSolutionToLinearEquations" => { - let usage = "Usage: pred create MinimumWeightSolutionToLinearEquations --matrix '[[1,2,3,1],[2,1,1,3]]' --rhs '5,4'"; - let matrix_str = args.matrix.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumWeightSolutionToLinearEquations requires --matrix (JSON 2D i64 array) and --rhs\n\n{usage}" - ) - })?; - let matrix: Vec> = serde_json::from_str(matrix_str).map_err(|err| { - anyhow::anyhow!( - "MinimumWeightSolutionToLinearEquations requires --matrix as a JSON 2D integer array (e.g., '[[1,2,3],[4,5,6]]')\n\n{usage}\n\nFailed to parse --matrix: {err}" - ) - })?; - let rhs_str = args.rhs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumWeightSolutionToLinearEquations requires --rhs (comma-separated integers)\n\n{usage}" - ) - })?; - let rhs: Vec = rhs_str - .split(',') - .map(|s| s.trim().parse::()) - .collect::, _>>() - .map_err(|err| { - anyhow::anyhow!( - "Failed to parse --rhs as comma-separated integers: {err}\n\n{usage}" - ) - })?; - ( - ser(MinimumWeightSolutionToLinearEquations::new(matrix, rhs))?, - resolved_variant.clone(), - ) - } - - // FeasibleBasisExtension - "FeasibleBasisExtension" => { - let usage = "Usage: pred create FeasibleBasisExtension --matrix '[[1,0,1],[0,1,0]]' --rhs '7,5' --required-columns '0'"; - let matrix_str = args.matrix.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "FeasibleBasisExtension requires --matrix (JSON 2D i64 array), --rhs, and --required-columns\n\n{usage}" - ) - })?; - let matrix: Vec> = serde_json::from_str(matrix_str).map_err(|err| { - anyhow::anyhow!( - "FeasibleBasisExtension requires --matrix as a JSON 2D integer array (e.g., '[[1,0,1],[0,1,0]]')\n\n{usage}\n\nFailed to parse --matrix: {err}" - ) - })?; - let rhs_str = args.rhs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "FeasibleBasisExtension requires --rhs (comma-separated integers)\n\n{usage}" - ) - })?; - let rhs: Vec = rhs_str - .split(',') - .map(|s| s.trim().parse::()) - .collect::, _>>() - .map_err(|err| { - anyhow::anyhow!( - "Failed to parse --rhs as comma-separated integers: {err}\n\n{usage}" - ) - })?; - let required_str = args.required_columns.as_deref().unwrap_or(""); - let required_columns: Vec = if required_str.is_empty() { - vec![] - } else { - required_str - .split(',') - .map(|s| s.trim().parse::()) - .collect::, _>>() - .map_err(|err| { - anyhow::anyhow!( - "Failed to parse --required-columns as comma-separated indices: {err}\n\n{usage}" - ) - })? - }; - ( - ser(FeasibleBasisExtension::new(matrix, rhs, required_columns))?, - resolved_variant.clone(), - ) - } - - // LongestCommonSubsequence - "LongestCommonSubsequence" => { - let usage = - "Usage: pred create LCS --strings \"010110;100101;001011\" [--alphabet-size 2]"; - let strings_str = args.strings.as_deref().ok_or_else(|| { - anyhow::anyhow!("LongestCommonSubsequence requires --strings\n\n{usage}") - })?; - - let segments: Vec<&str> = strings_str.split(';').map(str::trim).collect(); - let comma_mode = segments.iter().any(|segment| segment.contains(',')); - - let (strings, inferred_alphabet_size): (Vec>, usize) = if comma_mode { - let strings = segments - .iter() - .map(|segment| { - if segment.is_empty() { - return Ok(Vec::new()); - } - segment - .split(',') - .map(|value| { - value.trim().parse::().map_err(|e| { - anyhow::anyhow!("Invalid LCS alphabet index: {}", e) - }) - }) - .collect::>>() - }) - .collect::>>()?; - let inferred = strings - .iter() - .flat_map(|string| string.iter()) - .copied() - .max() - .map(|value| value + 1) - .unwrap_or(0); - (strings, inferred) - } else { - let mut encoding = BTreeMap::new(); - let mut next_symbol = 0usize; - let strings = segments - .iter() - .map(|segment| { - segment - .as_bytes() - .iter() - .map(|byte| { - let entry = encoding.entry(*byte).or_insert_with(|| { - let current = next_symbol; - next_symbol += 1; - current - }); - *entry - }) - .collect::>() - }) - .collect::>(); - (strings, next_symbol) - }; - - let alphabet_size = args.alphabet_size.unwrap_or(inferred_alphabet_size); - anyhow::ensure!( - alphabet_size >= inferred_alphabet_size, - "--alphabet-size {} is smaller than the inferred alphabet size ({})", - alphabet_size, - inferred_alphabet_size - ); - anyhow::ensure!( - strings.iter().any(|string| !string.is_empty()), - "LongestCommonSubsequence requires at least one non-empty string.\n\n{usage}" - ); - anyhow::ensure!( - alphabet_size > 0, - "LongestCommonSubsequence requires a positive alphabet. Provide --alphabet-size when all strings are empty.\n\n{usage}" - ); - ( - ser(LongestCommonSubsequence::new(alphabet_size, strings))?, - resolved_variant.clone(), - ) - } - - // GroupingBySwapping - "GroupingBySwapping" => { - let usage = - "Usage: pred create GroupingBySwapping --string \"0,1,2,0,1,2\" --bound 5 [--alphabet-size 3]"; - let string_str = args.string.as_deref().ok_or_else(|| { - anyhow::anyhow!("GroupingBySwapping requires --string\n\n{usage}") - })?; - let bound = parse_nonnegative_usize_bound( - args.bound.ok_or_else(|| { - anyhow::anyhow!("GroupingBySwapping requires --bound\n\n{usage}") - })?, - "GroupingBySwapping", - usage, - )?; - - let string = if string_str.trim().is_empty() { - Vec::new() - } else { - string_str - .split(',') - .map(|value| { - value - .trim() - .parse::() - .context("invalid symbol index") - }) - .collect::>>()? - }; - let inferred = string.iter().copied().max().map_or(0, |value| value + 1); - let alphabet_size = args.alphabet_size.unwrap_or(inferred); - anyhow::ensure!( - alphabet_size >= inferred, - "--alphabet-size {} is smaller than max symbol + 1 ({}) in the input string", - alphabet_size, - inferred - ); - anyhow::ensure!( - alphabet_size > 0 || string.is_empty(), - "GroupingBySwapping requires a positive alphabet for non-empty strings.\n\n{usage}" - ); - anyhow::ensure!( - !string.is_empty() || bound == 0, - "GroupingBySwapping requires --bound 0 when --string is empty.\n\n{usage}" - ); - ( - ser(GroupingBySwapping::new(alphabet_size, string, bound))?, - resolved_variant.clone(), - ) - } - - // MinimumExternalMacroDataCompression - "MinimumExternalMacroDataCompression" => { - let usage = "Usage: pred create MinimumExternalMacroDataCompression --string \"0,1,0,1\" --pointer-cost 2 [--alphabet-size 2]"; - let string_str = args.string.as_deref().ok_or_else(|| { - anyhow::anyhow!("MinimumExternalMacroDataCompression requires --string\n\n{usage}") - })?; - let pointer_cost = args.pointer_cost.ok_or_else(|| { - anyhow::anyhow!( - "MinimumExternalMacroDataCompression requires --pointer-cost\n\n{usage}" - ) - })?; - anyhow::ensure!( - pointer_cost > 0, - "--pointer-cost must be a positive integer\n\n{usage}" - ); - - let string: Vec = if string_str.trim().is_empty() { - Vec::new() - } else { - string_str - .split(',') - .map(|value| { - value - .trim() - .parse::() - .context("invalid symbol index") - }) - .collect::>>()? - }; - let inferred = string.iter().copied().max().map_or(0, |value| value + 1); - let alphabet_size = args.alphabet_size.unwrap_or(inferred); - anyhow::ensure!( - alphabet_size >= inferred, - "--alphabet-size {} is smaller than max symbol + 1 ({}) in the input string", - alphabet_size, - inferred - ); - anyhow::ensure!( - alphabet_size > 0 || string.is_empty(), - "MinimumExternalMacroDataCompression requires a positive alphabet for non-empty strings.\n\n{usage}" - ); - ( - ser(MinimumExternalMacroDataCompression::new( - alphabet_size, - string, - pointer_cost, - ))?, - resolved_variant.clone(), - ) - } - - // MinimumInternalMacroDataCompression - "MinimumInternalMacroDataCompression" => { - let usage = "Usage: pred create MinimumInternalMacroDataCompression --string \"0,1,0,1\" --pointer-cost 2 [--alphabet-size 2]"; - let string_str = args.string.as_deref().ok_or_else(|| { - anyhow::anyhow!("MinimumInternalMacroDataCompression requires --string\n\n{usage}") - })?; - let pointer_cost = args.pointer_cost.ok_or_else(|| { - anyhow::anyhow!( - "MinimumInternalMacroDataCompression requires --pointer-cost\n\n{usage}" - ) - })?; - anyhow::ensure!( - pointer_cost > 0, - "--pointer-cost must be a positive integer\n\n{usage}" - ); - - let string: Vec = if string_str.trim().is_empty() { - Vec::new() - } else { - string_str - .split(',') - .map(|value| { - value - .trim() - .parse::() - .context("invalid symbol index") - }) - .collect::>>()? - }; - let inferred = string.iter().copied().max().map_or(0, |value| value + 1); - let alphabet_size = args.alphabet_size.unwrap_or(inferred); - anyhow::ensure!( - alphabet_size >= inferred, - "--alphabet-size {} is smaller than max symbol + 1 ({}) in the input string", - alphabet_size, - inferred - ); - anyhow::ensure!( - alphabet_size > 0 || string.is_empty(), - "MinimumInternalMacroDataCompression requires a positive alphabet for non-empty strings.\n\n{usage}" - ); - ( - ser(MinimumInternalMacroDataCompression::new( - alphabet_size, - string, - pointer_cost, - ))?, - resolved_variant.clone(), - ) - } - - // ClosestVectorProblem - "ClosestVectorProblem" => { - let basis_str = args.basis.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "CVP requires --basis, --target-vec\n\n\ - Usage: pred create CVP --basis \"1,0;0,1\" --target-vec \"0.5,0.5\"" - ) - })?; - let target_str = args - .target_vec - .as_deref() - .ok_or_else(|| anyhow::anyhow!("CVP requires --target-vec (e.g., \"0.5,0.5\")"))?; - let basis: Vec> = basis_str - .split(';') - .map(|row| util::parse_comma_list(row.trim())) - .collect::>>()?; - let target: Vec = util::parse_comma_list(target_str)?; - let n = basis.len(); - let (lo, hi) = match args.bounds.as_deref() { - Some(s) => { - let parts: Vec = util::parse_comma_list(s)?; - if parts.len() != 2 { - bail!("--bounds expects \"lower,upper\" (e.g., \"-10,10\")"); - } - (parts[0], parts[1]) - } - None => (-10, 10), - }; - let bounds = vec![problemreductions::models::algebraic::VarBounds::bounded(lo, hi); n]; - ( - ser(ClosestVectorProblem::new(basis, target, bounds))?, - resolved_variant.clone(), - ) - } - - // ResourceConstrainedScheduling - "ResourceConstrainedScheduling" => { - let usage = "Usage: pred create ResourceConstrainedScheduling --num-processors 3 --resource-bounds \"20\" --resource-requirements \"6;7;7;6;8;6\" --deadline 2"; - let num_processors = args.num_processors.ok_or_else(|| { - anyhow::anyhow!( - "ResourceConstrainedScheduling requires --num-processors\n\n{usage}" - ) - })?; - let bounds_str = args.resource_bounds.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "ResourceConstrainedScheduling requires --resource-bounds\n\n{usage}" - ) - })?; - let reqs_str = args.resource_requirements.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "ResourceConstrainedScheduling requires --resource-requirements\n\n{usage}" - ) - })?; - let deadline = args.deadline.ok_or_else(|| { - anyhow::anyhow!("ResourceConstrainedScheduling requires --deadline\n\n{usage}") - })?; - - let resource_bounds: Vec = util::parse_comma_list(bounds_str)?; - let resource_requirements: Vec> = reqs_str - .split(';') - .map(|row| util::parse_comma_list(row.trim())) - .collect::>>()?; - - ( - ser(ResourceConstrainedScheduling::new( - num_processors, - resource_bounds, - resource_requirements, - deadline, - ))?, - resolved_variant.clone(), - ) - } - - // MultiprocessorScheduling - "MultiprocessorScheduling" => { - let usage = "Usage: pred create MultiprocessorScheduling --lengths 4,5,3,2,6 --num-processors 2 --deadline 10"; - let lengths_str = args.lengths.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MultiprocessorScheduling requires --lengths, --num-processors, and --deadline\n\n{usage}" - ) - })?; - let num_processors = args.num_processors.ok_or_else(|| { - anyhow::anyhow!("MultiprocessorScheduling requires --num-processors\n\n{usage}") - })?; - if num_processors == 0 { - bail!("MultiprocessorScheduling requires --num-processors > 0\n\n{usage}"); - } - let deadline = args.deadline.ok_or_else(|| { - anyhow::anyhow!("MultiprocessorScheduling requires --deadline\n\n{usage}") - })?; - let lengths: Vec = util::parse_comma_list(lengths_str)?; - ( - ser(MultiprocessorScheduling::new( - lengths, - num_processors, - deadline, - ))?, - resolved_variant.clone(), - ) - } - - "ProductionPlanning" => { - let usage = "Usage: pred create ProductionPlanning --num-periods 6 --demands 5,3,7,2,8,5 --capacities 12,12,12,12,12,12 --setup-costs 10,10,10,10,10,10 --production-costs 1,1,1,1,1,1 --inventory-costs 1,1,1,1,1,1 --cost-bound 80"; - let num_periods = args.num_periods.ok_or_else(|| { - anyhow::anyhow!("ProductionPlanning requires --num-periods\n\n{usage}") - })?; - let demands = parse_named_u64_list( - args.demands.as_deref(), - "ProductionPlanning", - "--demands", - usage, - )?; - let capacities = parse_named_u64_list( - args.capacities.as_deref(), - "ProductionPlanning", - "--capacities", - usage, - )?; - let setup_costs = parse_named_u64_list( - args.setup_costs.as_deref(), - "ProductionPlanning", - "--setup-costs", - usage, - )?; - let production_costs = parse_named_u64_list( - args.production_costs.as_deref(), - "ProductionPlanning", - "--production-costs", - usage, - )?; - let inventory_costs = parse_named_u64_list( - args.inventory_costs.as_deref(), - "ProductionPlanning", - "--inventory-costs", - usage, - )?; - let cost_bound = args.cost_bound.ok_or_else(|| { - anyhow::anyhow!("ProductionPlanning requires --cost-bound\n\n{usage}") - })? as u64; - - for (flag, len) in [ - ("--demands", demands.len()), - ("--capacities", capacities.len()), - ("--setup-costs", setup_costs.len()), - ("--production-costs", production_costs.len()), - ("--inventory-costs", inventory_costs.len()), - ] { - ensure_named_len(len, num_periods, flag, usage)?; - } - - ( - ser(ProductionPlanning::new( - num_periods, - demands, - capacities, - setup_costs, - production_costs, - inventory_costs, - cost_bound, - ))?, - resolved_variant.clone(), - ) - } - - // PreemptiveScheduling - "PreemptiveScheduling" => { - let usage = "Usage: pred create PreemptiveScheduling --sizes 2,1,3,2,1 --num-processors 2 [--precedence-pairs \"0>2,1>3\"]"; - let sizes_str = args.sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "PreemptiveScheduling requires --sizes and --num-processors\n\n{usage}" - ) - })?; - let num_processors = args.num_processors.ok_or_else(|| { - anyhow::anyhow!("PreemptiveScheduling requires --num-processors\n\n{usage}") - })?; - anyhow::ensure!( - num_processors > 0, - "PreemptiveScheduling requires --num-processors > 0\n\n{usage}" - ); - let lengths: Vec = util::parse_comma_list(sizes_str)?; - anyhow::ensure!( - lengths.iter().all(|&l| l > 0), - "PreemptiveScheduling: all task lengths must be positive\n\n{usage}" - ); - let num_tasks = lengths.len(); - let precedences: Vec<(usize, usize)> = match args.precedence_pairs.as_deref() { - Some(s) if !s.is_empty() => s - .split(',') - .map(|pair| { - let parts: Vec<&str> = pair.trim().split('>').collect(); - anyhow::ensure!( - parts.len() == 2, - "Invalid precedence format '{}', expected 'u>v'", - pair.trim() - ); - Ok(( - parts[0].trim().parse::()?, - parts[1].trim().parse::()?, - )) - }) - .collect::>>()?, - _ => vec![], - }; - for &(pred, succ) in &precedences { - anyhow::ensure!( - pred < num_tasks && succ < num_tasks, - "precedence index out of range: ({}, {}) but num_tasks = {}", - pred, - succ, - num_tasks - ); - } - ( - ser(PreemptiveScheduling::new( - lengths, - num_processors, - precedences, - ))?, - resolved_variant.clone(), - ) - } - - // SchedulingToMinimizeWeightedCompletionTime - "SchedulingToMinimizeWeightedCompletionTime" => { - let usage = "Usage: pred create SchedulingToMinimizeWeightedCompletionTime --lengths 1,2,3,4,5 --weights 6,4,3,2,1 --num-processors 2"; - let lengths_str = args.lengths.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SchedulingToMinimizeWeightedCompletionTime requires --lengths, --weights, and --num-processors\n\n{usage}" - ) - })?; - let weights_str = args.weights.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SchedulingToMinimizeWeightedCompletionTime requires --weights\n\n{usage}" - ) - })?; - let num_processors = args.num_processors.ok_or_else(|| { - anyhow::anyhow!("SchedulingToMinimizeWeightedCompletionTime requires --num-processors\n\n{usage}") - })?; - if num_processors == 0 { - bail!("SchedulingToMinimizeWeightedCompletionTime requires --num-processors > 0\n\n{usage}"); - } - let lengths: Vec = util::parse_comma_list(lengths_str)?; - let weights: Vec = util::parse_comma_list(weights_str)?; - ( - ser(SchedulingToMinimizeWeightedCompletionTime::new( - lengths, - weights, - num_processors, - ))?, - resolved_variant.clone(), - ) - } - - "CapacityAssignment" => { - let usage = "Usage: pred create CapacityAssignment --capacities 1,2,3 --cost-matrix \"1,3,6;2,4,7;1,2,5\" --delay-matrix \"8,4,1;7,3,1;6,3,1\" --delay-budget 12"; - let capacities_str = args.capacities.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "CapacityAssignment requires --capacities, --cost-matrix, --delay-matrix, and --delay-budget\n\n{usage}" - ) - })?; - let cost_matrix_str = args.cost_matrix.as_deref().ok_or_else(|| { - anyhow::anyhow!("CapacityAssignment requires --cost-matrix\n\n{usage}") - })?; - let delay_matrix_str = args.delay_matrix.as_deref().ok_or_else(|| { - anyhow::anyhow!("CapacityAssignment requires --delay-matrix\n\n{usage}") - })?; - let delay_budget = args.delay_budget.ok_or_else(|| { - anyhow::anyhow!("CapacityAssignment requires --delay-budget\n\n{usage}") - })?; - - let capacities: Vec = util::parse_comma_list(capacities_str)?; - anyhow::ensure!( - !capacities.is_empty(), - "CapacityAssignment requires at least one capacity value\n\n{usage}" - ); - anyhow::ensure!( - capacities.iter().all(|&capacity| capacity > 0), - "CapacityAssignment capacities must be positive\n\n{usage}" - ); - anyhow::ensure!( - capacities.windows(2).all(|w| w[0] < w[1]), - "CapacityAssignment capacities must be strictly increasing\n\n{usage}" - ); - - let cost = parse_u64_matrix_rows(cost_matrix_str, "cost")?; - let delay = parse_u64_matrix_rows(delay_matrix_str, "delay")?; - anyhow::ensure!( - cost.len() == delay.len(), - "cost matrix row count ({}) must match delay matrix row count ({})\n\n{usage}", - cost.len(), - delay.len() - ); - - for (index, row) in cost.iter().enumerate() { - anyhow::ensure!( - row.len() == capacities.len(), - "cost row {} length ({}) must match capacities length ({})\n\n{usage}", - index, - row.len(), - capacities.len() - ); - anyhow::ensure!( - row.windows(2).all(|w| w[0] <= w[1]), - "cost row {} must be non-decreasing\n\n{usage}", - index - ); - } - for (index, row) in delay.iter().enumerate() { - anyhow::ensure!( - row.len() == capacities.len(), - "delay row {} length ({}) must match capacities length ({})\n\n{usage}", - index, - row.len(), - capacities.len() - ); - anyhow::ensure!( - row.windows(2).all(|w| w[0] >= w[1]), - "delay row {} must be non-increasing\n\n{usage}", - index - ); - } - - ( - ser(CapacityAssignment::new( - capacities, - cost, - delay, - delay_budget, - ))?, - resolved_variant.clone(), - ) - } - - // MinimumMultiwayCut - "MinimumMultiwayCut" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create MinimumMultiwayCut --graph 0-1,1-2,2-3 --terminals 0,2 [--edge-weights 1,1,1]" - ) - })?; - let terminals = parse_terminals(args, graph.num_vertices())?; - let edge_weights = parse_edge_weights(args, graph.num_edges())?; - ( - ser(MinimumMultiwayCut::new(graph, terminals, edge_weights))?, - resolved_variant.clone(), - ) - } - - // MinimumTardinessSequencing - "MinimumTardinessSequencing" => { - let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumTardinessSequencing requires --deadlines\n\n\ - Usage: pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 [--precedence-pairs \"0>3,1>3,1>4,2>4\"] [--sizes 3,2,2,1,2]" - ) - })?; - let deadlines: Vec = util::parse_comma_list(deadlines_str)?; - let precedences = parse_precedence_pairs(args.precedence_pairs.as_deref())?; - - if let Some(sizes_str) = args.sizes.as_deref() { - // Arbitrary-length variant (W = i32) - let lengths: Vec = util::parse_comma_list(sizes_str)?; - anyhow::ensure!( - lengths.len() == deadlines.len(), - "sizes length ({}) must equal deadlines length ({})", - lengths.len(), - deadlines.len() - ); - validate_precedence_pairs(&precedences, lengths.len())?; - ( - ser(MinimumTardinessSequencing::::with_lengths( - lengths, - deadlines, - precedences, - ))?, - resolved_variant.clone(), - ) - } else { - // Unit-length variant (W = One) - let num_tasks = args.n.ok_or_else(|| { - anyhow::anyhow!( - "MinimumTardinessSequencing requires --n (number of tasks) or --sizes\n\n\ - Usage: pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3" - ) - })?; - anyhow::ensure!( - deadlines.len() == num_tasks, - "deadlines length ({}) must equal num_tasks ({})", - deadlines.len(), - num_tasks - ); - validate_precedence_pairs(&precedences, num_tasks)?; - ( - ser(MinimumTardinessSequencing::::new( - num_tasks, - deadlines, - precedences, - ))?, - resolved_variant.clone(), - ) - } - } - - // SchedulingWithIndividualDeadlines - "SchedulingWithIndividualDeadlines" => { - let usage = "Usage: pred create SchedulingWithIndividualDeadlines --n 7 --deadlines 2,1,2,2,3,3,2 [--num-processors 3 | --m 3] [--precedence-pairs \"0>3,1>3,1>4,2>4,2>5\"]"; - let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SchedulingWithIndividualDeadlines requires --deadlines, --n, and a processor count (--num-processors or --m)\n\n{usage}" - ) - })?; - let num_tasks = args.n.ok_or_else(|| { - anyhow::anyhow!( - "SchedulingWithIndividualDeadlines requires --n (number of tasks)\n\n{usage}" - ) - })?; - let num_processors = resolve_processor_count_flags( - "SchedulingWithIndividualDeadlines", - usage, - args.num_processors, - args.m, - )? - .ok_or_else(|| { - anyhow::anyhow!( - "SchedulingWithIndividualDeadlines requires --num-processors or --m\n\n{usage}" - ) - })?; - let deadlines: Vec = util::parse_comma_list(deadlines_str)?; - let precedences: Vec<(usize, usize)> = match args.precedence_pairs.as_deref() { - Some(s) if !s.is_empty() => s - .split(',') - .map(|pair| { - let parts: Vec<&str> = pair.trim().split('>').collect(); - anyhow::ensure!( - parts.len() == 2, - "Invalid precedence format '{}', expected 'u>v'", - pair.trim() - ); - Ok(( - parts[0].trim().parse::()?, - parts[1].trim().parse::()?, - )) - }) - .collect::>>()?, - _ => vec![], - }; - anyhow::ensure!( - deadlines.len() == num_tasks, - "deadlines length ({}) must equal num_tasks ({})", - deadlines.len(), - num_tasks - ); - for &(pred, succ) in &precedences { - anyhow::ensure!( - pred < num_tasks && succ < num_tasks, - "precedence index out of range: ({}, {}) but num_tasks = {}", - pred, - succ, - num_tasks - ); - } - ( - ser(SchedulingWithIndividualDeadlines::new( - num_tasks, - num_processors, - deadlines, - precedences, - ))?, - resolved_variant.clone(), - ) - } - - // SequencingToMinimizeTardyTaskWeight - "SequencingToMinimizeTardyTaskWeight" => { - let sizes_str = args.sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingToMinimizeTardyTaskWeight requires --sizes, --weights, and --deadlines\n\n\ - Usage: pred create SequencingToMinimizeTardyTaskWeight --sizes 3,2,4,1,2 --weights 5,3,7,2,4 --deadlines 6,4,10,2,8" - ) - })?; - let weights_str = args.weights.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingToMinimizeTardyTaskWeight requires --weights\n\n\ - Usage: pred create SequencingToMinimizeTardyTaskWeight --sizes 3,2,4,1,2 --weights 5,3,7,2,4 --deadlines 6,4,10,2,8" - ) - })?; - let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingToMinimizeTardyTaskWeight requires --deadlines\n\n\ - Usage: pred create SequencingToMinimizeTardyTaskWeight --sizes 3,2,4,1,2 --weights 5,3,7,2,4 --deadlines 6,4,10,2,8" - ) - })?; - let lengths: Vec = util::parse_comma_list(sizes_str)?; - let weights: Vec = util::parse_comma_list(weights_str)?; - let deadlines: Vec = util::parse_comma_list(deadlines_str)?; - anyhow::ensure!( - lengths.len() == weights.len(), - "sizes length ({}) must equal weights length ({})", - lengths.len(), - weights.len() - ); - anyhow::ensure!( - lengths.len() == deadlines.len(), - "sizes length ({}) must equal deadlines length ({})", - lengths.len(), - deadlines.len() - ); - anyhow::ensure!( - lengths.iter().all(|&l| l > 0), - "task lengths must be positive" - ); - anyhow::ensure!( - weights.iter().all(|&w| w > 0), - "task weights must be positive" - ); - ( - ser(SequencingToMinimizeTardyTaskWeight::new( - lengths, weights, deadlines, - ))?, - resolved_variant.clone(), - ) - } - - // SequencingWithDeadlinesAndSetUpTimes - "SequencingWithDeadlinesAndSetUpTimes" => { - let sizes_str = args.sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingWithDeadlinesAndSetUpTimes requires --sizes, --deadlines, --compilers, and --setup-times\n\n\ - Usage: pred create SequencingWithDeadlinesAndSetUpTimes --sizes 2,3,1,2,2 --deadlines 4,11,3,16,7 --compilers 0,1,0,1,0 --setup-times 1,2" - ) - })?; - let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingWithDeadlinesAndSetUpTimes requires --deadlines\n\n\ - Usage: pred create SequencingWithDeadlinesAndSetUpTimes --sizes 2,3,1,2,2 --deadlines 4,11,3,16,7 --compilers 0,1,0,1,0 --setup-times 1,2" - ) - })?; - let compilers_str = args.compilers.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingWithDeadlinesAndSetUpTimes requires --compilers\n\n\ - Usage: pred create SequencingWithDeadlinesAndSetUpTimes --sizes 2,3,1,2,2 --deadlines 4,11,3,16,7 --compilers 0,1,0,1,0 --setup-times 1,2" - ) - })?; - let setup_times_str = args.setup_times.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingWithDeadlinesAndSetUpTimes requires --setup-times\n\n\ - Usage: pred create SequencingWithDeadlinesAndSetUpTimes --sizes 2,3,1,2,2 --deadlines 4,11,3,16,7 --compilers 0,1,0,1,0 --setup-times 1,2" - ) - })?; - let lengths: Vec = util::parse_comma_list(sizes_str)?; - let deadlines: Vec = util::parse_comma_list(deadlines_str)?; - let compilers: Vec = util::parse_comma_list(compilers_str)?; - let setup_times: Vec = util::parse_comma_list(setup_times_str)?; - anyhow::ensure!( - lengths.len() == deadlines.len(), - "lengths length ({}) must equal deadlines length ({})", - lengths.len(), - deadlines.len() - ); - anyhow::ensure!( - lengths.len() == compilers.len(), - "lengths length ({}) must equal compilers length ({})", - lengths.len(), - compilers.len() - ); - anyhow::ensure!( - lengths.iter().all(|&l| l > 0), - "task lengths must be positive" - ); - let num_compilers = setup_times.len(); - for &c in &compilers { - anyhow::ensure!( - c < num_compilers, - "compiler index {c} is out of range for setup_times of length {num_compilers}" - ); - } - ( - ser(SequencingWithDeadlinesAndSetUpTimes::new( - lengths, - deadlines, - compilers, - setup_times, - ))?, - resolved_variant.clone(), - ) - } - - // SequencingToMinimizeWeightedCompletionTime - "SequencingToMinimizeWeightedCompletionTime" => { - let lengths_str = args.lengths.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingToMinimizeWeightedCompletionTime requires --lengths and --weights\n\n\ - Usage: pred create SequencingToMinimizeWeightedCompletionTime --lengths 2,1,3,1,2 --weights 3,5,1,4,2 [--precedence-pairs \"0>2,1>4\"]" - ) - })?; - let weights_str = args.weights.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingToMinimizeWeightedCompletionTime requires --weights\n\n\ - Usage: pred create SequencingToMinimizeWeightedCompletionTime --lengths 2,1,3,1,2 --weights 3,5,1,4,2" - ) - })?; - let lengths: Vec = util::parse_comma_list(lengths_str)?; - let weights: Vec = util::parse_comma_list(weights_str)?; - anyhow::ensure!( - lengths.len() == weights.len(), - "lengths length ({}) must equal weights length ({})", - lengths.len(), - weights.len() - ); - anyhow::ensure!( - lengths.iter().all(|&length| length > 0), - "task lengths must be positive" - ); - let num_tasks = lengths.len(); - let precedences: Vec<(usize, usize)> = match args.precedence_pairs.as_deref() { - Some(s) if !s.is_empty() => s - .split(',') - .map(|pair| { - let parts: Vec<&str> = pair.trim().split('>').collect(); - anyhow::ensure!( - parts.len() == 2, - "Invalid precedence format '{}', expected 'u>v'", - pair.trim() - ); - Ok(( - parts[0].trim().parse::()?, - parts[1].trim().parse::()?, - )) - }) - .collect::>>()?, - _ => vec![], - }; - for &(pred, succ) in &precedences { - anyhow::ensure!( - pred < num_tasks && succ < num_tasks, - "precedence index out of range: ({}, {}) but num_tasks = {}", - pred, - succ, - num_tasks - ); - } - ( - ser(SequencingToMinimizeWeightedCompletionTime::new( - lengths, - weights, - precedences, - ))?, - resolved_variant.clone(), - ) - } - - // SequencingToMinimizeWeightedTardiness - "SequencingToMinimizeWeightedTardiness" => { - let sizes_str = args.sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingToMinimizeWeightedTardiness requires --sizes, --weights, --deadlines, and --bound\n\n\ - Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" - ) - })?; - let weights_str = args.weights.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingToMinimizeWeightedTardiness requires --weights (comma-separated tardiness weights)\n\n\ - Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" - ) - })?; - let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingToMinimizeWeightedTardiness requires --deadlines (comma-separated job deadlines)\n\n\ - Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" - ) - })?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "SequencingToMinimizeWeightedTardiness requires --bound\n\n\ - Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" - ) - })?; - anyhow::ensure!(bound >= 0, "--bound must be non-negative"); - - let lengths: Vec = util::parse_comma_list(sizes_str)?; - let weights: Vec = util::parse_comma_list(weights_str)?; - let deadlines: Vec = util::parse_comma_list(deadlines_str)?; - - anyhow::ensure!( - lengths.len() == weights.len(), - "sizes length ({}) must equal weights length ({})", - lengths.len(), - weights.len() - ); - anyhow::ensure!( - lengths.len() == deadlines.len(), - "sizes length ({}) must equal deadlines length ({})", - lengths.len(), - deadlines.len() - ); - - ( - ser(SequencingToMinimizeWeightedTardiness::new( - lengths, - weights, - deadlines, - bound as u64, - ))?, - resolved_variant.clone(), - ) - } - - // SequencingToMinimizeMaximumCumulativeCost - "SequencingToMinimizeMaximumCumulativeCost" => { - let costs_str = args.costs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingToMinimizeMaximumCumulativeCost requires --costs\n\n\ - Usage: pred create SequencingToMinimizeMaximumCumulativeCost --costs 2,-1,3,-2,1,-3 --precedence-pairs \"0>2,1>2,1>3,2>4,3>5,4>5\"" - ) - })?; - let costs: Vec = util::parse_comma_list(costs_str)?; - let precedences = parse_precedence_pairs(args.precedence_pairs.as_deref())?; - validate_precedence_pairs(&precedences, costs.len())?; - ( - ser(SequencingToMinimizeMaximumCumulativeCost::new( - costs, - precedences, - ))?, - resolved_variant.clone(), - ) - } - - // SequencingWithinIntervals - "SequencingWithinIntervals" => { - let usage = "Usage: pred create SequencingWithinIntervals --release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1"; - let rt_str = args.release_times.as_deref().ok_or_else(|| { - anyhow::anyhow!("SequencingWithinIntervals requires --release-times\n\n{usage}") - })?; - let dl_str = args.deadlines.as_deref().ok_or_else(|| { - anyhow::anyhow!("SequencingWithinIntervals requires --deadlines\n\n{usage}") - })?; - let len_str = args.lengths.as_deref().ok_or_else(|| { - anyhow::anyhow!("SequencingWithinIntervals requires --lengths\n\n{usage}") - })?; - let release_times: Vec = util::parse_comma_list(rt_str)?; - let deadlines: Vec = util::parse_comma_list(dl_str)?; - let lengths: Vec = util::parse_comma_list(len_str)?; - validate_sequencing_within_intervals_inputs( - &release_times, - &deadlines, - &lengths, - usage, - )?; - ( - ser(SequencingWithinIntervals::new( - release_times, - deadlines, - lengths, - ))?, - resolved_variant.clone(), - ) - } - - // OptimalLinearArrangement — graph only (optimization) - "OptimalLinearArrangement" => { - let usage = "Usage: pred create OptimalLinearArrangement --graph 0-1,1-2,2-3"; - let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - ( - ser(OptimalLinearArrangement::new(graph))?, - resolved_variant.clone(), - ) - } - - // RootedTreeArrangement — graph + bound - "RootedTreeArrangement" => { - let usage = - "Usage: pred create RootedTreeArrangement --graph 0-1,0-2,1-2,2-3,3-4 --bound 7"; - let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let bound_raw = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "RootedTreeArrangement requires --bound (upper bound K on total tree stretch)\n\n{usage}" - ) - })?; - let bound = parse_nonnegative_usize_bound(bound_raw, "RootedTreeArrangement", usage)?; - ( - ser(RootedTreeArrangement::new(graph, bound))?, - resolved_variant.clone(), - ) - } - - // FlowShopScheduling - "FlowShopScheduling" => { - let task_str = args.task_lengths.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "FlowShopScheduling requires --task-lengths and --deadline\n\n\ - Usage: pred create FlowShopScheduling --task-lengths \"3,4,2;2,3,5;4,1,3\" --deadline 25 --num-processors 3" - ) - })?; - let deadline = args.deadline.ok_or_else(|| { - anyhow::anyhow!( - "FlowShopScheduling requires --deadline\n\n\ - Usage: pred create FlowShopScheduling --task-lengths \"3,4,2;2,3,5;4,1,3\" --deadline 25 --num-processors 3" - ) - })?; - let task_lengths: Vec> = task_str - .split(';') - .map(|row| util::parse_comma_list(row.trim())) - .collect::>>()?; - let num_processors = resolve_processor_count_flags( - "FlowShopScheduling", - "Usage: pred create FlowShopScheduling --task-lengths \"3,4,2;2,3,5;4,1,3\" --deadline 25 --num-processors 3", - args.num_processors, - args.m, - )? - .or_else(|| task_lengths.first().map(Vec::len)) - .ok_or_else(|| { - anyhow::anyhow!( - "Cannot infer num_processors from empty task list; use --num-processors" - ) - })?; - for (j, row) in task_lengths.iter().enumerate() { - if row.len() != num_processors { - bail!( - "task_lengths row {} has {} entries, expected {} (num_processors)", - j, - row.len(), - num_processors - ); - } - } - ( - ser(FlowShopScheduling::new( - num_processors, - task_lengths, - deadline, - ))?, - resolved_variant.clone(), - ) - } - - // JobShopScheduling - "JobShopScheduling" => { - let usage = "Usage: pred create JobShopScheduling --job-tasks \"0:3,1:4;1:2,0:3,1:2;0:4,1:3\" --num-processors 2"; - let job_tasks = args.job_tasks.as_deref().ok_or_else(|| { - anyhow::anyhow!("JobShopScheduling requires --job-tasks\n\n{usage}") - })?; - let jobs = parse_job_shop_jobs(job_tasks)?; - let inferred_processors = jobs - .iter() - .flat_map(|job| job.iter().map(|(processor, _)| *processor)) - .max() - .map(|processor| processor + 1); - let num_processors = resolve_processor_count_flags( - "JobShopScheduling", - usage, - args.num_processors, - args.m, - )? - .or(inferred_processors) - .ok_or_else(|| { - anyhow::anyhow!( - "Cannot infer num_processors from empty job list; use --num-processors" - ) - })?; - anyhow::ensure!( - num_processors > 0, - "JobShopScheduling requires --num-processors > 0\n\n{usage}" - ); - for (job_index, job) in jobs.iter().enumerate() { - for (task_index, &(processor, _)) in job.iter().enumerate() { - anyhow::ensure!( - processor < num_processors, - "job {job_index} task {task_index} uses processor {processor}, but num_processors = {num_processors}" - ); - } - for (task_index, pair) in job.windows(2).enumerate() { - anyhow::ensure!( - pair[0].0 != pair[1].0, - "job {job_index} tasks {task_index} and {} must use different processors\n\n{usage}", - task_index + 1 - ); - } - } - ( - ser(JobShopScheduling::new(num_processors, jobs))?, - resolved_variant.clone(), - ) - } - - // OpenShopScheduling - "OpenShopScheduling" => { - let task_str = args.task_lengths.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "OpenShopScheduling requires --task-lengths and --num-processors\n\n\ - Usage: pred create OpenShopScheduling --task-lengths \"1,2;2,1\" --num-processors 2" - ) - })?; - let task_lengths: Vec> = task_str - .split(';') - .map(|row| util::parse_comma_list(row.trim())) - .collect::>>()?; - let num_machines = resolve_processor_count_flags( - "OpenShopScheduling", - "Usage: pred create OpenShopScheduling --task-lengths \"1,2;2,1\" --num-processors 2", - args.num_processors, - args.m, - )? - .or_else(|| task_lengths.first().map(Vec::len)) - .ok_or_else(|| { - anyhow::anyhow!( - "Cannot infer num_processors from empty task list; use --num-processors" - ) - })?; - for (j, row) in task_lengths.iter().enumerate() { - if row.len() != num_machines { - bail!( - "task_lengths row {} has {} entries, expected {} (num_machines)", - j, - row.len(), - num_machines - ); - } - } - ( - ser(OpenShopScheduling::new(num_machines, task_lengths))?, - resolved_variant.clone(), - ) - } - - // StaffScheduling - "StaffScheduling" => { - let usage = "Usage: pred create 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"; - let schedules = parse_schedules(args, usage)?; - let requirements = parse_requirements(args, usage)?; - let num_workers = args.num_workers.ok_or_else(|| { - anyhow::anyhow!("StaffScheduling requires --num-workers\n\n{usage}") - })?; - let shifts_per_schedule = args - .k - .ok_or_else(|| anyhow::anyhow!("StaffScheduling requires --k\n\n{usage}"))?; - validate_staff_scheduling_args( - &schedules, - &requirements, - shifts_per_schedule, - num_workers, - usage, - )?; - - ( - ser(problemreductions::models::misc::StaffScheduling::new( - shifts_per_schedule, - schedules, - requirements, - num_workers, - ))?, - resolved_variant.clone(), - ) - } - - // TimetableDesign - "TimetableDesign" => { - let usage = "Usage: pred create TimetableDesign --num-periods 3 --num-craftsmen 5 --num-tasks 5 --craftsman-avail \"1,1,1;1,1,0;0,1,1;1,0,1;1,1,1\" --task-avail \"1,1,0;0,1,1;1,0,1;1,1,1;1,1,1\" --requirements \"1,0,1,0,0;0,1,0,0,1;0,0,0,1,0;0,0,0,0,1;0,1,0,0,0\""; - let num_periods = args.num_periods.ok_or_else(|| { - anyhow::anyhow!("TimetableDesign requires --num-periods\n\n{usage}") - })?; - let num_craftsmen = args.num_craftsmen.ok_or_else(|| { - anyhow::anyhow!("TimetableDesign requires --num-craftsmen\n\n{usage}") - })?; - let num_tasks = args.num_tasks.ok_or_else(|| { - anyhow::anyhow!("TimetableDesign requires --num-tasks\n\n{usage}") - })?; - let craftsman_avail = - parse_named_bool_rows(args.craftsman_avail.as_deref(), "--craftsman-avail", usage)?; - let task_avail = - parse_named_bool_rows(args.task_avail.as_deref(), "--task-avail", usage)?; - let requirements = parse_timetable_requirements(args.requirements.as_deref(), usage)?; - validate_timetable_design_args( - num_periods, - num_craftsmen, - num_tasks, - &craftsman_avail, - &task_avail, - &requirements, - usage, - )?; - - ( - ser(TimetableDesign::new( - num_periods, - num_craftsmen, - num_tasks, - craftsman_avail, - task_avail, - requirements, - ))?, - resolved_variant.clone(), - ) - } - - // DirectedTwoCommodityIntegralFlow - "DirectedTwoCommodityIntegralFlow" => { - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "DirectedTwoCommodityIntegralFlow requires --arcs\n\n\ - Usage: pred create DirectedTwoCommodityIntegralFlow \ - --arcs \"0>2,0>3,1>2,1>3,2>4,2>5,3>4,3>5\" \ - --capacities 1,1,1,1,1,1,1,1 \ - --source-1 0 --sink-1 4 --source-2 1 --sink-2 5 \ - --requirement-1 1 --requirement-2 1" - ) - })?; - let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?; - let capacities: Vec = if let Some(ref s) = args.capacities { - util::parse_comma_list(s)? - } else { - vec![1; num_arcs] - }; - anyhow::ensure!( - capacities.len() == num_arcs, - "capacities length ({}) must match number of arcs ({num_arcs})", - capacities.len() - ); - let n = graph.num_vertices(); - let source_1 = args.source_1.ok_or_else(|| { - anyhow::anyhow!("DirectedTwoCommodityIntegralFlow requires --source-1") - })?; - let sink_1 = args.sink_1.ok_or_else(|| { - anyhow::anyhow!("DirectedTwoCommodityIntegralFlow requires --sink-1") - })?; - let source_2 = args.source_2.ok_or_else(|| { - anyhow::anyhow!("DirectedTwoCommodityIntegralFlow requires --source-2") - })?; - let sink_2 = args.sink_2.ok_or_else(|| { - anyhow::anyhow!("DirectedTwoCommodityIntegralFlow requires --sink-2") - })?; - for (name, idx) in [ - ("source_1", source_1), - ("sink_1", sink_1), - ("source_2", source_2), - ("sink_2", sink_2), - ] { - anyhow::ensure!(idx < n, "{name} ({idx}) >= num_vertices ({n})"); - } - let requirement_1 = args.requirement_1.unwrap_or(1); - let requirement_2 = args.requirement_2.unwrap_or(1); - ( - ser(DirectedTwoCommodityIntegralFlow::new( - graph, - capacities, - source_1, - sink_1, - source_2, - sink_2, - requirement_1, - requirement_2, - ))?, - resolved_variant.clone(), - ) - } - - // IntegralFlowHomologousArcs - "IntegralFlowHomologousArcs" => { - let usage = "Usage: pred create IntegralFlowHomologousArcs --arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\""; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!("IntegralFlowHomologousArcs requires --arcs\n\n{usage}") - })?; - let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices) - .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let capacities: Vec = if let Some(ref s) = args.capacities { - s.split(',') - .map(|token| { - let trimmed = token.trim(); - trimmed - .parse::() - .with_context(|| format!("Invalid capacity `{trimmed}`\n\n{usage}")) - }) - .collect::>>()? - } else { - vec![1; num_arcs] - }; - anyhow::ensure!( - capacities.len() == num_arcs, - "Expected {} capacities but got {}\n\n{}", - num_arcs, - capacities.len(), - usage - ); - for (arc_index, &capacity) in capacities.iter().enumerate() { - let fits = usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .is_some(); - anyhow::ensure!( - fits, - "capacity {} at arc index {} is too large for this platform\n\n{}", - capacity, - arc_index, - usage - ); - } - let num_vertices = graph.num_vertices(); - let source = args.source.ok_or_else(|| { - anyhow::anyhow!("IntegralFlowHomologousArcs requires --source\n\n{usage}") - })?; - let sink = args.sink.ok_or_else(|| { - anyhow::anyhow!("IntegralFlowHomologousArcs requires --sink\n\n{usage}") - })?; - let requirement = args.requirement.ok_or_else(|| { - anyhow::anyhow!("IntegralFlowHomologousArcs requires --requirement\n\n{usage}") - })?; - validate_vertex_index("source", source, num_vertices, usage)?; - validate_vertex_index("sink", sink, num_vertices, usage)?; - let homologous_pairs = - parse_homologous_pairs(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - for &(a, b) in &homologous_pairs { - anyhow::ensure!( - a < num_arcs && b < num_arcs, - "homologous pair ({}, {}) references arc >= num_arcs ({})\n\n{}", - a, - b, - num_arcs, - usage - ); - } - ( - ser(IntegralFlowHomologousArcs::new( - graph, - capacities, - source, - sink, - requirement, - homologous_pairs, - ))?, - resolved_variant.clone(), - ) - } - - // PathConstrainedNetworkFlow - "PathConstrainedNetworkFlow" => { - let usage = "Usage: pred create PathConstrainedNetworkFlow --arcs \"0>1,0>2,1>3,1>4,2>4,3>5,4>5,4>6,5>7,6>7\" --capacities 2,1,1,1,1,1,1,1,2,1 --source 0 --sink 7 --paths \"0,2,5,8;0,3,6,8;0,3,7,9;1,4,6,8;1,4,7,9\" --requirement 3"; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!("PathConstrainedNetworkFlow requires --arcs\n\n{usage}") - })?; - let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices) - .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let capacities: Vec = if let Some(ref s) = args.capacities { - util::parse_comma_list(s)? - } else { - vec![1; num_arcs] - }; - anyhow::ensure!( - capacities.len() == num_arcs, - "capacities length ({}) must match number of arcs ({num_arcs})", - capacities.len() - ); - let source = args.source.ok_or_else(|| { - anyhow::anyhow!("PathConstrainedNetworkFlow requires --source\n\n{usage}") - })?; - let sink = args.sink.ok_or_else(|| { - anyhow::anyhow!("PathConstrainedNetworkFlow requires --sink\n\n{usage}") - })?; - let requirement = args.requirement.ok_or_else(|| { - anyhow::anyhow!("PathConstrainedNetworkFlow requires --requirement\n\n{usage}") - })?; - let paths = parse_prescribed_paths(args, num_arcs, usage)?; - ( - ser(PathConstrainedNetworkFlow::new( - graph, - capacities, - source, - sink, - paths, - requirement, - ))?, - resolved_variant.clone(), - ) - } - - // MinimumFeedbackArcSet - "MinimumFeedbackArcSet" => { - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumFeedbackArcSet requires --arcs\n\n\ - Usage: pred create FAS --arcs \"0>1,1>2,2>0\" [--weights 1,1,1] [--num-vertices N]" - ) - })?; - let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?; - let weights = parse_arc_weights(args, num_arcs)?; - ( - ser(MinimumFeedbackArcSet::new(graph, weights))?, - resolved_variant.clone(), - ) - } - - // DegreeConstrainedSpanningTree - "DegreeConstrainedSpanningTree" => { - let usage = "Usage: pred create DegreeConstrainedSpanningTree --graph 0-1,0-2,0-3,1-2,1-4,2-3,3-4 --k 2"; - let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let max_degree = args.k.ok_or_else(|| { - anyhow::anyhow!( - "DegreeConstrainedSpanningTree requires --k (maximum vertex degree)\n\n{usage}" - ) - })?; - anyhow::ensure!( - max_degree >= 1, - "DegreeConstrainedSpanningTree requires --k >= 1, got {}", - max_degree - ); - ( - ser( - problemreductions::models::graph::DegreeConstrainedSpanningTree::new( - graph, max_degree, - ), - )?, - resolved_variant.clone(), - ) - } - - // DirectedHamiltonianPath - "DirectedHamiltonianPath" => { - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "DirectedHamiltonianPath requires --arcs\n\n\ - Usage: pred create DirectedHamiltonianPath --arcs \"0>1,1>2,2>3\" [--num-vertices N]" - ) - })?; - let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; - ( - ser(DirectedHamiltonianPath::new(graph))?, - resolved_variant.clone(), - ) - } - - // Kernel - "Kernel" => { - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "Kernel requires --arcs\n\n\ - Usage: pred create Kernel --arcs \"0>1,1>2,2>0\" [--num-vertices N]" - ) - })?; - let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; - (ser(Kernel::new(graph))?, resolved_variant.clone()) - } - - // AcyclicPartition - "AcyclicPartition" => { - let usage = "Usage: pred create AcyclicPartition/i32 --arcs \"0>1,0>2,1>3,1>4,2>4,2>5,3>5,4>5\" --weights 2,3,2,1,3,1 --arc-costs 1,1,1,1,1,1,1,1 --weight-bound 5 --cost-bound 5"; - let arcs_str = args - .arcs - .as_deref() - .ok_or_else(|| anyhow::anyhow!("AcyclicPartition requires --arcs\n\n{usage}"))?; - let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?; - let vertex_weights = parse_vertex_weights(args, graph.num_vertices())?; - let arc_costs = parse_arc_costs(args, num_arcs)?; - let weight_bound = args.weight_bound.ok_or_else(|| { - anyhow::anyhow!("AcyclicPartition requires --weight-bound\n\n{usage}") - })?; - let cost_bound = args.cost_bound.ok_or_else(|| { - anyhow::anyhow!("AcyclicPartition requires --cost-bound\n\n{usage}") - })?; - if vertex_weights.iter().any(|&weight| weight <= 0) { - bail!("AcyclicPartition --weights must be positive (Z+)"); - } - if arc_costs.iter().any(|&cost| cost <= 0) { - bail!("AcyclicPartition --arc-costs must be positive (Z+)"); - } - if weight_bound <= 0 { - bail!("AcyclicPartition --weight-bound must be positive (Z+)"); - } - if cost_bound <= 0 { - bail!("AcyclicPartition --cost-bound must be positive (Z+)"); - } - ( - ser(AcyclicPartition::new( - graph, - vertex_weights, - arc_costs, - weight_bound, - cost_bound, - ))?, - resolved_variant.clone(), - ) - } - - // MinMaxMulticenter (vertex p-center) - "MinMaxMulticenter" => { - let usage = "Usage: pred create MinMaxMulticenter --graph 0-1,1-2,2-3 [--weights 1,1,1,1] [--edge-weights 1,1,1] --k 2"; - let (graph, n) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let vertex_weights = parse_vertex_weights(args, n)?; - let edge_lengths = parse_edge_weights(args, graph.num_edges())?; - let k = args.k.ok_or_else(|| { - anyhow::anyhow!( - "MinMaxMulticenter requires --k (number of centers)\n\n\ - Usage: pred create MinMaxMulticenter --graph 0-1,1-2,2-3 --k 2" - ) - })?; - if vertex_weights.iter().any(|&weight| weight < 0) { - bail!("MinMaxMulticenter --weights must be non-negative"); - } - if edge_lengths.iter().any(|&length| length < 0) { - bail!("MinMaxMulticenter --edge-weights must be non-negative"); - } - ( - ser(MinMaxMulticenter::new( - graph, - vertex_weights, - edge_lengths, - k, - ))?, - resolved_variant.clone(), - ) - } - - // StrongConnectivityAugmentation - "StrongConnectivityAugmentation" => { - let usage = "Usage: pred create StrongConnectivityAugmentation --arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1 [--num-vertices N]"; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "StrongConnectivityAugmentation requires --arcs\n\n\ - {usage}" - ) - })?; - let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; - let candidate_arcs = parse_candidate_arcs(args, graph.num_vertices())?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "StrongConnectivityAugmentation requires --bound\n\n\ - {usage}" - ) - })? as i32; - ( - ser( - StrongConnectivityAugmentation::try_new(graph, candidate_arcs, bound) - .map_err(|e| anyhow::anyhow!(e))?, - )?, - resolved_variant.clone(), - ) - } - - // MinimumGeometricConnectedDominatingSet - "MinimumGeometricConnectedDominatingSet" => { - let usage = "Usage: pred create MinimumGeometricConnectedDominatingSet --positions \"0,0;3,0;6,0\" --radius 3.5"; - let positions = parse_float_positions(args).map_err(|_| { - anyhow::anyhow!( - "MinimumGeometricConnectedDominatingSet requires --positions\n\n\ - {usage}" - ) - })?; - let radius = args.radius.ok_or_else(|| { - anyhow::anyhow!( - "MinimumGeometricConnectedDominatingSet requires --radius\n\n\ - {usage}" - ) - })?; - ( - ser( - MinimumGeometricConnectedDominatingSet::try_new(positions, radius) - .map_err(|e| anyhow::anyhow!(e))?, - )?, - resolved_variant.clone(), - ) - } - - // MinimumEdgeCostFlow - "MinimumEdgeCostFlow" => { - let usage = "Usage: pred create MinimumEdgeCostFlow --arcs \"0>1,0>2,0>3,1>4,2>4,3>4\" --edge-weights 3,1,2,0,0,0 --capacities 2,2,2,2,2,2 --source 0 --sink 4 --requirement 3"; - let arcs_str = args - .arcs - .as_deref() - .ok_or_else(|| anyhow::anyhow!("MinimumEdgeCostFlow requires --arcs\n\n{usage}"))?; - let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?; - let prices: Vec = if let Some(ref s) = args.edge_weights { - util::parse_comma_list(s)? - } else { - bail!("MinimumEdgeCostFlow requires --edge-weights (prices)\n\n{usage}"); - }; - anyhow::ensure!( - prices.len() == num_arcs, - "--edge-weights length ({}) must match number of arcs ({num_arcs})", - prices.len() - ); - let capacities: Vec = if let Some(ref s) = args.capacities { - util::parse_comma_list(s)? - } else { - bail!("MinimumEdgeCostFlow requires --capacities\n\n{usage}"); - }; - anyhow::ensure!( - capacities.len() == num_arcs, - "--capacities length ({}) must match number of arcs ({num_arcs})", - capacities.len() - ); - let n = graph.num_vertices(); - let source = args.source.ok_or_else(|| { - anyhow::anyhow!("MinimumEdgeCostFlow requires --source\n\n{usage}") - })?; - let sink = args - .sink - .ok_or_else(|| anyhow::anyhow!("MinimumEdgeCostFlow requires --sink\n\n{usage}"))?; - anyhow::ensure!(source < n, "--source ({source}) >= num_vertices ({n})"); - anyhow::ensure!(sink < n, "--sink ({sink}) >= num_vertices ({n})"); - anyhow::ensure!(source != sink, "--source and --sink must be distinct"); - let requirement = args.requirement.unwrap_or(1) as i64; - ( - ser(problemreductions::models::graph::MinimumEdgeCostFlow::new( - graph, - prices, - capacities, - source, - sink, - requirement, - ))?, - resolved_variant.clone(), - ) - } - - // MinimumDummyActivitiesPert - "MinimumDummyActivitiesPert" => { - let usage = "Usage: pred create MinimumDummyActivitiesPert --arcs \"0>2,0>3,1>3,1>4,2>5\" [--num-vertices N]"; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumDummyActivitiesPert requires --arcs\n\n\ - {usage}" - ) - })?; - let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; - ( - ser(MinimumDummyActivitiesPert::try_new(graph).map_err(|e| anyhow::anyhow!(e))?)?, - resolved_variant.clone(), - ) - } - - // FeasibleRegisterAssignment - "FeasibleRegisterAssignment" => { - let usage = "Usage: pred create FeasibleRegisterAssignment --arcs \"0>1,0>2,1>3\" --assignment 0,1,0,0 --k 2 [--num-vertices N]"; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "FeasibleRegisterAssignment requires --arcs, --assignment, and --k\n\n\ - {usage}" - ) - })?; - let k = args.k.ok_or_else(|| { - anyhow::anyhow!( - "FeasibleRegisterAssignment requires --k (number of registers)\n\n\ - {usage}" - ) - })?; - let assignment_str = args.assignment.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "FeasibleRegisterAssignment requires --assignment\n\n\ - {usage}" - ) - })?; - let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; - let n = graph.num_vertices(); - let arcs = graph.arcs(); - let assignment: Vec = assignment_str - .split(',') - .map(|s| { - s.trim() - .parse::() - .with_context(|| format!("Invalid assignment value: {s}")) - }) - .collect::>()?; - if assignment.len() != n { - bail!( - "Assignment length {} does not match vertex count {}\n\n{usage}", - assignment.len(), - n - ); - } - ( - ser(FeasibleRegisterAssignment::new(n, arcs, k, assignment))?, - resolved_variant.clone(), - ) - } - - // RegisterSufficiency - "RegisterSufficiency" => { - let usage = "Usage: pred create RegisterSufficiency --arcs \"2>0,2>1,3>1,4>2,4>3,5>0,6>4,6>5\" --bound 3 [--num-vertices N]"; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "RegisterSufficiency requires --arcs and --bound\n\n\ - {usage}" - ) - })?; - let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "RegisterSufficiency requires --bound\n\n\ - {usage}" - ) - })?; - if bound < 0 { - bail!("RegisterSufficiency --bound must be non-negative\n\n{usage}"); - } - let bound = bound as usize; - let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; - let n = graph.num_vertices(); - let arcs = graph.arcs(); - ( - ser(RegisterSufficiency::new(n, arcs, bound))?, - resolved_variant.clone(), - ) - } - - // MinimumCodeGenerationOneRegister - "MinimumCodeGenerationOneRegister" => { - let usage = "Usage: pred create MinimumCodeGenerationOneRegister --arcs \"0>1,0>2,1>3,1>4,2>3,2>5,3>5,3>6\" [--num-vertices N]"; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumCodeGenerationOneRegister requires --arcs\n\n\ - {usage}" - ) - })?; - let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; - let n = graph.num_vertices(); - let arcs = graph.arcs(); - // Compute num_leaves: vertices with out-degree 0 - let mut out_degree = vec![0usize; n]; - for &(parent, _child) in &arcs { - out_degree[parent] += 1; - } - let num_leaves = out_degree.iter().filter(|&&d| d == 0).count(); - ( - ser(MinimumCodeGenerationOneRegister::new(n, arcs, num_leaves))?, - resolved_variant.clone(), - ) - } - - // MinimumCodeGenerationUnlimitedRegisters - "MinimumCodeGenerationUnlimitedRegisters" => { - let usage = "Usage: pred create MinimumCodeGenerationUnlimitedRegisters --left-arcs \"1>3,2>3,0>1\" --right-arcs \"1>4,2>4,0>2\" [--num-vertices N]"; - let left_str = args.left_arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumCodeGenerationUnlimitedRegisters requires --left-arcs\n\n\ - {usage}" - ) - })?; - let right_str = args.right_arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumCodeGenerationUnlimitedRegisters requires --right-arcs\n\n\ - {usage}" - ) - })?; - let (left_graph, _) = parse_directed_graph(left_str, args.num_vertices)?; - let (right_graph, _) = parse_directed_graph(right_str, args.num_vertices)?; - let n = if let Some(nv) = args.num_vertices { - nv - } else { - left_graph.num_vertices().max(right_graph.num_vertices()) - }; - let left_arcs = left_graph.arcs(); - let right_arcs = right_graph.arcs(); - ( - ser(MinimumCodeGenerationUnlimitedRegisters::new( - n, left_arcs, right_arcs, - ))?, - resolved_variant.clone(), - ) - } - - // MinimumCodeGenerationParallelAssignments - "MinimumCodeGenerationParallelAssignments" => { - let usage = "Usage: pred create MinimumCodeGenerationParallelAssignments --num-variables 4 --assignments \"0:1,2;1:0;2:3;3:1,2\""; - let nv = args.num_variables.ok_or_else(|| { - anyhow::anyhow!( - "MinimumCodeGenerationParallelAssignments requires --num-variables and --assignments\n\n\ - {usage}" - ) - })?; - let assign_str = args.assignments.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumCodeGenerationParallelAssignments requires --assignments\n\n\ - {usage}" - ) - })?; - let assignments: Vec<(usize, Vec)> = assign_str - .split(';') - .map(|entry| { - let parts: Vec<&str> = entry.split(':').collect(); - anyhow::ensure!( - parts.len() == 2, - "each assignment must be 'target:read1,read2,...'; got '{entry}'" - ); - let target: usize = parts[0] - .trim() - .parse() - .context("invalid target variable index")?; - let reads: Vec = if parts[1].trim().is_empty() { - Vec::new() - } else { - parts[1] - .split(',') - .map(|s| { - s.trim() - .parse::() - .context("invalid read variable index") - }) - .collect::>>()? - }; - Ok((target, reads)) - }) - .collect::>>()?; - ( - ser(MinimumCodeGenerationParallelAssignments::new( - nv, - assignments, - ))?, - resolved_variant.clone(), - ) - } - - // MinimumRegisterSufficiencyForLoops - "MinimumRegisterSufficiencyForLoops" => { - let usage = "Usage: pred create MinimumRegisterSufficiencyForLoops --loop-length 6 --loop-variables \"0,3;2,3;4,3\""; - let loop_length = args.loop_length.ok_or_else(|| { - anyhow::anyhow!( - "MinimumRegisterSufficiencyForLoops requires --loop-length and --loop-variables\n\n\ - {usage}" - ) - })?; - let vars_str = args.loop_variables.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumRegisterSufficiencyForLoops requires --loop-variables\n\n\ - {usage}" - ) - })?; - let variables: Vec<(usize, usize)> = vars_str - .split(';') - .map(|pair| { - let parts: Vec<&str> = pair.split(',').collect(); - if parts.len() != 2 { - bail!("Each variable must be start,duration (got '{pair}')\n\n{usage}"); - } - let start: usize = parts[0] - .trim() - .parse() - .context(format!("Invalid start_time in '{pair}'\n\n{usage}"))?; - let dur: usize = parts[1] - .trim() - .parse() - .context(format!("Invalid duration in '{pair}'\n\n{usage}"))?; - Ok((start, dur)) - }) - .collect::>>()?; - ( - ser(MinimumRegisterSufficiencyForLoops::new( - loop_length, - variables, - ))?, - resolved_variant.clone(), - ) - } - - // MinimumFaultDetectionTestSet - "MinimumFaultDetectionTestSet" => { - let usage = "Usage: pred create MinimumFaultDetectionTestSet --arcs \"0>2,0>3,1>3,1>4,2>5,3>5,3>6,4>6\" --inputs 0,1 --outputs 5,6 [--num-vertices N]"; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumFaultDetectionTestSet requires --arcs, --inputs, and --outputs\n\n\ - {usage}" - ) - })?; - let inputs_str = args.inputs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumFaultDetectionTestSet requires --inputs\n\n\ - {usage}" - ) - })?; - let outputs_str = args.outputs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumFaultDetectionTestSet requires --outputs\n\n\ - {usage}" - ) - })?; - let (graph, _num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?; - let n = graph.num_vertices(); - let arcs = graph.arcs(); - let inputs: Vec = inputs_str - .split(',') - .map(|s| { - s.trim() - .parse::() - .map_err(|e| anyhow::anyhow!("Invalid input vertex '{}': {}", s.trim(), e)) - }) - .collect::>()?; - let outputs: Vec = outputs_str - .split(',') - .map(|s| { - s.trim() - .parse::() - .map_err(|e| anyhow::anyhow!("Invalid output vertex '{}': {}", s.trim(), e)) - }) - .collect::>()?; - ( - ser(MinimumFaultDetectionTestSet::new(n, arcs, inputs, outputs))?, - resolved_variant.clone(), - ) - } - - // MinimumWeightAndOrGraph - "MinimumWeightAndOrGraph" => { - let usage = "Usage: pred create MinimumWeightAndOrGraph --arcs \"0>1,0>2,1>3,1>4,2>5,2>6\" --source 0 --gate-types \"AND,OR,OR,L,L,L,L\" --weights 1,2,3,1,4,2 [--num-vertices N]"; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumWeightAndOrGraph requires --arcs, --source, --gate-types, and --weights\n\n\ - {usage}" - ) - })?; - let source = args.source.ok_or_else(|| { - anyhow::anyhow!( - "MinimumWeightAndOrGraph requires --source\n\n\ - {usage}" - ) - })?; - let gate_types_str = args.gate_types.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumWeightAndOrGraph requires --gate-types (e.g., \"AND,OR,OR,L,L,L,L\")\n\n\ - {usage}" - ) - })?; - let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?; - let n = graph.num_vertices(); - let arcs = graph.arcs(); - let arc_weights = parse_arc_weights(args, num_arcs)?; - let gate_types: Vec> = gate_types_str - .split(',') - .map(|s| match s.trim() { - "AND" | "and" => Ok(Some(true)), - "OR" | "or" => Ok(Some(false)), - "L" | "l" | "LEAF" | "leaf" => Ok(None), - other => Err(anyhow::anyhow!( - "Invalid gate type '{}': expected AND, OR, or L (leaf)\n\n{usage}", - other - )), - }) - .collect::>()?; - if gate_types.len() != n { - bail!( - "Gate types length {} does not match vertex count {}\n\n{usage}", - gate_types.len(), - n - ); - } - ( - ser(MinimumWeightAndOrGraph::new( - n, - arcs, - source, - gate_types, - arc_weights, - ))?, - resolved_variant.clone(), - ) - } - - // MixedChinesePostman - "MixedChinesePostman" => { - let usage = "Usage: pred create MixedChinesePostman --graph 0-2,1-3,0-4,4-2 --arcs \"0>1,1>2,2>3,3>0\" --edge-weights 2,3,1,2 --arc-costs 2,3,1,4 [--num-vertices N]"; - let graph = parse_mixed_graph(args, usage)?; - let arc_costs = parse_arc_costs(args, graph.num_arcs())?; - let edge_weights = parse_edge_weights(args, graph.num_edges())?; - if arc_costs.iter().any(|&cost| cost < 0) { - bail!("MixedChinesePostman --arc-costs must be non-negative\n\n{usage}"); - } - if edge_weights.iter().any(|&weight| weight < 0) { - bail!("MixedChinesePostman --edge-weights must be non-negative\n\n{usage}"); - } - if resolved_variant.get("weight").map(|w| w.as_str()) == Some("One") - && (arc_costs.iter().any(|&cost| cost != 1) - || edge_weights.iter().any(|&weight| weight != 1)) - { - bail!( - "Non-unit lengths are not supported for MixedChinesePostman/One.\n\n\ - Use the weighted variant instead:\n pred create MixedChinesePostman/i32 --graph ... --arcs ... --edge-weights ... --arc-costs ..." - ); - } - ( - ser(MixedChinesePostman::new(graph, arc_costs, edge_weights))?, - resolved_variant.clone(), - ) - } - - // MinimumSumMulticenter (p-median) - "MinimumSumMulticenter" => { - let (graph, n) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create MinimumSumMulticenter --graph 0-1,1-2,2-3 [--weights 1,1,1,1] [--edge-weights 1,1,1] --k 2" - ) - })?; - let vertex_weights = parse_vertex_weights(args, n)?; - let edge_lengths = parse_edge_weights(args, graph.num_edges())?; - let k = args.k.ok_or_else(|| { - anyhow::anyhow!( - "MinimumSumMulticenter requires --k (number of centers)\n\n\ - Usage: pred create MinimumSumMulticenter --graph 0-1,1-2,2-3 --k 2" - ) - })?; - ( - ser(MinimumSumMulticenter::new( - graph, - vertex_weights, - edge_lengths, - k, - ))?, - resolved_variant.clone(), - ) - } - - // SubgraphIsomorphism - "SubgraphIsomorphism" => { - let (host_graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create SubgraphIsomorphism --graph 0-1,1-2,2-0 --pattern 0-1" - ) - })?; - let pattern_str = args.pattern.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SubgraphIsomorphism requires --pattern (pattern graph edges)\n\n\ - Usage: pred create SubgraphIsomorphism --graph 0-1,1-2,2-0 --pattern 0-1" - ) - })?; - let pattern_edges: Vec<(usize, usize)> = pattern_str - .split(',') - .map(|pair| { - let parts: Vec<&str> = pair.trim().split('-').collect(); - if parts.len() != 2 { - bail!("Invalid edge '{}': expected format u-v", pair.trim()); - } - let u: usize = parts[0].parse()?; - let v: usize = parts[1].parse()?; - if u == v { - bail!( - "Invalid edge '{}': self-loops are not allowed in simple graphs", - pair.trim() - ); - } - Ok((u, v)) - }) - .collect::>>()?; - let pattern_nv = pattern_edges - .iter() - .flat_map(|(u, v)| [*u, *v]) - .max() - .map(|m| m + 1) - .unwrap_or(0); - let pattern_graph = SimpleGraph::new(pattern_nv, pattern_edges); - ( - ser(SubgraphIsomorphism::new(host_graph, pattern_graph))?, - resolved_variant.clone(), - ) - } - - // MonochromaticTriangle - "MonochromaticTriangle" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create MonochromaticTriangle --graph 0-1,0-2,0-3,1-2,1-3,2-3" - ) - })?; - ( - ser(MonochromaticTriangle::new(graph))?, - resolved_variant.clone(), - ) - } - - // PartitionIntoTriangles - "PartitionIntoTriangles" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create PartitionIntoTriangles --graph 0-1,1-2,0-2" - ) - })?; - anyhow::ensure!( - graph.num_vertices() % 3 == 0, - "PartitionIntoTriangles requires vertex count divisible by 3, got {}", - graph.num_vertices() - ); - ( - ser(PartitionIntoTriangles::new(graph))?, - resolved_variant.clone(), - ) - } - - // PartitionIntoCliques - "PartitionIntoCliques" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create PartitionIntoCliques --graph 0-1,0-2,1-2,3-4,3-5,4-5 --k 3" - ) - })?; - let num_cliques = args.k.ok_or_else(|| { - anyhow::anyhow!( - "PartitionIntoCliques requires --k (maximum number of clique groups)\n\n\ - Usage: pred create PartitionIntoCliques --graph 0-1,0-2,1-2,3-4,3-5,4-5 --k 3" - ) - })?; - anyhow::ensure!( - num_cliques >= 1, - "PartitionIntoCliques requires --k >= 1, got {}", - num_cliques - ); - anyhow::ensure!( - num_cliques <= graph.num_vertices(), - "PartitionIntoCliques requires --k <= num_vertices ({}), got {}", - graph.num_vertices(), - num_cliques - ); - ( - ser(problemreductions::models::graph::PartitionIntoCliques::new( - graph, - num_cliques, - ))?, - resolved_variant.clone(), - ) - } - - // PartitionIntoPerfectMatchings - "PartitionIntoPerfectMatchings" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create PartitionIntoPerfectMatchings --graph 0-1,2-3,0-2,1-3 --k 2" - ) - })?; - let num_matchings = args.k.ok_or_else(|| { - anyhow::anyhow!( - "PartitionIntoPerfectMatchings requires --k (maximum number of matching groups)\n\n\ - Usage: pred create PartitionIntoPerfectMatchings --graph 0-1,2-3,0-2,1-3 --k 2" - ) - })?; - anyhow::ensure!( - num_matchings >= 1, - "PartitionIntoPerfectMatchings requires --k >= 1, got {}", - num_matchings - ); - anyhow::ensure!( - num_matchings <= graph.num_vertices(), - "PartitionIntoPerfectMatchings requires --k <= num_vertices ({}), got {}", - graph.num_vertices(), - num_matchings - ); - ( - ser( - problemreductions::models::graph::PartitionIntoPerfectMatchings::new( - graph, - num_matchings, - ), - )?, - resolved_variant.clone(), - ) - } - - // PartitionIntoForests - "PartitionIntoForests" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create PartitionIntoForests --graph 0-1,1-2,2-0,3-4,4-5,5-3 --k 2" - ) - })?; - let num_forests = args.k.ok_or_else(|| { - anyhow::anyhow!( - "PartitionIntoForests requires --k (number of forest classes)\n\n\ - Usage: pred create PartitionIntoForests --graph 0-1,1-2,2-0,3-4,4-5,5-3 --k 2" - ) - })?; - anyhow::ensure!( - num_forests >= 1, - "PartitionIntoForests requires --k >= 1, got {}", - num_forests - ); - ( - ser(problemreductions::models::graph::PartitionIntoForests::new( - graph, - num_forests, - ))?, - resolved_variant.clone(), - ) - } - - // ShortestCommonSupersequence - "ShortestCommonSupersequence" => { - let usage = "Usage: pred create SCS --strings \"0,1,2;1,2,0\""; - let strings_str = args.strings.as_deref().ok_or_else(|| { - anyhow::anyhow!("ShortestCommonSupersequence requires --strings\n\n{usage}") - })?; - let strings: Vec> = strings_str - .split(';') - .map(|s| { - let trimmed = s.trim(); - if trimmed.is_empty() { - return Ok(Vec::new()); - } - trimmed - .split(',') - .map(|v| { - v.trim() - .parse::() - .map_err(|e| anyhow::anyhow!("Invalid alphabet index: {}", e)) - }) - .collect::>>() - }) - .collect::>>()?; - let inferred = strings - .iter() - .flat_map(|s| s.iter()) - .copied() - .max() - .map(|m| m + 1) - .unwrap_or(0); - let alphabet_size = args.alphabet_size.unwrap_or(inferred); - if alphabet_size < inferred { - anyhow::bail!( - "--alphabet-size {} is smaller than the largest symbol + 1 ({}) in the strings", - alphabet_size, - inferred - ); - } - ( - ser(ShortestCommonSupersequence::new(alphabet_size, strings))?, - resolved_variant.clone(), - ) - } - - // MinimumFeedbackVertexSet - "MinimumFeedbackVertexSet" => { - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "MinimumFeedbackVertexSet requires --arcs\n\n\ - Usage: pred create FVS --arcs \"0>1,1>2,2>0\" [--weights 1,1,1] [--num-vertices N]" - ) - })?; - let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; - let num_v = graph.num_vertices(); - let weights = parse_vertex_weights(args, num_v)?; - ( - ser(MinimumFeedbackVertexSet::new(graph, weights))?, - resolved_variant.clone(), - ) - } - - "ConjunctiveQueryFoldability" => { - bail!( - "ConjunctiveQueryFoldability has complex nested input.\n\n\ - Use: pred create --example ConjunctiveQueryFoldability\n\ - Or provide a JSON file directly." - ) - } - - "EquilibriumPoint" => { - bail!( - "EquilibriumPoint has complex nested input (polynomial factor lists).\n\n\ - Use: pred create --example EquilibriumPoint\n\ - Or provide a JSON file directly." - ) - } - - // PartitionIntoPathsOfLength2 - "PartitionIntoPathsOfLength2" => { - let (graph, _) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create PartitionIntoPathsOfLength2 --graph 0-1,1-2,3-4,4-5" - ) - })?; - if graph.num_vertices() % 3 != 0 { - bail!( - "PartitionIntoPathsOfLength2 requires vertex count divisible by 3, got {}", - graph.num_vertices() - ); - } - ( - ser(problemreductions::models::graph::PartitionIntoPathsOfLength2::new(graph))?, - resolved_variant.clone(), - ) - } - - // ConjunctiveBooleanQuery - "ConjunctiveBooleanQuery" => { - let usage = "Usage: pred create CBQ --domain-size 6 --relations \"2:0,3|1,3;3:0,1,5|1,2,5\" --conjuncts-spec \"0:v0,c3;0:v1,c3;1:v0,v1,c5\""; - let domain_size = args.domain_size.ok_or_else(|| { - anyhow::anyhow!("ConjunctiveBooleanQuery requires --domain-size\n\n{usage}") - })?; - let relations_str = args.relations.as_deref().ok_or_else(|| { - anyhow::anyhow!("ConjunctiveBooleanQuery requires --relations\n\n{usage}") - })?; - let conjuncts_str = args.conjuncts_spec.as_deref().ok_or_else(|| { - anyhow::anyhow!("ConjunctiveBooleanQuery requires --conjuncts-spec\n\n{usage}") - })?; - // Parse relations: "arity:t1,t2|t3,t4;arity:t5,t6,t7|t8,t9,t10" - // An empty tuple list (e.g., "2:") produces an empty relation. - let relations: Vec = relations_str - .split(';') - .map(|rel_str| { - let rel_str = rel_str.trim(); - let (arity_str, tuples_str) = rel_str.split_once(':').ok_or_else(|| { - anyhow::anyhow!( - "Invalid relation format: expected 'arity:tuples', got '{rel_str}'" - ) - })?; - let arity: usize = arity_str - .trim() - .parse() - .map_err(|e| anyhow::anyhow!("Invalid arity '{arity_str}': {e}"))?; - let tuples: Vec> = if tuples_str.trim().is_empty() { - Vec::new() - } else { - tuples_str - .split('|') - .filter(|t| !t.trim().is_empty()) - .map(|t| { - let tuple: Vec = t - .trim() - .split(',') - .map(|v| { - v.trim().parse::().map_err(|e| { - anyhow::anyhow!("Invalid tuple value: {e}") - }) - }) - .collect::>>()?; - if tuple.len() != arity { - bail!( - "Relation tuple has {} entries, expected arity {arity}", - tuple.len() - ); - } - for &val in &tuple { - if val >= domain_size { - bail!("Tuple value {val} >= domain-size {domain_size}"); - } - } - Ok(tuple) - }) - .collect::>>()? - }; - Ok(CbqRelation { arity, tuples }) - }) - .collect::>>()?; - // Parse conjuncts: "rel_idx:arg1,arg2;rel_idx:arg1,arg2,arg3" - let mut num_vars_inferred: usize = 0; - let conjuncts: Vec<(usize, Vec)> = conjuncts_str - .split(';') - .map(|conj_str| { - let conj_str = conj_str.trim(); - let (idx_str, args_str) = conj_str.split_once(':').ok_or_else(|| { - anyhow::anyhow!( - "Invalid conjunct format: expected 'rel_idx:args', got '{conj_str}'" - ) - })?; - let rel_idx: usize = idx_str.trim().parse().map_err(|e| { - anyhow::anyhow!("Invalid relation index '{idx_str}': {e}") - })?; - if rel_idx >= relations.len() { - bail!( - "Conjunct references relation {rel_idx}, but only {} relations exist", - relations.len() - ); - } - let query_args: Vec = args_str - .split(',') - .map(|a| { - let a = a.trim(); - if let Some(rest) = a.strip_prefix('v') { - let v: usize = rest.parse().map_err(|e| { - anyhow::anyhow!("Invalid variable index '{rest}': {e}") - })?; - if v + 1 > num_vars_inferred { - num_vars_inferred = v + 1; - } - Ok(QueryArg::Variable(v)) - } else if let Some(rest) = a.strip_prefix('c') { - let c: usize = rest.parse().map_err(|e| { - anyhow::anyhow!("Invalid constant value '{rest}': {e}") - })?; - if c >= domain_size { - bail!( - "Constant {c} >= domain-size {domain_size}" - ); - } - Ok(QueryArg::Constant(c)) - } else { - Err(anyhow::anyhow!( - "Invalid query arg '{a}': expected vN (variable) or cN (constant)" - )) - } - }) - .collect::>>()?; - let expected_arity = relations[rel_idx].arity; - if query_args.len() != expected_arity { - bail!( - "Conjunct has {} args, but relation {rel_idx} has arity {expected_arity}", - query_args.len() - ); - } - Ok((rel_idx, query_args)) - }) - .collect::>>()?; - ( - ser(ConjunctiveBooleanQuery::new( - domain_size, - relations, - num_vars_inferred, - conjuncts, - ))?, - resolved_variant.clone(), - ) - } - - // PartiallyOrderedKnapsack - "PartiallyOrderedKnapsack" => { - let sizes_str = args.sizes.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "PartiallyOrderedKnapsack requires --sizes, --values, and --capacity (--precedences is optional)\n\n\ - Usage: pred create PartiallyOrderedKnapsack --sizes 2,3,4,1,2,3 --values 3,2,5,4,3,8 --precedences \"0>2,0>3,1>4,3>5,4>5\" --capacity 11" - ) - })?; - let values_str = args.values.as_deref().ok_or_else(|| { - anyhow::anyhow!("PartiallyOrderedKnapsack requires --values (e.g., 3,2,5,4,3,8)") - })?; - let cap_str = args.capacity.as_deref().ok_or_else(|| { - anyhow::anyhow!("PartiallyOrderedKnapsack requires --capacity (e.g., 11)") - })?; - let weights: Vec = util::parse_comma_list(sizes_str)?; - let values: Vec = util::parse_comma_list(values_str)?; - let capacity: i64 = cap_str.parse()?; - let precedences = match args.precedences.as_deref() { - Some(s) if !s.trim().is_empty() => s - .split(',') - .map(|pair| { - let parts: Vec<&str> = pair.trim().split('>').collect(); - anyhow::ensure!( - parts.len() == 2, - "Invalid precedence format '{}', expected 'a>b'", - pair.trim() - ); - Ok(( - parts[0].trim().parse::()?, - parts[1].trim().parse::()?, - )) - }) - .collect::>>()?, - _ => vec![], - }; - ( - ser(PartiallyOrderedKnapsack::new( - weights, - values, - precedences, - capacity, - ))?, - resolved_variant.clone(), - ) - } - - // PrimeAttributeName - "PrimeAttributeName" => { - let universe = args.universe.ok_or_else(|| { - anyhow::anyhow!( - "PrimeAttributeName requires --universe, --deps, and --query\n\n\ - Usage: pred create PrimeAttributeName --universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3" - ) - })?; - let deps_str = args.deps.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "PrimeAttributeName requires --deps\n\n\ - Usage: pred create PrimeAttributeName --universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3" - ) - })?; - let query = args.query.ok_or_else(|| { - anyhow::anyhow!( - "PrimeAttributeName requires --query\n\n\ - Usage: pred create PrimeAttributeName --universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3" - ) - })?; - let dependencies = parse_deps(deps_str)?; - for (i, (lhs, rhs)) in dependencies.iter().enumerate() { - for &attr in lhs.iter().chain(rhs.iter()) { - if attr >= universe { - bail!( - "Dependency {} references attribute {} outside universe of size {}", - i, - attr, - universe - ); - } - } - } - if query >= universe { - bail!( - "Query attribute {} is outside universe of size {}", - query, - universe - ); - } - ( - ser(PrimeAttributeName::new(universe, dependencies, query))?, - resolved_variant.clone(), - ) - } - - // SequencingWithReleaseTimesAndDeadlines - "SequencingWithReleaseTimesAndDeadlines" => { - let lengths_str = args.lengths.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingWithReleaseTimesAndDeadlines requires --lengths, --release-times, and --deadlines\n\n\ - Usage: pred create SequencingWithReleaseTimesAndDeadlines --lengths 3,2,4 --release-times 0,1,5 --deadlines 5,6,10" - ) - })?; - let release_str = args.release_times.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingWithReleaseTimesAndDeadlines requires --release-times\n\n\ - Usage: pred create SequencingWithReleaseTimesAndDeadlines --lengths 3,2,4 --release-times 0,1,5 --deadlines 5,6,10" - ) - })?; - let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "SequencingWithReleaseTimesAndDeadlines requires --deadlines\n\n\ - Usage: pred create SequencingWithReleaseTimesAndDeadlines --lengths 3,2,4 --release-times 0,1,5 --deadlines 5,6,10" - ) - })?; - let lengths: Vec = util::parse_comma_list(lengths_str)?; - let release_times: Vec = util::parse_comma_list(release_str)?; - let deadlines: Vec = util::parse_comma_list(deadlines_str)?; - if lengths.len() != release_times.len() || lengths.len() != deadlines.len() { - bail!( - "All three lists must have the same length: lengths={}, release_times={}, deadlines={}", - lengths.len(), - release_times.len(), - deadlines.len() - ); - } - ( - ser(SequencingWithReleaseTimesAndDeadlines::new( - lengths, - release_times, - deadlines, - ))?, - resolved_variant.clone(), - ) - } - - // StringToStringCorrection - "StringToStringCorrection" => { - let usage = "Usage: pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2"; - let source_str = args.source_string.as_deref().ok_or_else(|| { - anyhow::anyhow!("StringToStringCorrection requires --source-string\n\n{usage}") - })?; - let target_str = args.target_string.as_deref().ok_or_else(|| { - anyhow::anyhow!("StringToStringCorrection requires --target-string\n\n{usage}") - })?; - let bound = parse_nonnegative_usize_bound( - args.bound.ok_or_else(|| { - anyhow::anyhow!("StringToStringCorrection requires --bound\n\n{usage}") - })?, - "StringToStringCorrection", - usage, - )?; - let parse_symbols = |s: &str| -> Result> { - if s.trim().is_empty() { - return Ok(Vec::new()); - } - s.split(',') - .map(|v| v.trim().parse::().context("invalid symbol index")) - .collect() - }; - let source = parse_symbols(source_str)?; - let target = parse_symbols(target_str)?; - let inferred = source - .iter() - .chain(target.iter()) - .copied() - .max() - .map_or(0, |m| m + 1); - let alphabet_size = args.alphabet_size.unwrap_or(inferred); - if alphabet_size < inferred { - anyhow::bail!( - "--alphabet-size {} is smaller than max symbol + 1 ({}) in the strings", - alphabet_size, - inferred - ); - } - ( - ser(StringToStringCorrection::new( - alphabet_size, - source, - target, - bound, - ))?, - resolved_variant.clone(), - ) - } - - // Clustering - "Clustering" => { - let usage = "Usage: pred create Clustering --distance-matrix \"0,1,1,3;1,0,1,3;1,1,0,3;3,3,3,0\" --k 2 --diameter-bound 1"; - let dist_str = args.distance_matrix.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "Clustering requires --distance-matrix, --k, and --diameter-bound\n\n{usage}" - ) - })?; - let distance_matrix = parse_u64_matrix_rows(dist_str, "distance matrix")?; - let k = args.k.ok_or_else(|| { - anyhow::anyhow!("Clustering requires --k (number of clusters)\n\n{usage}") - })?; - let diameter_bound = args - .diameter_bound - .ok_or_else(|| anyhow::anyhow!("Clustering requires --diameter-bound\n\n{usage}"))? - as u64; - ( - ser(Clustering::new(distance_matrix, k, diameter_bound))?, - resolved_variant.clone(), - ) - } - - // MinimumDecisionTree - "MinimumDecisionTree" => { - let usage = "Usage: pred create MinimumDecisionTree --test-matrix '[[true,true,false,false],[true,false,false,false],[false,true,false,true]]' --num-objects 4 --num-tests 3"; - let matrix_str = args.test_matrix.as_deref().ok_or_else(|| { - anyhow::anyhow!("MinimumDecisionTree requires --test-matrix\n\n{usage}") - })?; - let test_matrix: Vec> = serde_json::from_str(matrix_str) - .context("Failed to parse --test-matrix as JSON 2D bool array")?; - let num_objects = args.num_objects.ok_or_else(|| { - anyhow::anyhow!("MinimumDecisionTree requires --num-objects\n\n{usage}") - })?; - let num_tests = args.num_tests.ok_or_else(|| { - anyhow::anyhow!("MinimumDecisionTree requires --num-tests\n\n{usage}") - })?; - ( - ser(MinimumDecisionTree::new( - test_matrix, - num_objects, - num_tests, - ))?, - resolved_variant.clone(), - ) - } - - // MinimumDisjunctiveNormalForm - "MinimumDisjunctiveNormalForm" => { - let usage = "Usage: pred create MinDNF --num-vars 3 --truth-table 0,1,1,1,1,1,1,0"; - let num_vars = args.num_vars.ok_or_else(|| { - anyhow::anyhow!("MinimumDisjunctiveNormalForm requires --num-vars\n\n{usage}") - })?; - let tt_str = args.truth_table.as_deref().ok_or_else(|| { - anyhow::anyhow!("MinimumDisjunctiveNormalForm requires --truth-table\n\n{usage}") - })?; - let truth_table: Vec = tt_str - .split(',') - .map(|s| match s.trim() { - "1" | "true" => Ok(true), - "0" | "false" => Ok(false), - other => bail!("Invalid truth table entry '{}': expected 0 or 1", other), - }) - .collect::>>()?; - ( - ser(MinimumDisjunctiveNormalForm::new(num_vars, truth_table))?, - resolved_variant.clone(), - ) - } - - // SquareTiling - "SquareTiling" => { - let usage = "Usage: pred create SquareTiling --num-colors 3 --tiles \"0,1,2,0;0,0,2,1;2,1,0,0;2,0,0,1\" --grid-size 2"; - let num_colors = args - .num_colors - .ok_or_else(|| anyhow::anyhow!("SquareTiling requires --num-colors\n\n{usage}"))?; - let tiles_str = args - .tiles - .as_deref() - .ok_or_else(|| anyhow::anyhow!("SquareTiling requires --tiles\n\n{usage}"))?; - let tiles: Vec<(usize, usize, usize, usize)> = tiles_str - .split(';') - .map(|tile_s| { - let parts: Vec = tile_s - .split(',') - .map(|v| { - v.trim() - .parse::() - .context("invalid tile color index") - }) - .collect::>>()?; - if parts.len() != 4 { - bail!( - "Each tile must have exactly 4 values (top,right,bottom,left), got {}", - parts.len() - ); - } - Ok((parts[0], parts[1], parts[2], parts[3])) - }) - .collect::>>()?; - let grid_size = args - .grid_size - .ok_or_else(|| anyhow::anyhow!("SquareTiling requires --grid-size\n\n{usage}"))?; - ( - ser(SquareTiling::new(num_colors, tiles, grid_size))?, - resolved_variant.clone(), - ) - } - - _ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)), - }; - - let output = ProblemJsonOutput { - problem_type: canonical.to_string(), - variant, - data, - }; + let output = ProblemJsonOutput { + problem_type: canonical.to_string(), + variant, + data, + }; emit_problem_output(&output, out) } @@ -6800,83 +656,6 @@ fn reject_nonunit_weights_for_one_variant( } /// Create a vertex-weight problem dispatching on geometry graph type. -fn create_vertex_weight_problem( - args: &CreateArgs, - canonical: &str, - graph_type: &str, - resolved_variant: &BTreeMap, -) -> Result<(serde_json::Value, BTreeMap)> { - match graph_type { - "KingsSubgraph" => { - let positions = parse_int_positions(args)?; - let n = positions.len(); - let graph = KingsSubgraph::new(positions); - let weights = parse_vertex_weights(args, n)?; - reject_nonunit_weights_for_one_variant( - canonical, - graph_type, - resolved_variant, - &weights, - )?; - Ok(( - ser_vertex_weight_problem_with(canonical, graph, weights)?, - resolved_variant.clone(), - )) - } - "TriangularSubgraph" => { - let positions = parse_int_positions(args)?; - let n = positions.len(); - let graph = TriangularSubgraph::new(positions); - let weights = parse_vertex_weights(args, n)?; - reject_nonunit_weights_for_one_variant( - canonical, - graph_type, - resolved_variant, - &weights, - )?; - Ok(( - ser_vertex_weight_problem_with(canonical, graph, weights)?, - resolved_variant.clone(), - )) - } - "UnitDiskGraph" => { - let positions = parse_float_positions(args)?; - let n = positions.len(); - let radius = args.radius.unwrap_or(1.0); - let graph = UnitDiskGraph::new(positions, radius); - let weights = parse_vertex_weights(args, n)?; - reject_nonunit_weights_for_one_variant( - canonical, - graph_type, - resolved_variant, - &weights, - )?; - Ok(( - ser_vertex_weight_problem_with(canonical, graph, weights)?, - resolved_variant.clone(), - )) - } - _ => { - // SimpleGraph path (existing) - let (graph, n) = parse_graph(args).map_err(|e| { - anyhow::anyhow!( - "{e}\n\nUsage: pred create {} --graph 0-1,1-2,2-3 [--weights 1,1,1,1]", - canonical - ) - })?; - let weights = parse_vertex_weights(args, n)?; - reject_nonunit_weights_for_one_variant( - canonical, - graph_type, - resolved_variant, - &weights, - )?; - let data = ser_vertex_weight_problem_with(canonical, graph, weights)?; - Ok((data, resolved_variant.clone())) - } - } -} - /// Serialize a vertex-weight problem with a generic graph type. fn ser_vertex_weight_problem_with( canonical: &str, @@ -7292,125 +1071,16 @@ fn parse_bundle_capacities(args: &CreateArgs, num_bundles: usize, usage: &str) - } /// Parse `--couplings` as SpinGlass pairwise couplings (i32), defaulting to all 1s. -fn parse_couplings(args: &CreateArgs, num_edges: usize) -> Result> { - match &args.couplings { - Some(w) => { - let vals: Vec = w - .split(',') - .map(|s| s.trim().parse::()) - .collect::, _>>()?; - if vals.len() != num_edges { - bail!("Expected {} couplings but got {}", num_edges, vals.len()); - } - Ok(vals) - } - None => Ok(vec![1i32; num_edges]), - } -} - /// Parse `--fields` as SpinGlass on-site fields (i32), defaulting to all 0s. -fn parse_fields(args: &CreateArgs, num_vertices: usize) -> Result> { - match &args.fields { - Some(w) => { - let vals: Vec = w - .split(',') - .map(|s| s.trim().parse::()) - .collect::, _>>()?; - if vals.len() != num_vertices { - bail!("Expected {} fields but got {}", num_vertices, vals.len()); - } - Ok(vals) - } - None => Ok(vec![0i32; num_vertices]), - } -} - /// Check if a CLI string value contains float syntax (a decimal point). -fn has_float_syntax(value: &Option) -> bool { - value.as_ref().is_some_and(|s| s.contains('.')) -} - /// Parse `--couplings` as SpinGlass pairwise couplings (f64), defaulting to all 1.0. -fn parse_couplings_f64(args: &CreateArgs, num_edges: usize) -> Result> { - match &args.couplings { - Some(w) => { - let vals: Vec = w - .split(',') - .map(|s| s.trim().parse::()) - .collect::, _>>()?; - if vals.len() != num_edges { - bail!("Expected {} couplings but got {}", num_edges, vals.len()); - } - Ok(vals) - } - None => Ok(vec![1.0f64; num_edges]), - } -} - /// Parse `--fields` as SpinGlass on-site fields (f64), defaulting to all 0.0. -fn parse_fields_f64(args: &CreateArgs, num_vertices: usize) -> Result> { - match &args.fields { - Some(w) => { - let vals: Vec = w - .split(',') - .map(|s| s.trim().parse::()) - .collect::, _>>()?; - if vals.len() != num_vertices { - bail!("Expected {} fields but got {}", num_vertices, vals.len()); - } - Ok(vals) - } - None => Ok(vec![0.0f64; num_vertices]), - } -} - /// Parse `--clauses` as semicolon-separated clauses of comma-separated literals. /// E.g., "1,2;-1,3;2,-3" -fn parse_clauses(args: &CreateArgs) -> Result> { - let clauses_str = args - .clauses - .as_deref() - .ok_or_else(|| anyhow::anyhow!("SAT problems require --clauses (e.g., \"1,2;-1,3\")"))?; - - clauses_str - .split(';') - .map(|clause| { - let literals: Vec = clause - .trim() - .split(',') - .map(|s| s.trim().parse::()) - .collect::, _>>()?; - Ok(CNFClause::new(literals)) - }) - .collect() -} - -fn parse_disjuncts(args: &CreateArgs) -> Result>> { - let disjuncts_str = args - .disjuncts - .as_deref() - .or(args.clauses.as_deref()) - .ok_or_else(|| { - anyhow::anyhow!("NonTautology requires --disjuncts (e.g., \"1,2,3;-1,-2,-3\")") - })?; - - disjuncts_str - .split(';') - .map(|disjunct| { - disjunct - .trim() - .split(',') - .map(|s| s.trim().parse::()) - .collect::, _>>() - .map_err(anyhow::Error::from) - }) - .collect() -} - -/// Parse `--sets` as semicolon-separated sets of comma-separated usize. +/// Parse `--subsets` as semicolon-separated sets of comma-separated usize. /// E.g., "0,1;1,2;0,2" fn parse_sets(args: &CreateArgs) -> Result>> { - parse_named_sets(args.sets.as_deref(), "--sets") + parse_named_sets(args.sets.as_deref(), "--subsets") } fn parse_named_sets(sets_str: Option<&str>, flag: &str) -> Result>> { @@ -7462,31 +1132,7 @@ fn parse_homologous_pairs(args: &CreateArgs) -> Result> { /// Parse a dependency string as semicolon-separated `lhs>rhs` pairs. /// E.g., "0,1>2,3;2,3>0,1" -fn parse_deps(s: &str) -> Result, Vec)>> { - s.split(';') - .map(|dep| { - let parts: Vec<&str> = dep.split('>').collect(); - if parts.len() != 2 { - bail!("Invalid dependency format '{}': expected 'lhs>rhs'", dep); - } - let lhs = parse_index_list(parts[0])?; - let rhs = parse_index_list(parts[1])?; - Ok((lhs, rhs)) - }) - .collect() -} - /// Parse a comma-separated list of usize indices. -fn parse_index_list(s: &str) -> Result> { - s.split(',') - .map(|x| { - x.trim() - .parse::() - .map_err(|e| anyhow::anyhow!("Invalid index '{}': {}", x.trim(), e)) - }) - .collect() -} - /// Parse `--dependencies` as semicolon-separated "lhs>rhs" pairs. /// E.g., "0,1>2;0,2>3;1,3>4;2,4>5" means {0,1}->{2}, {0,2}->{3}, etc. fn parse_dependencies(input: &str) -> Result, Vec)>> { @@ -7633,9 +1279,9 @@ fn parse_bundles(args: &CreateArgs, num_arcs: usize, usage: &str) -> Result Result { - let raw_bound = args - .bound - .ok_or_else(|| anyhow::anyhow!("MultipleChoiceBranching requires --bound\n\n{usage}"))?; + let raw_bound = args.bound.ok_or_else(|| { + anyhow::anyhow!("MultipleChoiceBranching requires --threshold\n\n{usage}") + })?; anyhow::ensure!( raw_bound >= 0, "MultipleChoiceBranching threshold must be non-negative, got {raw_bound}" @@ -7648,10 +1294,6 @@ fn parse_multiple_choice_branching_threshold(args: &CreateArgs, usage: &str) -> } /// Parse `--weights` for set-based problems (i32), defaulting to all 1s. -fn parse_set_weights(args: &CreateArgs, num_sets: usize) -> Result> { - parse_named_set_weights(args.weights.as_deref(), num_sets, "--weights") -} - fn parse_named_set_weights( weights_str: Option<&str>, num_sets: usize, @@ -7734,14 +1376,6 @@ fn parse_bool_matrix(args: &CreateArgs) -> Result>> { parse_bool_rows(matrix_str) } -fn parse_schedules(args: &CreateArgs, usage: &str) -> Result>> { - let schedules_str = args - .schedules - .as_deref() - .ok_or_else(|| anyhow::anyhow!("StaffScheduling requires --schedules\n\n{usage}"))?; - parse_bool_rows(schedules_str) -} - fn parse_bool_rows(rows_str: &str) -> Result>> { let matrix: Vec> = rows_str .split(';') @@ -7769,14 +1403,6 @@ fn parse_bool_rows(rows_str: &str) -> Result>> { Ok(matrix) } -fn parse_requirements(args: &CreateArgs, usage: &str) -> Result> { - let requirements_str = args - .requirements - .as_deref() - .ok_or_else(|| anyhow::anyhow!("StaffScheduling requires --requirements\n\n{usage}"))?; - util::parse_comma_list(requirements_str) -} - fn parse_named_u64_list( raw: Option<&str>, problem: &str, @@ -7795,45 +1421,6 @@ fn ensure_named_len(len: usize, expected: usize, flag: &str, usage: &str) -> Res Ok(()) } -fn validate_staff_scheduling_args( - schedules: &[Vec], - requirements: &[u64], - shifts_per_schedule: usize, - num_workers: u64, - usage: &str, -) -> Result<()> { - if num_workers >= usize::MAX as u64 { - bail!( - "StaffScheduling requires --num-workers to fit in usize for brute-force enumeration\n\n{usage}" - ); - } - - let num_periods = requirements.len(); - for (index, schedule) in schedules.iter().enumerate() { - if schedule.len() != num_periods { - bail!( - "schedule {} has {} periods, expected {}\n\n{}", - index, - schedule.len(), - num_periods, - usage - ); - } - let ones = schedule.iter().filter(|&&active| active).count(); - if ones != shifts_per_schedule { - bail!( - "schedule {} has {} active periods, expected {}\n\n{}", - index, - ones, - shifts_per_schedule, - usage - ); - } - } - - Ok(()) -} - fn parse_named_bool_rows(rows: Option<&str>, flag: &str, usage: &str) -> Result>> { let rows = rows.ok_or_else(|| anyhow::anyhow!("TimetableDesign requires {flag}\n\n{usage}"))?; parse_bool_rows(rows).map_err(|err| { @@ -7975,68 +1562,13 @@ fn parse_u64_matrix_rows(matrix_str: &str, matrix_name: &str) -> Result Result> { - let q_str = args - .quantifiers - .as_deref() - .ok_or_else(|| anyhow::anyhow!("QBF requires --quantifiers (e.g., \"E,A,E\")"))?; - - let quantifiers: Vec = q_str - .split(',') - .map(|s| match s.trim().to_lowercase().as_str() { - "e" | "exists" => Ok(Quantifier::Exists), - "a" | "forall" => Ok(Quantifier::ForAll), - other => Err(anyhow::anyhow!( - "Invalid quantifier '{}': expected E/Exists or A/ForAll", - other - )), - }) - .collect::>>()?; - - if quantifiers.len() != num_vars { - bail!( - "Expected {} quantifiers but got {}", - num_vars, - quantifiers.len() - ); - } - Ok(quantifiers) -} - /// Parse a semicolon-separated matrix of i64 values. /// E.g., "0,5;5,0" -fn parse_i64_matrix(s: &str) -> Result>> { - let matrix: Vec> = s - .split(';') - .enumerate() - .map(|(row_idx, row)| { - row.trim() - .split(',') - .enumerate() - .map(|(col_idx, v)| { - v.trim().parse::().map_err(|e| { - anyhow::anyhow!("Invalid value at row {row_idx}, col {col_idx}: {e}") - }) - }) - .collect() - }) - .collect::>()?; - if let Some(first_len) = matrix.first().map(|r| r.len()) { - for (i, row) in matrix.iter().enumerate() { - if row.len() != first_len { - bail!( - "Ragged matrix: row {i} has {} columns, expected {first_len}", - row.len() - ); - } - } - } - Ok(matrix) -} - fn parse_potential_edges(args: &CreateArgs) -> Result> { let edges_str = args.potential_edges.as_deref().ok_or_else(|| { - anyhow::anyhow!("BiconnectivityAugmentation requires --potential-edges (e.g., 0-2:3,1-3:5)") + anyhow::anyhow!( + "BiconnectivityAugmentation requires --potential-weights (e.g., 0-2:3,1-3:5)" + ) })?; edges_str @@ -8219,68 +1751,24 @@ fn parse_arc_weights(args: &CreateArgs, num_arcs: usize) -> Result> { } } -/// Parse `--arc-costs` as per-arc costs (i32), defaulting to all 1s. -fn parse_arc_costs(args: &CreateArgs, num_arcs: usize) -> Result> { - match &args.arc_costs { - Some(costs) => { - let parsed: Vec = costs - .split(',') - .map(|s| s.trim().parse::()) - .collect::, _>>()?; - if parsed.len() != num_arcs { - bail!("Expected {} arc costs but got {}", num_arcs, parsed.len()); - } - Ok(parsed) - } - None => Ok(vec![1i32; num_arcs]), - } -} - -/// Parse `--candidate-arcs` as `u>v:w` entries for StrongConnectivityAugmentation. -fn parse_candidate_arcs( - args: &CreateArgs, - num_vertices: usize, -) -> Result> { - let arcs_str = args.candidate_arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "StrongConnectivityAugmentation requires --candidate-arcs (e.g., \"2>0:1,2>1:3\")" - ) - })?; - - arcs_str - .split(',') - .map(|entry| { - let entry = entry.trim(); - let (arc_part, weight_part) = entry.split_once(':').ok_or_else(|| { - anyhow::anyhow!( - "Invalid candidate arc '{}': expected format u>v:w (e.g., 2>0:1)", - entry - ) - })?; - let parts: Vec<&str> = arc_part.split('>').collect(); - if parts.len() != 2 { - bail!( - "Invalid candidate arc '{}': expected format u>v:w (e.g., 2>0:1)", - entry - ); - } - - let u: usize = parts[0].parse()?; - let v: usize = parts[1].parse()?; - anyhow::ensure!( - u < num_vertices && v < num_vertices, - "candidate arc ({}, {}) references vertex >= num_vertices ({})", - u, - v, - num_vertices - ); - - let w: i32 = weight_part.parse()?; - Ok((u, v, w)) - }) - .collect() +/// Parse `--arc-weights` / `--arc-lengths` as per-arc costs (i32), defaulting to all 1s. +fn parse_arc_costs(args: &CreateArgs, num_arcs: usize) -> Result> { + match &args.arc_costs { + Some(costs) => { + let parsed: Vec = costs + .split(',') + .map(|s| s.trim().parse::()) + .collect::, _>>()?; + if parsed.len() != num_arcs { + bail!("Expected {} arc costs but got {}", num_arcs, parsed.len()); + } + Ok(parsed) + } + None => Ok(vec![1i32; num_arcs]), + } } +/// Parse `--candidate-arcs` as `u>v:w` entries for StrongConnectivityAugmentation. /// Handle `pred create --random ...` fn create_random( args: &CreateArgs, @@ -8788,2262 +2276,4 @@ fn parse_implications(s: &str) -> Result, usize)>> { } #[cfg(test)] -mod tests { - use std::fs; - use std::path::PathBuf; - use std::time::{SystemTime, UNIX_EPOCH}; - - use clap::Parser; - - use super::help_flag_hint; - use super::help_flag_name; - use super::parse_bool_rows; - use super::*; - use super::{ensure_attribute_indices_in_range, problem_help_flag_name}; - use crate::cli::{Cli, Commands}; - use crate::output::OutputConfig; - - fn temp_output_path(name: &str) -> PathBuf { - let suffix = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - std::env::temp_dir().join(format!("{}_{}.json", name, suffix)) - } - - #[test] - fn test_problem_help_uses_bound_for_length_bounded_disjoint_paths() { - assert_eq!( - problem_help_flag_name("LengthBoundedDisjointPaths", "max_length", "usize", false), - "bound" - ); - } - - #[test] - fn test_problem_help_preserves_generic_field_kebab_case() { - assert_eq!( - problem_help_flag_name("LengthBoundedDisjointPaths", "max_paths", "usize", false,), - "max-paths" - ); - } - - #[test] - fn test_help_flag_name_mentions_m_alias_for_scheduling_processors() { - assert_eq!( - help_flag_name("SchedulingWithIndividualDeadlines", "num_processors"), - "num-processors/--m" - ); - assert_eq!( - help_flag_name("FlowShopScheduling", "num_processors"), - "num-processors/--m" - ); - } - - #[test] - fn test_ensure_attribute_indices_in_range_rejects_out_of_range_index() { - let err = ensure_attribute_indices_in_range(&[0, 4], 3, "Functional dependency '0:4' rhs") - .unwrap_err(); - assert!( - err.to_string().contains("out of range"), - "unexpected error: {err}" - ); - } - - #[test] - fn test_create_scheduling_with_individual_deadlines_accepts_m_alias() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "SchedulingWithIndividualDeadlines", - "--n", - "3", - "--deadlines", - "1,1,2", - "--m", - "2", - ]) - .expect("parse create command"); - - let Commands::Create(args) = cli.command else { - panic!("expected create subcommand"); - }; - - let out = OutputConfig { - output: Some( - std::env::temp_dir() - .join("pred_test_create_scheduling_with_individual_deadlines_m_alias.json"), - ), - quiet: true, - json: false, - auto_json: false, - }; - create(&args, &out).expect("`--m` should satisfy --num-processors alias"); - - let created: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(out.output.as_ref().unwrap()).unwrap()) - .unwrap(); - std::fs::remove_file(out.output.as_ref().unwrap()).ok(); - - assert_eq!(created["type"], "SchedulingWithIndividualDeadlines"); - assert_eq!(created["data"]["num_processors"], 2); - } - - #[test] - fn test_problem_help_uses_prime_attribute_name_cli_overrides() { - assert_eq!( - problem_help_flag_name("PrimeAttributeName", "num_attributes", "usize", false), - "universe" - ); - assert_eq!( - problem_help_flag_name( - "PrimeAttributeName", - "dependencies", - "Vec<(Vec, Vec)>", - false, - ), - "deps" - ); - assert_eq!( - problem_help_flag_name("PrimeAttributeName", "query_attribute", "usize", false), - "query" - ); - } - - #[test] - fn test_problem_help_uses_problem_specific_lcs_strings_hint() { - assert_eq!( - help_flag_hint( - "LongestCommonSubsequence", - "strings", - "Vec>", - None, - ), - "raw strings: \"ABAC;BACA\" or symbol lists: \"0,1,0;1,0,1\"" - ); - } - - #[test] - fn test_problem_help_uses_string_to_string_correction_cli_flags() { - assert_eq!( - problem_help_flag_name("StringToStringCorrection", "source", "Vec", false), - "source-string" - ); - assert_eq!( - problem_help_flag_name("StringToStringCorrection", "target", "Vec", false), - "target-string" - ); - assert_eq!( - problem_help_flag_name("StringToStringCorrection", "bound", "usize", false), - "bound" - ); - } - - #[test] - fn test_problem_help_keeps_generic_vec_vec_usize_hint_for_other_models() { - assert_eq!( - help_flag_hint("SetBasis", "sets", "Vec>", None), - "semicolon-separated sets: \"0,1;1,2;0,2\"" - ); - } - - #[test] - fn test_problem_help_uses_k_for_staff_scheduling() { - assert_eq!( - help_flag_name("StaffScheduling", "shifts_per_schedule"), - "k" - ); - assert_eq!( - problem_help_flag_name("StaffScheduling", "shifts_per_schedule", "usize", false), - "k" - ); - } - - #[test] - fn test_parse_bool_rows_reports_generic_invalid_boolean_entry() { - let err = parse_bool_rows("1,maybe").unwrap_err().to_string(); - assert_eq!( - err, - "Invalid boolean entry 'maybe': expected 0/1 or true/false" - ); - } - - #[test] - fn test_create_staff_scheduling_outputs_problem_json() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "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", - ]) - .unwrap(); - - let args = match cli.command { - Commands::Create(args) => args, - _ => panic!("expected create command"), - }; - - let suffix = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let output_path = - std::env::temp_dir().join(format!("staff-scheduling-create-{suffix}.json")); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&output_path).unwrap()).unwrap(); - assert_eq!(json["type"], "StaffScheduling"); - assert_eq!(json["data"]["num_workers"], 4); - assert_eq!( - json["data"]["requirements"], - serde_json::json!([2, 2, 2, 3, 3, 2, 1]) - ); - std::fs::remove_file(output_path).unwrap(); - } - - #[test] - fn test_create_path_constrained_network_flow_outputs_problem_json() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "PathConstrainedNetworkFlow", - "--arcs", - "0>1,0>2,1>3,1>4,2>4,3>5,4>5,4>6,5>7,6>7", - "--capacities", - "2,1,1,1,1,1,1,1,2,1", - "--source", - "0", - "--sink", - "7", - "--paths", - "0,2,5,8;0,3,6,8;0,3,7,9;1,4,6,8;1,4,7,9", - "--requirement", - "3", - ]) - .expect("parse create command"); - - let args = match cli.command { - Commands::Create(args) => args, - _ => panic!("expected create command"), - }; - - let output_path = temp_output_path("path_constrained_network_flow"); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).expect("create PathConstrainedNetworkFlow JSON"); - - let created: serde_json::Value = - serde_json::from_str(&fs::read_to_string(&output_path).unwrap()).unwrap(); - fs::remove_file(output_path).ok(); - - assert_eq!(created["type"], "PathConstrainedNetworkFlow"); - assert_eq!(created["data"]["source"], 0); - assert_eq!(created["data"]["sink"], 7); - assert_eq!(created["data"]["requirement"], 3); - assert_eq!(created["data"]["paths"][0], serde_json::json!([0, 2, 5, 8])); - } - - #[test] - fn test_create_path_constrained_network_flow_rejects_invalid_paths() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "PathConstrainedNetworkFlow", - "--arcs", - "0>1,1>2,2>3", - "--capacities", - "1,1,1", - "--source", - "0", - "--sink", - "3", - "--paths", - "0,3", - "--requirement", - "1", - ]) - .expect("parse create command"); - - let args = match cli.command { - Commands::Create(args) => args, - _ => panic!("expected create command"), - }; - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("out of bounds") || err.contains("not contiguous")); - } - - #[test] - fn test_create_staff_scheduling_reports_invalid_schedule_without_panic() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "StaffScheduling", - "--schedules", - "1,1,1,1,1,0,0;0,1,1,1,1,1", - "--requirements", - "2,2,2,3,3,2,1", - "--num-workers", - "4", - "--k", - "5", - ]) - .unwrap(); - - let args = match cli.command { - Commands::Create(args) => args, - _ => panic!("expected create command"), - }; - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let result = std::panic::catch_unwind(|| create(&args, &out)); - assert!(result.is_ok(), "create should return an error, not panic"); - let err = result.unwrap().unwrap_err().to_string(); - // parse_bool_rows catches ragged rows before validate_staff_scheduling_args - assert!( - err.contains("All rows") || err.contains("schedule 1 has 6 periods, expected 7"), - "expected row-length validation error, got: {err}" - ); - } - - #[test] - fn test_problem_help_uses_num_tasks_for_timetable_design() { - assert_eq!( - problem_help_flag_name("TimetableDesign", "num_tasks", "usize", false), - "num-tasks" - ); - assert_eq!( - help_flag_hint("TimetableDesign", "craftsman_avail", "Vec>", None), - "semicolon-separated 0/1 rows: \"1,1,0;0,1,1\"" - ); - } - - #[test] - fn test_example_for_path_constrained_network_flow_mentions_paths_flag() { - let example = example_for("PathConstrainedNetworkFlow", None); - assert!(example.contains("--paths")); - assert!(example.contains("--requirement")); - } - - #[test] - fn test_example_for_three_partition_mentions_sizes_and_bound() { - let example = example_for("ThreePartition", None); - assert!(example.contains("--sizes")); - assert!(example.contains("--bound")); - } - - #[test] - fn test_create_three_partition_outputs_problem_json() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "ThreePartition", - "--sizes", - "4,5,6,4,6,5", - "--bound", - "15", - ]) - .expect("parse create command"); - - let args = match cli.command { - Commands::Create(args) => args, - _ => panic!("expected create command"), - }; - - let output_path = temp_output_path("three_partition_create"); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).expect("create ThreePartition JSON"); - - let created: serde_json::Value = - serde_json::from_str(&fs::read_to_string(&output_path).unwrap()).unwrap(); - fs::remove_file(output_path).ok(); - - assert_eq!(created["type"], "ThreePartition"); - assert_eq!( - created["data"]["sizes"], - serde_json::json!([4, 5, 6, 4, 6, 5]) - ); - assert_eq!(created["data"]["bound"], 15); - } - - #[test] - fn test_create_three_partition_requires_bound() { - let cli = - Cli::try_parse_from(["pred", "create", "ThreePartition", "--sizes", "4,5,6,4,6,5"]) - .expect("parse create command"); - - let args = match cli.command { - Commands::Create(args) => args, - _ => panic!("expected create command"), - }; - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("ThreePartition requires --bound")); - } - - #[test] - fn test_create_three_partition_rejects_invalid_instance() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "ThreePartition", - "--sizes", - "4,5,6,4,6,5", - "--bound", - "14", - ]) - .expect("parse create command"); - - let args = match cli.command { - Commands::Create(args) => args, - _ => panic!("expected create command"), - }; - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("must equal m * bound")); - } - - #[test] - fn test_create_timetable_design_outputs_problem_json() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "TimetableDesign", - "--num-periods", - "3", - "--num-craftsmen", - "5", - "--num-tasks", - "5", - "--craftsman-avail", - "1,1,1;1,1,0;0,1,1;1,0,1;1,1,1", - "--task-avail", - "1,1,0;0,1,1;1,0,1;1,1,1;1,1,1", - "--requirements", - "1,0,1,0,0;0,1,0,0,1;0,0,0,1,0;0,0,0,0,1;0,1,0,0,0", - ]) - .unwrap(); - - let args = match cli.command { - Commands::Create(args) => args, - _ => panic!("expected create command"), - }; - - let suffix = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let output_path = - std::env::temp_dir().join(format!("timetable-design-create-{suffix}.json")); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&output_path).unwrap()).unwrap(); - assert_eq!(json["type"], "TimetableDesign"); - assert_eq!(json["data"]["num_periods"], 3); - assert_eq!(json["data"]["num_craftsmen"], 5); - assert_eq!(json["data"]["num_tasks"], 5); - assert_eq!( - json["data"]["craftsman_avail"], - serde_json::json!([ - [true, true, true], - [true, true, false], - [false, true, true], - [true, false, true], - [true, true, true] - ]) - ); - assert_eq!( - json["data"]["task_avail"], - serde_json::json!([ - [true, true, false], - [false, true, true], - [true, false, true], - [true, true, true], - [true, true, true] - ]) - ); - assert_eq!( - json["data"]["requirements"], - serde_json::json!([ - [1, 0, 1, 0, 0], - [0, 1, 0, 0, 1], - [0, 0, 0, 1, 0], - [0, 0, 0, 0, 1], - [0, 1, 0, 0, 0] - ]) - ); - std::fs::remove_file(output_path).unwrap(); - } - - #[test] - fn test_create_timetable_design_reports_invalid_matrix_without_panic() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "TimetableDesign", - "--num-periods", - "3", - "--num-craftsmen", - "5", - "--num-tasks", - "5", - "--craftsman-avail", - "1,1,1;1,1", - "--task-avail", - "1,1,0;0,1,1;1,0,1;1,1,1;1,1,1", - "--requirements", - "1,0,1,0,0;0,1,0,0,1;0,0,0,1,0;0,0,0,0,1;0,1,0,0,0", - ]) - .unwrap(); - - let args = match cli.command { - Commands::Create(args) => args, - _ => panic!("expected create command"), - }; - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let result = std::panic::catch_unwind(|| create(&args, &out)); - assert!(result.is_ok(), "create should return an error, not panic"); - let err = result.unwrap().unwrap_err().to_string(); - assert!( - err.contains("--craftsman-avail"), - "expected timetable matrix validation error, got: {err}" - ); - assert!(err.contains("Usage: pred create TimetableDesign")); - } - - #[test] - fn test_create_generalized_hex_serializes_problem_json() { - let output = temp_output_path("generalized_hex_create"); - let cli = Cli::try_parse_from([ - "pred", - "-o", - output.to_str().unwrap(), - "create", - "GeneralizedHex", - "--graph", - "0-1,0-2,0-3,1-4,2-4,3-4,4-5", - "--source", - "0", - "--sink", - "5", - ]) - .unwrap(); - let out = OutputConfig { - output: cli.output.clone(), - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - create(&args, &out).unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap(); - fs::remove_file(&output).unwrap(); - assert_eq!(json["type"], "GeneralizedHex"); - assert_eq!(json["variant"]["graph"], "SimpleGraph"); - assert_eq!(json["data"]["source"], 0); - assert_eq!(json["data"]["target"], 5); - } - - #[test] - fn test_create_generalized_hex_requires_sink() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "GeneralizedHex", - "--graph", - "0-1,1-2,2-3", - "--source", - "0", - ]) - .unwrap(); - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - let err = create(&args, &out).unwrap_err(); - assert!(err.to_string().contains("GeneralizedHex requires --sink")); - } - - #[test] - fn test_create_capacity_assignment_serializes_problem_json() { - let output = temp_output_path("capacity_assignment_create"); - let cli = Cli::try_parse_from([ - "pred", - "-o", - output.to_str().unwrap(), - "create", - "CapacityAssignment", - "--capacities", - "1,2,3", - "--cost-matrix", - "1,3,6;2,4,7;1,2,5", - "--delay-matrix", - "8,4,1;7,3,1;6,3,1", - "--delay-budget", - "12", - ]) - .expect("parse create command"); - let out = OutputConfig { - output: cli.output.clone(), - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - create(&args, &out).unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap(); - fs::remove_file(&output).unwrap(); - assert_eq!(json["type"], "CapacityAssignment"); - assert_eq!(json["data"]["capacities"], serde_json::json!([1, 2, 3])); - assert_eq!(json["data"]["delay_budget"], 12); - } - - #[test] - fn test_create_production_planning_serializes_problem_json() { - let output = temp_output_path("production_planning_create"); - let cli = Cli::try_parse_from([ - "pred", - "-o", - output.to_str().unwrap(), - "create", - "ProductionPlanning", - "--num-periods", - "6", - "--demands", - "5,3,7,2,8,5", - "--capacities", - "12,12,12,12,12,12", - "--setup-costs", - "10,10,10,10,10,10", - "--production-costs", - "1,1,1,1,1,1", - "--inventory-costs", - "1,1,1,1,1,1", - "--cost-bound", - "80", - ]) - .expect("parse create command"); - let out = OutputConfig { - output: cli.output.clone(), - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - create(&args, &out).unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap(); - fs::remove_file(&output).unwrap(); - assert_eq!(json["type"], "ProductionPlanning"); - assert_eq!(json["data"]["num_periods"], 6); - assert_eq!( - json["data"]["demands"], - serde_json::json!([5, 3, 7, 2, 8, 5]) - ); - assert_eq!( - json["data"]["capacities"], - serde_json::json!([12, 12, 12, 12, 12, 12]) - ); - assert_eq!( - json["data"]["setup_costs"], - serde_json::json!([10, 10, 10, 10, 10, 10]) - ); - assert_eq!( - json["data"]["production_costs"], - serde_json::json!([1, 1, 1, 1, 1, 1]) - ); - assert_eq!( - json["data"]["inventory_costs"], - serde_json::json!([1, 1, 1, 1, 1, 1]) - ); - assert_eq!(json["data"]["cost_bound"], 80); - } - - #[test] - fn test_create_production_planning_requires_all_period_vectors() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "ProductionPlanning", - "--num-periods", - "6", - "--demands", - "5,3,7,2,8,5", - "--capacities", - "12,12,12,12,12,12", - "--setup-costs", - "10,10,10,10,10,10", - "--inventory-costs", - "1,1,1,1,1,1", - "--cost-bound", - "80", - ]) - .expect("parse create command"); - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - let err = create(&args, &out).unwrap_err(); - assert!(err - .to_string() - .contains("ProductionPlanning requires --production-costs")); - } - - #[test] - fn test_create_production_planning_rejects_mismatched_period_lengths() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "ProductionPlanning", - "--num-periods", - "6", - "--demands", - "5,3,7,2,8", - "--capacities", - "12,12,12,12,12,12", - "--setup-costs", - "10,10,10,10,10,10", - "--production-costs", - "1,1,1,1,1,1", - "--inventory-costs", - "1,1,1,1,1,1", - "--cost-bound", - "80", - ]) - .expect("parse create command"); - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - let err = create(&args, &out).unwrap_err(); - assert!(err - .to_string() - .contains("--demands must contain exactly 6 entries")); - } - - #[test] - fn test_create_example_production_planning_uses_canonical_example() { - let output = temp_output_path("production_planning_example_create"); - let cli = Cli::try_parse_from([ - "pred", - "-o", - output.to_str().unwrap(), - "create", - "--example", - "ProductionPlanning", - ]) - .expect("parse create command"); - let out = OutputConfig { - output: cli.output.clone(), - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - create(&args, &out).unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap(); - fs::remove_file(&output).unwrap(); - assert_eq!(json["type"], "ProductionPlanning"); - assert_eq!(json["data"]["num_periods"], 4); - assert_eq!(json["data"]["demands"], serde_json::json!([2, 1, 3, 2])); - assert_eq!(json["data"]["cost_bound"], 16); - } - - #[test] - fn test_create_longest_path_serializes_problem_json() { - let output = temp_output_path("longest_path_create"); - let cli = Cli::try_parse_from([ - "pred", - "-o", - output.to_str().unwrap(), - "create", - "LongestPath", - "--graph", - "0-1,0-2,1-3,2-3,2-4,3-5,4-5,4-6,5-6,1-6", - "--edge-lengths", - "3,2,4,1,5,2,3,2,4,1", - "--source-vertex", - "0", - "--target-vertex", - "6", - ]) - .unwrap(); - let out = OutputConfig { - output: cli.output.clone(), - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - create(&args, &out).unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap(); - fs::remove_file(&output).unwrap(); - assert_eq!(json["type"], "LongestPath"); - assert_eq!(json["variant"]["graph"], "SimpleGraph"); - assert_eq!(json["variant"]["weight"], "i32"); - assert_eq!(json["data"]["source_vertex"], 0); - assert_eq!(json["data"]["target_vertex"], 6); - assert_eq!( - json["data"]["edge_lengths"], - serde_json::json!([3, 2, 4, 1, 5, 2, 3, 2, 4, 1]) - ); - } - - #[test] - fn test_create_undirected_flow_lower_bounds_serializes_problem_json() { - let output = temp_output_path("undirected_flow_lower_bounds_create"); - let cli = Cli::try_parse_from([ - "pred", - "-o", - output.to_str().unwrap(), - "create", - "UndirectedFlowLowerBounds", - "--graph", - "0-1,0-2,1-3,2-3,1-4,3-5,4-5", - "--capacities", - "2,2,2,2,1,3,2", - "--lower-bounds", - "1,1,0,0,1,0,1", - "--source", - "0", - "--sink", - "5", - "--requirement", - "3", - ]) - .unwrap(); - let out = OutputConfig { - output: cli.output.clone(), - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - create(&args, &out).unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap(); - fs::remove_file(&output).unwrap(); - assert_eq!(json["type"], "UndirectedFlowLowerBounds"); - assert_eq!(json["data"]["source"], 0); - assert_eq!(json["data"]["sink"], 5); - assert_eq!(json["data"]["requirement"], 3); - assert_eq!( - json["data"]["lower_bounds"], - serde_json::json!([1, 1, 0, 0, 1, 0, 1]) - ); - } - - #[test] - fn test_create_capacity_assignment_rejects_non_monotone_cost_row() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "CapacityAssignment", - "--capacities", - "1,2,3", - "--cost-matrix", - "1,3,2;2,4,7;1,2,5", - "--delay-matrix", - "8,4,1;7,3,1;6,3,1", - "--delay-budget", - "12", - ]) - .expect("parse create command"); - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("cost row 0")); - assert!(err.contains("non-decreasing")); - } - - #[test] - fn test_create_capacity_assignment_rejects_matrix_width_mismatch() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "CapacityAssignment", - "--capacities", - "1,2,3", - "--cost-matrix", - "1,3;2,4,7;1,2,5", - "--delay-matrix", - "8,4,1;7,3,1;6,3,1", - "--delay-budget", - "12", - ]) - .expect("parse create command"); - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("cost row 0")); - assert!(err.contains("capacities length")); - } - - #[test] - fn test_create_longest_path_requires_edge_lengths() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "LongestPath", - "--graph", - "0-1,1-2", - "--source-vertex", - "0", - "--target-vertex", - "2", - ]) - .unwrap(); - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - let err = create(&args, &out).unwrap_err(); - assert!(err - .to_string() - .contains("LongestPath requires --edge-lengths")); - } - - #[test] - fn test_create_longest_path_rejects_weights_flag() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "LongestPath", - "--graph", - "0-1,1-2", - "--weights", - "1,1,1", - "--source-vertex", - "0", - "--target-vertex", - "2", - "--edge-lengths", - "5,7", - ]) - .unwrap(); - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - let err = create(&args, &out).unwrap_err(); - assert!(err - .to_string() - .contains("LongestPath uses --edge-lengths, not --weights")); - } - - #[test] - fn test_create_undirected_flow_lower_bounds_requires_lower_bounds() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "UndirectedFlowLowerBounds", - "--graph", - "0-1,0-2,1-3,2-3,1-4,3-5,4-5", - "--capacities", - "2,2,2,2,1,3,2", - "--source", - "0", - "--sink", - "5", - "--requirement", - "3", - ]) - .unwrap(); - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - let err = create(&args, &out).unwrap_err(); - assert!(err - .to_string() - .contains("UndirectedFlowLowerBounds requires --lower-bounds")); - } - - fn empty_args() -> CreateArgs { - CreateArgs { - problem: Some("BiconnectivityAugmentation".to_string()), - example: None, - example_target: None, - example_side: crate::cli::ExampleSide::Source, - graph: None, - weights: None, - edge_weights: None, - edge_lengths: None, - capacities: None, - demands: None, - setup_costs: None, - production_costs: None, - inventory_costs: None, - bundle_capacities: None, - cost_matrix: None, - delay_matrix: None, - lower_bounds: None, - multipliers: None, - source: None, - sink: None, - requirement: None, - num_paths_required: None, - paths: None, - couplings: None, - fields: None, - clauses: None, - disjuncts: None, - num_vars: None, - matrix: None, - k: None, - num_partitions: None, - random: false, - source_vertex: None, - target_vertex: None, - num_vertices: None, - edge_prob: None, - seed: None, - target: None, - m: None, - n: None, - positions: None, - radius: None, - source_1: None, - sink_1: None, - source_2: None, - sink_2: None, - requirement_1: None, - requirement_2: None, - sizes: None, - probabilities: None, - capacity: None, - sequence: None, - sets: None, - r_sets: None, - s_sets: None, - r_weights: None, - s_weights: None, - partition: None, - partitions: None, - bundles: None, - universe: None, - biedges: None, - left: None, - right: None, - rank: None, - basis: None, - target_vec: None, - bounds: None, - release_times: None, - lengths: None, - terminals: None, - terminal_pairs: None, - tree: None, - required_edges: None, - bound: None, - latency_bound: None, - length_bound: None, - weight_bound: None, - diameter_bound: None, - cost_bound: None, - delay_budget: None, - pattern: None, - strings: None, - string: None, - arc_costs: None, - arcs: None, - left_arcs: None, - right_arcs: None, - values: None, - precedences: None, - distance_matrix: None, - potential_edges: None, - budget: None, - max_cycle_length: None, - candidate_arcs: None, - deadlines: None, - precedence_pairs: None, - task_lengths: None, - job_tasks: None, - resource_bounds: None, - resource_requirements: None, - deadline: None, - num_processors: None, - alphabet_size: None, - deps: None, - query: None, - dependencies: None, - num_attributes: None, - source_string: None, - target_string: None, - schedules: None, - requirements: None, - num_workers: None, - num_periods: None, - num_craftsmen: None, - num_tasks: None, - craftsman_avail: None, - task_avail: None, - num_groups: None, - num_sectors: None, - domain_size: None, - relations: None, - conjuncts_spec: None, - relation_attrs: None, - known_keys: None, - num_objects: None, - attribute_domains: None, - frequency_tables: None, - known_values: None, - costs: None, - cut_bound: None, - size_bound: None, - usage: None, - storage: None, - quantifiers: None, - homologous_pairs: None, - pointer_cost: None, - expression: None, - coeff_a: None, - coeff_b: None, - rhs: None, - coeff_c: None, - pairs: None, - required_columns: None, - compilers: None, - setup_times: None, - w_sizes: None, - x_sizes: None, - y_sizes: None, - equations: None, - assignment: None, - initial_marking: None, - output_arcs: None, - gate_types: None, - true_sentences: None, - implications: None, - loop_length: None, - loop_variables: None, - inputs: None, - outputs: None, - assignments: None, - num_variables: None, - truth_table: None, - test_matrix: None, - num_tests: None, - tiles: None, - grid_size: None, - num_colors: None, - } - } - - #[test] - fn test_all_data_flags_empty_treats_potential_edges_as_input() { - let mut args = empty_args(); - args.potential_edges = Some("0-2:3,1-3:5".to_string()); - assert!(!all_data_flags_empty(&args)); - } - - #[test] - fn test_all_data_flags_empty_treats_budget_as_input() { - let mut args = empty_args(); - args.budget = Some("7".to_string()); - assert!(!all_data_flags_empty(&args)); - } - - #[test] - fn test_all_data_flags_empty_treats_max_cycle_length_as_input() { - let mut args = empty_args(); - args.max_cycle_length = Some(4); - assert!(!all_data_flags_empty(&args)); - } - - #[test] - fn test_all_data_flags_empty_treats_homologous_pairs_as_input() { - let mut args = empty_args(); - args.homologous_pairs = Some("2=5;4=3".to_string()); - assert!(!all_data_flags_empty(&args)); - } - - #[test] - fn test_all_data_flags_empty_treats_job_tasks_as_input() { - let mut args = empty_args(); - args.job_tasks = Some("0:1,1:1;1:1,0:1".to_string()); - assert!(!all_data_flags_empty(&args)); - } - - #[test] - fn test_parse_potential_edges() { - let mut args = empty_args(); - args.potential_edges = Some("0-2:3,1-3:5".to_string()); - - let potential_edges = parse_potential_edges(&args).unwrap(); - - assert_eq!(potential_edges, vec![(0, 2, 3), (1, 3, 5)]); - } - - #[test] - fn test_parse_potential_edges_rejects_missing_weight() { - let mut args = empty_args(); - args.potential_edges = Some("0-2,1-3:5".to_string()); - - let err = parse_potential_edges(&args).unwrap_err().to_string(); - - assert!(err.contains("u-v:w")); - } - - #[test] - fn test_parse_budget() { - let mut args = empty_args(); - args.budget = Some("7".to_string()); - - assert_eq!(parse_budget(&args).unwrap(), 7); - } - - #[test] - fn test_create_disjoint_connecting_paths_json() { - use crate::dispatch::ProblemJsonOutput; - use problemreductions::models::graph::DisjointConnectingPaths; - - let mut args = empty_args(); - args.problem = Some("DisjointConnectingPaths".to_string()); - args.graph = Some("0-1,1-3,0-2,1-4,2-4,3-5,4-5".to_string()); - args.terminal_pairs = Some("0-3,2-5".to_string()); - - let output_path = - std::env::temp_dir().join(format!("dcp-create-{}.json", std::process::id())); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let json = std::fs::read_to_string(&output_path).unwrap(); - let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); - assert_eq!(created.problem_type, "DisjointConnectingPaths"); - assert_eq!( - created.variant, - BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]) - ); - - let problem: DisjointConnectingPaths = - serde_json::from_value(created.data).unwrap(); - assert_eq!(problem.num_vertices(), 6); - assert_eq!(problem.num_edges(), 7); - assert_eq!(problem.terminal_pairs(), &[(0, 3), (2, 5)]); - - let _ = std::fs::remove_file(output_path); - } - - #[test] - fn test_create_disjoint_connecting_paths_rejects_overlapping_terminal_pairs() { - let mut args = empty_args(); - args.problem = Some("DisjointConnectingPaths".to_string()); - args.graph = Some("0-1,1-2,2-3,3-4".to_string()); - args.terminal_pairs = Some("0-2,2-4".to_string()); - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("pairwise disjoint")); - } - - #[test] - fn test_parse_homologous_pairs() { - let mut args = empty_args(); - args.homologous_pairs = Some("2=5;4=3".to_string()); - - assert_eq!(parse_homologous_pairs(&args).unwrap(), vec![(2, 5), (4, 3)]); - } - - #[test] - fn test_parse_homologous_pairs_rejects_invalid_token() { - let mut args = empty_args(); - args.homologous_pairs = Some("2-5".to_string()); - - let err = parse_homologous_pairs(&args).unwrap_err().to_string(); - - assert!(err.contains("u=v")); - } - - #[test] - fn test_parse_graph_respects_explicit_num_vertices() { - let mut args = empty_args(); - args.graph = Some("0-1".to_string()); - args.num_vertices = Some(3); - - let (graph, num_vertices) = parse_graph(&args).unwrap(); - - assert_eq!(num_vertices, 3); - assert_eq!(graph.num_vertices(), 3); - assert_eq!(graph.edges(), vec![(0, 1)]); - } - - #[test] - fn test_validate_potential_edges_rejects_existing_graph_edge() { - let err = validate_potential_edges(&SimpleGraph::path(3), &[(0, 1, 5)]) - .unwrap_err() - .to_string(); - - assert!(err.contains("already exists in the graph")); - } - - #[test] - fn test_validate_potential_edges_rejects_duplicate_edges() { - let err = validate_potential_edges(&SimpleGraph::path(4), &[(0, 3, 1), (3, 0, 2)]) - .unwrap_err() - .to_string(); - - assert!(err.contains("Duplicate potential edge")); - } - - #[test] - fn test_create_biconnectivity_augmentation_json() { - let mut args = empty_args(); - args.graph = Some("0-1,1-2,2-3".to_string()); - args.potential_edges = Some("0-2:3,0-3:4,1-3:2".to_string()); - args.budget = Some("5".to_string()); - - let output_path = std::env::temp_dir().join("pred_test_create_biconnectivity.json"); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let content = std::fs::read_to_string(&output_path).unwrap(); - let json: serde_json::Value = serde_json::from_str(&content).unwrap(); - assert_eq!(json["type"], "BiconnectivityAugmentation"); - assert_eq!(json["data"]["budget"], 5); - assert_eq!( - json["data"]["potential_weights"][0], - serde_json::json!([0, 2, 3]) - ); - - std::fs::remove_file(output_path).ok(); - } - - #[test] - fn test_create_biconnectivity_augmentation_json_with_isolated_vertices() { - let mut args = empty_args(); - args.graph = Some("0-1".to_string()); - args.num_vertices = Some(3); - args.potential_edges = Some("1-2:1".to_string()); - args.budget = Some("1".to_string()); - - let output_path = - std::env::temp_dir().join("pred_test_create_biconnectivity_isolated.json"); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let content = std::fs::read_to_string(&output_path).unwrap(); - let json: serde_json::Value = serde_json::from_str(&content).unwrap(); - let problem: BiconnectivityAugmentation = - serde_json::from_value(json["data"].clone()).unwrap(); - - assert_eq!(problem.num_vertices(), 3); - assert_eq!(problem.potential_weights(), &[(1, 2, 1)]); - assert_eq!(problem.budget(), &1); - - std::fs::remove_file(output_path).ok(); - } - - #[test] - fn test_create_partial_feedback_edge_set_json() { - use problemreductions::models::graph::PartialFeedbackEdgeSet; - - let mut args = empty_args(); - args.problem = Some("PartialFeedbackEdgeSet".to_string()); - args.graph = Some("0-1,1-2,2-0".to_string()); - args.budget = Some("1".to_string()); - args.max_cycle_length = Some(3); - - let output_path = - std::env::temp_dir().join("pred_test_create_partial_feedback_edge_set.json"); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let content = std::fs::read_to_string(&output_path).unwrap(); - let json: serde_json::Value = serde_json::from_str(&content).unwrap(); - assert_eq!(json["type"], "PartialFeedbackEdgeSet"); - assert_eq!(json["data"]["budget"], 1); - assert_eq!(json["data"]["max_cycle_length"], 3); - - let problem: PartialFeedbackEdgeSet = - serde_json::from_value(json["data"].clone()).unwrap(); - assert_eq!(problem.num_vertices(), 3); - assert_eq!(problem.num_edges(), 3); - assert_eq!(problem.budget(), 1); - assert_eq!(problem.max_cycle_length(), 3); - - std::fs::remove_file(output_path).ok(); - } - - #[test] - fn test_create_partial_feedback_edge_set_requires_max_cycle_length() { - let mut args = empty_args(); - args.problem = Some("PartialFeedbackEdgeSet".to_string()); - args.graph = Some("0-1,1-2,2-0".to_string()); - args.budget = Some("1".to_string()); - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("PartialFeedbackEdgeSet requires --max-cycle-length")); - } - - #[test] - fn test_create_ensemble_computation_json() { - let mut args = empty_args(); - args.problem = Some("EnsembleComputation".to_string()); - args.universe = Some(4); - args.sets = Some("0,1,2;0,1,3".to_string()); - args.budget = Some("4".to_string()); - - let output_path = std::env::temp_dir().join("pred_test_create_ensemble_computation.json"); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let content = std::fs::read_to_string(&output_path).unwrap(); - let json: serde_json::Value = serde_json::from_str(&content).unwrap(); - assert_eq!(json["type"], "EnsembleComputation"); - assert_eq!(json["data"]["universe_size"], 4); - assert_eq!( - json["data"]["subsets"], - serde_json::json!([[0, 1, 2], [0, 1, 3]]) - ); - assert_eq!(json["data"]["budget"], 4); - - std::fs::remove_file(output_path).ok(); - } - - #[test] - fn test_create_expected_retrieval_cost_json() { - use crate::dispatch::ProblemJsonOutput; - use problemreductions::models::misc::ExpectedRetrievalCost; - - let mut args = empty_args(); - args.problem = Some("ExpectedRetrievalCost".to_string()); - args.probabilities = Some("0.2,0.15,0.15,0.2,0.1,0.2".to_string()); - args.num_sectors = Some(3); - - let output_path = std::env::temp_dir().join(format!( - "expected-retrieval-cost-{}.json", - std::process::id() - )); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let json = std::fs::read_to_string(&output_path).unwrap(); - let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); - assert_eq!(created.problem_type, "ExpectedRetrievalCost"); - - let problem: ExpectedRetrievalCost = serde_json::from_value(created.data).unwrap(); - assert_eq!(problem.num_records(), 6); - assert_eq!(problem.num_sectors(), 3); - use problemreductions::types::Min; - assert!(matches!( - problem.evaluate(&[0, 1, 2, 1, 0, 2]), - Min(Some(_)) - )); - - let _ = std::fs::remove_file(output_path); - } - - #[test] - fn test_create_job_shop_scheduling_json() { - use crate::dispatch::ProblemJsonOutput; - use problemreductions::models::misc::JobShopScheduling; - use problemreductions::traits::Problem; - use problemreductions::types::Min; - - let mut args = empty_args(); - args.problem = Some("JobShopScheduling".to_string()); - args.job_tasks = Some("0:3,1:4;1:2,0:3,1:2;0:4,1:3;1:5,0:2;0:2,1:3,0:1".to_string()); - - let output_path = - std::env::temp_dir().join(format!("job-shop-scheduling-{}.json", std::process::id())); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let json = std::fs::read_to_string(&output_path).unwrap(); - let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); - assert_eq!(created.problem_type, "JobShopScheduling"); - assert!(created.variant.is_empty()); - - let problem: JobShopScheduling = serde_json::from_value(created.data).unwrap(); - assert_eq!(problem.num_processors(), 2); - assert_eq!(problem.num_jobs(), 5); - assert_eq!( - problem.evaluate(&[0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]), - Min(Some(19)) - ); - - let _ = std::fs::remove_file(output_path); - } - - #[test] - fn test_create_job_shop_scheduling_requires_job_tasks() { - let mut args = empty_args(); - args.problem = Some("JobShopScheduling".to_string()); - args.num_processors = Some(2); - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("JobShopScheduling requires --job-tasks")); - } - - #[test] - fn test_create_job_shop_scheduling_rejects_malformed_operation() { - let mut args = empty_args(); - args.problem = Some("JobShopScheduling".to_string()); - args.job_tasks = Some("0-3,1:4".to_string()); - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("expected 'processor:length'")); - } - - #[test] - fn test_create_job_shop_scheduling_rejects_consecutive_same_processor() { - let mut args = empty_args(); - args.problem = Some("JobShopScheduling".to_string()); - args.job_tasks = Some("0:1,0:1".to_string()); - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("must use different processors")); - } - - #[test] - fn test_create_rooted_tree_storage_assignment_json() { - let mut args = empty_args(); - args.problem = Some("RootedTreeStorageAssignment".to_string()); - args.universe = Some(5); - args.sets = Some("0,2;1,3;0,4;2,4".to_string()); - args.bound = Some(1); - - let output_path = - std::env::temp_dir().join("pred_test_create_rooted_tree_storage_assignment.json"); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let content = std::fs::read_to_string(&output_path).unwrap(); - let json: serde_json::Value = serde_json::from_str(&content).unwrap(); - assert_eq!(json["type"], "RootedTreeStorageAssignment"); - assert_eq!(json["data"]["universe_size"], 5); - assert_eq!( - json["data"]["subsets"], - serde_json::json!([[0, 2], [1, 3], [0, 4], [2, 4]]) - ); - assert_eq!(json["data"]["bound"], 1); - - std::fs::remove_file(output_path).ok(); - } - - #[test] - fn test_create_stacker_crane_json() { - let mut args = empty_args(); - args.problem = Some("StackerCrane".to_string()); - args.num_vertices = Some(6); - args.arcs = Some("0>4,2>5,5>1,3>0,4>3".to_string()); - args.graph = Some("0-1,1-2,2-3,3-5,4-5,0-3,1-5".to_string()); - args.arc_costs = Some("3,4,2,5,3".to_string()); - args.edge_lengths = Some("2,1,3,2,1,4,3".to_string()); - - let output_path = std::env::temp_dir().join("pred_test_create_stacker_crane.json"); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let content = std::fs::read_to_string(&output_path).unwrap(); - let json: serde_json::Value = serde_json::from_str(&content).unwrap(); - assert_eq!(json["type"], "StackerCrane"); - assert_eq!(json["data"]["num_vertices"], 6); - assert_eq!(json["data"]["arcs"][0], serde_json::json!([0, 4])); - assert_eq!(json["data"]["edge_lengths"][6], 3); - - std::fs::remove_file(output_path).ok(); - } - - #[test] - fn test_create_stacker_crane_rejects_mismatched_arc_lengths() { - let mut args = empty_args(); - args.problem = Some("StackerCrane".to_string()); - args.num_vertices = Some(6); - args.arcs = Some("0>4,2>5,5>1,3>0,4>3".to_string()); - args.graph = Some("0-1,1-2,2-3,3-5,4-5,0-3,1-5".to_string()); - args.arc_costs = Some("3,4,2,5".to_string()); - args.edge_lengths = Some("2,1,3,2,1,4,3".to_string()); - args.bound = Some(20); - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("Expected 5 arc costs but got 4")); - } - - #[test] - fn test_create_stacker_crane_rejects_out_of_range_vertices() { - let mut args = empty_args(); - args.problem = Some("StackerCrane".to_string()); - args.num_vertices = Some(5); - args.arcs = Some("0>4,2>5,5>1,3>0,4>3".to_string()); - args.graph = Some("0-1,1-2,2-3,3-5,4-5,0-3,1-5".to_string()); - args.arc_costs = Some("3,4,2,5,3".to_string()); - args.edge_lengths = Some("2,1,3,2,1,4,3".to_string()); - args.bound = Some(20); - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("--num-vertices (5) is too small for the arcs")); - } - - #[test] - fn test_create_minimum_dummy_activities_pert_json() { - use crate::dispatch::ProblemJsonOutput; - use problemreductions::models::graph::MinimumDummyActivitiesPert; - - let mut args = empty_args(); - args.problem = Some("MinimumDummyActivitiesPert".to_string()); - args.num_vertices = Some(6); - args.arcs = Some("0>2,0>3,1>3,1>4,2>5".to_string()); - - let output_path = temp_output_path("minimum_dummy_activities_pert"); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let json = fs::read_to_string(&output_path).unwrap(); - let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); - assert_eq!(created.problem_type, "MinimumDummyActivitiesPert"); - assert!(created.variant.is_empty()); - - let problem: MinimumDummyActivitiesPert = serde_json::from_value(created.data).unwrap(); - assert_eq!(problem.num_vertices(), 6); - assert_eq!(problem.num_arcs(), 5); - - let _ = fs::remove_file(output_path); - } - - #[test] - fn test_create_minimum_dummy_activities_pert_rejects_cycles() { - let mut args = empty_args(); - args.problem = Some("MinimumDummyActivitiesPert".to_string()); - args.num_vertices = Some(3); - args.arcs = Some("0>1,1>2,2>0".to_string()); - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("requires the input graph to be a DAG")); - } - - #[test] - fn test_create_balanced_complete_bipartite_subgraph() { - use crate::dispatch::ProblemJsonOutput; - use problemreductions::models::graph::BalancedCompleteBipartiteSubgraph; - - let mut args = empty_args(); - args.problem = Some("BalancedCompleteBipartiteSubgraph".to_string()); - args.biedges = Some("0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3".to_string()); - args.left = Some(4); - args.right = Some(4); - args.k = Some(3); - args.graph = None; - - let output_path = - std::env::temp_dir().join(format!("bcbs-create-{}.json", std::process::id())); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let json = std::fs::read_to_string(&output_path).unwrap(); - let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); - assert_eq!(created.problem_type, "BalancedCompleteBipartiteSubgraph"); - assert!(created.variant.is_empty()); - - let problem: BalancedCompleteBipartiteSubgraph = - serde_json::from_value(created.data).unwrap(); - assert_eq!(problem.left_size(), 4); - assert_eq!(problem.right_size(), 4); - assert_eq!(problem.num_edges(), 12); - assert_eq!(problem.k(), 3); - - let _ = std::fs::remove_file(output_path); - } - - #[test] - fn test_create_balanced_complete_bipartite_subgraph_rejects_out_of_range_biedges() { - let mut args = empty_args(); - args.problem = Some("BalancedCompleteBipartiteSubgraph".to_string()); - args.biedges = Some("4-0".to_string()); - args.left = Some(4); - args.right = Some(4); - args.k = Some(3); - args.graph = None; - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("out of bounds for left partition size 4")); - } - - #[test] - fn test_create_kclique() { - use crate::dispatch::ProblemJsonOutput; - use problemreductions::models::graph::KClique; - - let mut args = empty_args(); - args.problem = Some("KClique".to_string()); - args.graph = Some("0-1,0-2,1-3,2-3,2-4,3-4".to_string()); - args.k = Some(3); - - let output_path = - std::env::temp_dir().join(format!("kclique-create-{}.json", std::process::id())); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let json = std::fs::read_to_string(&output_path).unwrap(); - let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); - assert_eq!(created.problem_type, "KClique"); - assert_eq!( - created.variant.get("graph").map(String::as_str), - Some("SimpleGraph") - ); - - let problem: KClique = serde_json::from_value(created.data).unwrap(); - assert_eq!(problem.k(), 3); - assert_eq!(problem.num_vertices(), 5); - assert!(problem.evaluate(&[0, 0, 1, 1, 1])); - - let _ = std::fs::remove_file(output_path); - } - - #[test] - fn test_create_kclique_requires_valid_k() { - let mut args = empty_args(); - args.problem = Some("KClique".to_string()); - args.graph = Some("0-1,0-2,1-3,2-3,2-4,3-4".to_string()); - args.k = None; - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err(); - assert!( - err.to_string().contains("KClique requires --k"), - "unexpected error: {err}" - ); - - args.k = Some(6); - let err = create(&args, &out).unwrap_err(); - assert!( - err.to_string().contains("k must be <= graph num_vertices"), - "unexpected error: {err}" - ); - } - - #[test] - fn test_create_sparse_matrix_compression_json() { - use crate::dispatch::ProblemJsonOutput; - - let mut args = empty_args(); - args.problem = Some("SparseMatrixCompression".to_string()); - args.matrix = Some("1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0".to_string()); - args.bound = Some(2); - - let output_path = - std::env::temp_dir().join(format!("smc-create-{}.json", std::process::id())); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let json = std::fs::read_to_string(&output_path).unwrap(); - let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); - assert_eq!(created.problem_type, "SparseMatrixCompression"); - assert!(created.variant.is_empty()); - assert_eq!( - created.data, - serde_json::json!({ - "matrix": [ - [true, false, false, true], - [false, true, false, false], - [false, false, true, false], - [true, false, false, false], - ], - "bound_k": 2, - }) - ); - - let _ = std::fs::remove_file(output_path); - } - - #[test] - fn test_create_sparse_matrix_compression_requires_bound() { - let mut args = empty_args(); - args.problem = Some("SparseMatrixCompression".to_string()); - args.matrix = Some("1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0".to_string()); - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("SparseMatrixCompression requires --matrix and --bound")); - assert!(err.contains("Usage: pred create SparseMatrixCompression")); - } - - #[test] - fn test_create_sparse_matrix_compression_rejects_zero_bound() { - let mut args = empty_args(); - args.problem = Some("SparseMatrixCompression".to_string()); - args.matrix = Some("1,0;0,1".to_string()); - args.bound = Some(0); - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("bound >= 1")); - } - - #[test] - fn test_create_graph_partitioning_with_num_partitions() { - use crate::dispatch::ProblemJsonOutput; - use problemreductions::models::graph::GraphPartitioning; - use problemreductions::topology::SimpleGraph; - - let cli = Cli::try_parse_from([ - "pred", - "create", - "GraphPartitioning", - "--graph", - "0-1,1-2,2-3,3-0", - "--num-partitions", - "2", - ]) - .unwrap(); - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - let output_path = temp_output_path("graph-partitioning-create"); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let json = fs::read_to_string(&output_path).unwrap(); - let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); - assert_eq!(created.problem_type, "GraphPartitioning"); - let problem: GraphPartitioning = serde_json::from_value(created.data).unwrap(); - assert_eq!(problem.num_vertices(), 4); - - let _ = fs::remove_file(output_path); - } - - #[test] - fn test_create_nontautology_with_disjuncts_flag() { - use crate::dispatch::ProblemJsonOutput; - use problemreductions::models::formula::NonTautology; - - let cli = Cli::try_parse_from([ - "pred", - "create", - "NonTautology", - "--num-vars", - "3", - "--disjuncts", - "1,2,3;-1,-2,-3", - ]) - .unwrap(); - let args = match cli.command { - Commands::Create(args) => args, - _ => unreachable!(), - }; - - let output_path = temp_output_path("non-tautology-create"); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let json = fs::read_to_string(&output_path).unwrap(); - let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); - assert_eq!(created.problem_type, "NonTautology"); - let problem: NonTautology = serde_json::from_value(created.data).unwrap(); - assert_eq!(problem.disjuncts(), &[vec![1, 2, 3], vec![-1, -2, -3]]); - - let _ = fs::remove_file(output_path); - } - - #[test] - fn test_create_consecutive_ones_matrix_augmentation_json() { - use crate::dispatch::ProblemJsonOutput; - - let mut args = empty_args(); - args.problem = Some("ConsecutiveOnesMatrixAugmentation".to_string()); - args.matrix = Some("1,0,0,1,1;1,1,0,0,0;0,1,1,0,1;0,0,1,1,0".to_string()); - args.bound = Some(2); - - let output_path = - std::env::temp_dir().join(format!("coma-create-{}.json", std::process::id())); - let out = OutputConfig { - output: Some(output_path.clone()), - quiet: true, - json: false, - auto_json: false, - }; - - create(&args, &out).unwrap(); - - let json = std::fs::read_to_string(&output_path).unwrap(); - let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); - assert_eq!(created.problem_type, "ConsecutiveOnesMatrixAugmentation"); - assert!(created.variant.is_empty()); - assert_eq!( - created.data, - serde_json::json!({ - "matrix": [ - [true, false, false, true, true], - [true, true, false, false, false], - [false, true, true, false, true], - [false, false, true, true, false], - ], - "bound": 2, - }) - ); - - let _ = std::fs::remove_file(output_path); - } - - #[test] - fn test_create_consecutive_ones_matrix_augmentation_requires_bound() { - let mut args = empty_args(); - args.problem = Some("ConsecutiveOnesMatrixAugmentation".to_string()); - args.matrix = Some("1,0;0,1".to_string()); - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("ConsecutiveOnesMatrixAugmentation requires --matrix and --bound")); - assert!(err.contains("Usage: pred create ConsecutiveOnesMatrixAugmentation")); - } - - #[test] - fn test_create_consecutive_ones_matrix_augmentation_negative_bound() { - let mut args = empty_args(); - args.problem = Some("ConsecutiveOnesMatrixAugmentation".to_string()); - args.matrix = Some("1,0;0,1".to_string()); - args.bound = Some(-1); - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("nonnegative")); - } -} +mod tests; diff --git a/problemreductions-cli/src/commands/create/schema_semantics.rs b/problemreductions-cli/src/commands/create/schema_semantics.rs new file mode 100644 index 000000000..3756dbd00 --- /dev/null +++ b/problemreductions-cli/src/commands/create/schema_semantics.rs @@ -0,0 +1,1308 @@ +use super::schema_support::*; +use super::*; + +pub(super) fn validate_schema_driven_semantics( + args: &CreateArgs, + canonical: &str, + resolved_variant: &BTreeMap, + _data: &serde_json::Value, +) -> Result<()> { + match canonical { + "BalancedCompleteBipartiteSubgraph" => { + let usage = "pred create BalancedCompleteBipartiteSubgraph --left 4 --right 4 --biedges 0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3 --k 3"; + let _ = parse_bipartite_problem_input( + args, + "BalancedCompleteBipartiteSubgraph", + "balanced biclique size", + usage, + )?; + } + "BiconnectivityAugmentation" => { + let usage = "Usage: pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-weights 0-2:3,0-3:4,1-3:2 --budget 5"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let potential_edges = parse_potential_edges(args)?; + validate_potential_edges(&graph, &potential_edges)?; + let _ = parse_budget(args)?; + } + "BoundedComponentSpanningForest" => { + let usage = "Usage: pred create BoundedComponentSpanningForest --graph 0-1,1-2,2-3,3-4,4-5,5-6,6-7,0-7,1-5,2-6 --weights 2,3,1,2,3,1,2,1 --k 3 --max-weight 6"; + let (_, n) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + args.weights.as_deref().ok_or_else(|| { + anyhow::anyhow!("BoundedComponentSpanningForest requires --weights\n\n{usage}") + })?; + let weights = parse_vertex_weights(args, n)?; + if weights.iter().any(|&weight| weight < 0) { + bail!("BoundedComponentSpanningForest requires nonnegative --weights\n\n{usage}"); + } + let max_components = args.k.ok_or_else(|| { + anyhow::anyhow!("BoundedComponentSpanningForest requires --k\n\n{usage}") + })?; + if max_components == 0 { + bail!("BoundedComponentSpanningForest requires --k >= 1\n\n{usage}"); + } + let bound_raw = args.bound.ok_or_else(|| { + anyhow::anyhow!("BoundedComponentSpanningForest requires --max-weight\n\n{usage}") + })?; + if bound_raw <= 0 { + bail!("BoundedComponentSpanningForest requires positive --max-weight\n\n{usage}"); + } + let _ = i32::try_from(bound_raw).map_err(|_| { + anyhow::anyhow!( + "BoundedComponentSpanningForest requires --max-weight within i32 range\n\n{usage}" + ) + })?; + } + "CapacityAssignment" => { + let usage = "Usage: pred create CapacityAssignment --capacities 1,2,3 --cost-matrix \"1,3,6;2,4,7;1,2,5\" --delay-matrix \"8,4,1;7,3,1;6,3,1\" --delay-budget 12"; + let capacities_str = args.capacities.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "CapacityAssignment requires --capacities, --cost-matrix, --delay-matrix, and --delay-budget\n\n{usage}" + ) + })?; + let cost_matrix_str = args.cost_matrix.as_deref().ok_or_else(|| { + anyhow::anyhow!("CapacityAssignment requires --cost-matrix\n\n{usage}") + })?; + let delay_matrix_str = args.delay_matrix.as_deref().ok_or_else(|| { + anyhow::anyhow!("CapacityAssignment requires --delay-matrix\n\n{usage}") + })?; + let _ = args.delay_budget.ok_or_else(|| { + anyhow::anyhow!("CapacityAssignment requires --delay-budget\n\n{usage}") + })?; + + let capacities: Vec = util::parse_comma_list(capacities_str)?; + anyhow::ensure!( + !capacities.is_empty(), + "CapacityAssignment requires at least one capacity value\n\n{usage}" + ); + anyhow::ensure!( + capacities.iter().all(|&capacity| capacity > 0), + "CapacityAssignment capacities must be positive\n\n{usage}" + ); + anyhow::ensure!( + capacities.windows(2).all(|w| w[0] < w[1]), + "CapacityAssignment capacities must be strictly increasing\n\n{usage}" + ); + + let cost = parse_u64_matrix_rows(cost_matrix_str, "cost")?; + let delay = parse_u64_matrix_rows(delay_matrix_str, "delay")?; + anyhow::ensure!( + cost.len() == delay.len(), + "cost matrix row count ({}) must match delay matrix row count ({})\n\n{usage}", + cost.len(), + delay.len() + ); + + for (index, row) in cost.iter().enumerate() { + anyhow::ensure!( + row.len() == capacities.len(), + "cost row {} length ({}) must match capacities length ({})\n\n{usage}", + index, + row.len(), + capacities.len() + ); + anyhow::ensure!( + row.windows(2).all(|w| w[0] <= w[1]), + "cost row {} must be non-decreasing\n\n{usage}", + index + ); + } + for (index, row) in delay.iter().enumerate() { + anyhow::ensure!( + row.len() == capacities.len(), + "delay row {} length ({}) must match capacities length ({})\n\n{usage}", + index, + row.len(), + capacities.len() + ); + anyhow::ensure!( + row.windows(2).all(|w| w[0] >= w[1]), + "delay row {} must be non-increasing\n\n{usage}", + index + ); + } + } + "BoyceCoddNormalFormViolation" => { + let n = args.n.ok_or_else(|| { + anyhow::anyhow!( + "BoyceCoddNormalFormViolation requires --n, --sets, and --target\n\n\ + Usage: pred create BoyceCoddNormalFormViolation --n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" + ) + })?; + let sets_str = args.sets.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "BoyceCoddNormalFormViolation requires --sets (functional deps as lhs:rhs;...)\n\n\ + Usage: pred create BoyceCoddNormalFormViolation --n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" + ) + })?; + let target_str = args.target.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "BoyceCoddNormalFormViolation requires --target (comma-separated attribute indices)\n\n\ + Usage: pred create BoyceCoddNormalFormViolation --n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" + ) + })?; + let _ = parse_bcnf_functional_deps(sets_str, n)?; + let target: Vec = util::parse_comma_list(target_str)?; + ensure_attribute_indices_in_range(&target, n, "Target subset")?; + } + "ClosestVectorProblem" => { + let basis_str = args.basis.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "CVP requires --basis, --target-vec\n\n\ + Usage: pred create CVP --basis \"1,0;0,1\" --target-vec \"0.5,0.5\"" + ) + })?; + let target_str = args + .target_vec + .as_deref() + .ok_or_else(|| anyhow::anyhow!("CVP requires --target-vec (e.g., \"0.5,0.5\")"))?; + let basis: Vec> = basis_str + .split(';') + .map(|row| util::parse_comma_list(row.trim())) + .collect::>>()?; + let target: Vec = util::parse_comma_list(target_str)?; + let n = basis.len(); + let bounds = serde_json::from_value(parse_cvp_bounds_value( + args.bounds.as_deref(), + &CreateContext::default() + .with_field("basis", serde_json::json!(vec![serde_json::json!([0]); n])), + )?)?; + let _ = ClosestVectorProblem::new(basis, target, bounds); + } + "ConsecutiveOnesMatrixAugmentation" => { + let matrix = parse_bool_matrix(args)?; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!( + "ConsecutiveOnesMatrixAugmentation requires --matrix and --bound\n\n\ + Usage: pred create ConsecutiveOnesMatrixAugmentation --matrix \"1,0,0,1,1;1,1,0,0,0;0,1,1,0,1;0,0,1,1,0\" --bound 2" + ) + })?; + ConsecutiveOnesMatrixAugmentation::try_new(matrix, bound) + .map_err(anyhow::Error::msg)?; + } + "ConsecutiveBlockMinimization" => { + let usage = "Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2"; + let matrix_str = args.matrix.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array and --bound-k\n\n{usage}" + ) + })?; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!("ConsecutiveBlockMinimization requires --bound-k\n\n{usage}") + })?; + let matrix: Vec> = serde_json::from_str(matrix_str).map_err(|err| { + anyhow::anyhow!( + "ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array (e.g., '[[true,false,true],[false,true,true]]')\n\n{usage}\n\nFailed to parse --matrix: {err}" + ) + })?; + ConsecutiveBlockMinimization::try_new(matrix, bound) + .map_err(|err| anyhow::anyhow!("{err}\n\n{usage}"))?; + } + "ComparativeContainment" => { + let universe = args.universe.ok_or_else(|| { + anyhow::anyhow!( + "ComparativeContainment requires --universe, --r-sets, and --s-sets\n\n\ + Usage: pred create ComparativeContainment --universe 4 --r-sets \"0,1,2,3;0,1\" --s-sets \"0,1,2,3;2,3\" [--r-weights 2,5] [--s-weights 3,6]" + ) + })?; + let r_sets = parse_named_sets(args.r_sets.as_deref(), "--r-sets")?; + let s_sets = parse_named_sets(args.s_sets.as_deref(), "--s-sets")?; + validate_comparative_containment_sets("R", "--r-sets", universe, &r_sets)?; + validate_comparative_containment_sets("S", "--s-sets", universe, &s_sets)?; + match resolved_variant.get("weight").map(|value| value.as_str()) { + Some("One") => { + let r_weights = parse_named_set_weights( + args.r_weights.as_deref(), + r_sets.len(), + "--r-weights", + )?; + let s_weights = parse_named_set_weights( + args.s_weights.as_deref(), + s_sets.len(), + "--s-weights", + )?; + anyhow::ensure!( + r_weights.iter().all(|&w| w == 1) && s_weights.iter().all(|&w| w == 1), + "Non-unit weights are not supported for ComparativeContainment/One.\n\n\ + Use `pred create ComparativeContainment/i32 ... --r-weights ... --s-weights ...` for weighted instances." + ); + } + Some("f64") => { + let r_weights = parse_named_set_weights_f64( + args.r_weights.as_deref(), + r_sets.len(), + "--r-weights", + )?; + validate_comparative_containment_f64_weights("R", "--r-weights", &r_weights)?; + let s_weights = parse_named_set_weights_f64( + args.s_weights.as_deref(), + s_sets.len(), + "--s-weights", + )?; + validate_comparative_containment_f64_weights("S", "--s-weights", &s_weights)?; + } + Some("i32") | None => { + let r_weights = parse_named_set_weights( + args.r_weights.as_deref(), + r_sets.len(), + "--r-weights", + )?; + validate_comparative_containment_i32_weights("R", "--r-weights", &r_weights)?; + let s_weights = parse_named_set_weights( + args.s_weights.as_deref(), + s_sets.len(), + "--s-weights", + )?; + validate_comparative_containment_i32_weights("S", "--s-weights", &s_weights)?; + } + Some(other) => bail!( + "Unsupported ComparativeContainment weight variant: {}", + other + ), + } + } + "DisjointConnectingPaths" => { + let usage = + "Usage: pred create DisjointConnectingPaths --graph 0-1,1-3,0-2,1-4,2-4,3-5,4-5 --terminal-pairs 0-3,2-5"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let _ = parse_terminal_pairs(args, graph.num_vertices()) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + } + "ExactCoverBy3Sets" => { + let universe = args.universe.ok_or_else(|| { + anyhow::anyhow!( + "ExactCoverBy3Sets requires --universe and --sets\n\n\ + Usage: pred create X3C --universe 6 --sets \"0,1,2;3,4,5\"" + ) + })?; + if universe % 3 != 0 { + bail!("Universe size must be divisible by 3, got {}", universe); + } + let sets = parse_sets(args)?; + for (i, set) in sets.iter().enumerate() { + if set.len() != 3 { + bail!( + "Subset {} has {} elements, but X3C requires exactly 3 elements per subset", + i, + set.len() + ); + } + if set[0] == set[1] || set[0] == set[2] || set[1] == set[2] { + bail!("Subset {} contains duplicate elements: {:?}", i, set); + } + for &elem in set { + if elem >= universe { + bail!( + "Subset {} contains element {} which is outside universe of size {}", + i, + elem, + universe + ); + } + } + } + } + "GeneralizedHex" => { + let usage = + "Usage: pred create GeneralizedHex --graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let num_vertices = graph.num_vertices(); + let source = args + .source + .ok_or_else(|| anyhow::anyhow!("GeneralizedHex requires --source\n\n{usage}"))?; + let sink = args + .sink + .ok_or_else(|| anyhow::anyhow!("GeneralizedHex requires --sink\n\n{usage}"))?; + validate_vertex_index("source", source, num_vertices, usage)?; + validate_vertex_index("sink", sink, num_vertices, usage)?; + anyhow::ensure!( + source != sink, + "GeneralizedHex requires distinct --source and --sink\n\n{usage}" + ); + } + "GroupingBySwapping" => { + let usage = + "Usage: pred create GroupingBySwapping --string \"0,1,2,0,1,2\" --bound 5 [--alphabet-size 3]"; + let string_str = args.string.as_deref().ok_or_else(|| { + anyhow::anyhow!("GroupingBySwapping requires --string\n\n{usage}") + })?; + let bound = parse_nonnegative_usize_bound( + args.bound.ok_or_else(|| { + anyhow::anyhow!("GroupingBySwapping requires --bound\n\n{usage}") + })?, + "GroupingBySwapping", + usage, + )?; + let string = parse_symbol_list_allow_empty(string_str)?; + let inferred = string.iter().copied().max().map_or(0, |value| value + 1); + let alphabet_size = args.alphabet_size.unwrap_or(inferred); + anyhow::ensure!( + alphabet_size >= inferred, + "--alphabet-size {} is smaller than max symbol + 1 ({}) in the input string", + alphabet_size, + inferred + ); + anyhow::ensure!( + alphabet_size > 0 || string.is_empty(), + "GroupingBySwapping requires a positive alphabet for non-empty strings.\n\n{usage}" + ); + anyhow::ensure!( + !string.is_empty() || bound == 0, + "GroupingBySwapping requires --bound 0 when --string is empty.\n\n{usage}" + ); + } + "IntegralFlowBundles" => { + let usage = "Usage: pred create IntegralFlowBundles --arcs \"0>1,0>2,1>3,2>3,1>2,2>1\" --bundles \"0,1;2,5;3,4\" --bundle-capacities 1,1,1 --source 0 --sink 3 --requirement 1 --num-vertices 4"; + let arcs_str = args + .arcs + .as_deref() + .ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles requires --arcs\n\n{usage}"))?; + let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let bundles = parse_bundles(args, num_arcs, usage)?; + let _ = parse_bundle_capacities(args, bundles.len(), usage)?; + let source = args.source.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowBundles requires --source\n\n{usage}") + })?; + let sink = args + .sink + .ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles requires --sink\n\n{usage}"))?; + let _ = args.requirement.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowBundles requires --requirement\n\n{usage}") + })?; + validate_vertex_index("source", source, graph.num_vertices(), usage)?; + validate_vertex_index("sink", sink, graph.num_vertices(), usage)?; + anyhow::ensure!( + source != sink, + "IntegralFlowBundles requires distinct --source and --sink\n\n{usage}" + ); + } + "IntegralFlowHomologousArcs" => { + let usage = "Usage: pred create IntegralFlowHomologousArcs --arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\""; + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowHomologousArcs requires --arcs\n\n{usage}") + })?; + let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let capacities: Vec = if let Some(ref s) = args.capacities { + s.split(',') + .map(|token| { + let trimmed = token.trim(); + trimmed + .parse::() + .with_context(|| format!("Invalid capacity `{trimmed}`\n\n{usage}")) + }) + .collect::>>()? + } else { + vec![1; num_arcs] + }; + anyhow::ensure!( + capacities.len() == num_arcs, + "Expected {} capacities but got {}\n\n{}", + num_arcs, + capacities.len(), + usage + ); + for (arc_index, &capacity) in capacities.iter().enumerate() { + let fits = usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .is_some(); + anyhow::ensure!( + fits, + "capacity {} at arc index {} is too large for this platform\n\n{}", + capacity, + arc_index, + usage + ); + } + let num_vertices = graph.num_vertices(); + let source = args.source.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowHomologousArcs requires --source\n\n{usage}") + })?; + let sink = args.sink.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowHomologousArcs requires --sink\n\n{usage}") + })?; + let _ = args.requirement.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowHomologousArcs requires --requirement\n\n{usage}") + })?; + validate_vertex_index("source", source, num_vertices, usage)?; + validate_vertex_index("sink", sink, num_vertices, usage)?; + let homologous_pairs = + parse_homologous_pairs(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + for &(a, b) in &homologous_pairs { + anyhow::ensure!( + a < num_arcs && b < num_arcs, + "homologous pair ({}, {}) references arc >= num_arcs ({})\n\n{}", + a, + b, + num_arcs, + usage + ); + } + } + "IntegralFlowWithMultipliers" => { + let usage = "Usage: pred create IntegralFlowWithMultipliers --arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2"; + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --arcs\n\n{usage}") + })?; + let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let capacities_str = args.capacities.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --capacities\n\n{usage}") + })?; + let capacities: Vec = util::parse_comma_list(capacities_str) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + if capacities.len() != num_arcs { + bail!( + "Expected {} capacities but got {}\n\n{}", + num_arcs, + capacities.len(), + usage + ); + } + for (arc_index, &capacity) in capacities.iter().enumerate() { + let fits = usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .is_some(); + if !fits { + bail!( + "capacity {} at arc index {} is too large for this platform\n\n{}", + capacity, + arc_index, + usage + ); + } + } + let num_vertices = graph.num_vertices(); + let source = args.source.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --source\n\n{usage}") + })?; + let sink = args.sink.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --sink\n\n{usage}") + })?; + validate_vertex_index("source", source, num_vertices, usage)?; + validate_vertex_index("sink", sink, num_vertices, usage)?; + if source == sink { + bail!( + "IntegralFlowWithMultipliers requires distinct --source and --sink\n\n{}", + usage + ); + } + let multipliers_str = args.multipliers.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --multipliers\n\n{usage}") + })?; + let multipliers: Vec = util::parse_comma_list(multipliers_str) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + if multipliers.len() != num_vertices { + bail!( + "Expected {} multipliers but got {}\n\n{}", + num_vertices, + multipliers.len(), + usage + ); + } + if multipliers + .iter() + .enumerate() + .any(|(vertex, &multiplier)| vertex != source && vertex != sink && multiplier == 0) + { + bail!("non-terminal multipliers must be positive\n\n{usage}"); + } + let _ = args.requirement.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --requirement\n\n{usage}") + })?; + } + "JobShopScheduling" => { + let usage = "Usage: pred create JobShopScheduling --jobs \"0:3,1:4;1:2,0:3,1:2;0:4,1:3\" --num-processors 2"; + let job_tasks = args + .job_tasks + .as_deref() + .ok_or_else(|| anyhow::anyhow!("JobShopScheduling requires --jobs\n\n{usage}"))?; + let jobs = parse_job_shop_jobs(job_tasks)?; + let inferred_processors = jobs + .iter() + .flat_map(|job| job.iter().map(|(processor, _)| *processor)) + .max() + .map(|processor| processor + 1); + let num_processors = resolve_processor_count_flags( + "JobShopScheduling", + usage, + args.num_processors, + args.m, + )? + .or(inferred_processors) + .ok_or_else(|| { + anyhow::anyhow!( + "Cannot infer num_processors from empty job list; use --num-processors" + ) + })?; + anyhow::ensure!( + num_processors > 0, + "JobShopScheduling requires --num-processors > 0\n\n{usage}" + ); + for (job_index, job) in jobs.iter().enumerate() { + for (task_index, &(processor, _)) in job.iter().enumerate() { + anyhow::ensure!( + processor < num_processors, + "job {job_index} task {task_index} uses processor {processor}, but num_processors = {num_processors}" + ); + } + for (task_index, pair) in job.windows(2).enumerate() { + anyhow::ensure!( + pair[0].0 != pair[1].0, + "job {job_index} tasks {task_index} and {} must use different processors\n\n{usage}", + task_index + 1 + ); + } + } + } + "KClique" => { + let usage = "Usage: pred create KClique --graph 0-1,0-2,1-3,2-3,2-4,3-4 --k 3"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let _ = parse_kclique_threshold(args.k, graph.num_vertices(), usage)?; + } + "KColoring" => { + let usage = "Usage: pred create KColoring --graph 0-1,1-2,2-0 --k 3"; + let _ = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let _ = util::validate_k_param(resolved_variant, args.k, None, "KColoring") + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + } + "KthBestSpanningTree" => { + reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; + let usage = + "Usage: pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let _ = parse_edge_weights(args, graph.num_edges())?; + let _ = util::validate_k_param(resolved_variant, args.k, None, "KthBestSpanningTree") + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let _ = args + .bound + .ok_or_else(|| anyhow::anyhow!("KthBestSpanningTree requires --bound\n\n{usage}"))? + as i32; + } + "LengthBoundedDisjointPaths" => { + let usage = "Usage: pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --max-length 3"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let source = args.source.ok_or_else(|| { + anyhow::anyhow!("LengthBoundedDisjointPaths requires --source\n\n{usage}") + })?; + let sink = args.sink.ok_or_else(|| { + anyhow::anyhow!("LengthBoundedDisjointPaths requires --sink\n\n{usage}") + })?; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!("LengthBoundedDisjointPaths requires --max-length\n\n{usage}") + })?; + let _ = validate_length_bounded_disjoint_paths_args( + graph.num_vertices(), + source, + sink, + bound, + Some(usage), + )?; + } + "LongestCommonSubsequence" => { + let usage = + "Usage: pred create LCS --strings \"010110;100101;001011\" [--alphabet-size 2]"; + let strings_str = args.strings.as_deref().ok_or_else(|| { + anyhow::anyhow!("LongestCommonSubsequence requires --strings\n\n{usage}") + })?; + let (strings, inferred_alphabet_size) = parse_lcs_strings(strings_str)?; + let alphabet_size = args.alphabet_size.unwrap_or(inferred_alphabet_size); + anyhow::ensure!( + alphabet_size >= inferred_alphabet_size, + "--alphabet-size {} is smaller than the inferred alphabet size ({})", + alphabet_size, + inferred_alphabet_size + ); + anyhow::ensure!( + strings.iter().any(|string| !string.is_empty()), + "LongestCommonSubsequence requires at least one non-empty string.\n\n{usage}" + ); + anyhow::ensure!( + alphabet_size > 0, + "LongestCommonSubsequence requires a positive alphabet. Provide --alphabet-size when all strings are empty.\n\n{usage}" + ); + } + "LongestPath" => { + let usage = "pred create LongestPath --graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,4-6,5-6,1-6 --edge-lengths 3,2,4,1,5,2,3,2,4,1 --source-vertex 0 --target-vertex 6"; + let (graph, _) = + parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\nUsage: {usage}"))?; + if args.weights.is_some() { + bail!("LongestPath uses --edge-lengths, not --weights\n\nUsage: {usage}"); + } + let edge_lengths_raw = args.edge_lengths.as_ref().ok_or_else(|| { + anyhow::anyhow!("LongestPath requires --edge-lengths\n\nUsage: {usage}") + })?; + let edge_lengths = + parse_i32_edge_values(Some(edge_lengths_raw), graph.num_edges(), "edge length")?; + ensure_positive_i32_values(&edge_lengths, "edge lengths")?; + let source_vertex = args.source_vertex.ok_or_else(|| { + anyhow::anyhow!("LongestPath requires --source-vertex\n\nUsage: {usage}") + })?; + let target_vertex = args.target_vertex.ok_or_else(|| { + anyhow::anyhow!("LongestPath requires --target-vertex\n\nUsage: {usage}") + })?; + ensure_vertex_in_bounds(source_vertex, graph.num_vertices(), "source_vertex")?; + ensure_vertex_in_bounds(target_vertex, graph.num_vertices(), "target_vertex")?; + } + "MixedChinesePostman" => { + let usage = "Usage: pred create MixedChinesePostman --graph 0-2,1-3,0-4,4-2 --arcs \"0>1,1>2,2>3,3>0\" --edge-weights 2,3,1,2 --arc-weights 2,3,1,4 [--num-vertices N]"; + let graph = parse_mixed_graph(args, usage)?; + let arc_costs = parse_arc_costs(args, graph.num_arcs())?; + let edge_weights = parse_edge_weights(args, graph.num_edges())?; + if arc_costs.iter().any(|&cost| cost < 0) { + bail!("MixedChinesePostman --arc-weights must be non-negative\n\n{usage}"); + } + if edge_weights.iter().any(|&weight| weight < 0) { + bail!("MixedChinesePostman --edge-weights must be non-negative\n\n{usage}"); + } + if resolved_variant.get("weight").map(String::as_str) == Some("One") + && (arc_costs.iter().any(|&cost| cost != 1) + || edge_weights.iter().any(|&weight| weight != 1)) + { + bail!( + "Non-unit lengths are not supported for MixedChinesePostman/One.\n\n\ + Use the weighted variant instead:\n pred create MixedChinesePostman/i32 --graph ... --arcs ... --edge-weights ... --arc-weights ..." + ); + } + } + "MinMaxMulticenter" => { + let usage = "Usage: pred create MinMaxMulticenter --graph 0-1,1-2,2-3 [--weights 1,1,1,1] [--edge-weights 1,1,1] --k 2"; + let (graph, n) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let vertex_weights = parse_vertex_weights(args, n)?; + let edge_lengths = parse_edge_weights(args, graph.num_edges())?; + let _ = args.k.ok_or_else(|| { + anyhow::anyhow!( + "MinMaxMulticenter requires --k (number of centers)\n\n\ + Usage: pred create MinMaxMulticenter --graph 0-1,1-2,2-3 --k 2" + ) + })?; + if vertex_weights.iter().any(|&weight| weight < 0) { + bail!("MinMaxMulticenter --weights must be non-negative"); + } + if edge_lengths.iter().any(|&length| length < 0) { + bail!("MinMaxMulticenter --edge-weights must be non-negative"); + } + } + "MaximumIndependentSet" + | "MinimumVertexCover" + | "MaximumClique" + | "MinimumDominatingSet" + | "MaximalIS" => { + let graph_type = resolved_graph_type(resolved_variant); + let num_vertices = match graph_type { + "KingsSubgraph" | "TriangularSubgraph" => parse_int_positions(args)?.len(), + "UnitDiskGraph" => parse_float_positions(args)?.len(), + _ => { + parse_graph(args) + .map_err(|e| { + anyhow::anyhow!( + "{e}\n\nUsage: pred create {} --graph 0-1,1-2,2-3 [--weights 1,1,1,1]", + canonical + ) + })? + .1 + } + }; + let weights = parse_vertex_weights(args, num_vertices)?; + reject_nonunit_weights_for_one_variant( + canonical, + graph_type, + resolved_variant, + &weights, + )?; + } + "MinimumHittingSet" => { + let universe = args.universe.ok_or_else(|| { + anyhow::anyhow!( + "MinimumHittingSet requires --universe and --sets\n\n\ + Usage: pred create MinimumHittingSet --universe 6 --sets \"0,1,2;0,3,4;1,3,5;2,4,5;0,1,5;2,3;1,4\"" + ) + })?; + let sets = parse_sets(args)?; + for (i, set) in sets.iter().enumerate() { + for &element in set { + if element >= universe { + bail!( + "Set {} contains element {} which is outside universe of size {}", + i, + element, + universe + ); + } + } + } + } + "MinimumDummyActivitiesPert" => { + let usage = "Usage: pred create MinimumDummyActivitiesPert --arcs \"0>2,0>3,1>3,1>4,2>5\" [--num-vertices N]"; + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!("MinimumDummyActivitiesPert requires --arcs\n\n{usage}") + })?; + let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; + let _ = MinimumDummyActivitiesPert::try_new(graph).map_err(anyhow::Error::msg)?; + } + "MinimumMultiwayCut" => { + let usage = + "Usage: pred create MinimumMultiwayCut --graph 0-1,1-2,2-3 --terminals 0,2 [--edge-weights 1,1,1]"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let _ = parse_terminals(args, graph.num_vertices())?; + let _ = parse_edge_weights(args, graph.num_edges())?; + } + "MultipleChoiceBranching" => { + let usage = "Usage: pred create MultipleChoiceBranching/i32 --arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --threshold 10"; + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!("MultipleChoiceBranching requires --arcs\n\n{usage}") + })?; + let (_, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?; + let _ = parse_arc_weights(args, num_arcs)?; + let _ = parse_partition_groups(args, num_arcs)?; + let _ = parse_multiple_choice_branching_threshold(args, usage)?; + } + "MultipleCopyFileAllocation" => { + let (_, num_vertices) = parse_graph(args) + .map_err(|e| anyhow::anyhow!("{e}\n\n{MULTIPLE_COPY_FILE_ALLOCATION_USAGE}"))?; + let _ = parse_vertex_i64_values( + args.usage.as_deref(), + "usage", + num_vertices, + "MultipleCopyFileAllocation", + MULTIPLE_COPY_FILE_ALLOCATION_USAGE, + )?; + let _ = parse_vertex_i64_values( + args.storage.as_deref(), + "storage", + num_vertices, + "MultipleCopyFileAllocation", + MULTIPLE_COPY_FILE_ALLOCATION_USAGE, + )?; + } + "MultiprocessorScheduling" => { + let usage = "Usage: pred create MultiprocessorScheduling --lengths 4,5,3,2,6 --num-processors 2 --deadline 10"; + let lengths_str = args.lengths.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "MultiprocessorScheduling requires --lengths, --num-processors, and --deadline\n\n{usage}" + ) + })?; + let num_processors = args.num_processors.ok_or_else(|| { + anyhow::anyhow!("MultiprocessorScheduling requires --num-processors\n\n{usage}") + })?; + anyhow::ensure!( + num_processors > 0, + "MultiprocessorScheduling requires --num-processors > 0\n\n{usage}" + ); + let _ = args.deadline.ok_or_else(|| { + anyhow::anyhow!("MultiprocessorScheduling requires --deadline\n\n{usage}") + })?; + let _: Vec = util::parse_comma_list(lengths_str)?; + } + "PartialFeedbackEdgeSet" => { + let usage = "Usage: pred create PartialFeedbackEdgeSet --graph 0-1,1-2,2-0,2-3,3-4,4-2,3-5,5-4,0-3 --budget 3 --max-cycle-length 4"; + let _ = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let _ = args + .budget + .as_deref() + .ok_or_else(|| { + anyhow::anyhow!("PartialFeedbackEdgeSet requires --budget\n\n{usage}") + })? + .parse::() + .map_err(|e| { + anyhow::anyhow!( + "Invalid --budget value for PartialFeedbackEdgeSet: {e}\n\n{usage}" + ) + })?; + let _ = args.max_cycle_length.ok_or_else(|| { + anyhow::anyhow!("PartialFeedbackEdgeSet requires --max-cycle-length\n\n{usage}") + })?; + } + "PathConstrainedNetworkFlow" => { + let usage = "Usage: pred create PathConstrainedNetworkFlow --arcs \"0>1,0>2,1>3,1>4,2>4,3>5,4>5,4>6,5>7,6>7\" --capacities 2,1,1,1,1,1,1,1,2,1 --source 0 --sink 7 --paths \"0,2,5,8;0,3,6,8;0,3,7,9;1,4,6,8;1,4,7,9\" --requirement 3"; + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!("PathConstrainedNetworkFlow requires --arcs\n\n{usage}") + })?; + let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let capacities: Vec = if let Some(ref s) = args.capacities { + util::parse_comma_list(s)? + } else { + vec![1; num_arcs] + }; + anyhow::ensure!( + capacities.len() == num_arcs, + "capacities length ({}) must match number of arcs ({num_arcs})", + capacities.len() + ); + let source = args.source.ok_or_else(|| { + anyhow::anyhow!("PathConstrainedNetworkFlow requires --source\n\n{usage}") + })?; + let sink = args.sink.ok_or_else(|| { + anyhow::anyhow!("PathConstrainedNetworkFlow requires --sink\n\n{usage}") + })?; + let _ = args.requirement.ok_or_else(|| { + anyhow::anyhow!("PathConstrainedNetworkFlow requires --requirement\n\n{usage}") + })?; + let paths = parse_prescribed_paths(args, num_arcs, usage)?; + validate_prescribed_paths_against_graph(&graph, &paths, source, sink, usage)?; + } + "ProductionPlanning" => { + let usage = "Usage: pred create ProductionPlanning --num-periods 6 --demands 5,3,7,2,8,5 --capacities 12,12,12,12,12,12 --setup-costs 10,10,10,10,10,10 --production-costs 1,1,1,1,1,1 --inventory-costs 1,1,1,1,1,1 --cost-bound 80"; + let num_periods = args.num_periods.ok_or_else(|| { + anyhow::anyhow!("ProductionPlanning requires --num-periods\n\n{usage}") + })?; + let demands = parse_named_u64_list( + args.demands.as_deref(), + "ProductionPlanning", + "--demands", + usage, + )?; + let capacities = parse_named_u64_list( + args.capacities.as_deref(), + "ProductionPlanning", + "--capacities", + usage, + )?; + let setup_costs = parse_named_u64_list( + args.setup_costs.as_deref(), + "ProductionPlanning", + "--setup-costs", + usage, + )?; + let production_costs = parse_named_u64_list( + args.production_costs.as_deref(), + "ProductionPlanning", + "--production-costs", + usage, + )?; + let inventory_costs = parse_named_u64_list( + args.inventory_costs.as_deref(), + "ProductionPlanning", + "--inventory-costs", + usage, + )?; + let _ = args.cost_bound.ok_or_else(|| { + anyhow::anyhow!("ProductionPlanning requires --cost-bound\n\n{usage}") + })?; + + for (flag, len) in [ + ("--demands", demands.len()), + ("--capacities", capacities.len()), + ("--setup-costs", setup_costs.len()), + ("--production-costs", production_costs.len()), + ("--inventory-costs", inventory_costs.len()), + ] { + ensure_named_len(len, num_periods, flag, usage)?; + } + } + "SchedulingWithIndividualDeadlines" => { + let usage = "Usage: pred create SchedulingWithIndividualDeadlines --num-tasks 7 --deadlines 2,1,2,2,3,3,2 [--num-processors 3 | --m 3] [--precedences \"0>3,1>3,1>4,2>4,2>5\"]"; + let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SchedulingWithIndividualDeadlines requires --deadlines, --num-tasks, and a processor count (--num-processors or --m)\n\n{usage}" + ) + })?; + let num_tasks = args.num_tasks.or(args.n).ok_or_else(|| { + anyhow::anyhow!( + "SchedulingWithIndividualDeadlines requires --num-tasks (number of tasks)\n\n{usage}" + ) + })?; + let num_processors = resolve_processor_count_flags( + "SchedulingWithIndividualDeadlines", + usage, + args.num_processors, + args.m, + )? + .ok_or_else(|| { + anyhow::anyhow!( + "SchedulingWithIndividualDeadlines requires --num-processors or --m\n\n{usage}" + ) + })?; + let deadlines: Vec = util::parse_comma_list(deadlines_str)?; + let precedences = parse_precedence_pairs( + args.precedences + .as_deref() + .or(args.precedence_pairs.as_deref()), + )?; + anyhow::ensure!( + deadlines.len() == num_tasks, + "deadlines length ({}) must equal num_tasks ({})", + deadlines.len(), + num_tasks + ); + for &(pred, succ) in &precedences { + anyhow::ensure!( + pred < num_tasks && succ < num_tasks, + "precedence index out of range: ({}, {}) but num_tasks = {}", + pred, + succ, + num_tasks + ); + } + let _ = SchedulingWithIndividualDeadlines::new( + num_tasks, + num_processors, + deadlines, + precedences, + ); + } + "StringToStringCorrection" => { + let usage = "Usage: pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2"; + let source_str = args.source_string.as_deref().ok_or_else(|| { + anyhow::anyhow!("StringToStringCorrection requires --source-string\n\n{usage}") + })?; + let target_str = args.target_string.as_deref().ok_or_else(|| { + anyhow::anyhow!("StringToStringCorrection requires --target-string\n\n{usage}") + })?; + let _ = parse_nonnegative_usize_bound( + args.bound.ok_or_else(|| { + anyhow::anyhow!("StringToStringCorrection requires --bound\n\n{usage}") + })?, + "StringToStringCorrection", + usage, + )?; + let source = parse_symbol_list_allow_empty(source_str)?; + let target = parse_symbol_list_allow_empty(target_str)?; + let inferred = source + .iter() + .chain(target.iter()) + .copied() + .max() + .map_or(0, |m| m + 1); + let alphabet_size = args.alphabet_size.unwrap_or(inferred); + anyhow::ensure!( + alphabet_size >= inferred, + "--alphabet-size {} is smaller than max symbol + 1 ({}) in the strings", + alphabet_size, + inferred + ); + } + "SparseMatrixCompression" => { + let matrix = parse_bool_matrix(args)?; + let usage = "Usage: pred create SparseMatrixCompression --matrix \"1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0\" --bound-k 2"; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!( + "SparseMatrixCompression requires --matrix and --bound-k\n\n{usage}" + ) + })?; + let bound = parse_nonnegative_usize_bound(bound, "SparseMatrixCompression", usage)?; + if bound == 0 { + anyhow::bail!("SparseMatrixCompression requires bound >= 1\n\n{usage}"); + } + let _ = SparseMatrixCompression::new(matrix, bound); + } + "StackerCrane" => { + let usage = "Usage: pred create StackerCrane --arcs \"0>4,2>5,5>1,3>0,4>3\" --graph \"0-1,1-2,2-3,3-5,4-5,0-3,1-5\" --arc-lengths 3,4,2,5,3 --edge-lengths 2,1,3,2,1,4,3 --num-vertices 6"; + let arcs_str = args + .arcs + .as_deref() + .ok_or_else(|| anyhow::anyhow!("StackerCrane requires --arcs\n\n{usage}"))?; + let (arcs_graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?; + let (edges_graph, num_vertices) = + parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + anyhow::ensure!( + edges_graph.num_vertices() == num_vertices, + "internal error: inconsistent graph vertex count" + ); + anyhow::ensure!( + num_vertices == arcs_graph.num_vertices(), + "StackerCrane requires the directed and undirected inputs to agree on --num-vertices\n\n{usage}" + ); + let arc_lengths = parse_arc_costs(args, num_arcs)?; + let edge_lengths = parse_i32_edge_values( + args.edge_lengths.as_ref(), + edges_graph.num_edges(), + "edge length", + )?; + let _ = problemreductions::models::misc::StackerCrane::try_new( + num_vertices, + arcs_graph.arcs(), + edges_graph.edges(), + arc_lengths, + edge_lengths, + ) + .map_err(|e| anyhow::anyhow!(e))?; + } + "ThreePartition" => { + let sizes_str = args.sizes.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "ThreePartition requires --sizes and --bound\n\n\ + Usage: pred create ThreePartition --sizes 4,5,6,4,6,5 --bound 15" + ) + })?; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!( + "ThreePartition requires --bound\n\n\ + Usage: pred create ThreePartition --sizes 4,5,6,4,6,5 --bound 15" + ) + })?; + let bound = u64::try_from(bound).map_err(|_| { + anyhow::anyhow!( + "ThreePartition requires a positive integer --bound\n\n\ + Usage: pred create ThreePartition --sizes 4,5,6,4,6,5 --bound 15" + ) + })?; + let sizes: Vec = util::parse_comma_list(sizes_str)?; + let _ = ThreePartition::try_new(sizes, bound).map_err(anyhow::Error::msg)?; + } + "UndirectedFlowLowerBounds" => { + let usage = "Usage: pred create UndirectedFlowLowerBounds --graph 0-1,0-2,1-3,2-3,1-4,3-5,4-5 --capacities 2,2,2,2,1,3,2 --lower-bounds 1,1,0,0,1,0,1 --source 0 --sink 5 --requirement 3"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let capacities = parse_capacities(args, graph.num_edges(), usage)?; + let lower_bounds = parse_lower_bounds(args, graph.num_edges(), usage)?; + let num_vertices = graph.num_vertices(); + let source = args.source.ok_or_else(|| { + anyhow::anyhow!("UndirectedFlowLowerBounds requires --source\n\n{usage}") + })?; + let sink = args.sink.ok_or_else(|| { + anyhow::anyhow!("UndirectedFlowLowerBounds requires --sink\n\n{usage}") + })?; + let requirement = args.requirement.ok_or_else(|| { + anyhow::anyhow!("UndirectedFlowLowerBounds requires --requirement\n\n{usage}") + })?; + validate_vertex_index("source", source, num_vertices, usage)?; + validate_vertex_index("sink", sink, num_vertices, usage)?; + let _ = UndirectedFlowLowerBounds::new( + graph, + capacities, + lower_bounds, + source, + sink, + requirement, + ); + } + "SequencingToMinimizeMaximumCumulativeCost" => { + let costs_str = args.costs.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SequencingToMinimizeMaximumCumulativeCost requires --costs\n\n\ + Usage: pred create SequencingToMinimizeMaximumCumulativeCost --costs 2,-1,3,-2,1,-3 --precedences \"0>2,1>2,1>3,2>4,3>5,4>5\"" + ) + })?; + let costs: Vec = util::parse_comma_list(costs_str)?; + let precedences = parse_precedence_pairs( + args.precedences + .as_deref() + .or(args.precedence_pairs.as_deref()), + )?; + validate_precedence_pairs(&precedences, costs.len())?; + } + "SequencingToMinimizeWeightedTardiness" => { + let lengths_str = args.lengths.as_deref().or(args.sizes.as_deref()).ok_or_else(|| { + anyhow::anyhow!( + "SequencingToMinimizeWeightedTardiness requires --lengths, --weights, --deadlines, and --bound\n\n\ + Usage: pred create SequencingToMinimizeWeightedTardiness --lengths 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" + ) + })?; + let weights_str = args.weights.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SequencingToMinimizeWeightedTardiness requires --weights (comma-separated tardiness weights)\n\n\ + Usage: pred create SequencingToMinimizeWeightedTardiness --lengths 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" + ) + })?; + let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SequencingToMinimizeWeightedTardiness requires --deadlines (comma-separated job deadlines)\n\n\ + Usage: pred create SequencingToMinimizeWeightedTardiness --lengths 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" + ) + })?; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!( + "SequencingToMinimizeWeightedTardiness requires --bound\n\n\ + Usage: pred create SequencingToMinimizeWeightedTardiness --lengths 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" + ) + })?; + anyhow::ensure!(bound >= 0, "--bound must be non-negative"); + let lengths: Vec = util::parse_comma_list(lengths_str)?; + let weights: Vec = util::parse_comma_list(weights_str)?; + let deadlines: Vec = util::parse_comma_list(deadlines_str)?; + anyhow::ensure!( + lengths.len() == weights.len(), + "lengths length ({}) must equal weights length ({})", + lengths.len(), + weights.len() + ); + anyhow::ensure!( + lengths.len() == deadlines.len(), + "lengths length ({}) must equal deadlines length ({})", + lengths.len(), + deadlines.len() + ); + } + "SequencingWithinIntervals" => { + let usage = + "Usage: pred create SequencingWithinIntervals --release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1"; + let rt_str = args.release_times.as_deref().ok_or_else(|| { + anyhow::anyhow!("SequencingWithinIntervals requires --release-times\n\n{usage}") + })?; + let dl_str = args.deadlines.as_deref().ok_or_else(|| { + anyhow::anyhow!("SequencingWithinIntervals requires --deadlines\n\n{usage}") + })?; + let len_str = args.lengths.as_deref().ok_or_else(|| { + anyhow::anyhow!("SequencingWithinIntervals requires --lengths\n\n{usage}") + })?; + let release_times: Vec = util::parse_comma_list(rt_str)?; + let deadlines: Vec = util::parse_comma_list(dl_str)?; + let lengths: Vec = util::parse_comma_list(len_str)?; + validate_sequencing_within_intervals_inputs( + &release_times, + &deadlines, + &lengths, + usage, + )?; + } + "SetBasis" => { + let universe = args.universe.ok_or_else(|| { + anyhow::anyhow!( + "SetBasis requires --universe, --sets, and --k\n\n\ + Usage: pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3" + ) + })?; + let _ = args.k.ok_or_else(|| { + anyhow::anyhow!( + "SetBasis requires --k\n\n\ + Usage: pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3" + ) + })?; + let sets = parse_sets(args)?; + for (i, set) in sets.iter().enumerate() { + for &element in set { + if element >= universe { + bail!( + "Set {} contains element {} which is outside universe of size {}", + i, + element, + universe + ); + } + } + } + } + "ShortestWeightConstrainedPath" => { + let usage = "Usage: pred create ShortestWeightConstrainedPath --graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,1-4 --edge-lengths 2,4,3,1,5,4,2,6 --edge-weights 5,1,2,3,2,3,1,1 --source-vertex 0 --target-vertex 5 --weight-bound 8"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + if args.weights.is_some() { + bail!( + "ShortestWeightConstrainedPath uses --edge-weights, not --weights\n\nUsage: {usage}" + ); + } + let edge_lengths_raw = args.edge_lengths.as_ref().ok_or_else(|| { + anyhow::anyhow!( + "ShortestWeightConstrainedPath requires --edge-lengths\n\nUsage: {usage}" + ) + })?; + let edge_weights_raw = args.edge_weights.as_ref().ok_or_else(|| { + anyhow::anyhow!( + "ShortestWeightConstrainedPath requires --edge-weights\n\nUsage: {usage}" + ) + })?; + let edge_lengths = + parse_i32_edge_values(Some(edge_lengths_raw), graph.num_edges(), "edge length")?; + let edge_weights = + parse_i32_edge_values(Some(edge_weights_raw), graph.num_edges(), "edge weight")?; + ensure_positive_i32_values(&edge_lengths, "edge lengths")?; + ensure_positive_i32_values(&edge_weights, "edge weights")?; + let source_vertex = args.source_vertex.ok_or_else(|| { + anyhow::anyhow!( + "ShortestWeightConstrainedPath requires --source-vertex\n\nUsage: {usage}" + ) + })?; + let target_vertex = args.target_vertex.ok_or_else(|| { + anyhow::anyhow!( + "ShortestWeightConstrainedPath requires --target-vertex\n\nUsage: {usage}" + ) + })?; + let weight_bound = args.weight_bound.ok_or_else(|| { + anyhow::anyhow!( + "ShortestWeightConstrainedPath requires --weight-bound\n\nUsage: {usage}" + ) + })?; + ensure_vertex_in_bounds(source_vertex, graph.num_vertices(), "source_vertex")?; + ensure_vertex_in_bounds(target_vertex, graph.num_vertices(), "target_vertex")?; + ensure_positive_i32(weight_bound, "weight_bound")?; + } + "SteinerTree" => { + let usage = "Usage: pred create SteinerTree --graph 0-1,1-2,1-3,3-4 --edge-weights 2,2,1,1 --terminals 0,2,4"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let _ = parse_edge_weights(args, graph.num_edges())?; + let _ = parse_terminals(args, graph.num_vertices()) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + } + "TimetableDesign" => { + let usage = "Usage: pred create TimetableDesign --num-periods 3 --num-craftsmen 5 --num-tasks 5 --craftsman-avail \"1,1,1;1,1,0;0,1,1;1,0,1;1,1,1\" --task-avail \"1,1,0;0,1,1;1,0,1;1,1,1;1,1,1\" --requirements \"1,0,1,0,0;0,1,0,0,1;0,0,0,1,0;0,0,0,0,1;0,1,0,0,0\""; + let num_periods = args.num_periods.ok_or_else(|| { + anyhow::anyhow!("TimetableDesign requires --num-periods\n\n{usage}") + })?; + let num_craftsmen = args.num_craftsmen.ok_or_else(|| { + anyhow::anyhow!("TimetableDesign requires --num-craftsmen\n\n{usage}") + })?; + let num_tasks = args.num_tasks.ok_or_else(|| { + anyhow::anyhow!("TimetableDesign requires --num-tasks\n\n{usage}") + })?; + let craftsman_avail = + parse_named_bool_rows(args.craftsman_avail.as_deref(), "--craftsman-avail", usage)?; + let task_avail = + parse_named_bool_rows(args.task_avail.as_deref(), "--task-avail", usage)?; + let requirements = parse_timetable_requirements(args.requirements.as_deref(), usage)?; + validate_timetable_design_args( + num_periods, + num_craftsmen, + num_tasks, + &craftsman_avail, + &task_avail, + &requirements, + usage, + )?; + } + "UndirectedTwoCommodityIntegralFlow" => { + let usage = "Usage: 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"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let capacities = parse_capacities(args, graph.num_edges(), usage)?; + for (edge_index, &capacity) in capacities.iter().enumerate() { + let fits = usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .is_some(); + if !fits { + bail!( + "capacity {} at edge index {} is too large for this platform\n\n{}", + capacity, + edge_index, + usage + ); + } + } + let num_vertices = graph.num_vertices(); + let source_1 = args.source_1.ok_or_else(|| { + anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --source-1\n\n{usage}") + })?; + let sink_1 = args.sink_1.ok_or_else(|| { + anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --sink-1\n\n{usage}") + })?; + let source_2 = args.source_2.ok_or_else(|| { + anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --source-2\n\n{usage}") + })?; + let sink_2 = args.sink_2.ok_or_else(|| { + anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --sink-2\n\n{usage}") + })?; + let _ = args.requirement_1.ok_or_else(|| { + anyhow::anyhow!( + "UndirectedTwoCommodityIntegralFlow requires --requirement-1\n\n{usage}" + ) + })?; + let _ = args.requirement_2.ok_or_else(|| { + anyhow::anyhow!( + "UndirectedTwoCommodityIntegralFlow requires --requirement-2\n\n{usage}" + ) + })?; + for (label, vertex) in [ + ("source-1", source_1), + ("sink-1", sink_1), + ("source-2", source_2), + ("sink-2", sink_2), + ] { + validate_vertex_index(label, vertex, num_vertices, usage)?; + } + } + _ => {} + } + + Ok(()) +} diff --git a/problemreductions-cli/src/commands/create/schema_support.rs b/problemreductions-cli/src/commands/create/schema_support.rs new file mode 100644 index 000000000..23390e6ef --- /dev/null +++ b/problemreductions-cli/src/commands/create/schema_support.rs @@ -0,0 +1,2519 @@ +use super::*; + +#[derive(Debug, Clone, Default)] +pub(super) struct CreateContext { + num_vertices: Option, + num_edges: Option, + num_arcs: Option, + parsed_fields: BTreeMap, +} + +impl CreateContext { + pub(super) fn with_field(mut self, name: &str, value: serde_json::Value) -> Self { + self.parsed_fields.insert(name.to_string(), value); + self + } + + fn seed_field(&mut self, name: &str, value: T) -> Result<()> { + let value = serde_json::to_value(value)?; + if name == "num_vertices" { + self.num_vertices = value.as_u64().and_then(|raw| usize::try_from(raw).ok()); + } + self.parsed_fields.insert(name.to_string(), value); + Ok(()) + } + + fn usize_field(&self, name: &str) -> Option { + self.parsed_fields + .get(name) + .and_then(serde_json::Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + } + + fn f64_field(&self, name: &str) -> Option { + self.parsed_fields + .get(name) + .and_then(serde_json::Value::as_f64) + } + + fn remember(&mut self, name: &str, concrete_type: &str, value: &serde_json::Value) { + self.parsed_fields.insert(name.to_string(), value.clone()); + + match normalize_type_name(concrete_type).as_str() { + "SimpleGraph" => { + self.num_vertices = value + .get("num_vertices") + .and_then(serde_json::Value::as_u64) + .and_then(|raw| usize::try_from(raw).ok()); + self.num_edges = value + .get("edges") + .and_then(serde_json::Value::as_array) + .map(Vec::len); + } + "DirectedGraph" => { + self.num_vertices = value + .get("num_vertices") + .and_then(serde_json::Value::as_u64) + .and_then(|raw| usize::try_from(raw).ok()); + self.num_arcs = value + .get("arcs") + .and_then(serde_json::Value::as_array) + .map(Vec::len); + } + "KingsSubgraph" | "TriangularSubgraph" => { + self.num_vertices = value + .get("positions") + .and_then(serde_json::Value::as_array) + .map(Vec::len); + } + "UnitDiskGraph" => { + self.num_vertices = value + .get("positions") + .and_then(serde_json::Value::as_array) + .map(Vec::len); + self.num_edges = value + .get("edges") + .and_then(serde_json::Value::as_array) + .map(Vec::len); + } + _ => {} + } + } +} + +pub(super) fn create_schema_driven( + args: &CreateArgs, + canonical: &str, + resolved_variant: &BTreeMap, +) -> Result)>> { + if !schema_driven_supported_problem(canonical) { + return Ok(None); + } + + let Some(schema) = collect_schemas() + .into_iter() + .find(|schema| schema.name == canonical) + else { + return Ok(None); + }; + let Some(variant_entry) = + problemreductions::registry::find_variant_entry(canonical, resolved_variant) + else { + return Ok(None); + }; + + let graph_type = resolved_graph_type(resolved_variant); + let is_geometry = matches!( + graph_type, + "KingsSubgraph" | "TriangularSubgraph" | "UnitDiskGraph" + ); + let flag_map = args.flag_map(); + let mut context = CreateContext::default(); + seed_schema_context_from_cli(args, graph_type, &mut context)?; + validate_schema_driven_semantics(args, canonical, resolved_variant, &serde_json::Value::Null) + .map_err(|error| with_schema_usage(error, canonical, resolved_variant))?; + let mut json_map = serde_json::Map::new(); + + for field in &schema.fields { + let concrete_type = resolve_schema_field_type(&field.type_name, resolved_variant); + let flag_keys = + schema_field_flag_keys(canonical, &field.name, &field.type_name, is_geometry); + let raw_value = get_schema_flag_value(&flag_map, &flag_keys); + let value = if !schema_field_requires_derived_input(&field.name, &concrete_type) { + if let Some(raw_value) = raw_value.clone() { + match parse_schema_field_value( + args, + canonical, + &concrete_type, + &field.name, + &raw_value, + &context, + ) { + Ok(value) => value, + Err(error) => { + return Err(with_schema_usage(error, canonical, resolved_variant)) + } + } + } else if let Some(derived) = + derive_schema_field_value(args, canonical, &field.name, &concrete_type, &context)? + { + derived + } else { + return Err(with_schema_usage( + missing_schema_field_error( + canonical, + &field.name, + &field.type_name, + is_geometry, + ), + canonical, + resolved_variant, + )); + } + } else if let Some(derived) = + derive_schema_field_value(args, canonical, &field.name, &concrete_type, &context)? + { + derived + } else if let Some(raw_value) = raw_value { + match parse_schema_field_value( + args, + canonical, + &concrete_type, + &field.name, + &raw_value, + &context, + ) { + Ok(value) => value, + Err(error) => return Err(with_schema_usage(error, canonical, resolved_variant)), + } + } else { + return Err(with_schema_usage( + missing_schema_field_error(canonical, &field.name, &field.type_name, is_geometry), + canonical, + resolved_variant, + )); + }; + + context.remember(&field.name, &concrete_type, &value); + json_map.insert(field.name.clone(), value); + } + + let data = serde_json::Value::Object(json_map); + validate_schema_driven_semantics(args, canonical, resolved_variant, &data) + .map_err(|error| with_schema_usage(error, canonical, resolved_variant))?; + (variant_entry.factory)(data.clone()).map_err(|error| { + with_schema_usage( + anyhow::anyhow!( + "Schema-driven factory rejected generated data for {canonical}: {error}" + ), + canonical, + resolved_variant, + ) + })?; + + Ok(Some((data, resolved_variant.clone()))) +} + +pub(super) fn missing_schema_field_error( + canonical: &str, + field_name: &str, + field_type: &str, + is_geometry: bool, +) -> anyhow::Error { + let display = problem_help_flag_name(canonical, field_name, field_type, is_geometry); + let flags: Vec = display + .split('/') + .filter_map(|part| { + let trimmed = part.trim().trim_start_matches("--"); + (!trimmed.is_empty()).then(|| format!("--{trimmed}")) + }) + .collect(); + let requirement = match flags.as_slice() { + [] => format!("--{}", field_name.replace('_', "-")), + [flag] => flag.clone(), + [first, second] => format!("{first} or {second}"), + _ => { + let last = flags.last().cloned().unwrap_or_default(); + format!("{}, or {}", flags[..flags.len() - 1].join(", "), last) + } + }; + anyhow::anyhow!("{canonical} requires {requirement}") +} + +pub(super) fn parse_schema_field_value( + args: &CreateArgs, + canonical: &str, + concrete_type: &str, + field_name: &str, + raw: &str, + context: &CreateContext, +) -> Result { + match (canonical, field_name) { + ("BoyceCoddNormalFormViolation", "functional_deps") => { + let num_attributes = args.n.ok_or_else(|| { + anyhow::anyhow!("BoyceCoddNormalFormViolation requires --n, --sets, and --target") + })?; + Ok(serde_json::to_value(parse_bcnf_functional_deps( + raw, + num_attributes, + )?)?) + } + ("BoundedComponentSpanningForest", "max_weight") => { + let usage = "Usage: pred create BoundedComponentSpanningForest --graph 0-1,1-2,2-3,3-4,4-5,5-6,6-7,0-7,1-5,2-6 --weights 2,3,1,2,3,1,2,1 --k 3 --max-weight 6"; + let bound_raw = args.bound.ok_or_else(|| { + anyhow::anyhow!("BoundedComponentSpanningForest requires --max-weight\n\n{usage}") + })?; + let max_weight = i32::try_from(bound_raw).map_err(|_| { + anyhow::anyhow!( + "BoundedComponentSpanningForest requires --max-weight within i32 range\n\n{usage}" + ) + })?; + Ok(serde_json::json!(max_weight)) + } + ("ConsecutiveBlockMinimization", "matrix") => { + let usage = "Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2"; + let matrix: Vec> = serde_json::from_str(raw).map_err(|err| { + anyhow::anyhow!( + "ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array (e.g., '[[true,false,true],[false,true,true]]')\n\n{usage}\n\nFailed to parse --matrix: {err}" + ) + })?; + Ok(serde_json::to_value(matrix)?) + } + ("FeasibleBasisExtension", "matrix") => { + let usage = "Usage: pred create FeasibleBasisExtension --matrix '[[1,0,1],[0,1,0]]' --rhs '7,5' --required-columns '0'"; + let matrix: Vec> = serde_json::from_str(raw).map_err(|err| { + anyhow::anyhow!( + "FeasibleBasisExtension requires --matrix as a JSON 2D integer array (e.g., '[[1,0,1],[0,1,0]]')\n\n{usage}\n\nFailed to parse --matrix: {err}" + ) + })?; + Ok(serde_json::to_value(matrix)?) + } + ("IntegralFlowBundles", "bundle_capacities") => { + let usage = "Usage: pred create IntegralFlowBundles --arcs \"0>1,0>2,1>3,2>3,1>2,2>1\" --bundles \"0,1;2,5;3,4\" --bundle-capacities 1,1,1 --source 0 --sink 3 --requirement 1 --num-vertices 4"; + let arcs_str = args + .arcs + .as_deref() + .ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles requires --arcs\n\n{usage}"))?; + let (_, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let bundles = parse_bundles(args, num_arcs, usage)?; + Ok(serde_json::to_value(parse_bundle_capacities( + args, + bundles.len(), + usage, + )?)?) + } + ("IntegralFlowHomologousArcs", "homologous_pairs") => { + Ok(serde_json::to_value(parse_homologous_pairs(args)?)?) + } + ("LengthBoundedDisjointPaths", "max_length") => { + let usage = "Usage: pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --max-length 3"; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!("LengthBoundedDisjointPaths requires --max-length\n\n{usage}") + })?; + let max_length = usize::try_from(bound).map_err(|_| { + anyhow::anyhow!( + "--max-length must be a nonnegative integer for LengthBoundedDisjointPaths\n\n{usage}" + ) + })?; + Ok(serde_json::json!(max_length)) + } + ("LongestCommonSubsequence", "strings") => { + let (strings, _) = parse_lcs_strings(raw)?; + Ok(serde_json::to_value(strings)?) + } + ("MinimumDecisionTree", "test_matrix") => { + let usage = "Usage: pred create MinimumDecisionTree --test-matrix '[[true,true,false,false],[true,false,false,false],[false,true,false,true]]' --num-objects 4 --num-tests 3"; + let matrix: Vec> = serde_json::from_str(raw).map_err(|err| { + anyhow::anyhow!( + "MinimumDecisionTree requires --test-matrix as a JSON 2D bool array\n\n{usage}\n\nFailed to parse --test-matrix: {err}" + ) + })?; + Ok(serde_json::to_value(matrix)?) + } + ("MinimumWeightDecoding", "matrix") => { + let usage = "Usage: pred create MinimumWeightDecoding --matrix '[[true,false,true],[false,true,true]]' --rhs 'true,true'"; + let matrix: Vec> = serde_json::from_str(raw).map_err(|err| { + anyhow::anyhow!( + "MinimumWeightDecoding requires --matrix as a JSON 2D bool array (e.g., '[[true,false],[false,true]]')\n\n{usage}\n\nFailed to parse --matrix: {err}" + ) + })?; + Ok(serde_json::to_value(matrix)?) + } + ("MinimumWeightSolutionToLinearEquations", "matrix") => { + let usage = "Usage: pred create MinimumWeightSolutionToLinearEquations --matrix '[[1,2,3,1],[2,1,1,3]]' --rhs '5,4'"; + let matrix: Vec> = serde_json::from_str(raw).map_err(|err| { + anyhow::anyhow!( + "MinimumWeightSolutionToLinearEquations requires --matrix as a JSON 2D integer array (e.g., '[[1,2,3],[4,5,6]]')\n\n{usage}\n\nFailed to parse --matrix: {err}" + ) + })?; + Ok(serde_json::to_value(matrix)?) + } + ("GroupingBySwapping", "string") + | ("StringToStringCorrection", "source") + | ("StringToStringCorrection", "target") => { + Ok(serde_json::to_value(parse_symbol_list_allow_empty(raw)?)?) + } + ("MultipleCopyFileAllocation", "usage") => { + let (_, num_vertices) = parse_graph(args) + .map_err(|e| anyhow::anyhow!("{e}\n\n{MULTIPLE_COPY_FILE_ALLOCATION_USAGE}"))?; + Ok(serde_json::to_value(parse_vertex_i64_values( + args.usage.as_deref(), + "usage", + num_vertices, + "MultipleCopyFileAllocation", + MULTIPLE_COPY_FILE_ALLOCATION_USAGE, + )?)?) + } + ("MultipleCopyFileAllocation", "storage") => { + let (_, num_vertices) = parse_graph(args) + .map_err(|e| anyhow::anyhow!("{e}\n\n{MULTIPLE_COPY_FILE_ALLOCATION_USAGE}"))?; + Ok(serde_json::to_value(parse_vertex_i64_values( + args.storage.as_deref(), + "storage", + num_vertices, + "MultipleCopyFileAllocation", + MULTIPLE_COPY_FILE_ALLOCATION_USAGE, + )?)?) + } + ("SequencingToMinimizeMaximumCumulativeCost", "precedences") => { + Ok(serde_json::to_value(parse_precedence_pairs( + args.precedences + .as_deref() + .or(args.precedence_pairs.as_deref()), + )?)?) + } + ("UndirectedTwoCommodityIntegralFlow", "capacities") => { + let usage = "Usage: 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"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + Ok(serde_json::to_value(parse_capacities( + args, + graph.num_edges(), + usage, + )?)?) + } + _ => parse_field_value(concrete_type, field_name, raw, context), + } +} + +pub(super) fn schema_driven_supported_problem(canonical: &str) -> bool { + canonical != "ILP" && canonical != "CircuitSAT" +} + +pub(super) fn schema_field_flag_keys( + canonical: &str, + field_name: &str, + field_type: &str, + is_geometry: bool, +) -> Vec { + let mut keys = vec![field_name.replace('_', "-")]; + for display_key in problem_help_flag_name(canonical, field_name, field_type, is_geometry) + .split('/') + .map(|key| key.trim().trim_start_matches("--").to_string()) + .filter(|key| !key.is_empty()) + { + if !keys.contains(&display_key) { + keys.push(display_key); + } + } + keys +} + +pub(super) fn get_schema_flag_value( + flag_map: &std::collections::HashMap<&'static str, Option>, + keys: &[String], +) -> Option { + keys.iter() + .find_map(|key| flag_map.get(key.as_str()).cloned().flatten()) +} + +pub(super) fn resolve_schema_field_type( + type_name: &str, + resolved_variant: &BTreeMap, +) -> String { + let normalized = normalize_type_name(type_name); + let graph_type = resolved_variant + .get("graph") + .map(String::as_str) + .unwrap_or("SimpleGraph"); + let weight_type = resolved_variant + .get("weight") + .map(String::as_str) + .unwrap_or("One"); + + match normalized.as_str() { + "G" => graph_type.to_string(), + "W" => weight_type.to_string(), + "W::Sum" => weight_sum_type(weight_type).to_string(), + "Vec" => format!("Vec<{weight_type}>"), + "Vec>" => format!("Vec>"), + "Vec<(usize,usize,W)>" => format!("Vec<(usize,usize,{weight_type})>"), + "Vec>" => format!("Vec>"), + other => other.to_string(), + } +} + +pub(super) fn weight_sum_type(weight_type: &str) -> &'static str { + match weight_type { + "One" | "i32" => "i32", + "f64" => "f64", + _ => "i32", + } +} + +pub(super) fn seed_schema_context_from_cli( + args: &CreateArgs, + graph_type: &str, + context: &mut CreateContext, +) -> Result<()> { + if let Some(num_vertices) = args.num_vertices { + context.seed_field("num_vertices", num_vertices)?; + } + if graph_type == "UnitDiskGraph" { + context.seed_field("radius", args.radius.unwrap_or(1.0))?; + } + Ok(()) +} + +pub(super) fn derive_schema_field_value( + args: &CreateArgs, + canonical: &str, + field_name: &str, + concrete_type: &str, + context: &CreateContext, +) -> Result> { + if let Some(defaulted) = + derive_schema_default_value(canonical, field_name, concrete_type, context)? + { + return Ok(Some(defaulted)); + } + + if field_name == "graph" && concrete_type == "MixedGraph" { + let usage = format!( + "Usage: pred create {canonical} {}", + example_for(canonical, None) + ); + return Ok(Some(serde_json::to_value(parse_mixed_graph( + args, &usage, + )?)?)); + } + + if field_name == "graph" && concrete_type == "BipartiteGraph" { + let left = args + .left + .ok_or_else(|| anyhow::anyhow!("{canonical} requires --left"))?; + let right = args + .right + .ok_or_else(|| anyhow::anyhow!("{canonical} requires --right"))?; + let edges_raw = args + .biedges + .as_deref() + .ok_or_else(|| anyhow::anyhow!("{canonical} requires --biedges"))?; + let edges = util::parse_edge_pairs(edges_raw)?; + validate_bipartite_edges(canonical, left, right, &edges)?; + return Ok(Some(serde_json::to_value(BipartiteGraph::new( + left, right, edges, + ))?)); + } + + if canonical == "ClosestVectorProblem" + && field_name == "bounds" + && normalize_type_name(concrete_type) == "Vec" + { + return Ok(Some(parse_cvp_bounds_value( + args.bounds.as_deref(), + context, + )?)); + } + + if canonical == "ConjunctiveBooleanQuery" + && field_name == "num_variables" + && normalize_type_name(concrete_type) == "usize" + { + let raw = args + .conjuncts_spec + .as_deref() + .ok_or_else(|| anyhow::anyhow!("ConjunctiveBooleanQuery requires --conjuncts-spec"))?; + return Ok(Some(serde_json::json!(infer_cbq_num_variables(raw)?))); + } + + if canonical == "GroupingBySwapping" + && field_name == "alphabet_size" + && normalize_type_name(concrete_type) == "usize" + { + let raw = args + .string + .as_deref() + .ok_or_else(|| anyhow::anyhow!("GroupingBySwapping requires --string"))?; + let string = parse_symbol_list_allow_empty(raw)?; + let inferred = string.iter().copied().max().map_or(0, |value| value + 1); + return Ok(Some(serde_json::json!(args + .alphabet_size + .unwrap_or(inferred)))); + } + + if canonical == "JobShopScheduling" + && field_name == "num_processors" + && normalize_type_name(concrete_type) == "usize" + { + let usage = "Usage: pred create JobShopScheduling --jobs \"0:3,1:4;1:2,0:3,1:2;0:4,1:3\" --num-processors 2"; + let inferred_processors = match args.job_tasks.as_deref() { + Some(job_tasks) => { + let jobs = parse_job_shop_jobs(job_tasks)?; + jobs.iter() + .flat_map(|job| job.iter().map(|(processor, _)| *processor)) + .max() + .map(|processor| processor + 1) + } + None => None, + }; + let num_processors = + resolve_processor_count_flags("JobShopScheduling", usage, args.num_processors, args.m)? + .or(inferred_processors) + .ok_or_else(|| { + anyhow::anyhow!( + "Cannot infer num_processors from empty job list; use --num-processors" + ) + })?; + return Ok(Some(serde_json::json!(num_processors))); + } + + if canonical == "LongestCommonSubsequence" + && field_name == "alphabet_size" + && normalize_type_name(concrete_type) == "usize" + { + let raw = args + .strings + .as_deref() + .ok_or_else(|| anyhow::anyhow!("LongestCommonSubsequence requires --strings"))?; + let (_, inferred_alphabet_size) = parse_lcs_strings(raw)?; + return Ok(Some(serde_json::json!(args + .alphabet_size + .unwrap_or(inferred_alphabet_size)))); + } + + if canonical == "LongestCommonSubsequence" + && field_name == "max_length" + && normalize_type_name(concrete_type) == "usize" + { + let strings: Vec> = + serde_json::from_value(context.parsed_fields.get("strings").cloned().ok_or_else( + || anyhow::anyhow!("LCS max_length derivation requires parsed strings"), + )?)?; + let max_length = strings.iter().map(Vec::len).min().unwrap_or(0); + return Ok(Some(serde_json::json!(max_length))); + } + + if canonical == "QUBO" + && field_name == "num_vars" + && normalize_type_name(concrete_type) == "usize" + { + let matrix = parse_matrix(args)?; + return Ok(Some(serde_json::json!(matrix.len()))); + } + + if canonical == "StringToStringCorrection" + && field_name == "alphabet_size" + && normalize_type_name(concrete_type) == "usize" + { + let source = parse_symbol_list_allow_empty(args.source_string.as_deref().unwrap_or(""))?; + let target = parse_symbol_list_allow_empty(args.target_string.as_deref().unwrap_or(""))?; + let inferred = source + .iter() + .chain(target.iter()) + .copied() + .max() + .map_or(0, |value| value + 1); + return Ok(Some(serde_json::json!(args + .alphabet_size + .unwrap_or(inferred)))); + } + + if field_name == "precedences" + && normalize_type_name(concrete_type) == "Vec<(usize,usize)>" + && args.precedences.is_none() + && args.precedence_pairs.is_none() + { + return Ok(Some(serde_json::json!([]))); + } + + if canonical == "ComparativeContainment" + && matches!(field_name, "r_weights" | "s_weights") + && matches!( + normalize_type_name(concrete_type).as_str(), + "Vec" | "Vec" | "Vec" + ) + { + let sets_len = context + .parsed_fields + .get(match field_name { + "r_weights" => "r_sets", + _ => "s_sets", + }) + .and_then(serde_json::Value::as_array) + .map(Vec::len); + if let Some(len) = sets_len { + let value = match normalize_type_name(concrete_type).as_str() { + "Vec" | "Vec" => serde_json::json!(vec![1_i32; len]), + "Vec" => serde_json::json!(vec![1.0_f64; len]), + _ => unreachable!(), + }; + return Ok(Some(value)); + } + } + + if canonical == "ConsistencyOfDatabaseFrequencyTables" + && field_name == "known_values" + && normalize_type_name(concrete_type) == "Vec" + && args.known_values.is_none() + { + return Ok(Some(serde_json::json!([]))); + } + + if canonical == "LengthBoundedDisjointPaths" + && field_name == "max_paths" + && normalize_type_name(concrete_type) == "usize" + { + let graph_value = context.parsed_fields.get("graph").cloned(); + let source = context.usize_field("source"); + let sink = context.usize_field("sink"); + if let (Some(graph_value), Some(source), Some(sink)) = (graph_value, source, sink) { + let graph: SimpleGraph = + serde_json::from_value(graph_value).context("Failed to deserialize graph")?; + let max_paths = graph + .neighbors(source) + .len() + .min(graph.neighbors(sink).len()); + return Ok(Some(serde_json::json!(max_paths))); + } + } + + Ok(None) +} + +pub(super) fn derive_schema_default_value( + canonical: &str, + field_name: &str, + concrete_type: &str, + context: &CreateContext, +) -> Result> { + let normalized = normalize_type_name(concrete_type); + + let one_list = |len: usize| match normalized.as_str() { + "Vec" | "Vec" => Some(serde_json::json!(vec![1_i32; len])), + "Vec" => Some(serde_json::json!(vec![1_u64; len])), + "Vec" => Some(serde_json::json!(vec![1_i64; len])), + "Vec" => Some(serde_json::json!(vec![1_usize; len])), + "Vec" => Some(serde_json::json!(vec![1.0_f64; len])), + _ => None, + }; + + let derived = match field_name { + "weights" | "vertex_weights" => context.num_vertices.and_then(one_list), + "edge_weights" | "edge_lengths" => context.num_edges.and_then(one_list), + "arc_weights" | "arc_lengths" if context.num_arcs.is_some() => { + context.num_arcs.and_then(one_list) + } + "capacities" if canonical == "PathConstrainedNetworkFlow" => { + context.num_arcs.and_then(one_list) + } + "couplings" if canonical == "SpinGlass" => context.num_edges.and_then(one_list), + "fields" if canonical == "SpinGlass" => match normalized.as_str() { + "Vec" => context + .num_vertices + .map(|len| serde_json::json!(vec![0_i32; len])), + "Vec" => context + .num_vertices + .map(|len| serde_json::json!(vec![0.0_f64; len])), + _ => None, + }, + _ => None, + }; + + Ok(derived) +} + +pub(super) fn schema_field_requires_derived_input(field_name: &str, concrete_type: &str) -> bool { + field_name == "graph" && matches!(concrete_type, "MixedGraph" | "BipartiteGraph") +} + +pub(super) fn with_schema_usage( + error: anyhow::Error, + canonical: &str, + resolved_variant: &BTreeMap, +) -> anyhow::Error { + let message = error.to_string(); + if message.contains("Usage: pred create") { + return error; + } + let graph_type = resolved_variant.get("graph").map(String::as_str); + anyhow::anyhow!( + "{message}\n\nUsage: pred create {canonical} {}", + example_for(canonical, graph_type) + ) +} + +pub(super) fn parse_field_value( + concrete_type: &str, + field_name: &str, + raw: &str, + context: &CreateContext, +) -> Result { + let normalized_type = normalize_type_name(concrete_type); + let value = match normalized_type.as_str() { + "SimpleGraph" => parse_simple_graph_value(raw, context)?, + "DirectedGraph" => parse_directed_graph_value(raw, context)?, + "KingsSubgraph" => parse_grid_subgraph_value(raw, true)?, + "TriangularSubgraph" => parse_grid_subgraph_value(raw, false)?, + "UnitDiskGraph" => parse_unit_disk_graph_value(raw, context)?, + "Vec" => parse_numeric_list_value::(raw)?, + "Vec" => parse_numeric_list_value::(raw)?, + "Vec" => parse_numeric_list_value::(raw)?, + "Vec" => parse_numeric_list_value::(raw)?, + "Vec" => parse_numeric_list_value::(raw)?, + "Vec" => parse_numeric_list_value::(raw)?, + "Vec" => parse_bool_list_value(raw)?, + "Vec>" => parse_nested_numeric_list_value::(raw)?, + "Vec>" => parse_nested_numeric_list_value::(raw)?, + "Vec>" => parse_nested_numeric_list_value::(raw)?, + "Vec>" => parse_nested_numeric_list_value::(raw)?, + "Vec>" => parse_nested_numeric_list_value::(raw)?, + "Vec>" => parse_nested_numeric_list_value::(raw)?, + "Vec>" => parse_bool_rows_value(raw, field_name)?, + "Vec>>" => parse_3d_numeric_list_value::(raw)?, + "Vec>>" => parse_3d_numeric_list_value::(raw)?, + "Vec<[usize;3]>" => parse_triple_array_list_value(raw)?, + "Vec" => serde_json::to_value(parse_clauses_raw(raw)?)?, + "Vec<(usize,usize)>" => parse_pair_list_value(raw)?, + "Vec<(u64,u64)>" => parse_semicolon_tuple_list_value::(raw)?, + "Vec<(usize,f64)>" => parse_indexed_numeric_pairs_value::(raw)?, + "Vec<(usize,usize,usize)>" => parse_semicolon_tuple_list_value::(raw)?, + "Vec<(usize,usize,usize,usize)>" => parse_semicolon_tuple_list_value::(raw)?, + "Vec<(usize,usize,One)>" => parse_weighted_edge_list_value::(raw)?, + "Vec<(usize,usize,i32)>" => parse_weighted_edge_list_value::(raw)?, + "Vec<(usize,usize,i64)>" => parse_weighted_edge_list_value::(raw)?, + "Vec<(usize,usize,u64)>" => parse_weighted_edge_list_value::(raw)?, + "Vec<(usize,usize,f64)>" => parse_weighted_edge_list_value::(raw)?, + "Vec<(Vec,Vec)>" => serde_json::to_value(parse_dependencies(raw)?)?, + "Vec<(Vec,usize)>" => serde_json::to_value(parse_implications(raw)?)?, + "Vec<(usize,Vec)>" => serde_json::to_value(parse_cbq_conjuncts(raw, context)?)?, + "Vec<(usize,Vec)>" => parse_indexed_usize_lists_value(raw)?, + "Vec>" => serde_json::to_value(parse_job_shop_jobs(raw)?)?, + "Vec<(f64,f64)>" => serde_json::to_value(util::parse_positions::(raw, "0.0,0.0")?)?, + "Vec" => { + serde_json::to_value(parse_cdft_frequency_tables_value(raw, context)?)? + } + "Vec" => serde_json::to_value(parse_cdft_known_values_value(raw, context)?)?, + "Vec" => serde_json::to_value(parse_cbq_relations(raw, context)?)?, + "Vec" => parse_string_list_value(raw)?, + "Vec" => parse_cvp_bounds_value(Some(raw), context)?, + "Vec" => parse_biguint_list_value(raw)?, + "BigUint" => parse_biguint_value(raw)?, + "Vec>" => parse_optional_bool_list_value(raw)?, + "Vec" => serde_json::to_value(parse_quantifiers_raw(raw, context)?)?, + "IntExpr" => parse_json_passthrough_value(raw)?, + "bool" => serde_json::to_value(parse_bool_token(raw.trim())?)?, + "One" => serde_json::json!(1), + "usize" => parse_scalar_value::(raw)?, + "u64" => parse_scalar_value::(raw)?, + "i32" => parse_scalar_value::(raw)?, + "i64" => parse_scalar_value::(raw)?, + "f64" => parse_scalar_value::(raw)?, + other => bail!("Unsupported schema parser for field '{field_name}' with type '{other}'"), + }; + + Ok(value) +} + +pub(super) fn normalize_type_name(type_name: &str) -> String { + type_name.chars().filter(|ch| !ch.is_whitespace()).collect() +} + +pub(super) fn parse_scalar_value(raw: &str) -> Result +where + T: std::str::FromStr + Serialize, + T::Err: std::fmt::Display, +{ + Ok(serde_json::to_value(raw.trim().parse::().map_err( + |err| anyhow::anyhow!("Invalid value '{}': {err}", raw.trim()), + )?)?) +} + +pub(super) fn parse_numeric_list_value(raw: &str) -> Result +where + T: std::str::FromStr + Serialize, + T::Err: std::fmt::Display, +{ + Ok(serde_json::to_value(util::parse_comma_list::(raw)?)?) +} + +pub(super) fn parse_bool_list_value(raw: &str) -> Result { + let values: Vec = raw + .split(',') + .map(|entry| parse_bool_token(entry.trim())) + .collect::>()?; + Ok(serde_json::to_value(values)?) +} + +pub(super) fn parse_bool_rows_value(raw: &str, field_name: &str) -> Result { + let flag = format!("--{}", field_name.replace('_', "-")); + let rows = parse_bool_rows(raw) + .map_err(|err| anyhow::anyhow!("{}", err.to_string().replace("--matrix", &flag)))?; + Ok(serde_json::to_value(rows)?) +} + +pub(super) fn parse_nested_numeric_list_value(raw: &str) -> Result +where + T: std::str::FromStr + Serialize, + T::Err: std::fmt::Display, +{ + let rows: Vec> = raw + .split(';') + .map(|row| util::parse_comma_list::(row.trim())) + .collect::>()?; + Ok(serde_json::to_value(rows)?) +} + +pub(super) fn parse_3d_numeric_list_value(raw: &str) -> Result +where + T: std::str::FromStr + Serialize, + T::Err: std::fmt::Display, +{ + let matrices: Vec>> = raw + .split('|') + .map(|matrix| { + matrix + .split(';') + .map(|row| util::parse_comma_list::(row.trim())) + .collect::>>() + }) + .collect::>()?; + Ok(serde_json::to_value(matrices)?) +} + +pub(super) fn parse_triple_array_list_value(raw: &str) -> Result { + let triples: Vec<[usize; 3]> = raw + .split(';') + .map(|entry| { + let values: Vec = util::parse_comma_list(entry.trim())?; + anyhow::ensure!( + values.len() == 3, + "Expected triple with exactly 3 entries, got {}", + values.len() + ); + Ok([values[0], values[1], values[2]]) + }) + .collect::>()?; + Ok(serde_json::to_value(triples)?) +} + +pub(super) fn parse_clauses_raw(raw: &str) -> Result> { + raw.split(';') + .map(|clause| { + let literals: Vec = clause + .trim() + .split(',') + .map(|value| value.trim().parse::()) + .collect::, _>>()?; + Ok(CNFClause::new(literals)) + }) + .collect() +} + +pub(super) fn parse_pair_list_value(raw: &str) -> Result { + let pairs: Vec<(usize, usize)> = raw + .split(',') + .map(|entry| { + let entry = entry.trim(); + let parts: Vec<&str> = if entry.contains('>') { + entry.split('>').collect() + } else { + entry.split('-').collect() + }; + anyhow::ensure!( + parts.len() == 2, + "Invalid pair '{entry}': expected u-v or u>v" + ); + Ok(( + parts[0].trim().parse::()?, + parts[1].trim().parse::()?, + )) + }) + .collect::>()?; + Ok(serde_json::to_value(pairs)?) +} + +pub(super) fn infer_cbq_num_variables(raw: &str) -> Result { + let mut num_vars = 0usize; + for conjunct in raw.split(';').filter(|entry| !entry.trim().is_empty()) { + let (_, args_str) = conjunct.trim().split_once(':').ok_or_else(|| { + anyhow::anyhow!( + "Invalid conjunct format: expected 'rel_idx:args', got '{}'", + conjunct.trim() + ) + })?; + for arg in args_str + .split(',') + .map(str::trim) + .filter(|arg| !arg.is_empty()) + { + if let Some(rest) = arg.strip_prefix('v') { + let index: usize = rest + .parse() + .map_err(|err| anyhow::anyhow!("Invalid variable index '{rest}': {err}"))?; + num_vars = num_vars.max(index + 1); + } + } + } + Ok(num_vars) +} + +pub(super) fn parse_cbq_relations(raw: &str, context: &CreateContext) -> Result> { + let domain_size = context.usize_field("domain_size").ok_or_else(|| { + anyhow::anyhow!("CBQ relation parsing requires a prior domain_size field") + })?; + + raw.split(';') + .filter(|entry| !entry.trim().is_empty()) + .map(|rel_str| { + let rel_str = rel_str.trim(); + let (arity_str, tuples_str) = rel_str.split_once(':').ok_or_else(|| { + anyhow::anyhow!("Invalid relation format: expected 'arity:tuples', got '{rel_str}'") + })?; + let arity: usize = arity_str + .trim() + .parse() + .map_err(|e| anyhow::anyhow!("Invalid arity '{arity_str}': {e}"))?; + let tuples: Vec> = if tuples_str.trim().is_empty() { + Vec::new() + } else { + tuples_str + .split('|') + .filter(|tuple| !tuple.trim().is_empty()) + .map(|tuple| { + let tuple: Vec = util::parse_comma_list(tuple.trim())?; + anyhow::ensure!( + tuple.len() == arity, + "Relation tuple has {} entries, expected arity {arity}", + tuple.len() + ); + for &value in &tuple { + anyhow::ensure!( + value < domain_size, + "Tuple value {value} >= domain-size {domain_size}" + ); + } + Ok(tuple) + }) + .collect::>()? + }; + Ok(CbqRelation { arity, tuples }) + }) + .collect() +} + +pub(super) fn parse_cbq_conjuncts( + raw: &str, + context: &CreateContext, +) -> Result)>> { + let relations: Vec = + serde_json::from_value(context.parsed_fields.get("relations").cloned().ok_or_else( + || anyhow::anyhow!("CBQ conjunct parsing requires prior relations field"), + )?) + .context("Failed to deserialize parsed CBQ relations")?; + let domain_size = context + .usize_field("domain_size") + .ok_or_else(|| anyhow::anyhow!("CBQ conjunct parsing requires prior domain_size field"))?; + let num_variables = context.usize_field("num_variables").unwrap_or(0); + + raw.split(';') + .filter(|entry| !entry.trim().is_empty()) + .map(|conj_str| { + let conj_str = conj_str.trim(); + let (idx_str, args_str) = conj_str.split_once(':').ok_or_else(|| { + anyhow::anyhow!( + "Invalid conjunct format: expected 'rel_idx:args', got '{conj_str}'" + ) + })?; + let rel_idx: usize = idx_str + .trim() + .parse() + .map_err(|e| anyhow::anyhow!("Invalid relation index '{idx_str}': {e}"))?; + anyhow::ensure!( + rel_idx < relations.len(), + "Conjunct references relation {rel_idx}, but only {} relations exist", + relations.len() + ); + + let query_args: Vec = args_str + .split(',') + .map(|arg| { + let arg = arg.trim(); + if let Some(rest) = arg.strip_prefix('v') { + let variable: usize = rest + .parse() + .map_err(|e| anyhow::anyhow!("Invalid variable index '{rest}': {e}"))?; + anyhow::ensure!( + variable < num_variables, + "Variable({variable}) >= num_variables ({num_variables})" + ); + Ok(QueryArg::Variable(variable)) + } else if let Some(rest) = arg.strip_prefix('c') { + let constant: usize = rest + .parse() + .map_err(|e| anyhow::anyhow!("Invalid constant value '{rest}': {e}"))?; + anyhow::ensure!( + constant < domain_size, + "Constant {constant} >= domain-size {domain_size}" + ); + Ok(QueryArg::Constant(constant)) + } else { + Err(anyhow::anyhow!( + "Invalid query arg '{arg}': expected vN (variable) or cN (constant)" + )) + } + }) + .collect::>()?; + anyhow::ensure!( + query_args.len() == relations[rel_idx].arity, + "Conjunct has {} args, but relation {rel_idx} has arity {}", + query_args.len(), + relations[rel_idx].arity + ); + Ok((rel_idx, query_args)) + }) + .collect() +} + +pub(super) fn parse_semicolon_tuple_list_value( + raw: &str, +) -> Result +where + T: std::str::FromStr + Serialize, + T::Err: std::fmt::Display, +{ + let tuples: Vec> = raw + .split(';') + .filter(|entry| !entry.trim().is_empty()) + .map(|entry| { + let values: Vec = util::parse_comma_list(entry.trim())?; + anyhow::ensure!( + values.len() == N, + "Expected tuple with {N} entries, got {}", + values.len() + ); + Ok(values) + }) + .collect::>()?; + Ok(serde_json::to_value(tuples)?) +} + +pub(super) fn parse_weighted_edge_list_value(raw: &str) -> Result +where + T: std::str::FromStr + Serialize, + T::Err: std::fmt::Display, +{ + let edges: Vec<(usize, usize, T)> = raw + .split(',') + .filter(|entry| !entry.trim().is_empty()) + .map(|entry| { + let entry = entry.trim(); + let (edge_part, weight_part) = entry.split_once(':').ok_or_else(|| { + anyhow::anyhow!("Invalid weighted edge '{entry}': expected u-v:w") + })?; + let (u_str, v_str) = if let Some((u, v)) = edge_part.split_once('-') { + (u, v) + } else if let Some((u, v)) = edge_part.split_once('>') { + (u, v) + } else { + bail!("Invalid weighted edge '{entry}': expected u-v:w or u>v:w"); + }; + Ok(( + u_str.trim().parse::()?, + v_str.trim().parse::()?, + weight_part.trim().parse::().map_err(|err| { + anyhow::anyhow!("Invalid edge weight '{}': {err}", weight_part.trim()) + })?, + )) + }) + .collect::>()?; + Ok(serde_json::to_value(edges)?) +} + +pub(super) fn parse_indexed_numeric_pairs_value(raw: &str) -> Result +where + T: std::str::FromStr + Serialize, + T::Err: std::fmt::Display, +{ + let pairs: Vec<(usize, T)> = + raw.split(',') + .filter(|entry| !entry.trim().is_empty()) + .map(|entry| { + let entry = entry.trim(); + let (index, value) = entry.split_once(':').ok_or_else(|| { + anyhow::anyhow!("Invalid pair '{entry}': expected index:value") + })?; + Ok(( + index.trim().parse::()?, + value.trim().parse::().map_err(|err| { + anyhow::anyhow!("Invalid value '{}': {err}", value.trim()) + })?, + )) + }) + .collect::>()?; + Ok(serde_json::to_value(pairs)?) +} + +pub(super) fn parse_indexed_usize_lists_value(raw: &str) -> Result { + let entries: Vec<(usize, Vec)> = raw + .split(';') + .filter(|entry| !entry.trim().is_empty()) + .map(|entry| { + let entry = entry.trim(); + let (index, values) = entry + .split_once(':') + .ok_or_else(|| anyhow::anyhow!("Invalid entry '{entry}': expected index:values"))?; + Ok(( + index.trim().parse::()?, + if values.trim().is_empty() { + Vec::new() + } else { + util::parse_comma_list(values.trim())? + }, + )) + }) + .collect::>()?; + Ok(serde_json::to_value(entries)?) +} + +pub(super) fn parse_string_list_value(raw: &str) -> Result { + let values: Vec = raw + .split(';') + .filter(|entry| !entry.trim().is_empty()) + .map(|entry| entry.trim().to_string()) + .collect(); + Ok(serde_json::to_value(values)?) +} + +pub(super) fn parse_symbol_list_allow_empty(raw: &str) -> Result> { + let raw = raw.trim(); + if raw.is_empty() { + return Ok(Vec::new()); + } + raw.split(',') + .map(|value| { + value + .trim() + .parse::() + .context("invalid symbol index") + }) + .collect() +} + +pub(super) fn parse_lcs_strings(raw: &str) -> Result<(Vec>, usize)> { + let segments: Vec<&str> = raw.split(';').map(str::trim).collect(); + let comma_mode = segments.iter().any(|segment| segment.contains(',')); + + if comma_mode { + let strings = segments + .iter() + .map(|segment| parse_symbol_list_allow_empty(segment)) + .collect::>>()?; + let inferred_alphabet_size = strings + .iter() + .flat_map(|string| string.iter()) + .copied() + .max() + .map(|value| value + 1) + .unwrap_or(0); + return Ok((strings, inferred_alphabet_size)); + } + + let mut encoding = BTreeMap::new(); + let mut next_symbol = 0usize; + let strings = segments + .iter() + .map(|segment| { + segment + .as_bytes() + .iter() + .map(|byte| { + let entry = encoding.entry(*byte).or_insert_with(|| { + let current = next_symbol; + next_symbol += 1; + current + }); + *entry + }) + .collect::>() + }) + .collect::>(); + Ok((strings, next_symbol)) +} + +pub(super) fn parse_bcnf_functional_deps( + raw: &str, + num_attributes: usize, +) -> Result, Vec)>> { + raw.split(';') + .map(|fd_str| { + let parts: Vec<&str> = fd_str.split(':').collect(); + anyhow::ensure!( + parts.len() == 2, + "Each FD must be lhs:rhs, got '{}'", + fd_str + ); + let lhs: Vec = util::parse_comma_list(parts[0])?; + let rhs: Vec = util::parse_comma_list(parts[1])?; + ensure_attribute_indices_in_range( + &lhs, + num_attributes, + &format!("Functional dependency '{fd_str}' lhs"), + )?; + ensure_attribute_indices_in_range( + &rhs, + num_attributes, + &format!("Functional dependency '{fd_str}' rhs"), + )?; + Ok((lhs, rhs)) + }) + .collect() +} + +pub(super) fn parse_cdft_frequency_tables_value( + raw: &str, + context: &CreateContext, +) -> Result> { + let attribute_domains: Vec = serde_json::from_value( + context + .parsed_fields + .get("attribute_domains") + .cloned() + .ok_or_else(|| { + anyhow::anyhow!( + "CDFT frequency table parsing requires prior attribute_domains field" + ) + })?, + ) + .context("Failed to deserialize parsed CDFT attribute domains")?; + let num_objects = context.usize_field("num_objects").ok_or_else(|| { + anyhow::anyhow!("CDFT frequency table parsing requires prior num_objects field") + })?; + parse_cdft_frequency_tables(raw, &attribute_domains, num_objects) +} + +pub(super) fn parse_cdft_known_values_value( + raw: &str, + context: &CreateContext, +) -> Result> { + let attribute_domains: Vec = serde_json::from_value( + context + .parsed_fields + .get("attribute_domains") + .cloned() + .ok_or_else(|| { + anyhow::anyhow!("CDFT known-value parsing requires prior attribute_domains field") + })?, + ) + .context("Failed to deserialize parsed CDFT attribute domains")?; + let num_objects = context.usize_field("num_objects").ok_or_else(|| { + anyhow::anyhow!("CDFT known-value parsing requires prior num_objects field") + })?; + parse_cdft_known_values(Some(raw), num_objects, &attribute_domains) +} + +pub(super) fn parse_cvp_bounds_value( + raw: Option<&str>, + context: &CreateContext, +) -> Result { + let basis_len = context + .parsed_fields + .get("basis") + .and_then(serde_json::Value::as_array) + .map(Vec::len) + .ok_or_else(|| anyhow::anyhow!("CVP bounds parsing requires a prior basis field"))?; + + let (lower, upper) = match raw { + Some(raw) => { + let parts: Vec = util::parse_comma_list(raw)?; + anyhow::ensure!( + parts.len() == 2, + "--bounds expects \"lower,upper\" (e.g., \"-10,10\")" + ); + (parts[0], parts[1]) + } + None => (-10, 10), + }; + let bounds = + vec![problemreductions::models::algebraic::VarBounds::bounded(lower, upper); basis_len]; + Ok(serde_json::to_value(bounds)?) +} + +pub(super) fn parse_biguint_list_value(raw: &str) -> Result { + let values: Vec = util::parse_biguint_list(raw)? + .into_iter() + .map(|value| value.to_string()) + .collect(); + Ok(serde_json::to_value(values)?) +} + +pub(super) fn parse_biguint_value(raw: &str) -> Result { + let value: BigUint = util::parse_decimal_biguint(raw)?; + Ok(serde_json::Value::String(value.to_string())) +} + +pub(super) fn parse_optional_bool_list_value(raw: &str) -> Result { + let values: Vec> = raw + .split(',') + .map(|entry| { + let entry = entry.trim(); + match entry { + "?" => Ok(None), + _ => Ok(Some(parse_bool_token(entry)?)), + } + }) + .collect::>()?; + Ok(serde_json::to_value(values)?) +} + +pub(super) fn parse_quantifiers_raw(raw: &str, context: &CreateContext) -> Result> { + let quantifiers: Vec = raw + .split(',') + .map(|entry| match entry.trim().to_lowercase().as_str() { + "e" | "exists" => Ok(Quantifier::Exists), + "a" | "forall" => Ok(Quantifier::ForAll), + other => Err(anyhow::anyhow!( + "Invalid quantifier '{}': expected E/Exists or A/ForAll", + other + )), + }) + .collect::>()?; + + if let Some(num_vars) = context.usize_field("num_vars") { + anyhow::ensure!( + quantifiers.len() == num_vars, + "Expected {num_vars} quantifiers but got {}", + quantifiers.len() + ); + } + + Ok(quantifiers) +} + +pub(super) fn parse_json_passthrough_value(raw: &str) -> Result { + serde_json::from_str(raw).context("Invalid JSON input") +} + +pub(super) fn parse_bool_token(raw: &str) -> Result { + match raw.trim() { + "1" | "true" | "TRUE" | "True" => Ok(true), + "0" | "false" | "FALSE" | "False" => Ok(false), + other => bail!("Invalid boolean entry '{other}': expected 0/1 or true/false"), + } +} + +pub(super) fn parse_simple_graph_value( + raw: &str, + context: &CreateContext, +) -> Result { + let raw = raw.trim(); + let num_vertices = context.usize_field("num_vertices").or(context.num_vertices); + let graph = if raw.is_empty() { + let num_vertices = num_vertices.ok_or_else(|| { + anyhow::anyhow!( + "Empty graph string. To create a graph with isolated vertices, provide num_vertices first." + ) + })?; + SimpleGraph::empty(num_vertices) + } else { + let edges = util::parse_edge_pairs(raw)?; + let inferred_num_vertices = edges + .iter() + .flat_map(|&(u, v)| [u, v]) + .max() + .map(|max_vertex| max_vertex + 1) + .unwrap_or(0); + let num_vertices = match num_vertices { + Some(explicit) => { + anyhow::ensure!( + explicit >= inferred_num_vertices, + "num_vertices ({explicit}) is too small for the graph: need at least {inferred_num_vertices}" + ); + explicit + } + None => inferred_num_vertices, + }; + SimpleGraph::new(num_vertices, edges) + }; + Ok(serde_json::to_value(graph)?) +} + +pub(super) fn parse_directed_graph_value( + raw: &str, + context: &CreateContext, +) -> Result { + let (graph, _) = parse_directed_graph( + raw, + context.usize_field("num_vertices").or(context.num_vertices), + )?; + Ok(serde_json::to_value(graph)?) +} + +pub(super) fn parse_grid_subgraph_value(raw: &str, kings: bool) -> Result { + let positions = util::parse_positions::(raw, "0,0")?; + if kings { + Ok(serde_json::to_value(KingsSubgraph::new(positions))?) + } else { + Ok(serde_json::to_value(TriangularSubgraph::new(positions))?) + } +} + +pub(super) fn parse_unit_disk_graph_value( + raw: &str, + context: &CreateContext, +) -> Result { + let positions = util::parse_positions::(raw, "0.0,0.0")?; + let radius = context + .f64_field("radius") + .ok_or_else(|| anyhow::anyhow!("UnitDiskGraph parsing requires a prior radius field"))?; + Ok(serde_json::to_value(UnitDiskGraph::new(positions, radius))?) +} + +pub(super) fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { + match type_name { + "SimpleGraph" => "edge list: 0-1,1-2,2-3", + "G" => match graph_type { + Some("KingsSubgraph" | "TriangularSubgraph") => "integer positions: \"0,0;1,0;1,1\"", + Some("UnitDiskGraph") => "float positions: \"0.0,0.0;1.0,0.0\"", + _ => "edge list: 0-1,1-2,2-3", + }, + "Vec<(Vec, Vec)>" => "semicolon-separated dependencies: \"0,1>2;0,2>3\"", + "Vec" => "comma-separated integers: 4,5,3,2,6", + "Vec" => "comma-separated: 1,2,3", + "W" | "N" | "W::Sum" | "N::Sum" => "numeric value: 10", + "Vec" => "comma-separated indices: 0,2,4", + "Vec<(usize, usize, W)>" | "Vec<(usize,usize,W)>" => { + "comma-separated weighted edges: 0-2:3,1-3:5" + } + "Vec>" => "semicolon-separated sets: \"0,1;1,2;0,2\"", + "Vec" => "semicolon-separated clauses: \"1,2;-1,3\"", + "Vec>" => "JSON 2D bool array: '[[true,false],[false,true]]'", + "Vec>" => "semicolon-separated rows: \"1,0.5;0.5,2\"", + "usize" => "integer", + "u64" => "integer", + "i64" => "integer", + "BigUint" => "nonnegative decimal integer", + "Vec" => "comma-separated nonnegative decimal integers: 3,7,1,8", + "Vec" => "comma-separated integers: 3,7,1,8", + "DirectedGraph" => "directed arcs: 0>1,1>2,2>0", + _ => "value", + } +} + +pub(super) fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { + match canonical { + "MaximumIndependentSet" + | "MinimumVertexCover" + | "MaximumClique" + | "MinimumDominatingSet" => match graph_type { + Some("KingsSubgraph") => "--positions \"0,0;1,0;1,1;0,1\"", + Some("TriangularSubgraph") => "--positions \"0,0;0,1;1,0;1,1\"", + Some("UnitDiskGraph") => "--positions \"0,0;1,0;0.5,0.8\" --radius 1.5", + _ => "--graph 0-1,1-2,2-3 --weights 1,1,1,1", + }, + "KClique" => "--graph 0-1,0-2,1-3,2-3,2-4,3-4 --k 3", + "VertexCover" => "--graph 0-1,1-2,0-2,2-3 --k 2", + "GeneralizedHex" => "--graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5", + "IntegralFlowBundles" => { + "--arcs \"0>1,0>2,1>3,2>3,1>2,2>1\" --bundles \"0,1;2,5;3,4\" --bundle-capacities 1,1,1 --source 0 --sink 3 --requirement 1 --num-vertices 4" + } + "IntegralFlowWithMultipliers" => { + "--arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2" + } + "MinimumCutIntoBoundedSets" => { + "--graph 0-1,1-2,2-3 --edge-weights 1,1,1 --source 0 --sink 3 --size-bound 3" + } + "BoundedComponentSpanningForest" => { + "--graph 0-1,1-2,2-3,3-4,4-5,5-6,6-7,0-7,1-5,2-6 --weights 2,3,1,2,3,1,2,1 --k 3 --max-weight 6" + } + "HamiltonianPath" => "--graph 0-1,1-2,2-3", + "HamiltonianPathBetweenTwoVertices" => { + "--graph 0-1,0-3,1-2,1-4,2-5,3-4,4-5,2-3 --source-vertex 0 --target-vertex 5" + } + "GraphPartitioning" => "--graph 0-1,1-2,2-3,3-0 --num-partitions 2", + "LongestPath" => { + "--graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,4-6,5-6,1-6 --edge-lengths 3,2,4,1,5,2,3,2,4,1 --source-vertex 0 --target-vertex 6" + } + "UndirectedFlowLowerBounds" => { + "--graph 0-1,0-2,1-3,2-3,1-4,3-5,4-5 --capacities 2,2,2,2,1,3,2 --lower-bounds 1,1,0,0,1,0,1 --source 0 --sink 5 --requirement 3" + } + "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" + }, + "DisjointConnectingPaths" => { + "--graph 0-1,1-3,0-2,1-4,2-4,3-5,4-5 --terminal-pairs 0-3,2-5" + } + "IntegralFlowHomologousArcs" => { + "--arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\"" + } + "LengthBoundedDisjointPaths" => { + "--graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --max-length 4" + } + "PathConstrainedNetworkFlow" => { + "--arcs \"0>1,0>2,1>3,1>4,2>4,3>5,4>5,4>6,5>7,6>7\" --capacities 2,1,1,1,1,1,1,1,2,1 --source 0 --sink 7 --paths \"0,2,5,8;0,3,6,8;0,3,7,9;1,4,6,8;1,4,7,9\" --requirement 3" + } + "IsomorphicSpanningTree" => "--graph 0-1,1-2,0-2 --tree 0-1,1-2", + "BoundedDiameterSpanningTree" => { + "--graph 0-1,0-2,0-3,1-2,1-4,2-3,3-4 --edge-weights 1,2,1,1,2,1,1 --weight-bound 5 --diameter-bound 3" + } + "KthBestSpanningTree" => "--graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3", + "LongestCircuit" => { + "--graph 0-1,1-2,2-3,3-4,4-5,5-0,0-3,1-4,2-5,3-5 --edge-weights 3,2,4,1,5,2,3,2,1,2" + } + "BottleneckTravelingSalesman" | "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { + "--graph 0-1,1-2,2-3 --edge-weights 1,1,1" + } + "ShortestWeightConstrainedPath" => { + "--graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,1-4 --edge-lengths 2,4,3,1,5,4,2,6 --edge-weights 5,1,2,3,2,3,1,1 --source-vertex 0 --target-vertex 5 --weight-bound 8" + } + "SteinerTreeInGraphs" => "--graph 0-1,1-2,2-3 --edge-weights 1,1,1 --terminals 0,3", + "BiconnectivityAugmentation" => { + "--graph 0-1,1-2,2-3 --potential-weights 0-2:3,0-3:4,1-3:2 --budget 5" + } + "PartialFeedbackEdgeSet" => { + "--graph 0-1,1-2,2-0,2-3,3-4,4-2,3-5,5-4,0-3 --budget 3 --max-cycle-length 4" + } + "Satisfiability" => "--num-vars 3 --clauses \"1,2;-1,3\"", + "NAESatisfiability" => "--num-vars 3 --clauses \"1,2,-3;-1,2,3\"", + "QuantifiedBooleanFormulas" => { + "--num-vars 3 --clauses \"1,2;-1,3\" --quantifiers \"E,A,E\"" + } + "KSatisfiability" => "--num-vars 3 --clauses \"1,2,3;-1,2,-3\" --k 3", + "Maximum2Satisfiability" => "--num-vars 4 --clauses \"1,2;1,-2;-1,3;-1,-3;2,4;-3,-4;3,4\"", + "NonTautology" => { + "--num-vars 3 --disjuncts \"1,2,3;-1,-2,-3\"" + } + "OneInThreeSatisfiability" => { + "--num-vars 4 --clauses \"1,2,3;-1,3,4;2,-3,-4\"" + } + "Planar3Satisfiability" => { + "--num-vars 4 --clauses \"1,2,3;-1,2,4;1,-3,4;-2,3,-4\"" + } + "QUBO" => "--matrix \"1,0.5;0.5,2\"", + "QuadraticAssignment" => "--matrix \"0,5;5,0\" --distance-matrix \"0,1;1,0\"", + "SpinGlass" => "--graph 0-1,1-2 --couplings 1,1", + "KColoring" => "--graph 0-1,1-2,2-0 --k 3", + "HamiltonianCircuit" => "--graph 0-1,1-2,2-3,3-0", + "MaximumLeafSpanningTree" => "--graph 0-1,0-2,0-3,1-4,2-4,2-5,3-5,4-5,1-3", + "EnsembleComputation" => "--universe-size 4 --subsets \"0,1,2;0,1,3\"", + "RootedTreeStorageAssignment" => { + "--universe-size 5 --subsets \"0,2;1,3;0,4;2,4\" --bound 1" + } + "MinMaxMulticenter" => { + "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2" + } + "MinimumSumMulticenter" => { + "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2" + } + "BalancedCompleteBipartiteSubgraph" => { + "--left 4 --right 4 --biedges 0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3 --k 3" + } + "MaximumAchromaticNumber" => "--graph 0-1,1-2,2-3,3-4,4-5,5-0", + "MaximumDomaticNumber" => "--graph 0-1,1-2,0-2", + "MinimumCoveringByCliques" => "--graph 0-1,1-2,0-2,2-3", + "MinimumIntersectionGraphBasis" => "--graph 0-1,1-2", + "MinimumMaximalMatching" => "--graph 0-1,1-2,2-3,3-4,4-5", + "DegreeConstrainedSpanningTree" => "--graph 0-1,0-2,0-3,1-2,1-4,2-3,3-4 --k 2", + "MonochromaticTriangle" => "--graph 0-1,0-2,0-3,1-2,1-3,2-3", + "PartitionIntoTriangles" => "--graph 0-1,1-2,0-2", + "PartitionIntoCliques" => "--graph 0-1,0-2,1-2,3-4,3-5,4-5 --k 3", + "PartitionIntoForests" => "--graph 0-1,1-2,2-0,3-4,4-5,5-3 --k 2", + "PartitionIntoPerfectMatchings" => "--graph 0-1,2-3,0-2,1-3 --k 2", + "Factoring" => "--target 15 --m 4 --n 4", + "CapacityAssignment" => { + "--capacities 1,2,3 --cost-matrix \"1,3,6;2,4,7;1,2,5\" --delay-matrix \"8,4,1;7,3,1;6,3,1\" --delay-budget 12" + } + "ProductionPlanning" => { + "--num-periods 6 --demands 5,3,7,2,8,5 --capacities 12,12,12,12,12,12 --setup-costs 10,10,10,10,10,10 --production-costs 1,1,1,1,1,1 --inventory-costs 1,1,1,1,1,1 --cost-bound 80" + } + "MultiprocessorScheduling" => "--lengths 4,5,3,2,6 --num-processors 2 --deadline 10", + "PreemptiveScheduling" => { + "--lengths 2,1,3,2,1 --num-processors 2 --precedences \"0>2,1>3\"" + } + "SchedulingToMinimizeWeightedCompletionTime" => { + "--lengths 1,2,3,4,5 --weights 6,4,3,2,1 --num-processors 2" + } + "JobShopScheduling" => { + "--jobs \"0:3,1:4;1:2,0:3,1:2;0:4,1:3;1:5,0:2;0:2,1:3,0:1\" --num-processors 2" + } + "MinimumMultiwayCut" => "--graph 0-1,1-2,2-3 --terminals 0,2 --edge-weights 1,1,1", + "ExpectedRetrievalCost" => EXPECTED_RETRIEVAL_COST_EXAMPLE_ARGS, + "SequencingWithinIntervals" => "--release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1", + "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" + } + "TimetableDesign" => { + "--num-periods 3 --num-craftsmen 5 --num-tasks 5 --craftsman-avail \"1,1,1;1,1,0;0,1,1;1,0,1;1,1,1\" --task-avail \"1,1,0;0,1,1;1,0,1;1,1,1;1,1,1\" --requirements \"1,0,1,0,0;0,1,0,0,1;0,0,0,1,0;0,0,0,0,1;0,1,0,0,0\"" + } + "SteinerTree" => "--graph 0-1,1-2,1-3,3-4 --edge-weights 2,2,1,1 --terminals 0,2,4", + "MultipleCopyFileAllocation" => { + MULTIPLE_COPY_FILE_ALLOCATION_EXAMPLE_ARGS + } + "AcyclicPartition" => { + "--arcs \"0>1,0>2,1>3,1>4,2>4,2>5,3>5,4>5\" --weights 2,3,2,1,3,1 --arc-weights 1,1,1,1,1,1,1,1 --weight-bound 5 --cost-bound 5" + } + "OptimalLinearArrangement" => "--graph 0-1,1-2,2-3", + "RootedTreeArrangement" => "--graph 0-1,0-2,1-2,2-3,3-4 --bound 7", + "DirectedTwoCommodityIntegralFlow" => { + "--arcs \"0>2,0>3,1>2,1>3,2>4,2>5,3>4,3>5\" --capacities 1,1,1,1,1,1,1,1 --source-1 0 --sink-1 4 --source-2 1 --sink-2 5 --requirement-1 1 --requirement-2 1" + } + "MinimumEdgeCostFlow" => { + "--arcs \"0>1,0>2,0>3,1>4,2>4,3>4\" --edge-weights 3,1,2,0,0,0 --capacities 2,2,2,2,2,2 --source 0 --sink 4 --requirement 3" + } + "MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"", + "DirectedHamiltonianPath" => { + "--arcs \"0>1,0>3,1>3,1>4,2>0,2>4,3>2,3>5,4>5,5>1\" --num-vertices 6" + } + "Kernel" => "--arcs \"0>1,0>2,1>3,2>3,3>4,4>0,4>1\"", + "MinimumGeometricConnectedDominatingSet" => { + "--positions \"0,0;3,0;6,0;9,0;0,3;3,3;6,3;9,3\" --radius 3.5" + } + "MinimumDummyActivitiesPert" => "--arcs \"0>2,0>3,1>3,1>4,2>5\" --num-vertices 6", + "FeasibleRegisterAssignment" => { + "--arcs \"0>1,0>2,1>3\" --assignment 0,1,0,0 --k 2 --num-vertices 4" + } + "MinimumFaultDetectionTestSet" => { + "--arcs \"0>2,0>3,1>3,1>4,2>5,3>5,3>6,4>6\" --inputs 0,1 --outputs 5,6 --num-vertices 7" + } + "MinimumWeightAndOrGraph" => { + "--arcs \"0>1,0>2,1>3,1>4,2>5,2>6\" --source 0 --gate-types \"AND,OR,OR,L,L,L,L\" --weights 1,2,3,1,4,2 --num-vertices 7" + } + "MinimumRegisterSufficiencyForLoops" => { + "--loop-length 6 --loop-variables \"0,3;2,3;4,3\"" + } + "RegisterSufficiency" => { + "--arcs \"2>0,2>1,3>1,4>2,4>3,5>0,6>4,6>5\" --bound 3 --num-vertices 7" + } + "StrongConnectivityAugmentation" => { + "--arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1" + } + "MixedChinesePostman" => { + "--graph 0-2,1-3,0-4,4-2 --arcs \"0>1,1>2,2>3,3>0\" --edge-weights 2,3,1,2 --arc-weights 2,3,1,4" + } + "RuralPostman" => { + "--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2" + } + "StackerCrane" => { + "--arcs \"0>4,2>5,5>1,3>0,4>3\" --graph \"0-1,1-2,2-3,3-5,4-5,0-3,1-5\" --arc-lengths 3,4,2,5,3 --edge-lengths 2,1,3,2,1,4,3 --num-vertices 6" + } + "MultipleChoiceBranching" => { + "--arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --threshold 10" + } + "AdditionalKey" => "--num-attributes 6 --dependencies \"0,1:2,3;2,3:4,5;4,5:0,1\" --relation-attrs 0,1,2,3,4,5 --known-keys \"0,1;2,3;4,5\"", + "ConsistencyOfDatabaseFrequencyTables" => { + "--num-objects 6 --attribute-domains \"2,3,2\" --frequency-tables \"0,1:1,1,1|1,1,1;1,2:1,1|0,2|1,1\" --known-values \"0,0,0;3,0,1;1,2,1\"" + } + "SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1", + "RectilinearPictureCompression" => { + "--matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --bound 2" + } + "SequencingToMinimizeWeightedTardiness" => { + "--lengths 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" + } + "IntegerKnapsack" => "--sizes 3,4,5,2,7 --values 4,5,7,3,9 --capacity 15", + "SubsetProduct" => "--sizes 2,3,5,7,6,10 --target 210", + "SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11", + "MinimumAxiomSet" => { + "--n 8 --true-sentences 0,1,2,3,4,5,6,7 --implications \"0>2;0>3;1>4;1>5;2,4>6;3,5>7;6,7>0;6,7>1\"" + } + "IntegerExpressionMembership" => { + "--expression '{\"Sum\":[{\"Sum\":[{\"Union\":[{\"Atom\":1},{\"Atom\":4}]},{\"Union\":[{\"Atom\":3},{\"Atom\":6}]}]},{\"Union\":[{\"Atom\":2},{\"Atom\":5}]}]}' --target 12" + } + "NonLivenessFreePetriNet" => { + "--n 4 --m 3 --arcs \"0>0,1>1,2>2\" --output-arcs \"0>1,1>2,2>3\" --initial-marking 1,0,0,0" + } + "Betweenness" => "--n 5 --sets \"0,1,2;2,3,4;0,2,4;1,3,4\"", + "CyclicOrdering" => "--n 5 --sets \"0,1,2;2,3,0;1,3,4\"", + "Numerical3DimensionalMatching" => "--w-sizes 4,5 --x-sizes 4,5 --y-sizes 5,7 --bound 15", + "ThreePartition" => "--sizes 4,5,6,4,6,5 --bound 15", + "DynamicStorageAllocation" => "--release-times 0,0,1,2,3 --deadlines 3,2,4,5,5 --sizes 2,3,1,3,2 --capacity 6", + "KthLargestMTuple" => "--sets \"2,5,8;3,6;1,4,7\" --k 14 --bound 12", + "AlgebraicEquationsOverGF2" => "--num-vars 3 --equations \"0,1:2;1,2:0:;0:1:2:\"", + "QuadraticCongruences" => "--coeff-a 4 --coeff-b 15 --coeff-c 10", + "QuadraticDiophantineEquations" => "--coeff-a 3 --coeff-b 5 --coeff-c 53", + "SimultaneousIncongruences" => "--pairs \"2,2;1,3;2,5;3,7\"", + "BoyceCoddNormalFormViolation" => { + "--n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" + } + "Clustering" => { + "--distance-matrix \"0,1,1,3;1,0,1,3;1,1,0,3;3,3,3,0\" --k 2 --diameter-bound 1" + } + "SumOfSquaresPartition" => "--sizes 5,3,8,2,7,1 --num-groups 3", + "ComparativeContainment" => { + "--universe-size 4 --r-sets \"0,1,2,3;0,1\" --s-sets \"0,1,2,3;2,3\" --r-weights 2,5 --s-weights 3,6" + } + "SetBasis" => "--universe-size 4 --subsets \"0,1;1,2;0,2;0,1,2\" --k 3", + "SetSplitting" => "--universe-size 6 --subsets \"0,1,2;2,3,4;0,4,5;1,3,5\"", + "LongestCommonSubsequence" => { + "--strings \"010110;100101;001011\" --alphabet-size 2" + } + "GroupingBySwapping" => "--string \"0,1,2,0,1,2\" --bound 5", + "MinimumExternalMacroDataCompression" | "MinimumInternalMacroDataCompression" => { + "--string \"0,1,0,1\" --pointer-cost 2 --alphabet-size 2" + } + "MinimumCardinalityKey" => { + "--num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\"" + } + "PrimeAttributeName" => { + "--universe 6 --dependencies \"0,1>2,3,4,5;2,3>0,1,4,5\" --query-attribute 3" + } + "TwoDimensionalConsecutiveSets" => { + "--alphabet-size 6 --subsets \"0,1,2;3,4,5;1,3;2,4;0,5\"" + } + "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\"", + "ConsecutiveBlockMinimization" => "--matrix '[[true,false,true],[false,true,true]]' --bound-k 2", + "ConsecutiveOnesMatrixAugmentation" => { + "--matrix \"1,0,0,1,1;1,1,0,0,0;0,1,1,0,1;0,0,1,1,0\" --bound 2" + } + "SparseMatrixCompression" => "--matrix \"1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0\" --bound-k 2", + "MaximumLikelihoodRanking" => "--matrix \"0,4,3,5;1,0,4,3;2,1,0,4;0,2,1,0\"", + "MinimumMatrixCover" => "--matrix \"0,3,1,0;3,0,0,2;1,0,0,4;0,2,4,0\"", + "MinimumMatrixDomination" => "--matrix \"0,1,0;1,0,1;0,1,0\"", + "MinimumWeightDecoding" => { + "--matrix '[[true,false,true,true],[false,true,true,false],[true,true,false,true]]' --rhs 'true,true,false'" + } + "MinimumWeightSolutionToLinearEquations" => { + "--matrix '[[1,2,3,1],[2,1,1,3]]' --rhs '5,4'" + } + "ConjunctiveBooleanQuery" => { + "--domain-size 6 --relations \"2:0,3|1,3|2,4;3:0,1,5|1,2,5\" --conjuncts-spec \"0:v0,c3;0:v1,c3;1:v0,v1,c5\"" + } + "ConjunctiveQueryFoldability" => "(use --example ConjunctiveQueryFoldability)", + "EquilibriumPoint" => "(use --example EquilibriumPoint)", + "SequencingToMinimizeMaximumCumulativeCost" => { + "--costs 2,-1,3,-2,1,-3 --precedence-pairs \"0>2,1>2,1>3,2>4,3>5,4>5\"" + } + "StringToStringCorrection" => { + "--source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2" + } + "FeasibleBasisExtension" => { + "--matrix '[[1,0,1,2,-1,0],[0,1,0,1,1,2],[0,0,1,1,0,1]]' --rhs '7,5,3' --required-columns '0,1'" + } + "MinimumCodeGenerationParallelAssignments" => { + "--num-variables 4 --assignments \"0:1,2;1:0;2:3;3:1,2\"" + } + "MinimumDecisionTree" => { + "--test-matrix '[[true,true,false,false],[true,false,false,false],[false,true,false,true]]' --num-objects 4 --num-tests 3" + } + "MinimumDisjunctiveNormalForm" => { + "--num-vars 3 --truth-table 0,1,1,1,1,1,1,0" + } + "SquareTiling" => { + "--num-colors 3 --tiles \"0,1,2,0;0,0,2,1;2,1,0,0;2,0,0,1\" --grid-size 2" + } + _ => "", + } +} + +pub(super) fn uses_edge_weights_flag(canonical: &str) -> bool { + matches!( + canonical, + "BottleneckTravelingSalesman" + | "BoundedDiameterSpanningTree" + | "KthBestSpanningTree" + | "LongestCircuit" + | "MaxCut" + | "MaximumMatching" + | "MixedChinesePostman" + | "RuralPostman" + | "TravelingSalesman" + ) +} + +pub(super) fn uses_edge_weights_flag_for_edge_lengths(canonical: &str) -> bool { + matches!( + canonical, + "LongestCircuit" | "MinMaxMulticenter" | "MinimumSumMulticenter" + ) +} + +pub(super) fn help_flag_name(canonical: &str, field_name: &str) -> String { + // Problem-specific overrides first + match (canonical, field_name) { + ("BoundedComponentSpanningForest", "max_components") => return "k".to_string(), + ("BoundedComponentSpanningForest", "max_weight") => return "max-weight".to_string(), + ("BoyceCoddNormalFormViolation", "num_attributes") => return "n".to_string(), + ("BoyceCoddNormalFormViolation", "functional_deps") => return "sets".to_string(), + ("BoyceCoddNormalFormViolation", "target_subset") => return "target".to_string(), + ("CapacityAssignment", "cost") => return "cost-matrix".to_string(), + ("CapacityAssignment", "delay") => return "delay-matrix".to_string(), + ("FlowShopScheduling", "num_processors") + | ("JobShopScheduling", "num_processors") + | ("OpenShopScheduling", "num_machines") + | ("SchedulingWithIndividualDeadlines", "num_processors") => { + return "num-processors/--m".to_string(); + } + ("JobShopScheduling", "jobs") => return "jobs".to_string(), + ("LengthBoundedDisjointPaths", "max_length") => return "max-length".to_string(), + ("ConsecutiveBlockMinimization", "bound") => return "bound-k".to_string(), + ("GroupingBySwapping", "budget") => return "bound".to_string(), + ("RectilinearPictureCompression", "bound") => return "bound".to_string(), + ("PrimeAttributeName", "num_attributes") => return "universe".to_string(), + ("PrimeAttributeName", "dependencies") => return "dependencies".to_string(), + ("PrimeAttributeName", "query_attribute") => return "query-attribute".to_string(), + ("ClosestVectorProblem", "target") => return "target-vec".to_string(), + ("ConjunctiveBooleanQuery", "conjuncts") => return "conjuncts-spec".to_string(), + ("MixedChinesePostman", "arc_weights") => return "arc-weights".to_string(), + ("ConsecutiveOnesMatrixAugmentation", "bound") => return "bound".to_string(), + ("ConsecutiveOnesSubmatrix", "bound") => return "bound".to_string(), + ("SparseMatrixCompression", "bound_k") => return "bound-k".to_string(), + ("MinimumCodeGenerationParallelAssignments", "num_variables") => { + return "num-variables".to_string(); + } + ("MinimumCodeGenerationParallelAssignments", "assignments") => { + return "assignments".to_string(); + } + ("StackerCrane", "edges") => return "graph".to_string(), + ("StackerCrane", "arc_lengths") => return "arc-lengths".to_string(), + ("StackerCrane", "edge_lengths") => return "edge-lengths".to_string(), + ("StaffScheduling", "shifts_per_schedule") => return "k".to_string(), + ("TimetableDesign", "num_tasks") => return "num-tasks".to_string(), + _ => {} + } + // Edge-weight problems use --edge-weights instead of --weights + if field_name == "weights" && uses_edge_weights_flag(canonical) { + return "edge-weights".to_string(); + } + if field_name == "edge_lengths" && uses_edge_weights_flag_for_edge_lengths(canonical) { + return "edge-weights".to_string(); + } + // General field-name overrides (previously in cli_flag_name) + match field_name { + "universe_size" => "universe-size".to_string(), + "collection" | "subsets" | "sets" => "subsets".to_string(), + "left_size" => "left".to_string(), + "right_size" => "right".to_string(), + "edges" => "biedges".to_string(), + "vertex_weights" => "weights".to_string(), + "potential_weights" => "potential-weights".to_string(), + "num_tasks" => "num-tasks".to_string(), + "precedences" => "precedences".to_string(), + "threshold" => "threshold".to_string(), + "lengths" => "lengths".to_string(), + _ => field_name.replace('_', "-"), + } +} + +pub(super) fn reject_vertex_weights_for_edge_weight_problem( + args: &CreateArgs, + canonical: &str, + graph_type: Option<&str>, +) -> Result<()> { + if args.weights.is_some() && uses_edge_weights_flag(canonical) { + bail!( + "{canonical} uses --edge-weights, not --weights.\n\n\ + Usage: pred create {} {}", + match graph_type { + Some(g) => format!("{canonical}/{g}"), + None => canonical.to_string(), + }, + example_for(canonical, graph_type) + ); + } + Ok(()) +} + +pub(super) fn help_flag_hint( + canonical: &str, + field_name: &str, + type_name: &str, + graph_type: Option<&str>, +) -> &'static str { + match (canonical, field_name) { + ("BoundedComponentSpanningForest", "max_weight") => "integer", + ("SequencingWithinIntervals", "release_times") => "comma-separated integers: 0,0,5", + ("DynamicStorageAllocation", "release_times") => "comma-separated arrival times: 0,0,1,2,3", + ("DynamicStorageAllocation", "deadlines") => "comma-separated departure times: 3,2,4,5,5", + ("DynamicStorageAllocation", "sizes") => "comma-separated item sizes: 2,3,1,3,2", + ("DynamicStorageAllocation", "capacity") => "memory size D: 6", + ("DisjointConnectingPaths", "terminal_pairs") => "comma-separated pairs: 0-3,2-5", + ("PrimeAttributeName", "dependencies") => { + "semicolon-separated dependencies: \"0,1>2,3;2,3>0,1\"" + } + ("LongestCommonSubsequence", "strings") => { + "raw strings: \"ABAC;BACA\" or symbol lists: \"0,1,0;1,0,1\"" + } + ("GroupingBySwapping", "string") => "symbol list: \"0,1,2,0,1,2\"", + ("MinimumExternalMacroDataCompression", "string") + | ("MinimumInternalMacroDataCompression", "string") => "symbol list: \"0,1,0,1\"", + ("MinimumExternalMacroDataCompression", "pointer_cost") + | ("MinimumInternalMacroDataCompression", "pointer_cost") => "positive integer: 2", + ("MinimumAxiomSet", "num_sentences") => "total number of sentences: 8", + ("MinimumAxiomSet", "true_sentences") => "comma-separated indices: 0,1,2,3,4,5,6,7", + ("MinimumAxiomSet", "implications") => "semicolon-separated rules: \"0>2;0>3;1>4;2,4>6\"", + ("ShortestCommonSupersequence", "strings") => "symbol lists: \"0,1,2;1,2,0\"", + ("MultipleChoiceBranching", "partition") => "semicolon-separated groups: \"0,1;2,3\"", + ("IntegralFlowHomologousArcs", "homologous_pairs") => { + "semicolon-separated arc-index equalities: \"2=5;4=3\"" + } + ("ConsistencyOfDatabaseFrequencyTables", "attribute_domains") => { + "comma-separated domain sizes: 2,3,2" + } + ("ConsistencyOfDatabaseFrequencyTables", "frequency_tables") => { + "semicolon-separated tables: \"0,1:1,1,1|1,1,1;1,2:1,1|0,2|1,1\"" + } + ("ConsistencyOfDatabaseFrequencyTables", "known_values") => { + "semicolon-separated triples: \"0,0,0;3,0,1;1,2,1\"" + } + ("IntegralFlowBundles", "bundles") => "semicolon-separated groups: \"0,1;2,5;3,4\"", + ("IntegralFlowBundles", "bundle_capacities") => "comma-separated capacities: 1,1,1", + ("PathConstrainedNetworkFlow", "paths") => { + "semicolon-separated arc-index paths: \"0,2,5,8;1,4,7,9\"" + } + ("ConsecutiveBlockMinimization", "matrix") => { + "JSON 2D bool array: '[[true,false,true],[false,true,true]]'" + } + ("ConsecutiveOnesMatrixAugmentation", "matrix") => { + "semicolon-separated 0/1 rows: \"1,0;0,1\"" + } + ("ConsecutiveOnesSubmatrix", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"", + ("SparseMatrixCompression", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"", + ("MaximumLikelihoodRanking", "matrix") => { + "semicolon-separated i32 rows: \"0,4,3,5;1,0,4,3;2,1,0,4;0,2,1,0\"" + } + ("MinimumMatrixCover", "matrix") => "semicolon-separated i64 rows: \"0,3,1;3,0,2;1,2,0\"", + ("MinimumMatrixDomination", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"", + ("MinimumWeightDecoding", "matrix") => "JSON 2D bool array: '[[true,false],[false,true]]'", + ("MinimumWeightDecoding", "target") => "comma-separated booleans: \"true,true,false\"", + ("MinimumWeightSolutionToLinearEquations", "matrix") => { + "JSON 2D integer array: '[[1,2,3],[4,5,6]]'" + } + ("MinimumWeightSolutionToLinearEquations", "rhs") => "comma-separated integers: \"5,4\"", + ("FeasibleBasisExtension", "matrix") => "JSON 2D integer array: '[[1,0,1],[0,1,0]]'", + ("FeasibleBasisExtension", "rhs") => "comma-separated integers: \"7,5,3\"", + ("FeasibleBasisExtension", "required_columns") => "comma-separated column indices: \"0,1\"", + ("MinimumCodeGenerationParallelAssignments", "assignments") => { + "semicolon-separated target:reads entries: \"0:1,2;1:0;2:3;3:1,2\"" + } + ("NonTautology", "disjuncts") => "semicolon-separated disjuncts: \"1,2,3;-1,-2,-3\"", + ("TimetableDesign", "craftsman_avail") | ("TimetableDesign", "task_avail") => { + "semicolon-separated 0/1 rows: \"1,1,0;0,1,1\"" + } + ("TimetableDesign", "requirements") => "semicolon-separated rows: \"1,0,1;0,1,0\"", + _ => type_format_hint(type_name, graph_type), + } +} + +pub(super) fn parse_nonnegative_usize_bound( + bound: i64, + problem_name: &str, + usage: &str, +) -> Result { + usize::try_from(bound) + .map_err(|_| anyhow::anyhow!("{problem_name} requires nonnegative --bound\n\n{usage}")) +} + +pub(super) fn validate_prescribed_paths_against_graph( + graph: &DirectedGraph, + paths: &[Vec], + source: usize, + sink: usize, + usage: &str, +) -> Result<()> { + let arcs = graph.arcs(); + for path in paths { + anyhow::ensure!( + !path.is_empty(), + "PathConstrainedNetworkFlow paths must be non-empty\n\n{usage}" + ); + let mut visited_vertices = BTreeSet::from([source]); + let mut current = source; + for &arc_index in path { + let &(tail, head) = arcs.get(arc_index).ok_or_else(|| { + anyhow::anyhow!( + "Path arc index {arc_index} out of bounds for {} arcs\n\n{usage}", + arcs.len() + ) + })?; + anyhow::ensure!( + tail == current, + "prescribed path is not contiguous: expected arc leaving vertex {current}, got {tail}->{head}\n\n{usage}" + ); + anyhow::ensure!( + visited_vertices.insert(head), + "prescribed path repeats vertex {head}, so it is not a simple path\n\n{usage}" + ); + current = head; + } + anyhow::ensure!( + current == sink, + "prescribed path must end at sink {sink}, ended at {current}\n\n{usage}" + ); + } + Ok(()) +} + +pub(super) fn resolve_processor_count_flags( + problem_name: &str, + usage: &str, + num_processors: Option, + m_alias: Option, +) -> Result> { + match (num_processors, m_alias) { + (Some(num_processors), Some(m_alias)) => { + anyhow::ensure!( + num_processors == m_alias, + "{problem_name} received conflicting processor counts: --num-processors={num_processors} but --m={m_alias}\n\n{usage}" + ); + Ok(Some(num_processors)) + } + (Some(num_processors), None) => Ok(Some(num_processors)), + (None, Some(m_alias)) => Ok(Some(m_alias)), + (None, None) => Ok(None), + } +} + +pub(super) fn validate_sequencing_within_intervals_inputs( + release_times: &[u64], + deadlines: &[u64], + lengths: &[u64], + usage: &str, +) -> Result<()> { + if release_times.len() != deadlines.len() { + bail!("release_times and deadlines must have the same length\n\n{usage}"); + } + if release_times.len() != lengths.len() { + bail!("release_times and lengths must have the same length\n\n{usage}"); + } + + for (i, ((&release_time, &deadline), &length)) in release_times + .iter() + .zip(deadlines.iter()) + .zip(lengths.iter()) + .enumerate() + { + let end = release_time.checked_add(length).ok_or_else(|| { + anyhow::anyhow!("Task {i}: overflow computing r(i) + l(i)\n\n{usage}") + })?; + if end > deadline { + bail!( + "Task {i}: r({}) + l({}) > d({}), time window is empty\n\n{usage}", + release_time, + length, + deadline + ); + } + } + + Ok(()) +} + +pub(super) fn print_problem_help( + canonical: &str, + resolved_variant: &BTreeMap, +) -> Result<()> { + let graph_type = resolved_variant + .get("graph") + .map(String::as_str) + .filter(|graph_type| *graph_type != "SimpleGraph"); + let is_geometry = matches!( + graph_type, + Some("KingsSubgraph" | "TriangularSubgraph" | "UnitDiskGraph") + ); + let schemas = collect_schemas(); + let schema = schemas.iter().find(|s| s.name == canonical); + + if let Some(s) = schema { + eprintln!("{}\n {}\n", canonical, s.description); + eprintln!("Parameters:"); + for field in &s.fields { + let flag_name = + problem_help_flag_name(canonical, &field.name, &field.type_name, is_geometry); + // For geometry variants, show --positions instead of --graph + if field.type_name == "G" && is_geometry { + let hint = type_format_hint(&field.type_name, graph_type); + eprintln!(" --{:<16} {} ({hint})", flag_name, field.description); + if graph_type == Some("UnitDiskGraph") { + eprintln!(" --{:<16} Distance threshold [default: 1.0]", "radius"); + } + } else if field.type_name == "DirectedGraph" { + // DirectedGraph fields use --arcs, not --graph + let hint = type_format_hint(&field.type_name, graph_type); + eprintln!(" --{:<16} {} ({})", "arcs", field.description, hint); + } else if field.type_name == "MixedGraph" { + eprintln!( + " --{:<16} Undirected edges E of the mixed graph (edge list: 0-1,1-2,2-3)", + "graph" + ); + eprintln!( + " --{:<16} Directed arcs A of the mixed graph (directed arcs: 0>1,1>2,2>0)", + "arcs" + ); + } else if field.type_name == "BipartiteGraph" { + eprintln!( + " --{:<16} Vertices in the left partition (integer)", + "left" + ); + eprintln!( + " --{:<16} Vertices in the right partition (integer)", + "right" + ); + eprintln!( + " --{:<16} Bipartite edges as left-right pairs (edge list: 0-0,0-1,1-2)", + "biedges" + ); + } else { + let hint = help_flag_hint(canonical, &field.name, &field.type_name, graph_type); + eprintln!(" --{:<16} {} ({})", flag_name, field.description, hint); + } + } + if canonical == "GraphPartitioning" { + eprintln!( + " --{:<16} Number of partitions in the balanced partitioning model (must be 2) (integer)", + "num-partitions" + ); + } + } else { + bail!("{}", crate::problem_name::unknown_problem_error(canonical)); + } + + let example = schema_help_example_for(canonical, resolved_variant).or_else(|| { + let fallback = example_for(canonical, graph_type); + (!fallback.is_empty()).then(|| fallback.to_string()) + }); + if let Some(example) = example { + eprintln!("\nExample:"); + eprintln!( + " pred create {} {}", + match graph_type { + Some(g) => format!("{canonical}/{g}"), + None => canonical.to_string(), + }, + example + ); + } + Ok(()) +} + +pub(super) fn schema_help_example_for( + canonical: &str, + resolved_variant: &BTreeMap, +) -> Option { + let schema = collect_schemas() + .into_iter() + .find(|schema| schema.name == canonical)?; + let example = problemreductions::example_db::find_model_example(&ProblemRef { + name: canonical.to_string(), + variant: resolved_variant.clone(), + }) + .ok()?; + let instance = example.instance.as_object()?; + let graph_type = resolved_variant + .get("graph") + .map(String::as_str) + .filter(|graph_type| *graph_type != "SimpleGraph"); + let is_geometry = matches!( + graph_type, + Some("KingsSubgraph" | "TriangularSubgraph" | "UnitDiskGraph") + ); + + let mut args = Vec::new(); + for field in &schema.fields { + let value = instance.get(&field.name)?; + let concrete_type = resolve_schema_field_type(&field.type_name, resolved_variant); + let flag_name = + schema_example_flag_name(canonical, &field.name, &field.type_name, is_geometry); + let rendered = + format_schema_help_example_value(canonical, &field.name, &concrete_type, value)?; + args.push(format!("--{flag_name} {}", quote_cli_arg(&rendered))); + } + Some(args.join(" ")) +} + +pub(super) fn schema_example_flag_name( + canonical: &str, + field_name: &str, + field_type: &str, + is_geometry: bool, +) -> String { + problem_help_flag_name(canonical, field_name, field_type, is_geometry) + .split('/') + .next() + .unwrap_or(field_name) + .trim_start_matches("--") + .to_string() +} + +pub(super) fn quote_cli_arg(raw: &str) -> String { + if raw.is_empty() + || raw.chars().any(|ch| { + ch.is_whitespace() + || matches!( + ch, + ';' | '>' | '|' | '[' | ']' | '{' | '}' | '(' | ')' | '"' | '\'' + ) + }) + { + format!("\"{}\"", raw.replace('\\', "\\\\").replace('"', "\\\"")) + } else { + raw.to_string() + } +} + +pub(super) fn format_schema_help_example_value( + canonical: &str, + field_name: &str, + concrete_type: &str, + value: &serde_json::Value, +) -> Option { + match (canonical, field_name) { + ("ConsecutiveBlockMinimization", "matrix") + | ("FeasibleBasisExtension", "matrix") + | ("MinimumWeightDecoding", "matrix") + | ("MinimumWeightSolutionToLinearEquations", "matrix") => { + return serde_json::to_string(value).ok(); + } + _ => {} + } + match normalize_type_name(concrete_type).as_str() { + "SimpleGraph" => format_simple_graph_example(value), + "DirectedGraph" => format_directed_graph_example(value), + "Vec" => format_cnf_clause_list_example(value), + "Vec" => format_quantifier_list_example(value), + "Vec>" => format_job_shop_example(value), + "Vec<(Vec,Vec)>" => format_dependency_example(value), + "Vec" | "Vec" | "Vec" | "Vec" | "Vec" | "Vec" => { + format_scalar_array_example(value) + } + "Vec" => format_bool_array_example(value), + "Vec>" | "Vec>" | "Vec>" | "Vec>" + | "Vec>" => format_nested_numeric_rows(value), + "Vec>" => format_bool_matrix_example(value), + "Vec" => Some( + value + .as_array()? + .iter() + .map(|entry| entry.as_str().map(str::to_string)) + .collect::>>()? + .join(";"), + ), + "usize" | "u64" | "i32" | "i64" | "f64" | "BigUint" => format_scalar_example(value), + _ => None, + } +} + +pub(super) fn format_scalar_example(value: &serde_json::Value) -> Option { + match value { + serde_json::Value::Number(number) => Some(number.to_string()), + serde_json::Value::String(string) => Some(string.clone()), + serde_json::Value::Bool(boolean) => Some(boolean.to_string()), + _ => None, + } +} + +pub(super) fn format_scalar_array_example(value: &serde_json::Value) -> Option { + Some( + value + .as_array()? + .iter() + .map(format_scalar_example) + .collect::>>()? + .join(","), + ) +} + +pub(super) fn format_bool_array_example(value: &serde_json::Value) -> Option { + Some( + value + .as_array()? + .iter() + .map(|entry| { + entry + .as_bool() + .map(|boolean| if boolean { "1" } else { "0" }.to_string()) + }) + .collect::>>()? + .join(","), + ) +} + +pub(super) fn format_nested_numeric_rows(value: &serde_json::Value) -> Option { + Some( + value + .as_array()? + .iter() + .map(format_scalar_array_example) + .collect::>>()? + .join(";"), + ) +} + +pub(super) fn format_cnf_clause_list_example(value: &serde_json::Value) -> Option { + Some( + value + .as_array()? + .iter() + .map(|clause| format_scalar_array_example(clause.get("literals")?)) + .collect::>>()? + .join(";"), + ) +} + +pub(super) fn format_bool_matrix_example(value: &serde_json::Value) -> Option { + Some( + value + .as_array()? + .iter() + .map(format_bool_array_example) + .collect::>>()? + .join(";"), + ) +} + +pub(super) fn format_simple_graph_example(value: &serde_json::Value) -> Option { + Some( + value + .get("edges")? + .as_array()? + .iter() + .map(|edge| { + let pair = edge.as_array()?; + Some(format!( + "{}-{}", + pair.first()?.as_u64()?, + pair.get(1)?.as_u64()? + )) + }) + .collect::>>()? + .join(","), + ) +} + +pub(super) fn format_directed_graph_example(value: &serde_json::Value) -> Option { + Some( + value + .get("arcs")? + .as_array()? + .iter() + .map(|arc| { + let pair = arc.as_array()?; + Some(format!( + "{}>{}", + pair.first()?.as_u64()?, + pair.get(1)?.as_u64()? + )) + }) + .collect::>>()? + .join(","), + ) +} + +pub(super) fn format_quantifier_list_example(value: &serde_json::Value) -> Option { + Some( + value + .as_array()? + .iter() + .map(|entry| match entry.as_str()? { + "Exists" => Some("E".to_string()), + "ForAll" => Some("A".to_string()), + _ => None, + }) + .collect::>>()? + .join(","), + ) +} + +pub(super) fn format_job_shop_example(value: &serde_json::Value) -> Option { + Some( + value + .as_array()? + .iter() + .map(|job| { + Some( + job.as_array()? + .iter() + .map(|task| { + let task = task.as_array()?; + Some(format!( + "{}:{}", + task.first()?.as_u64()?, + task.get(1)?.as_u64()? + )) + }) + .collect::>>()? + .join(","), + ) + }) + .collect::>>()? + .join(";"), + ) +} + +pub(super) fn format_dependency_example(value: &serde_json::Value) -> Option { + Some( + value + .as_array()? + .iter() + .map(|dependency| { + let dependency = dependency.as_array()?; + let lhs = format_scalar_array_example(dependency.first()?)?; + let rhs = format_scalar_array_example(dependency.get(1)?)?; + Some(format!("{lhs}>{rhs}")) + }) + .collect::>>()? + .join(";"), + ) +} + +pub(super) fn problem_help_flag_name( + canonical: &str, + field_name: &str, + field_type: &str, + is_geometry: bool, +) -> String { + if field_type == "G" && is_geometry { + return "positions".to_string(); + } + if field_type == "DirectedGraph" { + return "arcs".to_string(); + } + if field_type == "MixedGraph" { + return "graph".to_string(); + } + if canonical == "LengthBoundedDisjointPaths" && field_name == "max_length" { + return "max-length".to_string(); + } + if canonical == "GeneralizedHex" && field_name == "target" { + return "sink".to_string(); + } + if canonical == "StringToStringCorrection" { + return match field_name { + "source" => "source-string".to_string(), + "target" => "target-string".to_string(), + "bound" => "bound".to_string(), + _ => help_flag_name(canonical, field_name), + }; + } + help_flag_name(canonical, field_name) +} + +pub(super) fn lbdp_validation_error(message: &str, usage: Option<&str>) -> anyhow::Error { + match usage { + Some(usage) => anyhow::anyhow!("{message}\n\n{usage}"), + None => anyhow::anyhow!("{message}"), + } +} + +pub(super) fn validate_length_bounded_disjoint_paths_args( + num_vertices: usize, + source: usize, + sink: usize, + bound: i64, + usage: Option<&str>, +) -> Result { + let max_length = usize::try_from(bound).map_err(|_| { + lbdp_validation_error( + "--max-length must be a nonnegative integer for LengthBoundedDisjointPaths", + usage, + ) + })?; + if source >= num_vertices || sink >= num_vertices { + return Err(lbdp_validation_error( + "--source and --sink must be valid graph vertices", + usage, + )); + } + if source == sink { + return Err(lbdp_validation_error( + "--source and --sink must be distinct", + usage, + )); + } + if max_length == 0 { + return Err(lbdp_validation_error( + "--max-length must be positive", + usage, + )); + } + Ok(max_length) +} diff --git a/problemreductions-cli/src/commands/create/tests.rs b/problemreductions-cli/src/commands/create/tests.rs new file mode 100644 index 000000000..04ed464ff --- /dev/null +++ b/problemreductions-cli/src/commands/create/tests.rs @@ -0,0 +1,2698 @@ +use std::fs; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +use clap::Parser; + +use super::ensure_attribute_indices_in_range; +use super::parse_bool_rows; +use super::schema_support::*; +use super::*; +use crate::cli::{Cli, Commands}; +use crate::output::OutputConfig; + +fn temp_output_path(name: &str) -> PathBuf { + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + std::env::temp_dir().join(format!("{}_{}.json", name, suffix)) +} + +#[test] +fn test_problem_help_uses_bound_for_length_bounded_disjoint_paths() { + assert_eq!( + problem_help_flag_name("LengthBoundedDisjointPaths", "max_length", "usize", false), + "max-length" + ); +} + +#[test] +fn test_problem_help_preserves_generic_field_kebab_case() { + assert_eq!( + problem_help_flag_name("LengthBoundedDisjointPaths", "max_paths", "usize", false,), + "max-paths" + ); +} + +#[test] +fn test_help_flag_name_mentions_m_alias_for_scheduling_processors() { + assert_eq!( + help_flag_name("SchedulingWithIndividualDeadlines", "num_processors"), + "num-processors/--m" + ); + assert_eq!( + help_flag_name("FlowShopScheduling", "num_processors"), + "num-processors/--m" + ); +} + +#[test] +fn test_parse_field_value_parses_simple_graph_to_json() { + let value = parse_field_value("SimpleGraph", "graph", "0-1,1-2", &CreateContext::default()) + .expect("parse graph"); + + assert_eq!( + value, + serde_json::json!({ + "num_vertices": 3, + "edges": [[0, 1], [1, 2]], + }) + ); +} + +#[test] +fn test_parse_field_value_parses_dependency_pairs() { + let value = parse_field_value( + "Vec<(Vec, Vec)>", + "dependencies", + "0,1>2,3;2>4", + &CreateContext::default(), + ) + .expect("parse dependencies"); + + assert_eq!(value, serde_json::json!([[[0, 1], [2, 3]], [[2], [4]],])); +} + +#[test] +fn test_parse_field_value_parses_job_shop_jobs() { + let value = parse_field_value( + "Vec>", + "jobs", + "0:3,1:4;1:2,0:3,1:2", + &CreateContext::default(), + ) + .expect("parse jobs"); + + assert_eq!( + value, + serde_json::json!([[[0, 3], [1, 4]], [[1, 2], [0, 3], [1, 2]],]) + ); +} + +#[test] +fn test_parse_field_value_parses_quantifiers_using_context_num_vars() { + let context = CreateContext::default().with_field("num_vars", serde_json::json!(3)); + let value = parse_field_value("Vec", "quantifiers", "E,A,E", &context) + .expect("parse quantifiers"); + + assert_eq!(value, serde_json::json!(["Exists", "ForAll", "Exists"])); +} + +#[test] +fn test_schema_driven_supported_problem_includes_cli_creatable_problem() { + assert!( + schema_driven_supported_problem("ConjunctiveBooleanQuery"), + "all CLI-creatable problems should opt into schema-driven create unless explicitly excluded" + ); + assert!(!schema_driven_supported_problem("ILP")); + assert!(!schema_driven_supported_problem("CircuitSAT")); +} + +#[test] +fn test_create_schema_driven_builds_job_shop_scheduling() { + let cli = Cli::parse_from([ + "pred", + "create", + "JobShopScheduling", + "--jobs", + "0:3,1:4;1:2,0:3,1:2", + "--num-processors", + "2", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + let (data, variant) = create_schema_driven(&args, "JobShopScheduling", &BTreeMap::new()) + .expect("schema-driven create should parse") + .expect("schema-driven path should support JobShopScheduling"); + + let entry = problemreductions::registry::find_variant_entry("JobShopScheduling", &variant) + .expect("variant entry"); + (entry.factory)(data.clone()).expect("factory should deserialize generated JSON"); + assert_eq!(data["num_processors"], 2); + assert_eq!(data["jobs"][0], serde_json::json!([[0, 3], [1, 4]])); +} + +#[test] +fn test_create_schema_driven_builds_quantified_boolean_formulas() { + let cli = Cli::parse_from([ + "pred", + "create", + "QuantifiedBooleanFormulas", + "--num-vars", + "3", + "--quantifiers", + "E,A,E", + "--clauses", + "1,2;-1,3", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + let (data, variant) = + create_schema_driven(&args, "QuantifiedBooleanFormulas", &BTreeMap::new()) + .expect("schema-driven create should parse") + .expect("schema-driven path should support QBF"); + + let entry = + problemreductions::registry::find_variant_entry("QuantifiedBooleanFormulas", &variant) + .expect("variant entry"); + (entry.factory)(data.clone()).expect("factory should deserialize generated JSON"); + assert_eq!( + data["quantifiers"], + serde_json::json!(["Exists", "ForAll", "Exists"]) + ); +} + +#[test] +fn test_create_schema_driven_builds_undirected_flow_lower_bounds() { + let cli = Cli::parse_from([ + "pred", + "create", + "UndirectedFlowLowerBounds", + "--graph", + "0-1,0-2,1-3,2-3", + "--capacities", + "2,2,2,2", + "--lower-bounds", + "1,0,0,1", + "--source", + "0", + "--sink", + "3", + "--requirement", + "2", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + let (data, variant) = + create_schema_driven(&args, "UndirectedFlowLowerBounds", &BTreeMap::new()) + .expect("schema-driven create should parse") + .expect("schema-driven path should support UndirectedFlowLowerBounds"); + + let entry = + problemreductions::registry::find_variant_entry("UndirectedFlowLowerBounds", &variant) + .expect("variant entry"); + (entry.factory)(data.clone()).expect("factory should deserialize generated JSON"); + assert_eq!(data["graph"]["num_vertices"], 4); + assert_eq!(data["capacities"], serde_json::json!([2, 2, 2, 2])); + assert_eq!(data["lower_bounds"], serde_json::json!([1, 0, 0, 1])); +} + +#[test] +fn test_create_schema_driven_builds_conjunctive_boolean_query() { + let cli = Cli::parse_from([ + "pred", + "create", + "ConjunctiveBooleanQuery", + "--domain-size", + "6", + "--relations", + "2:0,3|1,3;3:0,1,5|1,2,5", + "--conjuncts-spec", + "0:v0,c3;0:v1,c3;1:v0,v1,c5", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + let (data, variant) = create_schema_driven(&args, "ConjunctiveBooleanQuery", &BTreeMap::new()) + .expect("schema-driven create should parse") + .expect("schema-driven path should support CBQ"); + + let entry = + problemreductions::registry::find_variant_entry("ConjunctiveBooleanQuery", &variant) + .expect("variant entry"); + (entry.factory)(data.clone()).expect("factory should deserialize generated JSON"); + assert_eq!(data["num_variables"], 2); + assert_eq!(data["relations"][0]["arity"], 2); + assert_eq!( + data["conjuncts"][1], + serde_json::json!([0, [{"Variable": 1}, {"Constant": 3}]]) + ); +} + +#[test] +fn test_create_schema_driven_builds_closest_vector_problem_with_default_bounds() { + let cli = Cli::parse_from([ + "pred", + "create", + "CVP", + "--basis", + "1,0;0,1", + "--target-vec", + "0.5,0.5", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + let resolved_variant = variant_map(&[("weight", "i32")]); + let (data, variant) = create_schema_driven(&args, "ClosestVectorProblem", &resolved_variant) + .expect("schema-driven create should parse") + .expect("schema-driven path should support CVP"); + + let entry = problemreductions::registry::find_variant_entry("ClosestVectorProblem", &variant) + .expect("variant entry"); + (entry.factory)(data.clone()).expect("factory should deserialize generated JSON"); + assert_eq!(data["basis"], serde_json::json!([[1, 0], [0, 1]])); + assert_eq!( + data["bounds"], + serde_json::json!([ + {"lower": -10, "upper": 10}, + {"lower": -10, "upper": 10}, + ]) + ); +} + +#[test] +fn test_create_schema_driven_builds_cdft() { + let cli = Cli::parse_from([ + "pred", + "create", + "ConsistencyOfDatabaseFrequencyTables", + "--num-objects", + "6", + "--attribute-domains", + "2,3,2", + "--frequency-tables", + "0,1:1,1,1|1,1,1;1,2:1,1|0,2|1,1", + "--known-values", + "0,0,0;3,0,1;1,2,1", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + let (data, variant) = create_schema_driven( + &args, + "ConsistencyOfDatabaseFrequencyTables", + &BTreeMap::new(), + ) + .expect("schema-driven create should parse") + .expect("schema-driven path should support CDFT"); + + let entry = problemreductions::registry::find_variant_entry( + "ConsistencyOfDatabaseFrequencyTables", + &variant, + ) + .expect("variant entry"); + (entry.factory)(data.clone()).expect("factory should deserialize generated JSON"); + assert_eq!(data["num_objects"], 6); + assert_eq!(data["frequency_tables"][0]["attribute_a"], 0); + assert_eq!(data["known_values"][2]["attribute"], 2); +} + +#[test] +fn test_create_schema_driven_builds_balanced_complete_bipartite_subgraph() { + let cli = Cli::parse_from([ + "pred", + "create", + "BalancedCompleteBipartiteSubgraph", + "--left", + "4", + "--right", + "4", + "--biedges", + "0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3", + "--k", + "3", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + let (data, variant) = + create_schema_driven(&args, "BalancedCompleteBipartiteSubgraph", &BTreeMap::new()) + .expect("schema-driven create should parse") + .expect("schema-driven path should support balanced biclique"); + + let entry = problemreductions::registry::find_variant_entry( + "BalancedCompleteBipartiteSubgraph", + &variant, + ) + .expect("variant entry"); + (entry.factory)(data.clone()).expect("factory should deserialize generated JSON"); + assert_eq!(data["graph"]["left_size"], 4); + assert_eq!(data["graph"]["right_size"], 4); + assert_eq!(data["k"], 3); +} + +#[test] +fn test_create_schema_driven_builds_mixed_chinese_postman() { + let cli = Cli::parse_from([ + "pred", + "create", + "MixedChinesePostman/i32", + "--graph", + "0-2,1-3,0-4,4-2", + "--arcs", + "0>1,1>2,2>3,3>0", + "--edge-weights", + "2,3,1,2", + "--arc-weights", + "2,3,1,4", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + let resolved_variant = variant_map(&[("weight", "i32")]); + let (data, variant) = create_schema_driven(&args, "MixedChinesePostman", &resolved_variant) + .expect("schema-driven create should parse") + .expect("schema-driven path should support mixed chinese postman"); + + let entry = problemreductions::registry::find_variant_entry("MixedChinesePostman", &variant) + .expect("variant entry"); + (entry.factory)(data.clone()).expect("factory should deserialize generated JSON"); + assert_eq!(data["graph"]["num_vertices"], 5); + assert_eq!(data["arc_weights"], serde_json::json!([2, 3, 1, 4])); + assert_eq!(data["edge_weights"], serde_json::json!([2, 3, 1, 2])); +} + +#[test] +fn test_create_schema_driven_builds_unit_disk_graph_problem_with_default_radius() { + let cli = Cli::parse_from([ + "pred", + "create", + "MIS/UnitDiskGraph", + "--positions", + "0,0;1,0;0.5,0.8", + ]); + + let Commands::Create(args) = cli.command else { + panic!("expected create command"); + }; + + let resolved_variant = variant_map(&[("graph", "UnitDiskGraph"), ("weight", "One")]); + let (data, variant) = create_schema_driven(&args, "MaximumIndependentSet", &resolved_variant) + .expect("schema-driven create should parse") + .expect("schema-driven path should support UnitDiskGraph variants"); + + let entry = problemreductions::registry::find_variant_entry("MaximumIndependentSet", &variant) + .expect("variant entry"); + (entry.factory)(data.clone()).expect("factory should deserialize generated JSON"); + assert_eq!(data["graph"]["positions"].as_array().unwrap().len(), 3); + assert_eq!( + data["graph"]["edges"], + serde_json::json!([[0, 1], [0, 2], [1, 2]]) + ); +} + +#[test] +fn test_schema_help_example_for_qbf_uses_example_db() { + let example = schema_help_example_for("QuantifiedBooleanFormulas", &BTreeMap::new()).unwrap(); + assert_eq!( + example, + "--num-vars 2 --quantifiers E,A --clauses \"1,2;1,-2\"" + ); +} + +#[test] +fn test_schema_help_example_for_cbm_uses_json_matrix_syntax() { + let example = + schema_help_example_for("ConsecutiveBlockMinimization", &BTreeMap::new()).unwrap(); + assert!(example.contains("--matrix \"[[false,true,false,false,false,false],[true,false,true,false,false,false],[false,true,false,true,false,false],[false,false,true,false,true,false],[false,false,false,true,false,true],[false,false,false,false,true,false]]\"")); + assert!(example.contains("--bound-k 6")); +} + +#[test] +fn test_problem_help_flag_name_uses_bound_for_grouping_by_swapping_budget() { + assert_eq!( + problem_help_flag_name("GroupingBySwapping", "budget", "usize", false), + "bound" + ); +} + +#[test] +fn test_problem_help_flag_name_preserves_edge_lengths_for_shortest_weight_constrained_path() { + assert_eq!( + problem_help_flag_name( + "ShortestWeightConstrainedPath", + "edge_lengths", + "Vec", + false + ), + "edge-lengths" + ); +} + +#[test] +fn test_problem_help_flag_name_uses_edge_weights_for_longest_circuit_edge_lengths() { + assert_eq!( + problem_help_flag_name("LongestCircuit", "edge_lengths", "Vec", false), + "edge-weights" + ); +} + +#[test] +fn test_ensure_attribute_indices_in_range_rejects_out_of_range_index() { + let err = ensure_attribute_indices_in_range(&[0, 4], 3, "Functional dependency '0:4' rhs") + .unwrap_err(); + assert!( + err.to_string().contains("out of range"), + "unexpected error: {err}" + ); +} + +#[test] +fn test_create_scheduling_with_individual_deadlines_accepts_m_alias() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "SchedulingWithIndividualDeadlines", + "--num-tasks", + "3", + "--deadlines", + "1,1,2", + "--m", + "2", + ]) + .expect("parse create command"); + + let Commands::Create(args) = cli.command else { + panic!("expected create subcommand"); + }; + + let out = OutputConfig { + output: Some( + std::env::temp_dir() + .join("pred_test_create_scheduling_with_individual_deadlines_m_alias.json"), + ), + quiet: true, + json: false, + auto_json: false, + }; + create(&args, &out).expect("`--m` should satisfy --num-processors alias"); + + let created: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(out.output.as_ref().unwrap()).unwrap()) + .unwrap(); + std::fs::remove_file(out.output.as_ref().unwrap()).ok(); + + assert_eq!(created["type"], "SchedulingWithIndividualDeadlines"); + assert_eq!(created["data"]["num_processors"], 2); +} + +#[test] +fn test_create_prime_attribute_name_accepts_canonical_flags() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "PrimeAttributeName", + "--universe", + "6", + "--dependencies", + "0,1>2,3,4,5;2,3>0,1,4,5", + "--query-attribute", + "3", + ]) + .expect("parse create command"); + + let Commands::Create(args) = cli.command else { + panic!("expected create subcommand"); + }; + + let output_path = temp_output_path("prime_attribute_name"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).expect("create PrimeAttributeName JSON"); + + let created: serde_json::Value = + serde_json::from_str(&fs::read_to_string(&output_path).unwrap()).unwrap(); + fs::remove_file(output_path).ok(); + + assert_eq!(created["type"], "PrimeAttributeName"); + assert_eq!(created["data"]["query_attribute"], 3); + assert_eq!( + created["data"]["dependencies"][0], + serde_json::json!([[0, 1], [2, 3, 4, 5]]) + ); +} + +#[test] +fn test_problem_help_uses_prime_attribute_name_cli_overrides() { + assert_eq!( + problem_help_flag_name("PrimeAttributeName", "num_attributes", "usize", false), + "universe" + ); + assert_eq!( + problem_help_flag_name( + "PrimeAttributeName", + "dependencies", + "Vec<(Vec, Vec)>", + false, + ), + "dependencies" + ); + assert_eq!( + problem_help_flag_name("PrimeAttributeName", "query_attribute", "usize", false), + "query-attribute" + ); +} + +#[test] +fn test_problem_help_uses_problem_specific_lcs_strings_hint() { + assert_eq!( + help_flag_hint( + "LongestCommonSubsequence", + "strings", + "Vec>", + None, + ), + "raw strings: \"ABAC;BACA\" or symbol lists: \"0,1,0;1,0,1\"" + ); +} + +#[test] +fn test_problem_help_uses_string_to_string_correction_cli_flags() { + assert_eq!( + problem_help_flag_name("StringToStringCorrection", "source", "Vec", false), + "source-string" + ); + assert_eq!( + problem_help_flag_name("StringToStringCorrection", "target", "Vec", false), + "target-string" + ); + assert_eq!( + problem_help_flag_name("StringToStringCorrection", "bound", "usize", false), + "bound" + ); +} + +#[test] +fn test_problem_help_keeps_generic_vec_vec_usize_hint_for_other_models() { + assert_eq!( + help_flag_hint("SetBasis", "sets", "Vec>", None), + "semicolon-separated sets: \"0,1;1,2;0,2\"" + ); +} + +#[test] +fn test_problem_help_uses_k_for_staff_scheduling() { + assert_eq!( + help_flag_name("StaffScheduling", "shifts_per_schedule"), + "k" + ); + assert_eq!( + problem_help_flag_name("StaffScheduling", "shifts_per_schedule", "usize", false), + "k" + ); +} + +#[test] +fn test_parse_bool_rows_reports_generic_invalid_boolean_entry() { + let err = parse_bool_rows("1,maybe").unwrap_err().to_string(); + assert_eq!( + err, + "Invalid boolean entry 'maybe': expected 0/1 or true/false" + ); +} + +#[test] +fn test_create_staff_scheduling_outputs_problem_json() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "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", + ]) + .unwrap(); + + let args = match cli.command { + Commands::Create(args) => args, + _ => panic!("expected create command"), + }; + + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let output_path = std::env::temp_dir().join(format!("staff-scheduling-create-{suffix}.json")); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&output_path).unwrap()).unwrap(); + assert_eq!(json["type"], "StaffScheduling"); + assert_eq!(json["data"]["num_workers"], 4); + assert_eq!( + json["data"]["requirements"], + serde_json::json!([2, 2, 2, 3, 3, 2, 1]) + ); + std::fs::remove_file(output_path).unwrap(); +} + +#[test] +fn test_create_path_constrained_network_flow_outputs_problem_json() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "PathConstrainedNetworkFlow", + "--arcs", + "0>1,0>2,1>3,1>4,2>4,3>5,4>5,4>6,5>7,6>7", + "--capacities", + "2,1,1,1,1,1,1,1,2,1", + "--source", + "0", + "--sink", + "7", + "--paths", + "0,2,5,8;0,3,6,8;0,3,7,9;1,4,6,8;1,4,7,9", + "--requirement", + "3", + ]) + .expect("parse create command"); + + let args = match cli.command { + Commands::Create(args) => args, + _ => panic!("expected create command"), + }; + + let output_path = temp_output_path("path_constrained_network_flow"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).expect("create PathConstrainedNetworkFlow JSON"); + + let created: serde_json::Value = + serde_json::from_str(&fs::read_to_string(&output_path).unwrap()).unwrap(); + fs::remove_file(output_path).ok(); + + assert_eq!(created["type"], "PathConstrainedNetworkFlow"); + assert_eq!(created["data"]["source"], 0); + assert_eq!(created["data"]["sink"], 7); + assert_eq!(created["data"]["requirement"], 3); + assert_eq!(created["data"]["paths"][0], serde_json::json!([0, 2, 5, 8])); +} + +#[test] +fn test_create_path_constrained_network_flow_rejects_invalid_paths() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "PathConstrainedNetworkFlow", + "--arcs", + "0>1,1>2,2>3", + "--capacities", + "1,1,1", + "--source", + "0", + "--sink", + "3", + "--paths", + "0,3", + "--requirement", + "1", + ]) + .expect("parse create command"); + + let args = match cli.command { + Commands::Create(args) => args, + _ => panic!("expected create command"), + }; + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("out of bounds") || err.contains("not contiguous")); +} + +#[test] +fn test_create_staff_scheduling_reports_invalid_schedule_without_panic() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "StaffScheduling", + "--schedules", + "1,1,1,1,1,0,0;0,1,1,1,1,1", + "--requirements", + "2,2,2,3,3,2,1", + "--num-workers", + "4", + "--k", + "5", + ]) + .unwrap(); + + let args = match cli.command { + Commands::Create(args) => args, + _ => panic!("expected create command"), + }; + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let result = std::panic::catch_unwind(|| create(&args, &out)); + assert!(result.is_ok(), "create should return an error, not panic"); + let err = result.unwrap().unwrap_err().to_string(); + // parse_bool_rows catches ragged rows before validate_staff_scheduling_args + assert!( + err.contains("All rows") || err.contains("schedule 1 has 6 periods, expected 7"), + "expected row-length validation error, got: {err}" + ); +} + +#[test] +fn test_problem_help_uses_num_tasks_for_timetable_design() { + assert_eq!( + problem_help_flag_name("TimetableDesign", "num_tasks", "usize", false), + "num-tasks" + ); + assert_eq!( + help_flag_hint("TimetableDesign", "craftsman_avail", "Vec>", None), + "semicolon-separated 0/1 rows: \"1,1,0;0,1,1\"" + ); +} + +#[test] +fn test_example_for_path_constrained_network_flow_mentions_paths_flag() { + let example = example_for("PathConstrainedNetworkFlow", None); + assert!(example.contains("--paths")); + assert!(example.contains("--requirement")); +} + +#[test] +fn test_example_for_three_partition_mentions_sizes_and_bound() { + let example = example_for("ThreePartition", None); + assert!(example.contains("--sizes")); + assert!(example.contains("--bound")); +} + +#[test] +fn test_create_three_partition_outputs_problem_json() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "ThreePartition", + "--sizes", + "4,5,6,4,6,5", + "--bound", + "15", + ]) + .expect("parse create command"); + + let args = match cli.command { + Commands::Create(args) => args, + _ => panic!("expected create command"), + }; + + let output_path = temp_output_path("three_partition_create"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).expect("create ThreePartition JSON"); + + let created: serde_json::Value = + serde_json::from_str(&fs::read_to_string(&output_path).unwrap()).unwrap(); + fs::remove_file(output_path).ok(); + + assert_eq!(created["type"], "ThreePartition"); + assert_eq!( + created["data"]["sizes"], + serde_json::json!([4, 5, 6, 4, 6, 5]) + ); + assert_eq!(created["data"]["bound"], 15); +} + +#[test] +fn test_create_three_partition_requires_bound() { + let cli = Cli::try_parse_from(["pred", "create", "ThreePartition", "--sizes", "4,5,6,4,6,5"]) + .expect("parse create command"); + + let args = match cli.command { + Commands::Create(args) => args, + _ => panic!("expected create command"), + }; + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("ThreePartition requires --bound")); +} + +#[test] +fn test_create_three_partition_rejects_invalid_instance() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "ThreePartition", + "--sizes", + "4,5,6,4,6,5", + "--bound", + "14", + ]) + .expect("parse create command"); + + let args = match cli.command { + Commands::Create(args) => args, + _ => panic!("expected create command"), + }; + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("must equal m * bound")); +} + +#[test] +fn test_create_timetable_design_outputs_problem_json() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "TimetableDesign", + "--num-periods", + "3", + "--num-craftsmen", + "5", + "--num-tasks", + "5", + "--craftsman-avail", + "1,1,1;1,1,0;0,1,1;1,0,1;1,1,1", + "--task-avail", + "1,1,0;0,1,1;1,0,1;1,1,1;1,1,1", + "--requirements", + "1,0,1,0,0;0,1,0,0,1;0,0,0,1,0;0,0,0,0,1;0,1,0,0,0", + ]) + .unwrap(); + + let args = match cli.command { + Commands::Create(args) => args, + _ => panic!("expected create command"), + }; + + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let output_path = std::env::temp_dir().join(format!("timetable-design-create-{suffix}.json")); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&output_path).unwrap()).unwrap(); + assert_eq!(json["type"], "TimetableDesign"); + assert_eq!(json["data"]["num_periods"], 3); + assert_eq!(json["data"]["num_craftsmen"], 5); + assert_eq!(json["data"]["num_tasks"], 5); + assert_eq!( + json["data"]["craftsman_avail"], + serde_json::json!([ + [true, true, true], + [true, true, false], + [false, true, true], + [true, false, true], + [true, true, true] + ]) + ); + assert_eq!( + json["data"]["task_avail"], + serde_json::json!([ + [true, true, false], + [false, true, true], + [true, false, true], + [true, true, true], + [true, true, true] + ]) + ); + assert_eq!( + json["data"]["requirements"], + serde_json::json!([ + [1, 0, 1, 0, 0], + [0, 1, 0, 0, 1], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1], + [0, 1, 0, 0, 0] + ]) + ); + std::fs::remove_file(output_path).unwrap(); +} + +#[test] +fn test_create_timetable_design_reports_invalid_matrix_without_panic() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "TimetableDesign", + "--num-periods", + "3", + "--num-craftsmen", + "5", + "--num-tasks", + "5", + "--craftsman-avail", + "1,1,1;1,1", + "--task-avail", + "1,1,0;0,1,1;1,0,1;1,1,1;1,1,1", + "--requirements", + "1,0,1,0,0;0,1,0,0,1;0,0,0,1,0;0,0,0,0,1;0,1,0,0,0", + ]) + .unwrap(); + + let args = match cli.command { + Commands::Create(args) => args, + _ => panic!("expected create command"), + }; + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let result = std::panic::catch_unwind(|| create(&args, &out)); + assert!(result.is_ok(), "create should return an error, not panic"); + let err = result.unwrap().unwrap_err().to_string(); + assert!( + err.contains("--craftsman-avail"), + "expected timetable matrix validation error, got: {err}" + ); + assert!(err.contains("Usage: pred create TimetableDesign")); +} + +#[test] +fn test_create_generalized_hex_serializes_problem_json() { + let output = temp_output_path("generalized_hex_create"); + let cli = Cli::try_parse_from([ + "pred", + "-o", + output.to_str().unwrap(), + "create", + "GeneralizedHex", + "--graph", + "0-1,0-2,0-3,1-4,2-4,3-4,4-5", + "--source", + "0", + "--sink", + "5", + ]) + .unwrap(); + let out = OutputConfig { + output: cli.output.clone(), + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + create(&args, &out).unwrap(); + + let json: serde_json::Value = + serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap(); + fs::remove_file(&output).unwrap(); + assert_eq!(json["type"], "GeneralizedHex"); + assert_eq!(json["variant"]["graph"], "SimpleGraph"); + assert_eq!(json["data"]["source"], 0); + assert_eq!(json["data"]["target"], 5); +} + +#[test] +fn test_create_generalized_hex_requires_sink() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "GeneralizedHex", + "--graph", + "0-1,1-2,2-3", + "--source", + "0", + ]) + .unwrap(); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + let err = create(&args, &out).unwrap_err(); + assert!(err.to_string().contains("GeneralizedHex requires --sink")); +} + +#[test] +fn test_create_capacity_assignment_serializes_problem_json() { + let output = temp_output_path("capacity_assignment_create"); + let cli = Cli::try_parse_from([ + "pred", + "-o", + output.to_str().unwrap(), + "create", + "CapacityAssignment", + "--capacities", + "1,2,3", + "--cost-matrix", + "1,3,6;2,4,7;1,2,5", + "--delay-matrix", + "8,4,1;7,3,1;6,3,1", + "--delay-budget", + "12", + ]) + .expect("parse create command"); + let out = OutputConfig { + output: cli.output.clone(), + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + create(&args, &out).unwrap(); + + let json: serde_json::Value = + serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap(); + fs::remove_file(&output).unwrap(); + assert_eq!(json["type"], "CapacityAssignment"); + assert_eq!(json["data"]["capacities"], serde_json::json!([1, 2, 3])); + assert_eq!(json["data"]["delay_budget"], 12); +} + +#[test] +fn test_create_production_planning_serializes_problem_json() { + let output = temp_output_path("production_planning_create"); + let cli = Cli::try_parse_from([ + "pred", + "-o", + output.to_str().unwrap(), + "create", + "ProductionPlanning", + "--num-periods", + "6", + "--demands", + "5,3,7,2,8,5", + "--capacities", + "12,12,12,12,12,12", + "--setup-costs", + "10,10,10,10,10,10", + "--production-costs", + "1,1,1,1,1,1", + "--inventory-costs", + "1,1,1,1,1,1", + "--cost-bound", + "80", + ]) + .expect("parse create command"); + let out = OutputConfig { + output: cli.output.clone(), + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + create(&args, &out).unwrap(); + + let json: serde_json::Value = + serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap(); + fs::remove_file(&output).unwrap(); + assert_eq!(json["type"], "ProductionPlanning"); + assert_eq!(json["data"]["num_periods"], 6); + assert_eq!( + json["data"]["demands"], + serde_json::json!([5, 3, 7, 2, 8, 5]) + ); + assert_eq!( + json["data"]["capacities"], + serde_json::json!([12, 12, 12, 12, 12, 12]) + ); + assert_eq!( + json["data"]["setup_costs"], + serde_json::json!([10, 10, 10, 10, 10, 10]) + ); + assert_eq!( + json["data"]["production_costs"], + serde_json::json!([1, 1, 1, 1, 1, 1]) + ); + assert_eq!( + json["data"]["inventory_costs"], + serde_json::json!([1, 1, 1, 1, 1, 1]) + ); + assert_eq!(json["data"]["cost_bound"], 80); +} + +#[test] +fn test_create_production_planning_requires_all_period_vectors() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "ProductionPlanning", + "--num-periods", + "6", + "--demands", + "5,3,7,2,8,5", + "--capacities", + "12,12,12,12,12,12", + "--setup-costs", + "10,10,10,10,10,10", + "--inventory-costs", + "1,1,1,1,1,1", + "--cost-bound", + "80", + ]) + .expect("parse create command"); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + let err = create(&args, &out).unwrap_err(); + assert!(err + .to_string() + .contains("ProductionPlanning requires --production-costs")); +} + +#[test] +fn test_create_production_planning_rejects_mismatched_period_lengths() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "ProductionPlanning", + "--num-periods", + "6", + "--demands", + "5,3,7,2,8", + "--capacities", + "12,12,12,12,12,12", + "--setup-costs", + "10,10,10,10,10,10", + "--production-costs", + "1,1,1,1,1,1", + "--inventory-costs", + "1,1,1,1,1,1", + "--cost-bound", + "80", + ]) + .expect("parse create command"); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + let err = create(&args, &out).unwrap_err(); + assert!(err + .to_string() + .contains("--demands must contain exactly 6 entries")); +} + +#[test] +fn test_create_example_production_planning_uses_canonical_example() { + let output = temp_output_path("production_planning_example_create"); + let cli = Cli::try_parse_from([ + "pred", + "-o", + output.to_str().unwrap(), + "create", + "--example", + "ProductionPlanning", + ]) + .expect("parse create command"); + let out = OutputConfig { + output: cli.output.clone(), + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + create(&args, &out).unwrap(); + + let json: serde_json::Value = + serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap(); + fs::remove_file(&output).unwrap(); + assert_eq!(json["type"], "ProductionPlanning"); + assert_eq!(json["data"]["num_periods"], 4); + assert_eq!(json["data"]["demands"], serde_json::json!([2, 1, 3, 2])); + assert_eq!(json["data"]["cost_bound"], 16); +} + +#[test] +fn test_create_longest_path_serializes_problem_json() { + let output = temp_output_path("longest_path_create"); + let cli = Cli::try_parse_from([ + "pred", + "-o", + output.to_str().unwrap(), + "create", + "LongestPath", + "--graph", + "0-1,0-2,1-3,2-3,2-4,3-5,4-5,4-6,5-6,1-6", + "--edge-lengths", + "3,2,4,1,5,2,3,2,4,1", + "--source-vertex", + "0", + "--target-vertex", + "6", + ]) + .unwrap(); + let out = OutputConfig { + output: cli.output.clone(), + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + create(&args, &out).unwrap(); + + let json: serde_json::Value = + serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap(); + fs::remove_file(&output).unwrap(); + assert_eq!(json["type"], "LongestPath"); + assert_eq!(json["variant"]["graph"], "SimpleGraph"); + assert_eq!(json["variant"]["weight"], "i32"); + assert_eq!(json["data"]["source_vertex"], 0); + assert_eq!(json["data"]["target_vertex"], 6); + assert_eq!( + json["data"]["edge_lengths"], + serde_json::json!([3, 2, 4, 1, 5, 2, 3, 2, 4, 1]) + ); +} + +#[test] +fn test_create_undirected_flow_lower_bounds_serializes_problem_json() { + let output = temp_output_path("undirected_flow_lower_bounds_create"); + let cli = Cli::try_parse_from([ + "pred", + "-o", + output.to_str().unwrap(), + "create", + "UndirectedFlowLowerBounds", + "--graph", + "0-1,0-2,1-3,2-3,1-4,3-5,4-5", + "--capacities", + "2,2,2,2,1,3,2", + "--lower-bounds", + "1,1,0,0,1,0,1", + "--source", + "0", + "--sink", + "5", + "--requirement", + "3", + ]) + .unwrap(); + let out = OutputConfig { + output: cli.output.clone(), + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + create(&args, &out).unwrap(); + + let json: serde_json::Value = + serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap(); + fs::remove_file(&output).unwrap(); + assert_eq!(json["type"], "UndirectedFlowLowerBounds"); + assert_eq!(json["data"]["source"], 0); + assert_eq!(json["data"]["sink"], 5); + assert_eq!(json["data"]["requirement"], 3); + assert_eq!( + json["data"]["lower_bounds"], + serde_json::json!([1, 1, 0, 0, 1, 0, 1]) + ); +} + +#[test] +fn test_create_capacity_assignment_rejects_non_monotone_cost_row() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "CapacityAssignment", + "--capacities", + "1,2,3", + "--cost-matrix", + "1,3,2;2,4,7;1,2,5", + "--delay-matrix", + "8,4,1;7,3,1;6,3,1", + "--delay-budget", + "12", + ]) + .expect("parse create command"); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("cost row 0")); + assert!(err.contains("non-decreasing")); +} + +#[test] +fn test_create_capacity_assignment_rejects_matrix_width_mismatch() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "CapacityAssignment", + "--capacities", + "1,2,3", + "--cost-matrix", + "1,3;2,4,7;1,2,5", + "--delay-matrix", + "8,4,1;7,3,1;6,3,1", + "--delay-budget", + "12", + ]) + .expect("parse create command"); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("cost row 0")); + assert!(err.contains("capacities length")); +} + +#[test] +fn test_create_longest_path_requires_edge_lengths() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "LongestPath", + "--graph", + "0-1,1-2", + "--source-vertex", + "0", + "--target-vertex", + "2", + ]) + .unwrap(); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + let err = create(&args, &out).unwrap_err(); + assert!(err + .to_string() + .contains("LongestPath requires --edge-lengths")); +} + +#[test] +fn test_create_longest_path_rejects_weights_flag() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "LongestPath", + "--graph", + "0-1,1-2", + "--weights", + "1,1,1", + "--source-vertex", + "0", + "--target-vertex", + "2", + "--edge-lengths", + "5,7", + ]) + .unwrap(); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + let err = create(&args, &out).unwrap_err(); + assert!(err + .to_string() + .contains("LongestPath uses --edge-lengths, not --weights")); +} + +#[test] +fn test_create_undirected_flow_lower_bounds_requires_lower_bounds() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "UndirectedFlowLowerBounds", + "--graph", + "0-1,0-2,1-3,2-3,1-4,3-5,4-5", + "--capacities", + "2,2,2,2,1,3,2", + "--source", + "0", + "--sink", + "5", + "--requirement", + "3", + ]) + .unwrap(); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + let err = create(&args, &out).unwrap_err(); + assert!(err + .to_string() + .contains("UndirectedFlowLowerBounds requires --lower-bounds")); +} + +fn empty_args() -> CreateArgs { + CreateArgs { + problem: Some("BiconnectivityAugmentation".to_string()), + example: None, + example_target: None, + example_side: crate::cli::ExampleSide::Source, + graph: None, + weights: None, + edge_weights: None, + edge_lengths: None, + capacities: None, + demands: None, + setup_costs: None, + production_costs: None, + inventory_costs: None, + bundle_capacities: None, + cost_matrix: None, + delay_matrix: None, + lower_bounds: None, + multipliers: None, + source: None, + sink: None, + requirement: None, + num_paths_required: None, + paths: None, + couplings: None, + fields: None, + clauses: None, + disjuncts: None, + num_vars: None, + matrix: None, + k: None, + num_partitions: None, + random: false, + source_vertex: None, + target_vertex: None, + num_vertices: None, + edge_prob: None, + seed: None, + target: None, + m: None, + n: None, + positions: None, + radius: None, + source_1: None, + sink_1: None, + source_2: None, + sink_2: None, + requirement_1: None, + requirement_2: None, + sizes: None, + probabilities: None, + capacity: None, + sequence: None, + sets: None, + r_sets: None, + s_sets: None, + r_weights: None, + s_weights: None, + partition: None, + partitions: None, + bundles: None, + universe: None, + biedges: None, + left: None, + right: None, + rank: None, + basis: None, + target_vec: None, + bounds: None, + release_times: None, + lengths: None, + terminals: None, + terminal_pairs: None, + tree: None, + required_edges: None, + bound: None, + latency_bound: None, + length_bound: None, + weight_bound: None, + diameter_bound: None, + cost_bound: None, + delay_budget: None, + pattern: None, + strings: None, + string: None, + arc_costs: None, + arcs: None, + left_arcs: None, + right_arcs: None, + values: None, + precedences: None, + distance_matrix: None, + potential_edges: None, + budget: None, + max_cycle_length: None, + candidate_arcs: None, + deadlines: None, + precedence_pairs: None, + task_lengths: None, + job_tasks: None, + resource_bounds: None, + resource_requirements: None, + deadline: None, + num_processors: None, + alphabet_size: None, + deps: None, + query: None, + dependencies: None, + num_attributes: None, + source_string: None, + target_string: None, + schedules: None, + requirements: None, + num_workers: None, + num_periods: None, + num_craftsmen: None, + num_tasks: None, + craftsman_avail: None, + task_avail: None, + num_groups: None, + num_sectors: None, + domain_size: None, + relations: None, + conjuncts_spec: None, + relation_attrs: None, + known_keys: None, + num_objects: None, + attribute_domains: None, + frequency_tables: None, + known_values: None, + costs: None, + cut_bound: None, + size_bound: None, + usage: None, + storage: None, + quantifiers: None, + homologous_pairs: None, + pointer_cost: None, + expression: None, + coeff_a: None, + coeff_b: None, + rhs: None, + coeff_c: None, + pairs: None, + required_columns: None, + compilers: None, + setup_times: None, + w_sizes: None, + x_sizes: None, + y_sizes: None, + equations: None, + assignment: None, + initial_marking: None, + output_arcs: None, + gate_types: None, + true_sentences: None, + implications: None, + loop_length: None, + loop_variables: None, + inputs: None, + outputs: None, + assignments: None, + num_variables: None, + truth_table: None, + test_matrix: None, + num_tests: None, + tiles: None, + grid_size: None, + num_colors: None, + } +} + +#[test] +fn test_all_data_flags_empty_treats_potential_edges_as_input() { + let mut args = empty_args(); + args.potential_edges = Some("0-2:3,1-3:5".to_string()); + assert!(!all_data_flags_empty(&args)); +} + +#[test] +fn test_all_data_flags_empty_treats_budget_as_input() { + let mut args = empty_args(); + args.budget = Some("7".to_string()); + assert!(!all_data_flags_empty(&args)); +} + +#[test] +fn test_all_data_flags_empty_treats_max_cycle_length_as_input() { + let mut args = empty_args(); + args.max_cycle_length = Some(4); + assert!(!all_data_flags_empty(&args)); +} + +#[test] +fn test_all_data_flags_empty_treats_homologous_pairs_as_input() { + let mut args = empty_args(); + args.homologous_pairs = Some("2=5;4=3".to_string()); + assert!(!all_data_flags_empty(&args)); +} + +#[test] +fn test_all_data_flags_empty_treats_job_tasks_as_input() { + let mut args = empty_args(); + args.job_tasks = Some("0:1,1:1;1:1,0:1".to_string()); + assert!(!all_data_flags_empty(&args)); +} + +#[test] +fn test_parse_potential_edges() { + let mut args = empty_args(); + args.potential_edges = Some("0-2:3,1-3:5".to_string()); + + let potential_edges = parse_potential_edges(&args).unwrap(); + + assert_eq!(potential_edges, vec![(0, 2, 3), (1, 3, 5)]); +} + +#[test] +fn test_parse_potential_edges_rejects_missing_weight() { + let mut args = empty_args(); + args.potential_edges = Some("0-2,1-3:5".to_string()); + + let err = parse_potential_edges(&args).unwrap_err().to_string(); + + assert!(err.contains("u-v:w")); +} + +#[test] +fn test_parse_budget() { + let mut args = empty_args(); + args.budget = Some("7".to_string()); + + assert_eq!(parse_budget(&args).unwrap(), 7); +} + +#[test] +fn test_create_disjoint_connecting_paths_json() { + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::graph::DisjointConnectingPaths; + + let mut args = empty_args(); + args.problem = Some("DisjointConnectingPaths".to_string()); + args.graph = Some("0-1,1-3,0-2,1-4,2-4,3-5,4-5".to_string()); + args.terminal_pairs = Some("0-3,2-5".to_string()); + + let output_path = std::env::temp_dir().join(format!("dcp-create-{}.json", std::process::id())); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = std::fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "DisjointConnectingPaths"); + assert_eq!( + created.variant, + BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]) + ); + + let problem: DisjointConnectingPaths = + serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.num_vertices(), 6); + assert_eq!(problem.num_edges(), 7); + assert_eq!(problem.terminal_pairs(), &[(0, 3), (2, 5)]); + + let _ = std::fs::remove_file(output_path); +} + +#[test] +fn test_create_disjoint_connecting_paths_rejects_overlapping_terminal_pairs() { + let mut args = empty_args(); + args.problem = Some("DisjointConnectingPaths".to_string()); + args.graph = Some("0-1,1-2,2-3,3-4".to_string()); + args.terminal_pairs = Some("0-2,2-4".to_string()); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("pairwise disjoint")); +} + +#[test] +fn test_parse_homologous_pairs() { + let mut args = empty_args(); + args.homologous_pairs = Some("2=5;4=3".to_string()); + + assert_eq!(parse_homologous_pairs(&args).unwrap(), vec![(2, 5), (4, 3)]); +} + +#[test] +fn test_parse_homologous_pairs_rejects_invalid_token() { + let mut args = empty_args(); + args.homologous_pairs = Some("2-5".to_string()); + + let err = parse_homologous_pairs(&args).unwrap_err().to_string(); + + assert!(err.contains("u=v")); +} + +#[test] +fn test_parse_graph_respects_explicit_num_vertices() { + let mut args = empty_args(); + args.graph = Some("0-1".to_string()); + args.num_vertices = Some(3); + + let (graph, num_vertices) = parse_graph(&args).unwrap(); + + assert_eq!(num_vertices, 3); + assert_eq!(graph.num_vertices(), 3); + assert_eq!(graph.edges(), vec![(0, 1)]); +} + +#[test] +fn test_validate_potential_edges_rejects_existing_graph_edge() { + let err = validate_potential_edges(&SimpleGraph::path(3), &[(0, 1, 5)]) + .unwrap_err() + .to_string(); + + assert!(err.contains("already exists in the graph")); +} + +#[test] +fn test_validate_potential_edges_rejects_duplicate_edges() { + let err = validate_potential_edges(&SimpleGraph::path(4), &[(0, 3, 1), (3, 0, 2)]) + .unwrap_err() + .to_string(); + + assert!(err.contains("Duplicate potential edge")); +} + +#[test] +fn test_create_biconnectivity_augmentation_json() { + let mut args = empty_args(); + args.graph = Some("0-1,1-2,2-3".to_string()); + args.potential_edges = Some("0-2:3,0-3:4,1-3:2".to_string()); + args.budget = Some("5".to_string()); + + let output_path = std::env::temp_dir().join("pred_test_create_biconnectivity.json"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let content = std::fs::read_to_string(&output_path).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "BiconnectivityAugmentation"); + assert_eq!(json["data"]["budget"], 5); + assert_eq!( + json["data"]["potential_weights"][0], + serde_json::json!([0, 2, 3]) + ); + + std::fs::remove_file(output_path).ok(); +} + +#[test] +fn test_create_biconnectivity_augmentation_json_with_isolated_vertices() { + let mut args = empty_args(); + args.graph = Some("0-1".to_string()); + args.num_vertices = Some(3); + args.potential_edges = Some("1-2:1".to_string()); + args.budget = Some("1".to_string()); + + let output_path = std::env::temp_dir().join("pred_test_create_biconnectivity_isolated.json"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let content = std::fs::read_to_string(&output_path).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + let problem: BiconnectivityAugmentation = + serde_json::from_value(json["data"].clone()).unwrap(); + + assert_eq!(problem.num_vertices(), 3); + assert_eq!(problem.potential_weights(), &[(1, 2, 1)]); + assert_eq!(problem.budget(), &1); + + std::fs::remove_file(output_path).ok(); +} + +#[test] +fn test_create_partial_feedback_edge_set_json() { + use problemreductions::models::graph::PartialFeedbackEdgeSet; + + let mut args = empty_args(); + args.problem = Some("PartialFeedbackEdgeSet".to_string()); + args.graph = Some("0-1,1-2,2-0".to_string()); + args.budget = Some("1".to_string()); + args.max_cycle_length = Some(3); + + let output_path = std::env::temp_dir().join("pred_test_create_partial_feedback_edge_set.json"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let content = std::fs::read_to_string(&output_path).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "PartialFeedbackEdgeSet"); + assert_eq!(json["data"]["budget"], 1); + assert_eq!(json["data"]["max_cycle_length"], 3); + + let problem: PartialFeedbackEdgeSet = + serde_json::from_value(json["data"].clone()).unwrap(); + assert_eq!(problem.num_vertices(), 3); + assert_eq!(problem.num_edges(), 3); + assert_eq!(problem.budget(), 1); + assert_eq!(problem.max_cycle_length(), 3); + + std::fs::remove_file(output_path).ok(); +} + +#[test] +fn test_create_partial_feedback_edge_set_requires_max_cycle_length() { + let mut args = empty_args(); + args.problem = Some("PartialFeedbackEdgeSet".to_string()); + args.graph = Some("0-1,1-2,2-0".to_string()); + args.budget = Some("1".to_string()); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("PartialFeedbackEdgeSet requires --max-cycle-length")); +} + +#[test] +fn test_create_ensemble_computation_json() { + let mut args = empty_args(); + args.problem = Some("EnsembleComputation".to_string()); + args.universe = Some(4); + args.sets = Some("0,1,2;0,1,3".to_string()); + args.budget = Some("4".to_string()); + + let output_path = std::env::temp_dir().join("pred_test_create_ensemble_computation.json"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let content = std::fs::read_to_string(&output_path).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "EnsembleComputation"); + assert_eq!(json["data"]["universe_size"], 4); + assert_eq!( + json["data"]["subsets"], + serde_json::json!([[0, 1, 2], [0, 1, 3]]) + ); + assert_eq!(json["data"]["budget"], 4); + + std::fs::remove_file(output_path).ok(); +} + +#[test] +fn test_create_expected_retrieval_cost_json() { + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::misc::ExpectedRetrievalCost; + + let mut args = empty_args(); + args.problem = Some("ExpectedRetrievalCost".to_string()); + args.probabilities = Some("0.2,0.15,0.15,0.2,0.1,0.2".to_string()); + args.num_sectors = Some(3); + + let output_path = std::env::temp_dir().join(format!( + "expected-retrieval-cost-{}.json", + std::process::id() + )); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = std::fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "ExpectedRetrievalCost"); + + let problem: ExpectedRetrievalCost = serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.num_records(), 6); + assert_eq!(problem.num_sectors(), 3); + use problemreductions::types::Min; + assert!(matches!( + problem.evaluate(&[0, 1, 2, 1, 0, 2]), + Min(Some(_)) + )); + + let _ = std::fs::remove_file(output_path); +} + +#[test] +fn test_create_job_shop_scheduling_json() { + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::misc::JobShopScheduling; + use problemreductions::traits::Problem; + use problemreductions::types::Min; + + let mut args = empty_args(); + args.problem = Some("JobShopScheduling".to_string()); + args.job_tasks = Some("0:3,1:4;1:2,0:3,1:2;0:4,1:3;1:5,0:2;0:2,1:3,0:1".to_string()); + + let output_path = + std::env::temp_dir().join(format!("job-shop-scheduling-{}.json", std::process::id())); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = std::fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "JobShopScheduling"); + assert!(created.variant.is_empty()); + + let problem: JobShopScheduling = serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.num_processors(), 2); + assert_eq!(problem.num_jobs(), 5); + assert_eq!( + problem.evaluate(&[0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]), + Min(Some(19)) + ); + + let _ = std::fs::remove_file(output_path); +} + +#[test] +fn test_create_job_shop_scheduling_requires_job_tasks() { + let mut args = empty_args(); + args.problem = Some("JobShopScheduling".to_string()); + args.num_processors = Some(2); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("JobShopScheduling requires --jobs")); +} + +#[test] +fn test_create_job_shop_scheduling_rejects_malformed_operation() { + let mut args = empty_args(); + args.problem = Some("JobShopScheduling".to_string()); + args.job_tasks = Some("0-3,1:4".to_string()); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("expected 'processor:length'")); +} + +#[test] +fn test_create_job_shop_scheduling_rejects_consecutive_same_processor() { + let mut args = empty_args(); + args.problem = Some("JobShopScheduling".to_string()); + args.job_tasks = Some("0:1,0:1".to_string()); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("must use different processors")); +} + +#[test] +fn test_create_rooted_tree_storage_assignment_json() { + let mut args = empty_args(); + args.problem = Some("RootedTreeStorageAssignment".to_string()); + args.universe = Some(5); + args.sets = Some("0,2;1,3;0,4;2,4".to_string()); + args.bound = Some(1); + + let output_path = + std::env::temp_dir().join("pred_test_create_rooted_tree_storage_assignment.json"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let content = std::fs::read_to_string(&output_path).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "RootedTreeStorageAssignment"); + assert_eq!(json["data"]["universe_size"], 5); + assert_eq!( + json["data"]["subsets"], + serde_json::json!([[0, 2], [1, 3], [0, 4], [2, 4]]) + ); + assert_eq!(json["data"]["bound"], 1); + + std::fs::remove_file(output_path).ok(); +} + +#[test] +fn test_create_stacker_crane_json() { + let mut args = empty_args(); + args.problem = Some("StackerCrane".to_string()); + args.num_vertices = Some(6); + args.arcs = Some("0>4,2>5,5>1,3>0,4>3".to_string()); + args.graph = Some("0-1,1-2,2-3,3-5,4-5,0-3,1-5".to_string()); + args.arc_costs = Some("3,4,2,5,3".to_string()); + args.edge_lengths = Some("2,1,3,2,1,4,3".to_string()); + + let output_path = std::env::temp_dir().join("pred_test_create_stacker_crane.json"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let content = std::fs::read_to_string(&output_path).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "StackerCrane"); + assert_eq!(json["data"]["num_vertices"], 6); + assert_eq!(json["data"]["arcs"][0], serde_json::json!([0, 4])); + assert_eq!(json["data"]["edge_lengths"][6], 3); + + std::fs::remove_file(output_path).ok(); +} + +#[test] +fn test_create_stacker_crane_rejects_mismatched_arc_lengths() { + let mut args = empty_args(); + args.problem = Some("StackerCrane".to_string()); + args.num_vertices = Some(6); + args.arcs = Some("0>4,2>5,5>1,3>0,4>3".to_string()); + args.graph = Some("0-1,1-2,2-3,3-5,4-5,0-3,1-5".to_string()); + args.arc_costs = Some("3,4,2,5".to_string()); + args.edge_lengths = Some("2,1,3,2,1,4,3".to_string()); + args.bound = Some(20); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("Expected 5 arc costs but got 4")); +} + +#[test] +fn test_create_stacker_crane_rejects_out_of_range_vertices() { + let mut args = empty_args(); + args.problem = Some("StackerCrane".to_string()); + args.num_vertices = Some(5); + args.arcs = Some("0>4,2>5,5>1,3>0,4>3".to_string()); + args.graph = Some("0-1,1-2,2-3,3-5,4-5,0-3,1-5".to_string()); + args.arc_costs = Some("3,4,2,5,3".to_string()); + args.edge_lengths = Some("2,1,3,2,1,4,3".to_string()); + args.bound = Some(20); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("--num-vertices (5) is too small for the arcs")); +} + +#[test] +fn test_create_minimum_dummy_activities_pert_json() { + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::graph::MinimumDummyActivitiesPert; + + let mut args = empty_args(); + args.problem = Some("MinimumDummyActivitiesPert".to_string()); + args.num_vertices = Some(6); + args.arcs = Some("0>2,0>3,1>3,1>4,2>5".to_string()); + + let output_path = temp_output_path("minimum_dummy_activities_pert"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "MinimumDummyActivitiesPert"); + assert!(created.variant.is_empty()); + + let problem: MinimumDummyActivitiesPert = serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.num_vertices(), 6); + assert_eq!(problem.num_arcs(), 5); + + let _ = fs::remove_file(output_path); +} + +#[test] +fn test_create_minimum_dummy_activities_pert_rejects_cycles() { + let mut args = empty_args(); + args.problem = Some("MinimumDummyActivitiesPert".to_string()); + args.num_vertices = Some(3); + args.arcs = Some("0>1,1>2,2>0".to_string()); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("requires the input graph to be a DAG")); +} + +#[test] +fn test_create_balanced_complete_bipartite_subgraph() { + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::graph::BalancedCompleteBipartiteSubgraph; + + let mut args = empty_args(); + args.problem = Some("BalancedCompleteBipartiteSubgraph".to_string()); + args.biedges = Some("0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3".to_string()); + args.left = Some(4); + args.right = Some(4); + args.k = Some(3); + args.graph = None; + + let output_path = std::env::temp_dir().join(format!("bcbs-create-{}.json", std::process::id())); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = std::fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "BalancedCompleteBipartiteSubgraph"); + assert!(created.variant.is_empty()); + + let problem: BalancedCompleteBipartiteSubgraph = serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.left_size(), 4); + assert_eq!(problem.right_size(), 4); + assert_eq!(problem.num_edges(), 12); + assert_eq!(problem.k(), 3); + + let _ = std::fs::remove_file(output_path); +} + +#[test] +fn test_create_balanced_complete_bipartite_subgraph_rejects_out_of_range_biedges() { + let mut args = empty_args(); + args.problem = Some("BalancedCompleteBipartiteSubgraph".to_string()); + args.biedges = Some("4-0".to_string()); + args.left = Some(4); + args.right = Some(4); + args.k = Some(3); + args.graph = None; + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("out of bounds for left partition size 4")); +} + +#[test] +fn test_create_kclique() { + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::graph::KClique; + + let mut args = empty_args(); + args.problem = Some("KClique".to_string()); + args.graph = Some("0-1,0-2,1-3,2-3,2-4,3-4".to_string()); + args.k = Some(3); + + let output_path = + std::env::temp_dir().join(format!("kclique-create-{}.json", std::process::id())); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = std::fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "KClique"); + assert_eq!( + created.variant.get("graph").map(String::as_str), + Some("SimpleGraph") + ); + + let problem: KClique = serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.k(), 3); + assert_eq!(problem.num_vertices(), 5); + assert!(problem.evaluate(&[0, 0, 1, 1, 1])); + + let _ = std::fs::remove_file(output_path); +} + +#[test] +fn test_create_kclique_requires_valid_k() { + let mut args = empty_args(); + args.problem = Some("KClique".to_string()); + args.graph = Some("0-1,0-2,1-3,2-3,2-4,3-4".to_string()); + args.k = None; + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err(); + assert!( + err.to_string().contains("KClique requires --k"), + "unexpected error: {err}" + ); + + args.k = Some(6); + let err = create(&args, &out).unwrap_err(); + assert!( + err.to_string().contains("k must be <= graph num_vertices"), + "unexpected error: {err}" + ); +} + +#[test] +fn test_create_sparse_matrix_compression_json() { + use crate::dispatch::ProblemJsonOutput; + + let mut args = empty_args(); + args.problem = Some("SparseMatrixCompression".to_string()); + args.matrix = Some("1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0".to_string()); + args.bound = Some(2); + + let output_path = std::env::temp_dir().join(format!("smc-create-{}.json", std::process::id())); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = std::fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "SparseMatrixCompression"); + assert!(created.variant.is_empty()); + assert_eq!( + created.data, + serde_json::json!({ + "matrix": [ + [true, false, false, true], + [false, true, false, false], + [false, false, true, false], + [true, false, false, false], + ], + "bound_k": 2, + }) + ); + + let _ = std::fs::remove_file(output_path); +} + +#[test] +fn test_create_sparse_matrix_compression_requires_bound() { + let mut args = empty_args(); + args.problem = Some("SparseMatrixCompression".to_string()); + args.matrix = Some("1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0".to_string()); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("SparseMatrixCompression requires --matrix and --bound")); + assert!(err.contains("Usage: pred create SparseMatrixCompression")); +} + +#[test] +fn test_create_sparse_matrix_compression_rejects_zero_bound() { + let mut args = empty_args(); + args.problem = Some("SparseMatrixCompression".to_string()); + args.matrix = Some("1,0;0,1".to_string()); + args.bound = Some(0); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("bound >= 1")); +} + +#[test] +fn test_create_graph_partitioning_with_num_partitions() { + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::graph::GraphPartitioning; + use problemreductions::topology::SimpleGraph; + + let cli = Cli::try_parse_from([ + "pred", + "create", + "GraphPartitioning", + "--graph", + "0-1,1-2,2-3,3-0", + "--num-partitions", + "2", + ]) + .unwrap(); + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + let output_path = temp_output_path("graph-partitioning-create"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "GraphPartitioning"); + let problem: GraphPartitioning = serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.num_vertices(), 4); + + let _ = fs::remove_file(output_path); +} + +#[test] +fn test_create_nontautology_with_disjuncts_flag() { + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::formula::NonTautology; + + let cli = Cli::try_parse_from([ + "pred", + "create", + "NonTautology", + "--num-vars", + "3", + "--disjuncts", + "1,2,3;-1,-2,-3", + ]) + .unwrap(); + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + let output_path = temp_output_path("non-tautology-create"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "NonTautology"); + let problem: NonTautology = serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.disjuncts(), &[vec![1, 2, 3], vec![-1, -2, -3]]); + + let _ = fs::remove_file(output_path); +} + +#[test] +fn test_create_consecutive_ones_matrix_augmentation_json() { + use crate::dispatch::ProblemJsonOutput; + + let mut args = empty_args(); + args.problem = Some("ConsecutiveOnesMatrixAugmentation".to_string()); + args.matrix = Some("1,0,0,1,1;1,1,0,0,0;0,1,1,0,1;0,0,1,1,0".to_string()); + args.bound = Some(2); + + let output_path = std::env::temp_dir().join(format!("coma-create-{}.json", std::process::id())); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = std::fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "ConsecutiveOnesMatrixAugmentation"); + assert!(created.variant.is_empty()); + assert_eq!( + created.data, + serde_json::json!({ + "matrix": [ + [true, false, false, true, true], + [true, true, false, false, false], + [false, true, true, false, true], + [false, false, true, true, false], + ], + "bound": 2, + }) + ); + + let _ = std::fs::remove_file(output_path); +} + +#[test] +fn test_create_consecutive_ones_matrix_augmentation_requires_bound() { + let mut args = empty_args(); + args.problem = Some("ConsecutiveOnesMatrixAugmentation".to_string()); + args.matrix = Some("1,0;0,1".to_string()); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("ConsecutiveOnesMatrixAugmentation requires --matrix and --bound")); + assert!(err.contains("Usage: pred create ConsecutiveOnesMatrixAugmentation")); +} + +#[test] +fn test_create_consecutive_ones_matrix_augmentation_negative_bound() { + let mut args = empty_args(); + args.problem = Some("ConsecutiveOnesMatrixAugmentation".to_string()); + args.matrix = Some("1,0;0,1".to_string()); + args.bound = Some(-1); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("nonnegative")); +} diff --git a/problemreductions-cli/src/util.rs b/problemreductions-cli/src/util.rs index 9f2ffb115..0f9b08a3d 100644 --- a/problemreductions-cli/src/util.rs +++ b/problemreductions-cli/src/util.rs @@ -102,6 +102,7 @@ pub fn ser_kcoloring( } /// Serialize a KSatisfiability instance given clauses and validated k. +#[cfg(feature = "mcp")] pub fn ser_ksat( num_vars: usize, clauses: Vec, diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 448f86c7b..9c0eccae4 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -153,12 +153,12 @@ fn test_create_stacker_crane_schema_help_uses_documented_flags() { assert!(stderr.contains("StackerCrane"), "stderr: {stderr}"); assert!(stderr.contains("--arcs"), "stderr: {stderr}"); assert!(stderr.contains("--graph"), "stderr: {stderr}"); - assert!(stderr.contains("--arc-costs"), "stderr: {stderr}"); + assert!(stderr.contains("--arc-lengths"), "stderr: {stderr}"); assert!(stderr.contains("--edge-lengths"), "stderr: {stderr}"); assert!(stderr.contains("--num-vertices"), "stderr: {stderr}"); assert!(!stderr.contains("--bound"), "stderr: {stderr}"); assert!(!stderr.contains("--biedges"), "stderr: {stderr}"); - assert!(!stderr.contains("--arc-lengths"), "stderr: {stderr}"); + assert!(!stderr.contains("--arc-weights"), "stderr: {stderr}"); assert!(!stderr.contains("--edge-weights"), "stderr: {stderr}"); } @@ -1855,10 +1855,10 @@ fn test_create_comparative_containment_no_flags_shows_help() { "should exit non-zero when showing help without data flags" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("--universe"), "stderr: {stderr}"); + assert!(stderr.contains("--universe-size"), "stderr: {stderr}"); assert!(stderr.contains("--r-sets"), "stderr: {stderr}"); assert!(stderr.contains("--s-sets"), "stderr: {stderr}"); - assert!(!stderr.contains("--universe-size"), "stderr: {stderr}"); + assert!(!stderr.contains("--universe "), "stderr: {stderr}"); } #[test] @@ -1936,7 +1936,7 @@ fn test_create_help_lists_minimum_hitting_set_flags() { ); let stdout = String::from_utf8(output.stdout).unwrap(); assert!( - stdout.contains("MinimumHittingSet") && stdout.contains("--universe, --sets"), + stdout.contains("MinimumHittingSet") && stdout.contains("--universe-size, --subsets"), "stdout: {stdout}" ); } @@ -2030,7 +2030,7 @@ fn test_create_sequencing_to_minimize_weighted_tardiness_rejects_mismatched_leng .args([ "create", "SequencingToMinimizeWeightedTardiness", - "--sizes", + "--lengths", "3,4,2", "--weights", "2,3", @@ -2044,7 +2044,7 @@ fn test_create_sequencing_to_minimize_weighted_tardiness_rejects_mismatched_leng assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("sizes length (3) must equal weights length (2)"), + stderr.contains("lengths length (3) must equal weights length (2)"), "stderr: {stderr}" ); } @@ -3316,14 +3316,14 @@ fn test_create_bounded_component_spanning_forest_rejects_negative_bound() { "1,1,1,1", "--k", "2", - "--bound", + "--max-weight", "-1", ]) .output() .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("positive --bound"), "stderr: {stderr}"); + assert!(stderr.contains("positive --max-weight"), "stderr: {stderr}"); } #[test] @@ -3364,17 +3364,13 @@ fn test_create_bounded_component_spanning_forest_no_flags_shows_actual_cli_flags "expected '--k' in help output, got: {stderr}" ); assert!( - stderr.contains("--bound"), - "expected '--bound' in help output, got: {stderr}" + stderr.contains("--max-weight"), + "expected '--max-weight' in help output, got: {stderr}" ); assert!( !stderr.contains("--max-components"), "help should not advertise nonexistent '--max-components' flag: {stderr}" ); - assert!( - !stderr.contains("--max-weight"), - "help should not advertise nonexistent '--max-weight' flag: {stderr}" - ); } #[test] @@ -3989,7 +3985,7 @@ fn test_create_sequencing_to_minimize_weighted_tardiness_no_flags_shows_help() { .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("--sizes")); + assert!(stderr.contains("--lengths")); assert!(stderr.contains("--weights")); assert!(stderr.contains("--deadlines")); assert!(stderr.contains("--bound")); @@ -3997,7 +3993,7 @@ fn test_create_sequencing_to_minimize_weighted_tardiness_no_flags_shows_help() { } #[test] -fn test_create_multiple_choice_branching_help_uses_bound_flag() { +fn test_create_multiple_choice_branching_help_uses_threshold_flag() { let output = pred() .args(["create", "MultipleChoiceBranching/i32"]) .output() @@ -4005,12 +4001,12 @@ fn test_create_multiple_choice_branching_help_uses_bound_flag() { assert!(!output.status.success()); let stderr = String::from_utf8(output.stderr).unwrap(); assert!( - stderr.contains("--bound"), - "expected '--bound' in help output, got: {stderr}" + stderr.contains("--threshold"), + "expected '--threshold' in help output, got: {stderr}" ); assert!( - !stderr.contains("--threshold"), - "help output should not advertise '--threshold', got: {stderr}" + !stderr.contains("--bound"), + "help output should not advertise '--bound', got: {stderr}" ); assert!( stderr.contains("semicolon-separated groups"), @@ -4024,21 +4020,17 @@ fn test_create_set_basis_no_flags_uses_actual_cli_flag_names() { assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("--universe"), - "expected '--universe' in help output, got: {stderr}" + stderr.contains("--universe-size"), + "expected '--universe-size' in help output, got: {stderr}" ); assert!( - stderr.contains("--sets"), - "expected '--sets' in help output, got: {stderr}" + stderr.contains("--subsets"), + "expected '--subsets' in help output, got: {stderr}" ); assert!( stderr.contains("--k"), "expected '--k' in help output, got: {stderr}" ); - assert!( - !stderr.contains("--universe-size"), - "help should not advertise schema field names: {stderr}" - ); assert!( !stderr.contains("--collection"), "help should not advertise schema field names: {stderr}" @@ -4144,7 +4136,7 @@ fn test_create_help_uses_generic_matrix_and_k_descriptions() { } #[test] -fn test_create_length_bounded_disjoint_paths_help_uses_bound_flag() { +fn test_create_length_bounded_disjoint_paths_help_uses_max_length_flag() { let output = pred() .args(["create", "LengthBoundedDisjointPaths"]) .output() @@ -4152,12 +4144,12 @@ fn test_create_length_bounded_disjoint_paths_help_uses_bound_flag() { assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("--bound"), - "expected '--bound' in help output, got: {stderr}" + stderr.contains("--max-length"), + "expected '--max-length' in help output, got: {stderr}" ); assert!( - !stderr.contains("--max-length"), - "help should advertise the actual CLI flag name, got: {stderr}" + !stderr.contains("--bound"), + "help should advertise the canonical CLI flag name, got: {stderr}" ); } @@ -4196,24 +4188,24 @@ fn test_create_prime_attribute_name_no_flags_uses_actual_cli_flag_names() { "expected '--universe' in help output, got: {stderr}" ); assert!( - stderr.contains("--deps"), - "expected '--deps' in help output, got: {stderr}" + stderr.contains("--dependencies"), + "expected '--dependencies' in help output, got: {stderr}" ); assert!( - stderr.contains("--query"), - "expected '--query' in help output, got: {stderr}" + stderr.contains("--query-attribute"), + "expected '--query-attribute' in help output, got: {stderr}" ); assert!( !stderr.contains("--num-attributes"), "help should not advertise schema field names: {stderr}" ); assert!( - !stderr.contains("--dependencies"), - "help should not advertise schema field names: {stderr}" + !stderr.contains("--deps"), + "help should not advertise the legacy flag name: {stderr}" ); assert!( - !stderr.contains("--query-attribute"), - "help should not advertise schema field names: {stderr}" + !stderr.contains("--query\n"), + "help should not advertise the legacy flag name: {stderr}" ); } @@ -4567,7 +4559,7 @@ fn test_create_length_bounded_disjoint_paths_rejects_negative_bound_value() { "0", "--sink", "1", - "--bound", + "--max-length", "-1", ]) .output() @@ -4575,7 +4567,8 @@ fn test_create_length_bounded_disjoint_paths_rejects_negative_bound_value() { assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("--bound must be a nonnegative integer for LengthBoundedDisjointPaths"), + stderr + .contains("--max-length must be a nonnegative integer for LengthBoundedDisjointPaths"), "expected user-facing negative-bound error, got: {stderr}" ); } @@ -4591,14 +4584,15 @@ fn test_create_random_length_bounded_disjoint_paths_rejects_negative_bound_value "3", "--seed", "7", - "--bound=-1", + "--max-length=-1", ]) .output() .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("--bound must be a nonnegative integer for LengthBoundedDisjointPaths"), + stderr + .contains("--max-length must be a nonnegative integer for LengthBoundedDisjointPaths"), "expected shared negative-bound validation, got: {stderr}" ); } @@ -7023,7 +7017,7 @@ fn test_create_sequencing_to_minimize_maximum_cumulative_cost_invalid_precedence "SequencingToMinimizeMaximumCumulativeCost", "--costs", "1,-1,2", - "--precedence-pairs", + "--precedences", "a>b", "--bound", "2", @@ -7033,7 +7027,7 @@ fn test_create_sequencing_to_minimize_maximum_cumulative_cost_invalid_precedence assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("--precedence-pairs"), + stderr.contains("--precedences"), "expected flag-specific precedence parse error, got: {stderr}" ); } @@ -8227,20 +8221,20 @@ fn test_create_ensemble_computation_no_flags_uses_cli_flag_names() { ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("--universe"), - "expected --universe in help, got: {stderr}" + stderr.contains("--universe-size"), + "expected --universe-size in help, got: {stderr}" ); assert!( - stderr.contains("--sets"), - "expected --sets in help, got: {stderr}" + stderr.contains("--subsets"), + "expected --subsets in help, got: {stderr}" ); assert!( stderr.contains("--budget"), "expected --budget in help, got: {stderr}" ); assert!( - !stderr.contains("--universe-size"), - "help should use actual CLI flags, got: {stderr}" + !stderr.contains("--universe "), + "help should use canonical CLI flags, got: {stderr}" ); }