use std::sync::Arc;
use crate::ids::VariableId;
use super::error::QueryError;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct AnomalyAttributionQuery {
pub targets: Arc<[VariableId]>,
pub unit_rows: Option<Arc<[usize]>>,
pub max_units: usize,
}
impl AnomalyAttributionQuery {
#[must_use]
pub fn new(targets: impl Into<Arc<[VariableId]>>, max_units: usize) -> Self {
Self { targets: targets.into(), unit_rows: None, max_units }
}
#[must_use]
pub fn with_unit_rows(mut self, rows: impl Into<Arc<[usize]>>) -> Self {
self.unit_rows = Some(rows.into());
self
}
pub fn validate(&self) -> Result<(), QueryError> {
if self.targets.is_empty() {
return Err(QueryError::EmptyAnomalyTargets);
}
if self.max_units == 0 {
return Err(QueryError::NonPositiveAnomalyLimit);
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PopulationSelector {
All,
Rows(Arc<[usize]>),
Environment {
env_index: usize,
},
TimeRange {
start: usize,
end: usize,
},
}
impl PopulationSelector {
pub fn validate(&self) -> Result<(), QueryError> {
match self {
Self::All | Self::Environment { .. } => Ok(()),
Self::Rows(rows) => {
if rows.is_empty() {
Err(QueryError::EmptyPopulationRows)
} else {
Ok(())
}
}
Self::TimeRange { start, end } => {
if *end <= *start {
Err(QueryError::InvalidPopulationTimeRange { start: *start, end: *end })
} else {
Ok(())
}
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum AttributionComponents {
Inputs,
Mechanisms,
Structure,
InputsAndMechanisms,
All,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum ShapleyMode {
Exact,
MonteCarlo {
n_samples: usize,
},
Permutation {
n_permutations: usize,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct ShapleyConfig {
pub mode: ShapleyMode,
pub max_exact_components: usize,
pub allow_exact_override: bool,
pub seed: u64,
}
impl ShapleyConfig {
#[must_use]
pub const fn exact() -> Self {
Self {
mode: ShapleyMode::Exact,
max_exact_components: 12,
allow_exact_override: false,
seed: 0,
}
}
#[must_use]
pub const fn monte_carlo(n_samples: usize) -> Self {
Self {
mode: ShapleyMode::MonteCarlo { n_samples },
max_exact_components: 12,
allow_exact_override: false,
seed: 0,
}
}
#[must_use]
pub const fn permutation(n_permutations: usize) -> Self {
Self {
mode: ShapleyMode::Permutation { n_permutations },
max_exact_components: 12,
allow_exact_override: false,
seed: 0,
}
}
#[must_use]
pub const fn with_max_exact_components(mut self, max: usize) -> Self {
self.max_exact_components = max;
self
}
#[must_use]
pub const fn with_exact_override(mut self, allow: bool) -> Self {
self.allow_exact_override = allow;
self
}
#[must_use]
pub const fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn validate(&self) -> Result<(), QueryError> {
if self.max_exact_components == 0 {
return Err(QueryError::NonPositiveShapleyLimit);
}
match self.mode {
ShapleyMode::Exact => Ok(()),
ShapleyMode::MonteCarlo { n_samples } => {
if n_samples == 0 {
Err(QueryError::NonPositiveShapleySamples)
} else {
Ok(())
}
}
ShapleyMode::Permutation { n_permutations } => {
if n_permutations == 0 {
Err(QueryError::NonPositiveShapleySamples)
} else {
Ok(())
}
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AllocationMethod {
Sequential {
order: Arc<[crate::ids::ComponentId]>,
},
Shapley {
approximation: ShapleyConfig,
},
PathBased,
}
impl AllocationMethod {
pub fn validate(&self) -> Result<(), QueryError> {
match self {
Self::Sequential { order } if order.is_empty() => Err(QueryError::EmptyAllocationOrder),
Self::Sequential { order } => {
for (i, component) in order.iter().enumerate() {
if order[..i].contains(component) {
return Err(QueryError::DuplicateAllocationComponent);
}
}
Ok(())
}
Self::PathBased => Ok(()),
Self::Shapley { approximation } => approximation.validate(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ChangeAttributionQuery {
pub outcome: VariableId,
pub baseline: PopulationSelector,
pub comparison: PopulationSelector,
pub components: AttributionComponents,
pub allocation: AllocationMethod,
pub max_components: usize,
}
impl ChangeAttributionQuery {
#[must_use]
pub fn new(
outcome: VariableId,
baseline: PopulationSelector,
comparison: PopulationSelector,
) -> Self {
Self {
outcome,
baseline,
comparison,
components: AttributionComponents::Mechanisms,
allocation: AllocationMethod::Shapley {
approximation: ShapleyConfig::monte_carlo(2_000),
},
max_components: 64,
}
}
#[must_use]
pub const fn with_components(mut self, components: AttributionComponents) -> Self {
self.components = components;
self
}
#[must_use]
pub fn with_allocation(mut self, allocation: AllocationMethod) -> Self {
self.allocation = allocation;
self
}
#[must_use]
pub const fn with_max_components(mut self, max_components: usize) -> Self {
self.max_components = max_components;
self
}
pub fn validate(&self) -> Result<(), QueryError> {
if self.max_components == 0 {
return Err(QueryError::NonPositiveComponentLimit);
}
self.baseline.validate()?;
self.comparison.validate()?;
self.allocation.validate()?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct MechanismChangeQuery {
pub targets: Arc<[VariableId]>,
pub baseline: PopulationSelector,
pub comparison: PopulationSelector,
pub significance_level: OrderedFloatBits,
pub max_targets: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct OrderedFloatBits(u64);
impl OrderedFloatBits {
#[must_use]
pub fn from_f64(v: f64) -> Self {
Self(if v.is_nan() { 0 } else { v.to_bits() })
}
#[must_use]
pub const fn to_f64(self) -> f64 {
f64::from_bits(self.0)
}
}
impl MechanismChangeQuery {
#[must_use]
pub fn new(
targets: impl Into<Arc<[VariableId]>>,
baseline: PopulationSelector,
comparison: PopulationSelector,
significance_level: f64,
max_targets: usize,
) -> Self {
Self {
targets: targets.into(),
baseline,
comparison,
significance_level: OrderedFloatBits::from_f64(significance_level),
max_targets,
}
}
pub fn validate(&self) -> Result<(), QueryError> {
if self.targets.is_empty() {
return Err(QueryError::EmptyMechanismChangeTargets);
}
if self.max_targets == 0 {
return Err(QueryError::NonPositiveComponentLimit);
}
let alpha = self.significance_level.to_f64();
if !(alpha > 0.0 && alpha < 1.0) {
return Err(QueryError::InvalidSignificanceLevel);
}
self.baseline.validate()?;
self.comparison.validate()?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct UnitChangeQuery {
pub outcome: VariableId,
pub unit_rows: Option<Arc<[usize]>>,
pub components: AttributionComponents,
pub allocation: AllocationMethod,
pub max_units: usize,
}
impl UnitChangeQuery {
#[must_use]
pub fn new(outcome: VariableId, max_units: usize) -> Self {
Self {
outcome,
unit_rows: None,
components: AttributionComponents::Inputs,
allocation: AllocationMethod::Shapley {
approximation: ShapleyConfig::monte_carlo(500),
},
max_units,
}
}
#[must_use]
pub fn with_unit_rows(mut self, rows: impl Into<Arc<[usize]>>) -> Self {
self.unit_rows = Some(rows.into());
self
}
#[must_use]
pub const fn with_components(mut self, components: AttributionComponents) -> Self {
self.components = components;
self
}
#[must_use]
pub fn with_allocation(mut self, allocation: AllocationMethod) -> Self {
self.allocation = allocation;
self
}
pub fn validate(&self) -> Result<(), QueryError> {
if self.max_units == 0 {
return Err(QueryError::NonPositiveAnomalyLimit);
}
self.allocation.validate()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ComponentId;
#[test]
fn sequential_allocation_rejects_duplicate_components() {
let component = ComponentId::from_raw(7);
let allocation = AllocationMethod::Sequential { order: Arc::from([component, component]) };
assert_eq!(allocation.validate(), Err(QueryError::DuplicateAllocationComponent));
}
}