use std::{
borrow::Cow,
ops::{Div, DivAssign, Mul, MulAssign},
};
use approx::{AbsDiffEq, RelativeEq};
use itertools::Itertools;
use ndarray::prelude::*;
use serde::{
Deserialize, Deserializer, Serialize, Serializer,
de::{MapAccess, Visitor},
ser::SerializeMap,
};
use crate::{
datasets::{CatEv, CatEvT},
impl_json_io,
models::{CPD, CatCPD, CatSupport, HasLabels, Phi},
types::{Error, Labels, Result, Set},
};
#[derive(Clone, Debug)]
pub struct CatPhi {
labels: Labels,
support: CatSupport,
shape: Array1<usize>,
parameters: ArrayD<f64>,
}
impl CatPhi {
pub fn new(mut support: CatSupport, mut parameters: ArrayD<f64>) -> Result<Self> {
let mut labels: Labels = support.keys().cloned().collect();
let mut shape = Array::from_iter(support.values().map(Set::len));
let shape_slice = shape.as_slice().ok_or_else(|| {
Error::Shape("Failed to convert shape array to slice: shape is not contiguous")
})?;
if parameters.shape() != shape_slice {
return Err(Error::Shape(&format!(
"Parameters shape does not match support shape: \n\
\t expected: {:?} , \n\
\t found: {:?} .",
shape_slice,
parameters.shape(),
)));
}
if !support.keys().is_sorted() {
let mut axes: Vec<_> = (0..support.len()).collect();
axes.sort_by(|&i, &j| {
support
.get_index(i)
.map(|(l, _)| l)
.cmp(&support.get_index(j).map(|(l, _)| l))
});
support.sort_keys();
parameters = parameters.permuted_axes(axes);
labels = support.keys().cloned().collect();
shape = support.values().map(Set::len).collect();
}
Ok(Self {
labels,
support,
shape,
parameters,
})
}
#[inline]
pub const fn support(&self) -> &CatSupport {
&self.support
}
#[inline]
pub const fn shape(&self) -> &Array1<usize> {
&self.shape
}
}
impl HasLabels for CatPhi {
#[inline]
fn labels(&self) -> &Labels {
&self.labels
}
}
impl PartialEq for CatPhi {
fn eq(&self, other: &Self) -> bool {
self.labels.eq(&other.labels)
&& self.support.eq(&other.support)
&& self.shape.eq(&other.shape)
&& self.parameters.eq(&other.parameters)
}
}
impl AbsDiffEq for CatPhi {
type Epsilon = f64;
fn default_epsilon() -> Self::Epsilon {
Self::Epsilon::default_epsilon()
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
self.labels.eq(&other.labels)
&& self.support.eq(&other.support)
&& self.shape.eq(&other.shape)
&& self.parameters.abs_diff_eq(&other.parameters, epsilon)
}
}
impl RelativeEq for CatPhi {
fn default_max_relative() -> Self::Epsilon {
Self::Epsilon::default_max_relative()
}
fn relative_eq(
&self,
other: &Self,
epsilon: Self::Epsilon,
max_relative: Self::Epsilon,
) -> bool {
self.labels.eq(&other.labels)
&& self.support.eq(&other.support)
&& self.shape.eq(&other.shape)
&& self
.parameters
.relative_eq(&other.parameters, epsilon, max_relative)
}
}
impl MulAssign<&CatPhi> for CatPhi {
fn mul_assign(&mut self, rhs: &CatPhi) {
let mut support = self.support.clone();
support.extend(rhs.support.clone());
support.sort_keys();
let mut lhs_axes: Vec<_> = (0..self.support.len()).collect();
lhs_axes.sort_by(|&i, &j| {
self.support
.get_index(i)
.map(|(l, _)| l)
.cmp(&self.support.get_index(j).map(|(l, _)| l))
});
let mut lhs_parameters = self.parameters.clone().permuted_axes(lhs_axes);
let lhs_axes = support.keys().enumerate();
let lhs_axes = lhs_axes.filter_map(|(i, k)| (!self.support.contains_key(k)).then_some(i));
let lhs_axes: Vec<_> = lhs_axes.sorted().collect();
lhs_axes.into_iter().for_each(|i| {
lhs_parameters.insert_axis_inplace(Axis(i));
});
let mut rhs_axes: Vec<_> = (0..rhs.support.len()).collect();
rhs_axes.sort_by(|&i, &j| {
rhs.support
.get_index(i)
.map(|(l, _)| l)
.cmp(&rhs.support.get_index(j).map(|(l, _)| l))
});
let mut rhs_parameters = rhs.parameters.clone().permuted_axes(rhs_axes);
let rhs_axes = support.keys().enumerate();
let rhs_axes = rhs_axes.filter_map(|(i, k)| (!rhs.support.contains_key(k)).then_some(i));
let rhs_axes: Vec<_> = rhs_axes.sorted().collect();
rhs_axes.into_iter().for_each(|i| {
rhs_parameters.insert_axis_inplace(Axis(i));
});
let parameters = lhs_parameters * rhs_parameters;
let labels: Labels = support.keys().cloned().collect();
let shape = Array::from_iter(support.values().map(Set::len));
self.support = support;
self.labels = labels;
self.shape = shape;
self.parameters = parameters;
}
}
impl Mul<&CatPhi> for &CatPhi {
type Output = CatPhi;
#[inline]
fn mul(self, rhs: &CatPhi) -> Self::Output {
let mut lhs = self.clone();
lhs *= rhs;
lhs
}
}
impl CatPhi {
pub fn div_assign(&mut self, rhs: &CatPhi) -> Result<()> {
if !rhs.support.keys().all(|k| self.support.contains_key(k)) {
return Err(Error::InvalidParameter(
"rhs",
"RHS support must be a subset of LHS support",
));
}
let rhs_parameters = &rhs.parameters + f64::MIN_POSITIVE;
let mut rhs_axes: Vec<_> = (0..rhs.support.len()).collect();
rhs_axes.sort_by(|&i, &j| {
rhs.support
.get_index(i)
.map(|(l, _)| l)
.cmp(&rhs.support.get_index(j).map(|(l, _)| l))
});
let mut rhs_parameters = rhs_parameters.permuted_axes(rhs_axes);
let rhs_axes = self.support.keys().enumerate();
let rhs_axes = rhs_axes.filter_map(|(i, k)| (!rhs.support.contains_key(k)).then_some(i));
let rhs_axes: Vec<_> = rhs_axes.sorted().collect();
rhs_axes.into_iter().for_each(|i| {
rhs_parameters.insert_axis_inplace(Axis(i));
});
self.parameters /= &rhs_parameters;
Ok(())
}
pub fn div(&self, rhs: &CatPhi) -> Result<CatPhi> {
let mut lhs = self.clone();
lhs.div_assign(rhs)?;
Ok(lhs)
}
}
impl DivAssign<&CatPhi> for CatPhi {
fn div_assign(&mut self, rhs: &CatPhi) {
self.div_assign(rhs).unwrap_or_else(|_| {
unreachable!(
"potential division requires `rhs` support to be a subset of `self` support"
)
});
}
}
impl Div<&CatPhi> for &CatPhi {
type Output = CatPhi;
#[inline]
fn div(self, rhs: &CatPhi) -> Self::Output {
self.div(rhs).unwrap_or_else(|_| {
unreachable!(
"potential division requires `rhs` support to be a subset of `self` support"
)
})
}
}
impl Phi for CatPhi {
type CPD = CatCPD;
type Support = CatSupport;
type Parameters = ArrayD<f64>;
type Evidence = CatEv;
#[inline]
fn support(&self) -> Cow<'_, Self::Support> {
Cow::Borrowed(&self.support)
}
#[inline]
fn parameters(&self) -> &Self::Parameters {
&self.parameters
}
fn parameters_size(&self) -> usize {
self.parameters.len()
}
fn condition(&self, evidence: &Self::Evidence) -> Result<Self> {
if evidence.support() != self.support() {
return Err(Error::InvalidParameter(
"evidence",
&format!(
"Failed to condition on evidence: \n\
\t expected: evidence support to match potential support , \n\
\t found: potential support = {:?} , \n\
\t evidence support = {:?} .",
self.support(),
evidence.support(),
),
));
}
let evidence = evidence.evidences().iter().flatten().map(|ev| match ev {
CatEvT::CertainPositive { event, state } => Ok((event, state)),
_ => Err(Error::InvalidParameter(
"evidence",
&format!(
"Failed to condition on evidence: \n\
\t expected: CertainPositive , \n\
\t found: {:?} .",
ev
),
)),
});
let mut support = self.support.clone();
let mut parameters = self.parameters.clone();
evidence.rev().try_for_each(|evidence| -> Result<_> {
let (&event, &state) = evidence?;
parameters.index_axis_inplace(Axis(event), state);
support.shift_remove_index(event);
Ok(())
})?;
Self::new(support, parameters)
}
fn marginalize(&self, x: &Set<usize>) -> Result<Self> {
if x.is_empty() {
return Ok(self.clone());
}
x.iter().try_for_each(|&x| {
if x >= self.labels.len() {
return Err(Error::IndexOutOfBounds(x));
}
Ok(())
})?;
let support = self.support.clone();
let mut parameters = self.parameters.clone();
let support = support.into_iter().enumerate();
let support = support.filter_map(|(i, stats)| (!x.contains(&i)).then_some(stats));
let support = support.collect();
x.iter().sorted().rev().for_each(|&i| {
parameters = parameters.sum_axis(Axis(i));
});
Self::new(support, parameters)
}
#[inline]
fn normalize(&self) -> Result<Self> {
let mut parameters = self.parameters.clone();
parameters /= parameters.sum();
Self::new(self.support.clone(), parameters)
}
fn from_cpd(distribution: Self::CPD) -> Result<Self> {
let mut support = distribution.conditioning_support().clone();
support.extend(distribution.support().clone());
let shape: Vec<_> = support.values().map(Set::len).collect();
let parameters = distribution.parameters().clone();
let parameters = parameters
.into_dyn()
.into_shape_with_order(shape)
.map_err(Error::NdarrayShape)?;
let mut axes: Vec<_> = (0..support.len()).collect();
axes.sort_by(|&i, &j| {
support
.get_index(i)
.map(|(l, _)| l)
.cmp(&support.get_index(j).map(|(l, _)| l))
});
support.sort_keys();
let parameters = parameters.permuted_axes(axes);
Self::new(support, parameters)
}
fn into_cpd(self, x: &Set<usize>, z: &Set<usize>) -> Result<Self::CPD> {
if !x.is_disjoint(z) {
return Err(Error::InvalidParameter(
"x,z",
"Variables and conditioning variables must be disjoint.",
));
}
if !(x | z).iter().sorted().cloned().eq(0..self.labels.len()) {
return Err(Error::InvalidParameter(
"x,z",
"Variables and conditioning variables must cover all potential variables.",
));
}
let states_x: CatSupport = x
.iter()
.map(|&i| {
self.support
.get_index(i)
.map(|(k, v)| (k.clone(), v.clone()))
.ok_or_else(|| Error::IndexOutOfBounds(i))
})
.collect::<Result<_>>()?;
let states_z: CatSupport = z
.iter()
.map(|&i| {
self.support
.get_index(i)
.map(|(k, v)| (k.clone(), v.clone()))
.ok_or_else(|| Error::IndexOutOfBounds(i))
})
.collect::<Result<_>>()?;
let axes: Vec<_> = z.iter().chain(x).cloned().collect();
let parameters = self.parameters.permuted_axes(axes);
let shape: (usize, usize) = (
states_z.values().map(Set::len).product(),
states_x.values().map(Set::len).product(),
);
let mut parameters = parameters.into_shape_clone(shape).map_err(|evidence| {
Error::Shape(&format!("Failed to reshape parameters: {}", evidence))
})?;
parameters /= ¶meters.sum_axis(Axis(1)).insert_axis(Axis(1));
CatCPD::new(states_x, states_z, parameters)
}
}
impl Serialize for CatPhi {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(4))?;
map.serialize_entry("support", &self.support)?;
let shape: Vec<usize> = self.shape.to_vec();
map.serialize_entry("shape", &shape)?;
let parameters: Vec<f64> = self.parameters.iter().cloned().collect();
map.serialize_entry("parameters", ¶meters)?;
map.serialize_entry("type", "catphi")?;
map.end()
}
}
impl<'de> Deserialize<'de> for CatPhi {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
Support,
Shape,
Parameters,
Type,
}
struct CatPhiVisitor;
impl<'de> Visitor<'de> for CatPhiVisitor {
type Value = CatPhi;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("struct CatPhi")
}
fn visit_map<V>(self, mut map: V) -> std::result::Result<CatPhi, V::Error>
where
V: MapAccess<'de>,
{
use serde::de::Error as E;
let mut support = None;
let mut shape = None;
let mut parameters = None;
let mut type_ = None;
while let Some(key) = map.next_key()? {
match key {
Field::Support => {
if support.is_some() {
return Err(E::duplicate_field("support"));
}
support = Some(map.next_value()?);
}
Field::Shape => {
if shape.is_some() {
return Err(E::duplicate_field("shape"));
}
shape = Some(map.next_value()?);
}
Field::Parameters => {
if parameters.is_some() {
return Err(E::duplicate_field("parameters"));
}
parameters = Some(map.next_value()?);
}
Field::Type => {
if type_.is_some() {
return Err(E::duplicate_field("type"));
}
type_ = Some(map.next_value()?);
}
}
}
let support = support.ok_or_else(|| E::missing_field("support"))?;
let shape: Vec<usize> = shape.ok_or_else(|| E::missing_field("shape"))?;
let parameters: Vec<f64> =
parameters.ok_or_else(|| E::missing_field("parameters"))?;
let type_: String = type_.ok_or_else(|| E::missing_field("type"))?;
if type_ != "catphi" {
return Err(E::custom(format!(
"Invalid type for CatPhi: expected 'catphi', found '{type_}'"
)));
}
let parameters = ArrayD::from_shape_vec(shape, parameters).map_err(|evidence| {
E::custom(format!("Invalid parameters shape: {evidence}"))
})?;
CatPhi::new(support, parameters).map_err(E::custom)
}
}
const FIELDS: &[&str] = &["support", "shape", "parameters", "type"];
deserializer.deserialize_struct("CatPhi", FIELDS, CatPhiVisitor)
}
}
impl_json_io!(CatPhi);