Skip to main content

problemreductions/
export.rs

1//! JSON export schema for example payloads.
2
3use crate::rules::registry::{ParameterContractError, ReductionParameterContract};
4use crate::rules::ReductionGraph;
5use crate::traits::Problem;
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8use std::fs;
9use std::path::Path;
10
11/// One side (source or target) of a reduction.
12#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
13pub struct ProblemSide {
14    /// Problem name matching `Problem::NAME` (e.g., `"MaximumIndependentSet"`).
15    pub problem: String,
16    /// Variant attributes (e.g., `{"graph": "SimpleGraph", "weight": "One"}`).
17    pub variant: BTreeMap<String, String>,
18    /// Problem-specific instance data (edges, matrix, clauses, etc.).
19    pub instance: serde_json::Value,
20}
21
22impl ProblemSide {
23    /// Build a serializable problem side from a typed problem.
24    pub fn from_problem<P>(problem: &P) -> Self
25    where
26        P: Problem + Serialize,
27    {
28        Self {
29            problem: P::NAME.to_string(),
30            variant: variant_to_map(P::variant()),
31            instance: serde_json::to_value(problem).expect("Failed to serialize problem instance"),
32        }
33    }
34
35    /// Extract the structural identity of this problem side.
36    pub fn problem_ref(&self) -> ProblemRef {
37        ProblemRef {
38            name: self.problem.clone(),
39            variant: self.variant.clone(),
40        }
41    }
42}
43
44/// Canonical structural identity for a problem node in the reduction graph.
45#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
46pub struct ProblemRef {
47    pub name: String,
48    pub variant: BTreeMap<String, String>,
49}
50
51/// One source↔target solution pair.
52#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
53pub struct SolutionPair {
54    pub source_config: serde_json::Value,
55    pub target_config: serde_json::Value,
56}
57
58/// A complete rule example: reduction + solutions in one file.
59#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
60pub struct RuleExample {
61    pub source: ProblemSide,
62    pub target: ProblemSide,
63    pub solutions: Vec<SolutionPair>,
64}
65
66/// A complete model example: instance + known optimal solution.
67#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
68pub struct ModelExample {
69    pub problem: String,
70    pub variant: BTreeMap<String, String>,
71    pub instance: serde_json::Value,
72    pub optimal_config: serde_json::Value,
73    pub optimal_value: serde_json::Value,
74}
75
76impl ModelExample {
77    pub fn new(
78        problem: &str,
79        variant: BTreeMap<String, String>,
80        instance: serde_json::Value,
81        optimal_config: serde_json::Value,
82        optimal_value: serde_json::Value,
83    ) -> Self {
84        Self {
85            problem: problem.to_string(),
86            variant,
87            instance,
88            optimal_config,
89            optimal_value,
90        }
91    }
92
93    pub fn problem_ref(&self) -> ProblemRef {
94        ProblemRef {
95            name: self.problem.clone(),
96            variant: self.variant.clone(),
97        }
98    }
99}
100
101/// Canonical exported database of rule examples.
102#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
103pub struct RuleDb {
104    pub rules: Vec<RuleExample>,
105}
106
107/// Canonical exported database of model examples.
108#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
109pub struct ModelDb {
110    pub models: Vec<ModelExample>,
111}
112
113/// Canonical exported database of model and rule examples.
114#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
115pub struct ExampleDb {
116    pub models: Vec<ModelExample>,
117    pub rules: Vec<RuleExample>,
118}
119
120/// Look up the explicit parameter contract for an exact direct reduction entry.
121pub fn lookup_parameter_contract(
122    source_name: &str,
123    source_variant: &BTreeMap<String, String>,
124    target_name: &str,
125    target_variant: &BTreeMap<String, String>,
126) -> Result<Option<ReductionParameterContract>, ParameterContractError> {
127    let graph = ReductionGraph::new();
128    let Some(matched) = graph.find_entry(source_name, source_variant, target_name, target_variant)
129    else {
130        return Ok(None);
131    };
132    matched.parameter_contract.map(Some)
133}
134
135/// Convert `Problem::variant()` output to a stable `BTreeMap`.
136///
137/// Normalizes empty `"graph"` values to `"SimpleGraph"` for consistency
138/// with the reduction graph convention.
139pub fn variant_to_map(variant: Vec<(&str, &str)>) -> BTreeMap<String, String> {
140    variant
141        .into_iter()
142        .map(|(k, v)| {
143            let value = if k == "graph" && v.is_empty() {
144                "SimpleGraph".to_string()
145            } else {
146                v.to_string()
147            };
148            (k.to_string(), value)
149        })
150        .collect()
151}
152
153fn write_json_file<T: Serialize>(dir: &Path, name: &str, payload: &T) {
154    fs::create_dir_all(dir).expect("Failed to create examples directory");
155    let path = dir.join(format!("{name}.json"));
156    let json = serde_json::to_string_pretty(payload).expect("Failed to serialize example");
157    fs::write(&path, json).expect("Failed to write example JSON");
158    println!("Exported: {}", path.display());
159}
160
161fn render_compact_array<T: Serialize>(items: &[T]) -> String {
162    if items.is_empty() {
163        "[]".to_string()
164    } else {
165        let rows = items
166            .iter()
167            .map(|item| {
168                format!(
169                    "    {}",
170                    serde_json::to_string(item).expect("Failed to serialize example entry")
171                )
172            })
173            .collect::<Vec<_>>()
174            .join(",\n");
175        format!("[\n{rows}\n  ]")
176    }
177}
178
179fn write_example_db_file(dir: &Path, db: &ExampleDb) {
180    fs::create_dir_all(dir).expect("Failed to create examples directory");
181    let path = dir.join("examples.json");
182    let json = format!(
183        "{{\n  \"models\": {},\n  \"rules\": {}\n}}\n",
184        render_compact_array(&db.models),
185        render_compact_array(&db.rules)
186    );
187    fs::write(&path, json).expect("Failed to write example JSON");
188    println!("Exported: {}", path.display());
189}
190
191/// Write a merged rule example JSON file.
192pub fn write_rule_example_to(dir: &Path, name: &str, example: &RuleExample) {
193    write_json_file(dir, name, example);
194}
195
196/// Write a model example JSON file to a target directory.
197pub fn write_model_example_to(dir: &Path, name: &str, example: &ModelExample) {
198    write_json_file(dir, name, example);
199}
200
201/// Write the canonical rule database as a wrapped JSON object.
202pub fn write_rule_db_to(dir: &Path, db: &RuleDb) {
203    write_json_file(dir, "rules", db);
204}
205
206/// Write the canonical model database as a wrapped JSON object.
207pub fn write_model_db_to(dir: &Path, db: &ModelDb) {
208    write_json_file(dir, "models", db);
209}
210
211/// Write the canonical example database as a wrapped JSON object.
212pub fn write_example_db_to(dir: &Path, db: &ExampleDb) {
213    write_example_db_file(dir, db);
214}
215
216#[cfg(test)]
217#[path = "unit_tests/export.rs"]
218mod tests;