1use 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 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 pub fn attribute_a(&self) -> usize {
33 self.attribute_a
34 }
35
36 pub fn attribute_b(&self) -> usize {
38 self.attribute_b
39 }
40
41 pub fn counts(&self) -> &[Vec<i64>] {
43 &self.counts
44 }
45
46 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 pub fn new(object: usize, attribute: usize, value: usize) -> Self {
62 Self {
63 object,
64 attribute,
65 value,
66 }
67 }
68
69 pub fn object(&self) -> usize {
71 self.object
72 }
73
74 pub fn attribute(&self) -> usize {
76 self.attribute
77 }
78
79 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#[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 num_objects: usize,
111 #[create(codec = "comma-separated")]
113 attribute_domains: Vec<usize>,
114 #[create(codec = "json")]
116 frequency_tables: Vec<FrequencyTable>,
117 #[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 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 pub fn num_objects(&self) -> usize {
241 self.num_objects
242 }
243
244 pub fn num_attributes(&self) -> usize {
246 self.attribute_domains.len()
247 }
248
249 pub fn attribute_domains(&self) -> &[usize] {
251 &self.attribute_domains
252 }
253
254 pub fn frequency_tables(&self) -> &[FrequencyTable] {
256 &self.frequency_tables
257 }
258
259 pub fn known_values(&self) -> &[KnownValue] {
261 &self.known_values
262 }
263
264 pub fn domain_size_product(&self) -> usize {
266 self.attribute_domains.iter().copied().product()
267 }
268
269 pub fn total_domain_size(&self) -> usize {
271 self.attribute_domains.iter().sum()
272 }
273
274 pub fn num_assignment_variables(&self) -> usize {
276 self.num_objects * self.num_attributes()
277 }
278
279 pub fn num_frequency_tables(&self) -> usize {
281 self.frequency_tables.len()
282 }
283
284 pub fn num_known_values(&self) -> usize {
286 self.known_values.len()
287 }
288
289 pub fn num_assignment_indicators(&self) -> usize {
291 self.num_objects * self.attribute_domains.iter().sum::<usize>()
292 }
293
294 pub fn num_frequency_cells(&self) -> usize {
296 self.frequency_tables
297 .iter()
298 .map(FrequencyTable::num_cells)
299 .sum()
300 }
301
302 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;