1use std::sync::Arc;
6
7use crate::ids::VariableId;
8
9use super::error::QueryError;
10
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub struct AnomalyAttributionQuery {
14 pub targets: Arc<[VariableId]>,
16 pub unit_rows: Option<Arc<[usize]>>,
18 pub max_units: usize,
20}
21
22impl AnomalyAttributionQuery {
23 #[must_use]
25 pub fn new(targets: impl Into<Arc<[VariableId]>>, max_units: usize) -> Self {
26 Self { targets: targets.into(), unit_rows: None, max_units }
27 }
28
29 #[must_use]
31 pub fn with_unit_rows(mut self, rows: impl Into<Arc<[usize]>>) -> Self {
32 self.unit_rows = Some(rows.into());
33 self
34 }
35
36 pub fn validate(&self) -> Result<(), QueryError> {
42 if self.targets.is_empty() {
43 return Err(QueryError::EmptyAnomalyTargets);
44 }
45 if self.max_units == 0 {
46 return Err(QueryError::NonPositiveAnomalyLimit);
47 }
48 Ok(())
49 }
50}
51
52#[derive(Clone, Debug, PartialEq, Eq, Hash)]
54#[non_exhaustive]
55pub enum PopulationSelector {
56 All,
58 Rows(Arc<[usize]>),
60 Environment {
62 env_index: usize,
64 },
65 TimeRange {
67 start: usize,
69 end: usize,
71 },
72}
73
74impl PopulationSelector {
75 pub fn validate(&self) -> Result<(), QueryError> {
81 match self {
82 Self::All | Self::Environment { .. } => Ok(()),
83 Self::Rows(rows) => {
84 if rows.is_empty() {
85 Err(QueryError::EmptyPopulationRows)
86 } else {
87 Ok(())
88 }
89 }
90 Self::TimeRange { start, end } => {
91 if *end <= *start {
92 Err(QueryError::InvalidPopulationTimeRange { start: *start, end: *end })
93 } else {
94 Ok(())
95 }
96 }
97 }
98 }
99}
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
103#[non_exhaustive]
104pub enum AttributionComponents {
105 Inputs,
107 Mechanisms,
109 Structure,
111 InputsAndMechanisms,
113 All,
115}
116
117#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
119#[non_exhaustive]
120pub enum ShapleyMode {
121 Exact,
123 MonteCarlo {
125 n_samples: usize,
127 },
128 Permutation {
130 n_permutations: usize,
132 },
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
137pub struct ShapleyConfig {
138 pub mode: ShapleyMode,
140 pub max_exact_components: usize,
142 pub allow_exact_override: bool,
144 pub seed: u64,
146}
147
148impl ShapleyConfig {
149 #[must_use]
151 pub const fn exact() -> Self {
152 Self {
153 mode: ShapleyMode::Exact,
154 max_exact_components: 12,
155 allow_exact_override: false,
156 seed: 0,
157 }
158 }
159
160 #[must_use]
162 pub const fn monte_carlo(n_samples: usize) -> Self {
163 Self {
164 mode: ShapleyMode::MonteCarlo { n_samples },
165 max_exact_components: 12,
166 allow_exact_override: false,
167 seed: 0,
168 }
169 }
170
171 #[must_use]
173 pub const fn permutation(n_permutations: usize) -> Self {
174 Self {
175 mode: ShapleyMode::Permutation { n_permutations },
176 max_exact_components: 12,
177 allow_exact_override: false,
178 seed: 0,
179 }
180 }
181
182 #[must_use]
184 pub const fn with_max_exact_components(mut self, max: usize) -> Self {
185 self.max_exact_components = max;
186 self
187 }
188
189 #[must_use]
191 pub const fn with_exact_override(mut self, allow: bool) -> Self {
192 self.allow_exact_override = allow;
193 self
194 }
195
196 #[must_use]
198 pub const fn with_seed(mut self, seed: u64) -> Self {
199 self.seed = seed;
200 self
201 }
202
203 pub fn validate(&self) -> Result<(), QueryError> {
209 if self.max_exact_components == 0 {
210 return Err(QueryError::NonPositiveShapleyLimit);
211 }
212 match self.mode {
213 ShapleyMode::Exact => Ok(()),
214 ShapleyMode::MonteCarlo { n_samples } => {
215 if n_samples == 0 {
216 Err(QueryError::NonPositiveShapleySamples)
217 } else {
218 Ok(())
219 }
220 }
221 ShapleyMode::Permutation { n_permutations } => {
222 if n_permutations == 0 {
223 Err(QueryError::NonPositiveShapleySamples)
224 } else {
225 Ok(())
226 }
227 }
228 }
229 }
230}
231
232#[derive(Clone, Debug, PartialEq, Eq, Hash)]
234#[non_exhaustive]
235pub enum AllocationMethod {
236 Sequential {
238 order: Arc<[crate::ids::ComponentId]>,
240 },
241 Shapley {
243 approximation: ShapleyConfig,
245 },
246 PathBased,
248}
249
250impl AllocationMethod {
251 pub fn validate(&self) -> Result<(), QueryError> {
257 match self {
258 Self::Sequential { order } if order.is_empty() => Err(QueryError::EmptyAllocationOrder),
259 Self::Sequential { .. } | Self::PathBased => Ok(()),
260 Self::Shapley { approximation } => approximation.validate(),
261 }
262 }
263}
264
265#[derive(Clone, Debug, PartialEq, Eq, Hash)]
267pub struct ChangeAttributionQuery {
268 pub outcome: VariableId,
270 pub baseline: PopulationSelector,
272 pub comparison: PopulationSelector,
274 pub components: AttributionComponents,
276 pub allocation: AllocationMethod,
278 pub max_components: usize,
280}
281
282impl ChangeAttributionQuery {
283 #[must_use]
285 pub fn new(
286 outcome: VariableId,
287 baseline: PopulationSelector,
288 comparison: PopulationSelector,
289 ) -> Self {
290 Self {
291 outcome,
292 baseline,
293 comparison,
294 components: AttributionComponents::Mechanisms,
295 allocation: AllocationMethod::Shapley {
296 approximation: ShapleyConfig::monte_carlo(2_000),
297 },
298 max_components: 64,
299 }
300 }
301
302 #[must_use]
304 pub const fn with_components(mut self, components: AttributionComponents) -> Self {
305 self.components = components;
306 self
307 }
308
309 #[must_use]
311 pub fn with_allocation(mut self, allocation: AllocationMethod) -> Self {
312 self.allocation = allocation;
313 self
314 }
315
316 #[must_use]
318 pub const fn with_max_components(mut self, max_components: usize) -> Self {
319 self.max_components = max_components;
320 self
321 }
322
323 pub fn validate(&self) -> Result<(), QueryError> {
329 if self.max_components == 0 {
330 return Err(QueryError::NonPositiveComponentLimit);
331 }
332 self.baseline.validate()?;
333 self.comparison.validate()?;
334 self.allocation.validate()?;
335 Ok(())
336 }
337}
338
339#[derive(Clone, Debug, PartialEq, Eq, Hash)]
341pub struct MechanismChangeQuery {
342 pub targets: Arc<[VariableId]>,
344 pub baseline: PopulationSelector,
346 pub comparison: PopulationSelector,
348 pub significance_level: OrderedFloatBits,
350 pub max_targets: usize,
352}
353
354#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
356pub struct OrderedFloatBits(u64);
357
358impl OrderedFloatBits {
359 #[must_use]
361 pub fn from_f64(v: f64) -> Self {
362 Self(if v.is_nan() { 0 } else { v.to_bits() })
363 }
364
365 #[must_use]
367 pub const fn to_f64(self) -> f64 {
368 f64::from_bits(self.0)
369 }
370}
371
372impl MechanismChangeQuery {
373 #[must_use]
375 pub fn new(
376 targets: impl Into<Arc<[VariableId]>>,
377 baseline: PopulationSelector,
378 comparison: PopulationSelector,
379 significance_level: f64,
380 max_targets: usize,
381 ) -> Self {
382 Self {
383 targets: targets.into(),
384 baseline,
385 comparison,
386 significance_level: OrderedFloatBits::from_f64(significance_level),
387 max_targets,
388 }
389 }
390
391 pub fn validate(&self) -> Result<(), QueryError> {
397 if self.targets.is_empty() {
398 return Err(QueryError::EmptyMechanismChangeTargets);
399 }
400 if self.max_targets == 0 {
401 return Err(QueryError::NonPositiveComponentLimit);
402 }
403 let alpha = self.significance_level.to_f64();
404 if !(alpha > 0.0 && alpha < 1.0) {
405 return Err(QueryError::InvalidSignificanceLevel);
406 }
407 self.baseline.validate()?;
408 self.comparison.validate()?;
409 Ok(())
410 }
411}
412
413#[derive(Clone, Debug, PartialEq, Eq, Hash)]
415pub struct UnitChangeQuery {
416 pub outcome: VariableId,
418 pub unit_rows: Option<Arc<[usize]>>,
420 pub components: AttributionComponents,
422 pub allocation: AllocationMethod,
424 pub max_units: usize,
426}
427
428impl UnitChangeQuery {
429 #[must_use]
431 pub fn new(outcome: VariableId, max_units: usize) -> Self {
432 Self {
433 outcome,
434 unit_rows: None,
435 components: AttributionComponents::Inputs,
436 allocation: AllocationMethod::Shapley {
437 approximation: ShapleyConfig::monte_carlo(500),
438 },
439 max_units,
440 }
441 }
442
443 #[must_use]
445 pub fn with_unit_rows(mut self, rows: impl Into<Arc<[usize]>>) -> Self {
446 self.unit_rows = Some(rows.into());
447 self
448 }
449
450 #[must_use]
452 pub const fn with_components(mut self, components: AttributionComponents) -> Self {
453 self.components = components;
454 self
455 }
456
457 #[must_use]
459 pub fn with_allocation(mut self, allocation: AllocationMethod) -> Self {
460 self.allocation = allocation;
461 self
462 }
463
464 pub fn validate(&self) -> Result<(), QueryError> {
470 if self.max_units == 0 {
471 return Err(QueryError::NonPositiveAnomalyLimit);
472 }
473 self.allocation.validate()?;
474 Ok(())
475 }
476}