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)]
100#[serde(try_from = "ConsistencyOfDatabaseFrequencyTablesData")]
101pub struct ConsistencyOfDatabaseFrequencyTables {
102    num_objects: usize,
103    attribute_domains: Vec<usize>,
104    frequency_tables: Vec<FrequencyTable>,
105    known_values: Vec<KnownValue>,
106}
107
108#[derive(Deserialize)]
109struct ConsistencyOfDatabaseFrequencyTablesData {
110    num_objects: usize,
111    attribute_domains: Vec<usize>,
112    frequency_tables: Vec<FrequencyTable>,
113    known_values: Vec<KnownValue>,
114}
115
116impl TryFrom<ConsistencyOfDatabaseFrequencyTablesData> for ConsistencyOfDatabaseFrequencyTables {
117    type Error = crate::registry::ConstructionError;
118    fn try_from(data: ConsistencyOfDatabaseFrequencyTablesData) -> Result<Self, Self::Error> {
119        Self::try_new(
120            data.num_objects,
121            data.attribute_domains,
122            data.frequency_tables,
123            data.known_values,
124        )
125    }
126}
127
128#[derive(Debug, Deserialize, crate::CreateSpec)]
129struct ConsistencyOfDatabaseFrequencyTablesCreateSpec {
130    /// Number of database objects.
131    num_objects: usize,
132    /// Domain size for each attribute.
133    #[create(codec = "comma-separated")]
134    attribute_domains: Vec<usize>,
135    /// Pairwise frequency tables as JSON objects.
136    #[create(codec = "json")]
137    frequency_tables: Vec<FrequencyTable>,
138    /// Known object-attribute values as JSON objects; defaults to empty.
139    #[create(codec = "json")]
140    known_values: Option<Vec<KnownValue>>,
141}
142
143impl TryFrom<ConsistencyOfDatabaseFrequencyTablesCreateSpec>
144    for ConsistencyOfDatabaseFrequencyTables
145{
146    type Error = crate::registry::ConstructionError;
147    fn try_from(spec: ConsistencyOfDatabaseFrequencyTablesCreateSpec) -> Result<Self, Self::Error> {
148        Self::try_new(
149            spec.num_objects,
150            spec.attribute_domains,
151            spec.frequency_tables,
152            spec.known_values.unwrap_or_default(),
153        )
154    }
155}
156
157fn validate_cdft_create(
158    num_objects: usize,
159    domains: &[usize],
160    tables: &[FrequencyTable],
161    known: &[KnownValue],
162) -> Result<(), crate::registry::ConstructionError> {
163    for (attribute, &size) in domains.iter().enumerate() {
164        if size == 0 {
165            return Err(
166                format!("attribute domain size at index {attribute} must be positive").into(),
167            );
168        }
169    }
170    num_objects.checked_mul(domains.len()).ok_or_else(|| {
171        crate::registry::ConstructionError::IntegerOverflow(
172            "representing the table-assignment witness length".into(),
173        )
174    })?;
175    domains
176        .iter()
177        .try_fold(0usize, |sum, &size| sum.checked_add(size))
178        .ok_or_else(|| {
179            crate::registry::ConstructionError::IntegerOverflow(
180                "representing the total domain size".into(),
181            )
182        })?;
183    tables
184        .iter()
185        .flat_map(|table| table.counts())
186        .try_fold(0usize, |sum, row| sum.checked_add(row.len()))
187        .ok_or_else(|| {
188            crate::registry::ConstructionError::IntegerOverflow(
189                "representing the number of frequency-table cells".into(),
190            )
191        })?;
192    let mut pairs = BTreeSet::new();
193    for table in tables {
194        let a = table.attribute_a();
195        let b = table.attribute_b();
196        if a >= domains.len() || b >= domains.len() {
197            return Err("frequency table attribute is out of range".into());
198        }
199        if a == b {
200            return Err("frequency table attributes must be distinct".into());
201        }
202        let pair = if a < b { (a, b) } else { (b, a) };
203        if !pairs.insert(pair) {
204            return Err(format!("duplicate frequency table pair ({}, {})", pair.0, pair.1).into());
205        }
206        if table.counts().len() != domains[a] {
207            return Err(
208                format!("frequency table rows must equal domain size for attribute {a}").into(),
209            );
210        }
211        if table.counts().iter().any(|row| row.len() != domains[b]) {
212            return Err(format!(
213                "frequency table column count must equal domain size for attribute {b}"
214            )
215            .into());
216        }
217        if table.counts().iter().flatten().any(|&count| count < 0) {
218            return Err("frequency table counts must be nonnegative".into());
219        }
220        let total = table
221            .counts()
222            .iter()
223            .flatten()
224            .try_fold(0_i64, |sum, &value| {
225                sum.checked_add(value)
226                    .ok_or("frequency table count total overflows i64")
227            })?;
228        let expected_total =
229            i64::try_from(num_objects).map_err(|_| "num_objects cannot be represented as i64")?;
230        if total != expected_total {
231            return Err(format!(
232                "frequency table total {total} must equal num_objects {num_objects}"
233            )
234            .into());
235        }
236    }
237    for value in known {
238        if value.object() >= num_objects {
239            return Err("known value object is out of range".into());
240        }
241        if value.attribute() >= domains.len() {
242            return Err("known value attribute is out of range".into());
243        }
244        if value.value() >= domains[value.attribute()] {
245            return Err("known value value is outside the attribute domain".into());
246        }
247    }
248    Ok(())
249}
250
251impl ConsistencyOfDatabaseFrequencyTables {
252    /// Create a new consistency-of-database-frequency-tables instance.
253    /// Input parameter counts and the table-assignment witness length must fit in `usize`.
254    pub fn new(
255        num_objects: usize,
256        attribute_domains: Vec<usize>,
257        frequency_tables: Vec<FrequencyTable>,
258        known_values: Vec<KnownValue>,
259    ) -> Self {
260        Self::try_new(
261            num_objects,
262            attribute_domains,
263            frequency_tables,
264            known_values,
265        )
266        .unwrap_or_else(|error| panic!("{error}"))
267    }
268
269    fn try_new(
270        num_objects: usize,
271        attribute_domains: Vec<usize>,
272        frequency_tables: Vec<FrequencyTable>,
273        known_values: Vec<KnownValue>,
274    ) -> Result<Self, crate::registry::ConstructionError> {
275        validate_cdft_create(
276            num_objects,
277            &attribute_domains,
278            &frequency_tables,
279            &known_values,
280        )?;
281
282        Ok(Self {
283            num_objects,
284            attribute_domains,
285            frequency_tables,
286            known_values,
287        })
288    }
289
290    /// Returns the number of objects.
291    pub fn num_objects(&self) -> usize {
292        self.num_objects
293    }
294
295    /// Returns the number of attributes.
296    pub fn num_attributes(&self) -> usize {
297        self.attribute_domains.len()
298    }
299
300    /// Returns the attribute-domain sizes.
301    pub fn attribute_domains(&self) -> &[usize] {
302        &self.attribute_domains
303    }
304
305    /// Returns the published frequency tables.
306    pub fn frequency_tables(&self) -> &[FrequencyTable] {
307        &self.frequency_tables
308    }
309
310    /// Returns the known values.
311    pub fn known_values(&self) -> &[KnownValue] {
312        &self.known_values
313    }
314
315    /// Largest attribute domain; one for no attributes, whose assignment count is one.
316    pub fn max_domain_size(&self) -> usize {
317        self.attribute_domains.iter().copied().max().unwrap_or(1)
318    }
319
320    /// Returns the sum of all attribute-domain sizes.
321    pub fn total_domain_size(&self) -> usize {
322        self.attribute_domains.iter().sum()
323    }
324
325    /// Returns the number of object-attribute assignment variables in the direct encoding.
326    pub fn num_assignment_variables(&self) -> usize {
327        self.num_objects * self.num_attributes()
328    }
329
330    /// Returns the number of published frequency tables.
331    pub fn num_frequency_tables(&self) -> usize {
332        self.frequency_tables.len()
333    }
334
335    /// Returns the number of known value constraints.
336    pub fn num_known_values(&self) -> usize {
337        self.known_values.len()
338    }
339
340    /// Returns the total number of published frequency-table cells.
341    pub fn num_frequency_cells(&self) -> usize {
342        self.frequency_tables
343            .iter()
344            .map(FrequencyTable::num_cells)
345            .sum()
346    }
347
348    fn config_index(&self, object: usize, attribute: usize) -> usize {
349        object * self.num_attributes() + attribute
350    }
351}
352
353impl Problem for ConsistencyOfDatabaseFrequencyTables {
354    const NAME: &'static str = "ConsistencyOfDatabaseFrequencyTables";
355    type Solution = Vec<usize>;
356    type Value = crate::types::Or;
357
358    crate::problem_parameters![
359        ("num_objects", num_objects),
360        ("num_attributes", num_attributes),
361        ("total_domain_size", total_domain_size),
362        ("max_domain_size", max_domain_size),
363        ("num_frequency_tables", num_frequency_tables),
364        ("num_frequency_cells", num_frequency_cells),
365        ("num_known_values", num_known_values),
366    ];
367
368    fn variant() -> Vec<(&'static str, &'static str)> {
369        crate::variant_params![]
370    }
371
372    fn evaluate(
373        &self,
374        config: &Self::Solution,
375    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
376        Ok({
377            crate::types::Or({
378                if config.len() != self.num_assignment_variables() {
379                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
380                        "table-assignment length does not match the instance".into(),
381                    ));
382                }
383
384                for object in 0..self.num_objects {
385                    for (attribute, &domain_size) in self.attribute_domains.iter().enumerate() {
386                        if config[self.config_index(object, attribute)] >= domain_size {
387                            return Err(crate::traits::EvaluationError::InvalidConfiguration(
388                                "table assignment contains an out-of-range domain value".into(),
389                            ));
390                        }
391                    }
392                }
393
394                for known_value in &self.known_values {
395                    if config[self.config_index(known_value.object(), known_value.attribute())]
396                        != known_value.value()
397                    {
398                        return Ok(crate::types::Or(false));
399                    }
400                }
401
402                for table in &self.frequency_tables {
403                    let rows = self.attribute_domains[table.attribute_a()];
404                    let cols = self.attribute_domains[table.attribute_b()];
405                    let mut observed = vec![vec![0_i64; cols]; rows];
406
407                    for object in 0..self.num_objects {
408                        let value_a = config[self.config_index(object, table.attribute_a())];
409                        let value_b = config[self.config_index(object, table.attribute_b())];
410                        observed[value_a][value_b] =
411                            observed[value_a][value_b].checked_add(1).ok_or_else(|| {
412                                crate::traits::EvaluationError::IntegerOverflow(
413                                    "counting observed database frequencies".to_string(),
414                                )
415                            })?;
416                    }
417
418                    if observed != table.counts {
419                        return Ok(crate::types::Or(false));
420                    }
421                }
422
423                true
424            })
425        })
426    }
427}
428
429impl crate::solvers::BruteForceProblem for ConsistencyOfDatabaseFrequencyTables {
430    fn dimensions(&self) -> Vec<usize> {
431        let mut dims = Vec::with_capacity(self.num_assignment_variables());
432        for _ in 0..self.num_objects {
433            dims.extend(self.attribute_domains.iter().copied());
434        }
435        dims
436    }
437}
438
439crate::declare_variants! {
440    // Bound each attribute's choices by the largest domain, rather than storing their product.
441    default ConsistencyOfDatabaseFrequencyTables => "max_domain_size^(num_objects * num_attributes)" create ConsistencyOfDatabaseFrequencyTablesCreateSpec,
442}
443
444crate::register_brute_force! {
445    ConsistencyOfDatabaseFrequencyTables,
446}
447
448#[cfg(feature = "example-db")]
449pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
450    vec![crate::example_db::specs::ModelExampleSpec {
451        id: "consistency_of_database_frequency_tables",
452        instance: Box::new(ConsistencyOfDatabaseFrequencyTables::new(
453            6,
454            vec![2, 3, 2],
455            vec![
456                FrequencyTable::new(0, 1, vec![vec![1, 1, 1], vec![1, 1, 1]]),
457                FrequencyTable::new(1, 2, vec![vec![1, 1], vec![0, 2], vec![1, 1]]),
458            ],
459            vec![
460                KnownValue::new(0, 0, 0),
461                KnownValue::new(3, 0, 1),
462                KnownValue::new(1, 2, 1),
463            ],
464        )),
465        optimal_config: serde_json::json!(vec![
466            0, 0, 0, 0, 1, 1, 0, 2, 1, 1, 0, 1, 1, 1, 1, 1, 2, 0
467        ]),
468        optimal_value: serde_json::json!(true),
469    }]
470}
471
472#[cfg(test)]
473#[path = "../../unit_tests/models/misc/consistency_of_database_frequency_tables.rs"]
474mod tests;