causal_hub/models/bayesian_network/mixed/
model.rs1use std::borrow::Cow;
2
3use approx::{AbsDiffEq, RelativeEq};
4use serde::{
5 Deserialize, Deserializer, Serialize, Serializer,
6 de::{MapAccess, Visitor},
7 ser::SerializeMap,
8};
9
10use crate::{
11 datasets::{
12 CatEv, CatIncTable, CatTable, CatWtdTable, GaussEv, GaussIncTable, GaussTable,
13 GaussWtdTable,
14 },
15 impl_json_io,
16 inference::TopologicalOrder,
17 models::{BN, CPD, DiGraph, Graph, HasLabels, MixedCPD, MixedSample, MixedSupport},
18 set,
19 types::{Error, Labels, Map, Result, Set},
20};
21
22#[non_exhaustive]
24#[derive(Clone, Debug, Serialize, Deserialize)]
25#[serde(tag = "type")]
26pub enum MixedEv {
27 Categorical(CatEv),
29 Gaussian(GaussEv),
31}
32
33#[non_exhaustive]
35#[derive(Clone, Debug, Serialize, Deserialize)]
36#[serde(tag = "type")]
37pub enum MixedTable {
38 Categorical(CatTable),
40 Gaussian(GaussTable),
42}
43
44#[non_exhaustive]
46#[derive(Clone, Debug, Serialize, Deserialize)]
47#[serde(tag = "type")]
48pub enum MixedIncTable {
49 Categorical(CatIncTable),
51 Gaussian(GaussIncTable),
53}
54
55#[non_exhaustive]
57#[derive(Clone, Debug, Serialize, Deserialize)]
58#[serde(tag = "type")]
59pub enum MixedWtdTable {
60 Categorical(CatWtdTable),
62 Gaussian(GaussWtdTable),
64}
65
66impl MixedEv {
67 pub fn events(&self) -> Set<usize> {
69 match self {
70 Self::Categorical(ev) => ev
71 .evidences()
72 .iter()
73 .flatten()
74 .map(|evidence| evidence.event())
75 .collect(),
76 Self::Gaussian(ev) => ev
77 .evidences()
78 .iter()
79 .flatten()
80 .map(|evidence| evidence.event())
81 .collect(),
82 }
83 }
84}
85
86impl From<CatEv> for MixedEv {
87 #[inline]
88 fn from(ev: CatEv) -> Self {
89 Self::Categorical(ev)
90 }
91}
92
93impl From<GaussEv> for MixedEv {
94 #[inline]
95 fn from(ev: GaussEv) -> Self {
96 Self::Gaussian(ev)
97 }
98}
99
100impl From<CatTable> for MixedTable {
101 #[inline]
102 fn from(table: CatTable) -> Self {
103 Self::Categorical(table)
104 }
105}
106
107impl From<GaussTable> for MixedTable {
108 #[inline]
109 fn from(table: GaussTable) -> Self {
110 Self::Gaussian(table)
111 }
112}
113
114impl From<CatIncTable> for MixedIncTable {
115 #[inline]
116 fn from(table: CatIncTable) -> Self {
117 Self::Categorical(table)
118 }
119}
120
121impl From<GaussIncTable> for MixedIncTable {
122 #[inline]
123 fn from(table: GaussIncTable) -> Self {
124 Self::Gaussian(table)
125 }
126}
127
128impl From<CatWtdTable> for MixedWtdTable {
129 #[inline]
130 fn from(table: CatWtdTable) -> Self {
131 Self::Categorical(table)
132 }
133}
134
135impl From<GaussWtdTable> for MixedWtdTable {
136 #[inline]
137 fn from(table: GaussWtdTable) -> Self {
138 Self::Gaussian(table)
139 }
140}
141
142impl From<MixedTable> for MixedWtdTable {
143 #[inline]
144 fn from(table: MixedTable) -> Self {
145 match table {
146 MixedTable::Categorical(t) => MixedWtdTable::Categorical(t.into()),
147 MixedTable::Gaussian(t) => MixedWtdTable::Gaussian(t.into()),
148 }
149 }
150}
151
152#[derive(Clone, Debug)]
154pub struct MixedBN {
155 name: Option<String>,
157 description: Option<String>,
159 labels: Labels,
161 graph: DiGraph,
163 cpds: Map<String, MixedCPD>,
165 topological_order: Vec<usize>,
167}
168
169impl PartialEq for MixedBN {
170 fn eq(&self, other: &Self) -> bool {
171 self.labels.eq(&other.labels)
172 && self.graph.eq(&other.graph)
173 && self.topological_order.eq(&other.topological_order)
174 && self.cpds.eq(&other.cpds)
175 }
176}
177
178impl AbsDiffEq for MixedBN {
179 type Epsilon = f64;
180
181 fn default_epsilon() -> Self::Epsilon {
182 Self::Epsilon::default_epsilon()
183 }
184
185 fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
186 self.labels.eq(&other.labels)
187 && self.graph.eq(&other.graph)
188 && self.topological_order.eq(&other.topological_order)
189 && self.cpds.iter().zip(&other.cpds).all(
190 |((label, distribution), (other_label, other_cpd))| {
191 label.eq(other_label) && distribution.abs_diff_eq(other_cpd, epsilon)
192 },
193 )
194 }
195}
196
197impl RelativeEq for MixedBN {
198 fn default_max_relative() -> Self::Epsilon {
199 Self::Epsilon::default_max_relative()
200 }
201
202 fn relative_eq(
203 &self,
204 other: &Self,
205 epsilon: Self::Epsilon,
206 max_relative: Self::Epsilon,
207 ) -> bool {
208 self.labels.eq(&other.labels)
209 && self.graph.eq(&other.graph)
210 && self.topological_order.eq(&other.topological_order)
211 && self.cpds.iter().zip(&other.cpds).all(
212 |((label, distribution), (other_label, other_cpd))| {
213 label.eq(other_label)
214 && distribution.relative_eq(other_cpd, epsilon, max_relative)
215 },
216 )
217 }
218}
219
220impl HasLabels for MixedBN {
221 #[inline]
222 fn labels(&self) -> &Labels {
223 &self.labels
224 }
225}
226
227impl BN for MixedBN {
228 type CPD = MixedCPD;
229 type Support = Map<String, MixedSupport>;
230 type Evidence = MixedEv;
231 type Sample = MixedSample;
232 type Samples = MixedTable;
233 type IncSamples = MixedIncTable;
234 type WtdSamples = MixedWtdTable;
235
236 #[inline]
237 fn support(&self) -> Cow<'_, Self::Support> {
238 Cow::Owned(
239 self.cpds
240 .iter()
241 .map(|(label, distribution)| (label.clone(), distribution.support().into_owned()))
242 .collect(),
243 )
244 }
245
246 fn new<I>(graph: DiGraph, cpds: I) -> Result<Self>
247 where
248 I: IntoIterator<Item = Self::CPD>,
249 {
250 let mut cpds: Map<_, _> = cpds
251 .into_iter()
252 .map(|x| {
253 if x.labels().len() != 1 {
254 return Err(Error::InvalidParameter(
255 "cpd",
256 "CPD must contain exactly one label.",
257 ));
258 }
259 Ok((x.labels()[0].to_owned(), x))
260 })
261 .collect::<Result<_>>()?;
262 cpds.sort_keys();
263
264 if !graph.labels().iter().eq(cpds.keys()) {
265 return Err(Error::LabelMismatch("graph labels", "distributions labels"));
266 }
267
268 let labels: Labels = graph.labels().clone();
269
270 graph.vertices().into_iter().try_for_each(|i| {
271 let pa_i = graph.parents(&set![i])?.into_iter();
272 let pa_i: &Labels = &pa_i.map(|j| labels[j].to_owned()).collect();
273 let pa_j = cpds[&labels[i]].conditioning_labels();
274 if pa_i != pa_j {
275 return Err(Error::LabelMismatch(
276 &format!("{pa_i:?}"),
277 &format!("{pa_j:?}"),
278 ));
279 }
280 Ok(())
281 })?;
282
283 let topological_order = graph.topological_order().ok_or_else(|| Error::NotADag())?;
284
285 Ok(Self {
286 name: None,
287 description: None,
288 labels,
289 graph,
290 cpds,
291 topological_order,
292 })
293 }
294
295 #[inline]
296 fn name(&self) -> Option<&str> {
297 self.name.as_deref()
298 }
299
300 #[inline]
301 fn description(&self) -> Option<&str> {
302 self.description.as_deref()
303 }
304
305 #[inline]
306 fn graph(&self) -> &DiGraph {
307 &self.graph
308 }
309
310 #[inline]
311 fn cpds(&self) -> &Map<String, Self::CPD> {
312 &self.cpds
313 }
314
315 #[inline]
316 fn parameters_size(&self) -> usize {
317 self.cpds.iter().map(|(_, x)| x.parameters_size()).sum()
318 }
319
320 fn select(&self, x: &Set<usize>) -> Result<Self>
321 where
322 Self: Sized,
323 {
324 x.iter().try_for_each(|&i| {
325 if i >= self.labels.len() {
326 return Err(Error::IndexOutOfBounds(i));
327 }
328 Ok(())
329 })?;
330
331 let mut x = x.clone();
332 x.sort();
333
334 let graph = self.graph.select(&x)?;
335 let cpds = x.iter().map(|&i| self.cpds[i].clone());
336
337 Self::with_optionals(self.name.clone(), self.description.clone(), graph, cpds)
338 }
339
340 #[inline]
341 fn topological_order(&self) -> &[usize] {
342 &self.topological_order
343 }
344
345 fn with_optionals<I>(
346 name: Option<String>,
347 description: Option<String>,
348 graph: DiGraph,
349 cpds: I,
350 ) -> Result<Self>
351 where
352 I: IntoIterator<Item = Self::CPD>,
353 {
354 if let Some(name) = &name
355 && name.is_empty()
356 {
357 return Err(Error::InvalidParameter("name", "cannot be empty"));
358 }
359 if let Some(description) = &description
360 && description.is_empty()
361 {
362 return Err(Error::InvalidParameter("description", "cannot be empty"));
363 }
364
365 let mut bayesian_network = Self::new(graph, cpds)?;
366 bayesian_network.name = name;
367 bayesian_network.description = description;
368
369 Ok(bayesian_network)
370 }
371}
372
373impl Serialize for MixedBN {
376 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
377 where
378 S: Serializer,
379 {
380 let mut size = 3usize;
381 size += self.name.is_some() as usize;
382 size += self.description.is_some() as usize;
383
384 let mut map = serializer.serialize_map(Some(size))?;
385
386 if let Some(name) = &self.name {
387 map.serialize_entry("name", name)?;
388 }
389 if let Some(description) = &self.description {
390 map.serialize_entry("description", description)?;
391 }
392 map.serialize_entry("graph", &self.graph)?;
393
394 let cpds: Vec<_> = self.cpds.values().cloned().collect();
395 map.serialize_entry("cpds", &cpds)?;
396
397 map.serialize_entry("type", "mixedbn")?;
398
399 map.end()
400 }
401}
402
403impl<'de> Deserialize<'de> for MixedBN {
404 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
405 where
406 D: Deserializer<'de>,
407 {
408 #[derive(Deserialize)]
409 #[serde(field_identifier, rename_all = "snake_case")]
410 enum Field {
411 Name,
412 Description,
413 Graph,
414 Cpds,
415 Type,
416 }
417
418 struct MixedBNVisitor;
419
420 impl<'de> Visitor<'de> for MixedBNVisitor {
421 type Value = MixedBN;
422
423 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
424 formatter.write_str("struct MixedBN")
425 }
426
427 fn visit_map<V>(self, mut map: V) -> std::result::Result<MixedBN, V::Error>
428 where
429 V: MapAccess<'de>,
430 {
431 use serde::de::Error as E;
432
433 let mut name = None;
434 let mut description = None;
435 let mut graph = None;
436 let mut cpds = None;
437 let mut type_ = None;
438
439 while let Some(key) = map.next_key()? {
440 match key {
441 Field::Name => {
442 if name.is_some() {
443 return Err(E::duplicate_field("name"));
444 }
445 name = Some(map.next_value()?);
446 }
447 Field::Description => {
448 if description.is_some() {
449 return Err(E::duplicate_field("description"));
450 }
451 description = Some(map.next_value()?);
452 }
453 Field::Graph => {
454 if graph.is_some() {
455 return Err(E::duplicate_field("graph"));
456 }
457 graph = Some(map.next_value()?);
458 }
459 Field::Cpds => {
460 if cpds.is_some() {
461 return Err(E::duplicate_field("cpds"));
462 }
463 cpds = Some(map.next_value::<Vec<MixedCPD>>()?);
464 }
465 Field::Type => {
466 if type_.is_some() {
467 return Err(E::duplicate_field("type"));
468 }
469 type_ = Some(map.next_value()?);
470 }
471 }
472 }
473
474 let graph = graph.ok_or_else(|| E::missing_field("graph"))?;
475 let cpds = cpds.ok_or_else(|| E::missing_field("cpds"))?;
476
477 let type_: String = type_.ok_or_else(|| E::missing_field("type"))?;
478 if type_ != "mixedbn" {
479 return Err(E::custom(format!(
480 "Invalid type for MixedBN: expected 'mixedbn', found '{type_}'"
481 )));
482 }
483
484 MixedBN::with_optionals(name, description, graph, cpds)
485 .map_err(serde::de::Error::custom)
486 }
487 }
488
489 const FIELDS: &[&str] = &["name", "description", "graph", "cpds", "type"];
490
491 deserializer.deserialize_struct("MixedBN", FIELDS, MixedBNVisitor)
492 }
493}
494
495impl_json_io!(MixedBN);