Skip to main content

causal_hub/models/bayesian_network/mixed/
model.rs

1use 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/// A unified evidence type for mixed Bayesian networks.
23#[non_exhaustive]
24#[derive(Clone, Debug, Serialize, Deserialize)]
25#[serde(tag = "type")]
26pub enum MixedEv {
27    /// Categorical evidence.
28    Categorical(CatEv),
29    /// Gaussian evidence.
30    Gaussian(GaussEv),
31}
32
33/// A unified complete dataset type for mixed Bayesian networks.
34#[non_exhaustive]
35#[derive(Clone, Debug, Serialize, Deserialize)]
36#[serde(tag = "type")]
37pub enum MixedTable {
38    /// Categorical table.
39    Categorical(CatTable),
40    /// Gaussian table.
41    Gaussian(GaussTable),
42}
43
44/// A unified incomplete dataset type for mixed Bayesian networks.
45#[non_exhaustive]
46#[derive(Clone, Debug, Serialize, Deserialize)]
47#[serde(tag = "type")]
48pub enum MixedIncTable {
49    /// Categorical incomplete table.
50    Categorical(CatIncTable),
51    /// Gaussian incomplete table.
52    Gaussian(GaussIncTable),
53}
54
55/// A unified weighted dataset type for mixed Bayesian networks.
56#[non_exhaustive]
57#[derive(Clone, Debug, Serialize, Deserialize)]
58#[serde(tag = "type")]
59pub enum MixedWtdTable {
60    /// Categorical weighted table.
61    Categorical(CatWtdTable),
62    /// Gaussian weighted table.
63    Gaussian(GaussWtdTable),
64}
65
66impl MixedEv {
67    /// Return the events indices where evidence is present.
68    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/// A mixed Bayesian network.
153#[derive(Clone, Debug)]
154pub struct MixedBN {
155    /// The name of the model.
156    name: Option<String>,
157    /// The description of the model.
158    description: Option<String>,
159    /// The labels of the variables.
160    labels: Labels,
161    /// The graph of the model.
162    graph: DiGraph,
163    /// The parameters of the model.
164    cpds: Map<String, MixedCPD>,
165    /// The topological order of the graph.
166    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
373// ── Serde for MixedBN ──────────────────────────────────────────
374
375impl 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);