Skip to main content

problemreductions/models/misc/
minimum_axiom_set.rs

1//! Minimum Axiom Set problem implementation.
2//!
3//! Given a finite set of sentences S, a subset T ⊆ S of true sentences, and a set
4//! of implications (where each implication has a set of antecedent sentences and a
5//! single consequent sentence), find a smallest subset S₀ ⊆ T such that the
6//! deductive closure of S₀ under the implications equals T.
7
8use crate::registry::{FieldInfo, ProblemSchemaEntry};
9use crate::traits::Problem;
10use crate::types::Min;
11use serde::{Deserialize, Serialize};
12
13inventory::submit! {
14    ProblemSchemaEntry {
15        name: "MinimumAxiomSet",
16        display_name: "Minimum Axiom Set",
17        aliases: &[],
18        dimensions: &[],
19        category: crate::registry::ProblemCategory::Misc,
20        module_path: module_path!(),
21        description: "Find smallest axiom subset whose deductive closure equals the true sentences",
22        fields: &[
23            FieldInfo { name: "num_sentences", type_name: "usize", description: "Total number of sentences |S|" },
24            FieldInfo { name: "true_sentences", type_name: "Vec<usize>", description: "Indices of true sentences T ⊆ S" },
25            FieldInfo { name: "implications", type_name: "Vec<(Vec<usize>, usize)>", description: "Implication rules (antecedents, consequent)" },
26        ],
27    }
28}
29
30/// The Minimum Axiom Set problem.
31///
32/// Given a set of sentences `S = {0, ..., num_sentences - 1}`, a subset
33/// `T ⊆ S` of true sentences, and a list of implications where each
34/// implication `(A, c)` means "if all sentences in A hold, then c holds",
35/// find a smallest subset `S₀ ⊆ T` whose deductive closure under the
36/// implications equals `T`.
37///
38/// # Representation
39///
40/// Each true sentence has a binary variable: `config[i] = 1` if
41/// `true_sentences[i]` is selected as an axiom, `0` otherwise.
42/// The configuration space is `vec![2; |T|]`.
43///
44/// # Example
45///
46/// ```
47/// use problemreductions::models::misc::MinimumAxiomSet;
48/// use problemreductions::{Problem, BruteForce};
49///
50/// // 8 sentences, all true, with implications forming a cycle
51/// let problem = MinimumAxiomSet::new(
52///     8,
53///     vec![0, 1, 2, 3, 4, 5, 6, 7],
54///     vec![
55///         (vec![0], 2), (vec![0], 3),
56///         (vec![1], 4), (vec![1], 5),
57///         (vec![2, 4], 6), (vec![3, 5], 7),
58///         (vec![6, 7], 0), (vec![6, 7], 1),
59///     ],
60/// );
61/// let solver = BruteForce::new();
62/// let solution = solver.solve(&problem).unwrap();
63/// assert!(solution.is_some());
64/// ```
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(try_from = "MinimumAxiomSetData")]
67pub struct MinimumAxiomSet {
68    /// Total number of sentences |S|.
69    num_sentences: usize,
70    /// Indices of true sentences T ⊆ S.
71    true_sentences: Vec<usize>,
72    /// Implication rules: each (antecedents, consequent).
73    implications: Vec<(Vec<usize>, usize)>,
74}
75
76#[derive(Deserialize)]
77struct MinimumAxiomSetData {
78    num_sentences: usize,
79    true_sentences: Vec<usize>,
80    implications: Vec<(Vec<usize>, usize)>,
81}
82
83impl TryFrom<MinimumAxiomSetData> for MinimumAxiomSet {
84    type Error = crate::registry::ConstructionError;
85    fn try_from(data: MinimumAxiomSetData) -> Result<Self, Self::Error> {
86        Self::try_new(data.num_sentences, data.true_sentences, data.implications)
87    }
88}
89
90impl MinimumAxiomSet {
91    /// Create a new Minimum Axiom Set instance.
92    ///
93    /// # Panics
94    ///
95    /// Panics if any true sentence index is out of range,
96    /// if true sentences contain duplicates,
97    /// or if any implication references a sentence outside S.
98    pub fn new(
99        num_sentences: usize,
100        true_sentences: Vec<usize>,
101        implications: Vec<(Vec<usize>, usize)>,
102    ) -> Self {
103        Self::try_new(num_sentences, true_sentences, implications)
104            .unwrap_or_else(|error| panic!("{error}"))
105    }
106
107    fn try_new(
108        num_sentences: usize,
109        true_sentences: Vec<usize>,
110        implications: Vec<(Vec<usize>, usize)>,
111    ) -> Result<Self, crate::registry::ConstructionError> {
112        // Validate true sentences
113        for &s in &true_sentences {
114            if s >= num_sentences {
115                return Err(
116                    format!("True sentence index {s} out of range [0, {num_sentences})").into(),
117                );
118            }
119        }
120        // Check no duplicates
121        let mut seen = vec![false; num_sentences];
122        for &s in &true_sentences {
123            if seen[s] {
124                return Err(format!("Duplicate true sentence index {s}").into());
125            }
126            seen[s] = true;
127        }
128        // Validate implications
129        for (antecedents, consequent) in &implications {
130            for &a in antecedents {
131                if a >= num_sentences {
132                    return Err(format!(
133                        "Implication antecedent {a} out of range [0, {num_sentences})"
134                    )
135                    .into());
136                }
137            }
138            if *consequent >= num_sentences {
139                return Err(format!(
140                    "Implication consequent {consequent} out of range [0, {num_sentences})"
141                )
142                .into());
143            }
144        }
145        Ok(Self {
146            num_sentences,
147            true_sentences,
148            implications,
149        })
150    }
151
152    /// Returns the total number of sentences |S|.
153    pub fn num_sentences(&self) -> usize {
154        self.num_sentences
155    }
156
157    /// Returns the number of true sentences |T|.
158    pub fn num_true_sentences(&self) -> usize {
159        self.true_sentences.len()
160    }
161
162    /// Returns the number of implications.
163    pub fn num_implications(&self) -> usize {
164        self.implications.len()
165    }
166
167    /// Returns the true sentence indices.
168    pub fn true_sentences(&self) -> &[usize] {
169        &self.true_sentences
170    }
171
172    /// Returns the implications.
173    pub fn implications(&self) -> &[(Vec<usize>, usize)] {
174        &self.implications
175    }
176}
177
178/// Compute the deductive closure of a set of sentences under the given implications.
179///
180/// Starting from `current`, repeatedly applies implications until a fixpoint.
181fn deductive_closure(current: &mut [bool], implications: &[(Vec<usize>, usize)]) {
182    loop {
183        let mut changed = false;
184        for (antecedents, consequent) in implications {
185            if !current[*consequent] && antecedents.iter().all(|&a| current[a]) {
186                current[*consequent] = true;
187                changed = true;
188            }
189        }
190        if !changed {
191            break;
192        }
193    }
194}
195
196impl Problem for MinimumAxiomSet {
197    const NAME: &'static str = "MinimumAxiomSet";
198    type Solution = Vec<bool>;
199    type Value = Min<i64>;
200
201    crate::problem_parameters![
202        ("num_implications", num_implications),
203        ("num_sentences", num_sentences),
204        ("num_true_sentences", num_true_sentences),
205    ];
206
207    fn variant() -> Vec<(&'static str, &'static str)> {
208        crate::variant_params![]
209    }
210
211    fn evaluate(
212        &self,
213        config: &Self::Solution,
214    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
215        Ok({
216            if config.len() != self.num_true_sentences() {
217                return Err(crate::traits::EvaluationError::InvalidConfiguration(
218                    "axiom-selection length does not match the true sentences".into(),
219                ));
220            }
221            // Build the initial set of selected axioms
222            let mut current = vec![false; self.num_sentences];
223            let mut count = 0usize;
224            for (i, &v) in config.iter().enumerate() {
225                if v {
226                    current[self.true_sentences[i]] = true;
227                    count += 1;
228                }
229            }
230
231            // Compute deductive closure
232            deductive_closure(&mut current, &self.implications);
233
234            // Check if closure equals T
235            let closure_equals_t = self.true_sentences.iter().all(|&s| current[s]);
236
237            if closure_equals_t {
238                Min(Some(i64::try_from(count).map_err(|_| {
239                    crate::traits::EvaluationError::IntegerOverflow(
240                        "converting axiom-set size to i64".into(),
241                    )
242                })?))
243            } else {
244                Min(None)
245            }
246        })
247    }
248}
249
250impl crate::solvers::BruteForceProblem for MinimumAxiomSet {
251    fn dimensions(&self) -> Vec<usize> {
252        vec![2; self.num_true_sentences()]
253    }
254}
255
256crate::declare_variants! {
257    default MinimumAxiomSet => "2^num_true_sentences",
258}
259
260crate::register_brute_force! {
261    MinimumAxiomSet decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
262}
263
264#[cfg(feature = "example-db")]
265pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
266    // 8 sentences, all true, with implications forming a cycle
267    // Optimal: select {a, b} (indices 0, 1) → closure = all 8
268    vec![crate::example_db::specs::ModelExampleSpec {
269        id: "minimum_axiom_set",
270        instance: Box::new(MinimumAxiomSet::new(
271            8,
272            vec![0, 1, 2, 3, 4, 5, 6, 7],
273            vec![
274                (vec![0], 2),
275                (vec![0], 3),
276                (vec![1], 4),
277                (vec![1], 5),
278                (vec![2, 4], 6),
279                (vec![3, 5], 7),
280                (vec![6, 7], 0),
281                (vec![6, 7], 1),
282            ],
283        )),
284        optimal_config: serde_json::json!(vec![
285            true, true, false, false, false, false, false, false
286        ]),
287        optimal_value: serde_json::json!(2),
288    }]
289}
290
291#[cfg(test)]
292#[path = "../../unit_tests/models/misc/minimum_axiom_set.rs"]
293mod tests;