Skip to main content

problemreductions/models/misc/
consistency_of_database_frequency_tables.rs

1//! Consistency of Database Frequency Tables problem implementation.
2//!
3//! Given a finite set of objects, categorical attributes with finite domains,
4//! pairwise frequency tables for selected attribute pairs, and some known
5//! object-attribute values, determine whether there exists a complete
6//! assignment of attribute values to all objects that matches every published
7//! frequency table and every known value.
8
9use crate::registry::{CreateSpec, ProblemSchemaEntry};
10use crate::traits::Problem;
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeSet;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct FrequencyTable {
16    attribute_a: usize,
17    attribute_b: usize,
18    counts: Vec<Vec<i64>>,
19}
20
21impl FrequencyTable {
22    /// Create a new pairwise frequency table.
23    pub fn new(attribute_a: usize, attribute_b: usize, counts: Vec<Vec<i64>>) -> Self {
24        Self {
25            attribute_a,
26            attribute_b,
27            counts,
28        }
29    }
30
31    /// Returns the first attribute index.
32    pub fn attribute_a(&self) -> usize {
33        self.attribute_a
34    }
35
36    /// Returns the second attribute index.
37    pub fn attribute_b(&self) -> usize {
38        self.attribute_b
39    }
40
41    /// Returns the table counts.
42    pub fn counts(&self) -> &[Vec<i64>] {
43        &self.counts
44    }
45
46    /// Returns the number of table cells.
47    pub fn num_cells(&self) -> usize {
48        self.counts.iter().map(Vec::len).sum()
49    }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct KnownValue {
54    object: usize,
55    attribute: usize,
56    value: usize,
57}
58
59impl KnownValue {
60    /// Create a new known object-attribute value.
61    pub fn new(object: usize, attribute: usize, value: usize) -> Self {
62        Self {
63            object,
64            attribute,
65            value,
66        }
67    }
68
69    /// Returns the object index.
70    pub fn object(&self) -> usize {
71        self.object
72    }
73
74    /// Returns the attribute index.
75    pub fn attribute(&self) -> usize {
76        self.attribute
77    }
78
79    /// Returns the fixed categorical value.
80    pub fn value(&self) -> usize {
81        self.value
82    }
83}
84
85inventory::submit! {
86    ProblemSchemaEntry {
87        name: "ConsistencyOfDatabaseFrequencyTables",
88        display_name: "Consistency of Database Frequency Tables",
89        aliases: &[],
90        dimensions: &[],
91        category: crate::registry::ProblemCategory::Misc,
92        module_path: module_path!(),
93        description: "Determine whether pairwise frequency tables and known values admit a consistent complete database assignment",
94        fields: ConsistencyOfDatabaseFrequencyTablesCreateSpec::FIELDS,
95    }
96}
97
98/// The Consistency of Database Frequency Tables decision problem.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct ConsistencyOfDatabaseFrequencyTables {
101    num_objects: usize,
102    attribute_domains: Vec<usize>,
103    frequency_tables: Vec<FrequencyTable>,
104    known_values: Vec<KnownValue>,
105}
106
107#[derive(Debug, Deserialize, crate::CreateSpec)]
108struct ConsistencyOfDatabaseFrequencyTablesCreateSpec {
109    /// Number of database objects.
110    num_objects: usize,
111    /// Domain size for each attribute.
112    #[create(codec = "comma-separated")]
113    attribute_domains: Vec<usize>,
114    /// Pairwise frequency tables as JSON objects.
115    #[create(codec = "json")]
116    frequency_tables: Vec<FrequencyTable>,
117    /// Known object-attribute values as JSON objects; defaults to empty.
118    #[create(codec = "json")]
119    known_values: Option<Vec<KnownValue>>,
120}
121
122impl TryFrom<ConsistencyOfDatabaseFrequencyTablesCreateSpec>
123    for ConsistencyOfDatabaseFrequencyTables
124{
125    type Error = crate::registry::ConstructionError;
126    fn try_from(spec: ConsistencyOfDatabaseFrequencyTablesCreateSpec) -> Result<Self, Self::Error> {
127        let known_values = spec.known_values.unwrap_or_default();
128        validate_cdft_create(
129            spec.num_objects,
130            &spec.attribute_domains,
131            &spec.frequency_tables,
132            &known_values,
133        )?;
134        Ok(Self {
135            num_objects: spec.num_objects,
136            attribute_domains: spec.attribute_domains,
137            frequency_tables: spec.frequency_tables,
138            known_values,
139        })
140    }
141}
142
143fn validate_cdft_create(
144    num_objects: usize,
145    domains: &[usize],
146    tables: &[FrequencyTable],
147    known: &[KnownValue],
148) -> Result<(), crate::registry::ConstructionError> {
149    for (attribute, &size) in domains.iter().enumerate() {
150        if size == 0 {
151            return Err(
152                format!("attribute domain size at index {attribute} must be positive").into(),
153            );
154        }
155    }
156    let mut pairs = BTreeSet::new();
157    for table in tables {
158        let a = table.attribute_a();
159        let b = table.attribute_b();
160        if a >= domains.len() || b >= domains.len() {
161            return Err("frequency table attribute is out of range".into());
162        }
163        if a == b {
164            return Err("frequency table attributes must be distinct".into());
165        }
166        let pair = if a < b { (a, b) } else { (b, a) };
167        if !pairs.insert(pair) {
168            return Err(format!("duplicate frequency table pair ({}, {})", pair.0, pair.1).into());
169        }
170        if table.counts().len() != domains[a] {
171            return Err(
172                format!("frequency table rows must equal domain size for attribute {a}").into(),
173            );
174        }
175        if table.counts().iter().any(|row| row.len() != domains[b]) {
176            return Err(format!(
177                "frequency table column count must equal domain size for attribute {b}"
178            )
179            .into());
180        }
181        if table.counts().iter().flatten().any(|&count| count < 0) {
182            return Err("frequency table counts must be nonnegative".into());
183        }
184        let total = table
185            .counts()
186            .iter()
187            .flatten()
188            .try_fold(0_i64, |sum, &value| {
189                sum.checked_add(value)
190                    .ok_or("frequency table count total overflows i64")
191            })?;
192        let expected_total =
193            i64::try_from(num_objects).map_err(|_| "num_objects cannot be represented as i64")?;
194        if total != expected_total {
195            return Err(format!(
196                "frequency table total {total} must equal num_objects {num_objects}"
197            )
198            .into());
199        }
200    }
201    for value in known {
202        if value.object() >= num_objects {
203            return Err("known value object is out of range".into());
204        }
205        if value.attribute() >= domains.len() {
206            return Err("known value attribute is out of range".into());
207        }
208        if value.value() >= domains[value.attribute()] {
209            return Err("known value value is outside the attribute domain".into());
210        }
211    }
212    Ok(())
213}
214
215impl ConsistencyOfDatabaseFrequencyTables {
216    /// Create a new consistency-of-database-frequency-tables instance.
217    pub fn new(
218        num_objects: usize,
219        attribute_domains: Vec<usize>,
220        frequency_tables: Vec<FrequencyTable>,
221        known_values: Vec<KnownValue>,
222    ) -> Self {
223        validate_cdft_create(
224            num_objects,
225            &attribute_domains,
226            &frequency_tables,
227            &known_values,
228        )
229        .unwrap_or_else(|error| panic!("{error}"));
230
231        Self {
232            num_objects,
233            attribute_domains,
234            frequency_tables,
235            known_values,
236        }
237    }
238
239    /// Returns the number of objects.
240    pub fn num_objects(&self) -> usize {
241        self.num_objects
242    }
243
244    /// Returns the number of attributes.
245    pub fn num_attributes(&self) -> usize {
246        self.attribute_domains.len()
247    }
248
249    /// Returns the attribute-domain sizes.
250    pub fn attribute_domains(&self) -> &[usize] {
251        &self.attribute_domains
252    }
253
254    /// Returns the published frequency tables.
255    pub fn frequency_tables(&self) -> &[FrequencyTable] {
256        &self.frequency_tables
257    }
258
259    /// Returns the known values.
260    pub fn known_values(&self) -> &[KnownValue] {
261        &self.known_values
262    }
263
264    /// Returns the product of attribute domain sizes.
265    pub fn domain_size_product(&self) -> usize {
266        self.attribute_domains.iter().copied().product()
267    }
268
269    /// Returns the sum of all attribute-domain sizes.
270    pub fn total_domain_size(&self) -> usize {
271        self.attribute_domains.iter().sum()
272    }
273
274    /// Returns the number of object-attribute assignment variables in the direct encoding.
275    pub fn num_assignment_variables(&self) -> usize {
276        self.num_objects * self.num_attributes()
277    }
278
279    /// Returns the number of published frequency tables.
280    pub fn num_frequency_tables(&self) -> usize {
281        self.frequency_tables.len()
282    }
283
284    /// Returns the number of known value constraints.
285    pub fn num_known_values(&self) -> usize {
286        self.known_values.len()
287    }
288
289    /// Returns the number of one-hot assignment indicators used by the ILP reduction.
290    pub fn num_assignment_indicators(&self) -> usize {
291        self.num_objects * self.attribute_domains.iter().sum::<usize>()
292    }
293
294    /// Returns the total number of published frequency-table cells.
295    pub fn num_frequency_cells(&self) -> usize {
296        self.frequency_tables
297            .iter()
298            .map(FrequencyTable::num_cells)
299            .sum()
300    }
301
302    /// Returns the number of auxiliary ILP indicators used for frequency-cell counting.
303    pub fn num_auxiliary_frequency_indicators(&self) -> usize {
304        self.num_objects * self.num_frequency_cells()
305    }
306
307    fn config_index(&self, object: usize, attribute: usize) -> usize {
308        object * self.num_attributes() + attribute
309    }
310}
311
312impl Problem for ConsistencyOfDatabaseFrequencyTables {
313    const NAME: &'static str = "ConsistencyOfDatabaseFrequencyTables";
314    type Solution = Vec<usize>;
315    type Value = crate::types::Or;
316
317    crate::problem_parameters![
318        ("num_objects", num_objects),
319        ("num_attributes", num_attributes),
320        ("total_domain_size", total_domain_size),
321        ("domain_size_product", domain_size_product),
322        ("num_frequency_tables", num_frequency_tables),
323        ("num_frequency_cells", num_frequency_cells),
324        ("num_known_values", num_known_values),
325    ];
326
327    fn variant() -> Vec<(&'static str, &'static str)> {
328        crate::variant_params![]
329    }
330
331    fn evaluate(
332        &self,
333        config: &Self::Solution,
334    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
335        Ok({
336            crate::types::Or({
337                if config.len() != self.num_assignment_variables() {
338                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
339                        "table-assignment length does not match the instance".into(),
340                    ));
341                }
342
343                for object in 0..self.num_objects {
344                    for (attribute, &domain_size) in self.attribute_domains.iter().enumerate() {
345                        if config[self.config_index(object, attribute)] >= domain_size {
346                            return Err(crate::traits::EvaluationError::InvalidConfiguration(
347                                "table assignment contains an out-of-range domain value".into(),
348                            ));
349                        }
350                    }
351                }
352
353                for known_value in &self.known_values {
354                    if config[self.config_index(known_value.object(), known_value.attribute())]
355                        != known_value.value()
356                    {
357                        return Ok(crate::types::Or(false));
358                    }
359                }
360
361                for table in &self.frequency_tables {
362                    let rows = self.attribute_domains[table.attribute_a()];
363                    let cols = self.attribute_domains[table.attribute_b()];
364                    let mut observed = vec![vec![0_i64; cols]; rows];
365
366                    for object in 0..self.num_objects {
367                        let value_a = config[self.config_index(object, table.attribute_a())];
368                        let value_b = config[self.config_index(object, table.attribute_b())];
369                        observed[value_a][value_b] =
370                            observed[value_a][value_b].checked_add(1).ok_or_else(|| {
371                                crate::traits::EvaluationError::IntegerOverflow(
372                                    "counting observed database frequencies".to_string(),
373                                )
374                            })?;
375                    }
376
377                    if observed != table.counts {
378                        return Ok(crate::types::Or(false));
379                    }
380                }
381
382                true
383            })
384        })
385    }
386}
387
388impl crate::solvers::BruteForceProblem for ConsistencyOfDatabaseFrequencyTables {
389    fn dimensions(&self) -> Vec<usize> {
390        let mut dims = Vec::with_capacity(self.num_assignment_variables());
391        for _ in 0..self.num_objects {
392            dims.extend(self.attribute_domains.iter().copied());
393        }
394        dims
395    }
396}
397
398crate::declare_variants! {
399    default ConsistencyOfDatabaseFrequencyTables => "domain_size_product^num_objects" create ConsistencyOfDatabaseFrequencyTablesCreateSpec,
400}
401
402crate::register_brute_force! {
403    ConsistencyOfDatabaseFrequencyTables,
404}
405
406#[cfg(feature = "example-db")]
407pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
408    vec![crate::example_db::specs::ModelExampleSpec {
409        id: "consistency_of_database_frequency_tables",
410        instance: Box::new(ConsistencyOfDatabaseFrequencyTables::new(
411            6,
412            vec![2, 3, 2],
413            vec![
414                FrequencyTable::new(0, 1, vec![vec![1, 1, 1], vec![1, 1, 1]]),
415                FrequencyTable::new(1, 2, vec![vec![1, 1], vec![0, 2], vec![1, 1]]),
416            ],
417            vec![
418                KnownValue::new(0, 0, 0),
419                KnownValue::new(3, 0, 1),
420                KnownValue::new(1, 2, 1),
421            ],
422        )),
423        optimal_config: serde_json::json!(vec![
424            0, 0, 0, 0, 1, 1, 0, 2, 1, 1, 0, 1, 1, 1, 1, 1, 2, 0
425        ]),
426        optimal_value: serde_json::json!(true),
427    }]
428}
429
430#[cfg(test)]
431#[path = "../../unit_tests/models/misc/consistency_of_database_frequency_tables.rs"]
432mod tests;