1use crate::registry::{ConstructionError, CreateSpec, ProblemSchemaEntry, VariantDimension};
8use crate::traits::Problem;
9use crate::types::{One, WeightElement};
10use num_traits::Zero;
11use serde::{Deserialize, Serialize};
12
13inventory::submit! {
14 ProblemSchemaEntry {
15 name: "ComparativeContainment",
16 display_name: "Comparative Containment",
17 aliases: &[],
18 dimensions: &[VariantDimension::new("weight", "i64", &["One", "i64", "f64"])],
19 category: crate::registry::ProblemCategory::Set,
20 module_path: module_path!(),
21 description: "Compare containment-weight sums for two set families over a shared universe",
22 fields: ComparativeContainmentI64CreateSpec::FIELDS,
23 }
24}
25
26#[derive(Debug, Clone, Serialize)]
33pub struct ComparativeContainment<W = i64> {
34 universe_size: usize,
35 r_sets: Vec<Vec<usize>>,
36 s_sets: Vec<Vec<usize>>,
37 r_weights: Vec<W>,
38 s_weights: Vec<W>,
39}
40
41macro_rules! comparative_containment_create_spec {
42 ($name:ident, $weight:ty, $one:expr $(, $r_weights:ident, $s_weights:ident)?) => {
43 #[derive(Debug, Deserialize, crate::CreateSpec)]
44 struct $name {
45 universe_size: usize,
47 #[create(codec = "semicolon-separated")]
49 r_sets: Vec<Vec<usize>>,
50 #[create(codec = "semicolon-separated")]
52 s_sets: Vec<Vec<usize>>,
53 $(
54 #[create(codec = "comma-separated")]
56 $r_weights: Option<Vec<$weight>>,
57 )?
58 $(
59 #[create(codec = "comma-separated")]
61 $s_weights: Option<Vec<$weight>>,
62 )?
63 }
64
65 impl TryFrom<$name> for ComparativeContainment<$weight> {
66 type Error = ConstructionError;
67 fn try_from(spec: $name) -> Result<Self, Self::Error> {
68 let r_weights = { $(if let Some(value) = spec.$r_weights { value } else)? { vec![$one; spec.r_sets.len()] } };
69 let s_weights = { $(if let Some(value) = spec.$s_weights { value } else)? { vec![$one; spec.s_sets.len()] } };
70 ComparativeContainment::with_weights(
71 spec.universe_size,
72 spec.r_sets,
73 spec.s_sets,
74 r_weights,
75 s_weights,
76 )
77 }
78 }
79 };
80}
81
82fn validate_create_set_family(
83 label: &str,
84 universe_size: usize,
85 sets: &[Vec<usize>],
86) -> Result<(), ConstructionError> {
87 for (set_index, set) in sets.iter().enumerate() {
88 for &element in set {
89 if element >= universe_size {
90 return Err(ConstructionError::Conversion(format!("{label} set {set_index} contains element {element} outside universe of size {universe_size}")));
91 }
92 }
93 }
94 Ok(())
95}
96
97fn validate_create_weights<W: WeightElement>(
98 label: &str,
99 count: usize,
100 weights: &[W],
101) -> Result<(), ConstructionError> {
102 if weights.len() != count {
103 return Err(ConstructionError::Conversion(format!(
104 "number of {label} sets and weights must match"
105 )));
106 }
107 for (index, weight) in weights.iter().enumerate() {
108 match weight.to_sum().partial_cmp(&W::Sum::zero()) {
109 None => {
110 return Err(ConstructionError::NonFiniteFloat(format!(
111 "{label} weight at index {index} must be finite"
112 )));
113 }
114 Some(std::cmp::Ordering::Greater) => {}
115 Some(_) => {
116 return Err(ConstructionError::Conversion(format!(
117 "{label} weight at index {index} must be positive"
118 )));
119 }
120 }
121 }
122 Ok(())
123}
124
125comparative_containment_create_spec!(
126 ComparativeContainmentI64CreateSpec,
127 i64,
128 1_i64,
129 r_weights,
130 s_weights
131);
132comparative_containment_create_spec!(
133 ComparativeContainmentF64CreateSpec,
134 f64,
135 1.0_f64,
136 r_weights,
137 s_weights
138);
139comparative_containment_create_spec!(ComparativeContainmentOneCreateSpec, One, One);
140
141impl<W: WeightElement> ComparativeContainment<W> {
142 pub fn new(
144 universe_size: usize,
145 r_sets: Vec<Vec<usize>>,
146 s_sets: Vec<Vec<usize>>,
147 ) -> Result<Self, ConstructionError>
148 where
149 W: WeightElement,
150 {
151 let r_weights = vec![W::unit(); r_sets.len()];
152 let s_weights = vec![W::unit(); s_sets.len()];
153 Self::with_weights(universe_size, r_sets, s_sets, r_weights, s_weights)
154 }
155
156 pub fn with_weights(
158 universe_size: usize,
159 r_sets: Vec<Vec<usize>>,
160 s_sets: Vec<Vec<usize>>,
161 r_weights: Vec<W>,
162 s_weights: Vec<W>,
163 ) -> Result<Self, ConstructionError> {
164 validate_create_set_family("R", universe_size, &r_sets)?;
165 validate_create_set_family("S", universe_size, &s_sets)?;
166 validate_create_weights("R", r_sets.len(), &r_weights)?;
167 validate_create_weights("S", s_sets.len(), &s_weights)?;
168 Ok(Self {
169 universe_size,
170 r_sets,
171 s_sets,
172 r_weights,
173 s_weights,
174 })
175 }
176
177 pub fn universe_size(&self) -> usize {
179 self.universe_size
180 }
181
182 pub fn num_r_sets(&self) -> usize {
184 self.r_sets.len()
185 }
186
187 pub fn num_s_sets(&self) -> usize {
189 self.s_sets.len()
190 }
191
192 pub fn r_sets(&self) -> &[Vec<usize>] {
194 &self.r_sets
195 }
196
197 pub fn s_sets(&self) -> &[Vec<usize>] {
199 &self.s_sets
200 }
201
202 pub fn r_weights(&self) -> &[W] {
204 &self.r_weights
205 }
206
207 pub fn s_weights(&self) -> &[W] {
209 &self.s_weights
210 }
211
212 pub fn contains_selected_subset(&self, config: &[bool], set: &[usize]) -> bool {
214 self.valid_config(config) && contains_selected_subset_unchecked(config, set)
215 }
216
217 fn valid_config(&self, config: &[bool]) -> bool {
218 config.len() == self.universe_size
219 }
220}
221
222impl<'de, W> Deserialize<'de> for ComparativeContainment<W>
223where
224 W: WeightElement + Deserialize<'de>,
225{
226 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
227 where
228 D: serde::Deserializer<'de>,
229 {
230 #[derive(Deserialize)]
231 struct Raw<W> {
232 universe_size: usize,
233 r_sets: Vec<Vec<usize>>,
234 s_sets: Vec<Vec<usize>>,
235 r_weights: Vec<W>,
236 s_weights: Vec<W>,
237 }
238
239 let raw = Raw::deserialize(deserializer)?;
240 Self::with_weights(
241 raw.universe_size,
242 raw.r_sets,
243 raw.s_sets,
244 raw.r_weights,
245 raw.s_weights,
246 )
247 .map_err(serde::de::Error::custom)
248 }
249}
250
251impl<W> ComparativeContainment<W>
252where
253 W: WeightElement,
254{
255 pub fn r_weight_sum(
257 &self,
258 config: &[bool],
259 ) -> Result<Option<W::Sum>, crate::traits::EvaluationError> {
260 self.sum_containing_weights(config, &self.r_sets, &self.r_weights)
261 }
262
263 pub fn s_weight_sum(
265 &self,
266 config: &[bool],
267 ) -> Result<Option<W::Sum>, crate::traits::EvaluationError> {
268 self.sum_containing_weights(config, &self.s_sets, &self.s_weights)
269 }
270
271 pub fn is_valid_solution(
273 &self,
274 config: &[bool],
275 ) -> Result<bool, crate::traits::EvaluationError> {
276 Ok(
277 match (self.r_weight_sum(config)?, self.s_weight_sum(config)?) {
278 (Some(r_total), Some(s_total)) => r_total >= s_total,
279 _ => false,
280 },
281 )
282 }
283
284 fn sum_containing_weights(
285 &self,
286 config: &[bool],
287 sets: &[Vec<usize>],
288 weights: &[W],
289 ) -> Result<Option<W::Sum>, crate::traits::EvaluationError> {
290 if !self.valid_config(config) {
291 return Ok(None);
292 }
293
294 let mut total = W::Sum::zero();
295 for (set, weight) in sets.iter().zip(weights.iter()) {
296 if contains_selected_subset_unchecked(config, set) {
297 total = W::checked_add_to_sum(
298 total,
299 weight.to_sum(),
300 "summing comparative containment weights",
301 )?;
302 }
303 }
304 Ok(Some(total))
305 }
306}
307
308impl<W> Problem for ComparativeContainment<W>
309where
310 W: WeightElement + crate::variant::VariantParam,
311{
312 const NAME: &'static str = "ComparativeContainment";
313 type Solution = Vec<bool>;
314 type Value = crate::types::Or;
315
316 crate::problem_parameters![
317 ("num_r_sets", num_r_sets),
318 ("num_s_sets", num_s_sets),
319 ("universe_size", universe_size),
320 ];
321
322 fn evaluate(
323 &self,
324 config: &Self::Solution,
325 ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
326 if config.len() != self.universe_size {
327 return Err(crate::traits::EvaluationError::InvalidConfiguration(
328 "element-selection length does not match the universe".into(),
329 ));
330 }
331 Ok(crate::types::Or(self.is_valid_solution(config)?))
332 }
333
334 fn variant() -> Vec<(&'static str, &'static str)> {
335 crate::variant_params![W]
336 }
337}
338
339impl<W> crate::solvers::BruteForceProblem for ComparativeContainment<W>
340where
341 W: WeightElement + crate::variant::VariantParam,
342{
343 fn dimensions(&self) -> Vec<usize> {
344 vec![2; self.universe_size]
345 }
346}
347
348crate::declare_variants! {
349 ComparativeContainment<One> => "2^universe_size" create ComparativeContainmentOneCreateSpec,
350 default ComparativeContainment<i64> => "2^universe_size" create ComparativeContainmentI64CreateSpec,
351 ComparativeContainment<f64> => "2^universe_size" create ComparativeContainmentF64CreateSpec,
352}
353
354crate::register_brute_force! {
355 ComparativeContainment<One> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
356 ComparativeContainment<i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
357 ComparativeContainment<f64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
358}
359
360fn contains_selected_subset_unchecked(config: &[bool], set: &[usize]) -> bool {
361 config
362 .iter()
363 .enumerate()
364 .all(|(element, &selected)| !selected || set.contains(&element))
365}
366
367#[cfg(feature = "example-db")]
368pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
369 vec![crate::example_db::specs::ModelExampleSpec {
370 id: "comparative_containment",
371 instance: Box::new(
372 ComparativeContainment::with_weights(
373 4,
374 vec![vec![0, 1, 2, 3], vec![0, 1]],
375 vec![vec![0, 1, 2, 3], vec![2, 3]],
376 vec![2, 5],
377 vec![3, 6],
378 )
379 .expect("canonical comparative-containment instance must be valid"),
380 ),
381 optimal_config: serde_json::json!(vec![false, true, false, false]),
382 optimal_value: serde_json::json!(true),
383 }]
384}
385
386#[cfg(test)]
387#[path = "../../unit_tests/models/set/comparative_containment.rs"]
388mod tests;