Skip to main content

problemreductions/models/
decision.rs

1//! Generic decision wrapper for optimization problems.
2
3use crate::rules::{AggregateReductionResult, ReduceTo, ReduceToAggregate, ReductionResult};
4use crate::traits::Problem;
5use crate::types::{OptimizationValue, Or};
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Deserializer, Serialize};
8
9/// Metadata for concrete optimization problems that expose a decision wrapper.
10pub trait DecisionProblemMeta: Problem
11where
12    Self::Value: OptimizationValue,
13{
14    /// Problem name used by the corresponding `Decision<Self>` variant.
15    const DECISION_NAME: &'static str;
16}
17
18/// Register the decision problem name for a concrete optimization problem.
19#[macro_export]
20macro_rules! decision_problem_meta {
21    ($inner:ty, $name:literal) => {
22        impl $crate::models::decision::DecisionProblemMeta for $inner {
23            const DECISION_NAME: &'static str = $name;
24        }
25    };
26}
27
28/// Register the boilerplate inventory entries for a concrete `Decision<P>` variant.
29///
30/// Optional `additional: [Inner => "complexity", ...]` entries share the primary
31/// variant's construction fields and decoder without adding another schema or default.
32/// Both decision/optimization edges derive their identity parameter transforms directly
33/// from the inner problem's canonical parameter schema.
34#[macro_export]
35macro_rules! register_decision_variant {
36    (
37        $inner:ty,
38        $name:literal,
39        $complexity:literal,
40        $aliases:expr,
41        $description:literal,
42        category: $category:expr,
43        dims: [$($dim:expr),* $(,)?],
44        fields: [$($field:expr),* $(,)?],
45        $(additional: [$($additional:ty => $additional_complexity:literal),* $(,)?],)?
46        decode: $decoder:expr
47        $(, $random:ident)?
48    ) => {
49        $crate::register_decision_variant!(@declare $inner, $complexity, $decoder, [$($($additional => $additional_complexity),*)?] $(, $random)?);
50
51        $crate::inventory::submit! {
52            $crate::registry::ProblemSchemaEntry {
53                name: $name,
54                display_name: $crate::register_decision_variant!(@display_name $name),
55                aliases: $aliases,
56                dimensions: &[$($dim),*],
57                category: $category,
58                module_path: module_path!(),
59                description: $description,
60                fields: &[$($field),*],
61            }
62        }
63
64        $crate::register_decision_variant!(@edges $inner, $name);
65        $($(
66            $crate::register_brute_force! {
67                $crate::models::decision::Decision<$additional> decode $decoder,
68            }
69            $crate::register_decision_variant!(@edges $additional, $name);
70        )*)?
71    };
72
73    (@edges $inner:ty, $name:literal) => {
74        // Decision<P> → P: both witness (identity config) and aggregate (solve + compare)
75        $crate::inventory::submit! {
76            $crate::rules::ReductionEntry {
77                source_name: $name,
78                target_name: <$inner as $crate::traits::Problem>::NAME,
79                source_variant_fn: <$crate::models::decision::Decision<$inner> as $crate::traits::Problem>::variant,
80                target_variant_fn: <$inner as $crate::traits::Problem>::variant,
81                parameter_declarations_fn: || $crate::rules::registry::ReductionParameterDeclarations {
82                    relation: Some($crate::parameters::ParameterRelation::Exact),
83                    fields: <$inner as $crate::traits::Problem>::parameter_names()
84                        .iter()
85                        .map(|&name| (name, $crate::expr::Expr::variable(name)))
86                        .collect(),
87                    unavailable: vec![],
88                },
89                module_path: module_path!(),
90                reduce_fn: Some(|any| {
91                    let source = any
92                        .downcast_ref::<$crate::models::decision::Decision<$inner>>()
93                        .ok_or_else($crate::rules::ReductionError::source_type_mismatch::<
94                            $crate::models::decision::Decision<$inner>,
95                            $inner,
96                        >)?;
97                    let result =
98                        <$crate::models::decision::Decision<$inner> as $crate::rules::ReduceTo<$inner>>::reduce_to(source)?;
99                    Ok(Box::new(result))
100                }),
101                reduce_aggregate_fn: Some(|any| {
102                    let source = any
103                        .downcast_ref::<$crate::models::decision::Decision<$inner>>()
104                        .ok_or_else($crate::rules::ReductionError::source_type_mismatch::<
105                            $crate::models::decision::Decision<$inner>,
106                            $inner,
107                        >)?;
108                    let result =
109                        <$crate::models::decision::Decision<$inner> as $crate::rules::ReduceToAggregate<$inner>>::reduce_to_aggregate(source)?;
110                    Ok(Box::new(result))
111                }),
112                turing: false,
113            }
114        }
115
116        // Reverse edge: P → Decision<P> (Turing/multi-query reduction via binary search)
117        $crate::inventory::submit! {
118            $crate::rules::ReductionEntry {
119                source_name: <$inner as $crate::traits::Problem>::NAME,
120                target_name: $name,
121                source_variant_fn: <$inner as $crate::traits::Problem>::variant,
122                target_variant_fn: <$crate::models::decision::Decision<$inner> as $crate::traits::Problem>::variant,
123                parameter_declarations_fn: || $crate::rules::registry::ReductionParameterDeclarations {
124                    relation: Some($crate::parameters::ParameterRelation::Exact),
125                    fields: <$inner as $crate::traits::Problem>::parameter_names()
126                        .iter()
127                        .map(|&name| (name, $crate::expr::Expr::variable(name)))
128                        .collect(),
129                    unavailable: vec![],
130                },
131                module_path: module_path!(),
132                reduce_fn: None,
133                reduce_aggregate_fn: None,
134                turing: true,
135            }
136        }
137    };
138
139    (@declare $inner:ty, $complexity:literal, $decoder:expr, [$($additional:ty => $additional_complexity:literal),*], random) => {
140        $crate::declare_variants! {
141            default $crate::models::decision::Decision<$inner> => $complexity create $crate::models::decision::DecisionCreateSpec<$inner> random,
142            $($crate::models::decision::Decision<$additional> => $additional_complexity create $crate::models::decision::DecisionCreateSpec<$additional>,)*
143        }
144        $crate::register_brute_force! {
145            $crate::models::decision::Decision<$inner> decode $decoder,
146        }
147    };
148    (@declare $inner:ty, $complexity:literal, $decoder:expr, [$($additional:ty => $additional_complexity:literal),*]) => {
149        $crate::declare_variants! {
150            default $crate::models::decision::Decision<$inner> => $complexity create $crate::models::decision::DecisionCreateSpec<$inner>,
151            $($crate::models::decision::Decision<$additional> => $additional_complexity create $crate::models::decision::DecisionCreateSpec<$additional>,)*
152        }
153        $crate::register_brute_force! {
154            $crate::models::decision::Decision<$inner> decode $decoder,
155        }
156    };
157    (@display_name "DecisionMinimumVertexCover") => {
158        "Decision Minimum Vertex Cover"
159    };
160    (@display_name "DecisionMinimumDominatingSet") => {
161        "Decision Minimum Dominating Set"
162    };
163    (@display_name "DecisionMaximumIndependentSet") => {
164        "Decision Maximum Independent Set"
165    };
166    (@display_name $name:literal) => {
167        $name
168    };
169}
170
171/// Flat construction DTO used by [`register_decision_variant!`].
172///
173/// Persisted decision problems remain `{ "inner": ..., "bound": ... }`, while
174/// construction inputs expose the inner problem's fields beside `bound`.
175#[doc(hidden)]
176pub struct DecisionCreateSpec<P>
177where
178    P: Problem,
179    P::Value: OptimizationValue,
180{
181    inner: P,
182    bound: <P::Value as OptimizationValue>::Inner,
183}
184
185impl<P> crate::registry::CreateSpec for DecisionCreateSpec<P>
186where
187    P: Problem,
188    P::Value: OptimizationValue,
189{
190    const FIELDS: &'static [crate::registry::FieldInfo] = &[];
191
192    fn inputs() -> Vec<crate::registry::CreateInputInfo> {
193        let variant = P::variant()
194            .into_iter()
195            .map(|(k, v)| (k.to_string(), v.to_string()))
196            .collect();
197        let entry = crate::registry::find_variant_entry(P::NAME, &variant)
198            .expect("decision inner variant must be registered");
199        let mut inputs = entry.inputs();
200        inputs.push(crate::registry::CreateInputInfo {
201            name: "bound",
202            type_name: std::any::type_name::<<P::Value as OptimizationValue>::Inner>(),
203            description: "Decision objective bound",
204            required: true,
205            codec: crate::registry::CreateInputCodec::Scalar,
206        });
207        inputs
208    }
209}
210
211impl<'de, P> Deserialize<'de> for DecisionCreateSpec<P>
212where
213    P: Problem + 'static,
214    P::Value: OptimizationValue,
215    <P::Value as OptimizationValue>::Inner: DeserializeOwned,
216{
217    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
218    where
219        D: Deserializer<'de>,
220    {
221        let value = serde_json::Value::deserialize(deserializer)?;
222        let mut inputs = value.as_object().cloned().ok_or_else(|| {
223            serde::de::Error::custom("decision construction inputs must be an object")
224        })?;
225        let bound = inputs
226            .remove("bound")
227            .ok_or_else(|| serde::de::Error::missing_field("bound"))?;
228        let variant = P::variant()
229            .into_iter()
230            .map(|(k, v)| (k.to_string(), v.to_string()))
231            .collect();
232        let entry = crate::registry::find_variant_entry(P::NAME, &variant)
233            .expect("decision inner variant must be registered");
234        let constructed = (entry.construct_fn)(serde_json::Value::Object(inputs))
235            .map_err(serde::de::Error::custom)?;
236        let inner = *(constructed as Box<dyn std::any::Any>)
237            .downcast::<P>()
238            .expect("registered constructor must return the declared inner type");
239        let bound = serde_json::from_value(bound).map_err(serde::de::Error::custom)?;
240        Ok(Self { inner, bound })
241    }
242}
243
244impl<P> From<DecisionCreateSpec<P>> for Decision<P>
245where
246    P: Problem,
247    P::Value: OptimizationValue,
248{
249    fn from(spec: DecisionCreateSpec<P>) -> Self {
250        Self::new(spec.inner, spec.bound)
251    }
252}
253
254/// Decision version of an optimization problem with a fixed objective bound.
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct Decision<P: Problem>
257where
258    P::Value: OptimizationValue,
259{
260    inner: P,
261    bound: <P::Value as OptimizationValue>::Inner,
262}
263
264impl<P: Problem> Decision<P>
265where
266    P::Value: OptimizationValue,
267{
268    /// Create a decision wrapper around `inner` with the provided bound.
269    pub fn new(inner: P, bound: <P::Value as OptimizationValue>::Inner) -> Self {
270        Self { inner, bound }
271    }
272
273    /// Borrow the wrapped optimization problem.
274    pub fn inner(&self) -> &P {
275        &self.inner
276    }
277
278    /// Borrow the decision bound.
279    pub fn bound(&self) -> &<P::Value as OptimizationValue>::Inner {
280        &self.bound
281    }
282}
283
284impl<P> Problem for Decision<P>
285where
286    P: DecisionProblemMeta,
287    P::Value: OptimizationValue,
288{
289    const NAME: &'static str = P::DECISION_NAME;
290    type Solution = P::Solution;
291    type Value = Or;
292
293    fn parameter_names() -> &'static [&'static str] {
294        P::parameter_names()
295    }
296
297    fn parameters(&self) -> crate::types::ProblemParameters {
298        self.inner.parameters()
299    }
300
301    fn evaluate(&self, config: &Self::Solution) -> Result<Or, crate::traits::EvaluationError> {
302        Ok({
303            Or(<P::Value as OptimizationValue>::meets_bound(
304                &self.inner.evaluate(config)?,
305                &self.bound,
306            ))
307        })
308    }
309
310    fn variant() -> Vec<(&'static str, &'static str)> {
311        P::variant()
312    }
313}
314
315impl<P> crate::solvers::BruteForceProblem for Decision<P>
316where
317    P: DecisionProblemMeta + crate::solvers::BruteForceProblem,
318    P::Value: OptimizationValue,
319{
320    fn dimensions(&self) -> Vec<usize> {
321        self.inner.dimensions()
322    }
323}
324
325/// Aggregate reduction result for `Decision<P> -> P`.
326#[derive(Debug, Clone)]
327pub struct DecisionToOptimizationResult<P>
328where
329    P: Problem,
330    P::Value: OptimizationValue,
331{
332    target: P,
333    bound: <P::Value as OptimizationValue>::Inner,
334}
335
336impl<P> AggregateReductionResult for DecisionToOptimizationResult<P>
337where
338    P: DecisionProblemMeta + 'static,
339    P::Value: OptimizationValue + Serialize + DeserializeOwned,
340{
341    type Source = Decision<P>;
342    type Target = P;
343
344    fn target_problem(&self) -> &Self::Target {
345        &self.target
346    }
347
348    fn extract_value(&self, target_value: P::Value) -> Or {
349        Or(<P::Value as OptimizationValue>::meets_bound(
350            &target_value,
351            &self.bound,
352        ))
353    }
354}
355
356impl<P> ReduceToAggregate<P> for Decision<P>
357where
358    P: DecisionProblemMeta + Clone + 'static,
359    P::Value: OptimizationValue + Serialize + DeserializeOwned,
360{
361    type Result = DecisionToOptimizationResult<P>;
362
363    fn reduce_to_aggregate(&self) -> Result<Self::Result, crate::rules::ReductionError> {
364        Ok(DecisionToOptimizationResult {
365            target: self.inner.clone(),
366            bound: self.bound.clone(),
367        })
368    }
369}
370
371/// Witness reduction result for `Decision<P> -> P`.
372///
373/// The configuration spaces are identical — a config that is optimal for
374/// `P` and meets the bound is a valid `Decision<P>` witness. The
375/// `extract_solution` is the identity function.
376#[derive(Debug, Clone)]
377pub struct DecisionToOptimizationWitnessResult<P>
378where
379    P: Problem,
380    P::Value: OptimizationValue,
381{
382    target: P,
383}
384
385impl<P> ReductionResult for DecisionToOptimizationWitnessResult<P>
386where
387    P: DecisionProblemMeta + 'static,
388    P::Solution: Clone,
389    P::Value: OptimizationValue + Serialize + DeserializeOwned,
390{
391    type Source = Decision<P>;
392    type Target = P;
393
394    fn target_problem(&self) -> &Self::Target {
395        &self.target
396    }
397
398    fn extract_solution(
399        &self,
400        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
401    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
402        crate::rules::validate_target_solution(self.target_problem(), target_solution)?;
403
404        Ok(target_solution.clone())
405    }
406}
407
408impl<P> ReduceTo<P> for Decision<P>
409where
410    P: DecisionProblemMeta + Clone + 'static,
411    P::Solution: Clone,
412    P::Value: OptimizationValue + Serialize + DeserializeOwned,
413{
414    type Result = DecisionToOptimizationWitnessResult<P>;
415
416    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
417        Ok(DecisionToOptimizationWitnessResult {
418            target: self.inner.clone(),
419        })
420    }
421}
422
423#[cfg(test)]
424#[path = "../unit_tests/models/decision.rs"]
425mod tests;