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 { order } => {
260 for (i, component) in order.iter().enumerate() {
261 if order[..i].contains(component) {
262 return Err(QueryError::DuplicateAllocationComponent);
263 }
264 }
265 Ok(())
266 }
267 Self::PathBased => Ok(()),
268 Self::Shapley { approximation } => approximation.validate(),
269 }
270 }
271}
272
273#[derive(Clone, Debug, PartialEq, Eq, Hash)]
275pub struct ChangeAttributionQuery {
276 pub outcome: VariableId,
278 pub baseline: PopulationSelector,
280 pub comparison: PopulationSelector,
282 pub components: AttributionComponents,
284 pub allocation: AllocationMethod,
286 pub max_components: usize,
288}
289
290impl ChangeAttributionQuery {
291 #[must_use]
293 pub fn new(
294 outcome: VariableId,
295 baseline: PopulationSelector,
296 comparison: PopulationSelector,
297 ) -> Self {
298 Self {
299 outcome,
300 baseline,
301 comparison,
302 components: AttributionComponents::Mechanisms,
303 allocation: AllocationMethod::Shapley {
304 approximation: ShapleyConfig::monte_carlo(2_000),
305 },
306 max_components: 64,
307 }
308 }
309
310 #[must_use]
312 pub const fn with_components(mut self, components: AttributionComponents) -> Self {
313 self.components = components;
314 self
315 }
316
317 #[must_use]
319 pub fn with_allocation(mut self, allocation: AllocationMethod) -> Self {
320 self.allocation = allocation;
321 self
322 }
323
324 #[must_use]
326 pub const fn with_max_components(mut self, max_components: usize) -> Self {
327 self.max_components = max_components;
328 self
329 }
330
331 pub fn validate(&self) -> Result<(), QueryError> {
337 if self.max_components == 0 {
338 return Err(QueryError::NonPositiveComponentLimit);
339 }
340 self.baseline.validate()?;
341 self.comparison.validate()?;
342 self.allocation.validate()?;
343 Ok(())
344 }
345}
346
347#[derive(Clone, Debug, PartialEq, Eq, Hash)]
349pub struct MechanismChangeQuery {
350 pub targets: Arc<[VariableId]>,
352 pub baseline: PopulationSelector,
354 pub comparison: PopulationSelector,
356 pub significance_level: OrderedFloatBits,
358 pub max_targets: usize,
360}
361
362#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
364pub struct OrderedFloatBits(u64);
365
366impl OrderedFloatBits {
367 #[must_use]
369 pub fn from_f64(v: f64) -> Self {
370 Self(if v.is_nan() { 0 } else { v.to_bits() })
371 }
372
373 #[must_use]
375 pub const fn to_f64(self) -> f64 {
376 f64::from_bits(self.0)
377 }
378}
379
380impl MechanismChangeQuery {
381 #[must_use]
383 pub fn new(
384 targets: impl Into<Arc<[VariableId]>>,
385 baseline: PopulationSelector,
386 comparison: PopulationSelector,
387 significance_level: f64,
388 max_targets: usize,
389 ) -> Self {
390 Self {
391 targets: targets.into(),
392 baseline,
393 comparison,
394 significance_level: OrderedFloatBits::from_f64(significance_level),
395 max_targets,
396 }
397 }
398
399 pub fn validate(&self) -> Result<(), QueryError> {
405 if self.targets.is_empty() {
406 return Err(QueryError::EmptyMechanismChangeTargets);
407 }
408 if self.max_targets == 0 {
409 return Err(QueryError::NonPositiveComponentLimit);
410 }
411 let alpha = self.significance_level.to_f64();
412 if !(alpha > 0.0 && alpha < 1.0) {
413 return Err(QueryError::InvalidSignificanceLevel);
414 }
415 self.baseline.validate()?;
416 self.comparison.validate()?;
417 Ok(())
418 }
419}
420
421#[derive(Clone, Debug, PartialEq, Eq, Hash)]
423pub struct UnitChangeQuery {
424 pub outcome: VariableId,
426 pub unit_rows: Option<Arc<[usize]>>,
428 pub components: AttributionComponents,
430 pub allocation: AllocationMethod,
432 pub max_units: usize,
434}
435
436impl UnitChangeQuery {
437 #[must_use]
439 pub fn new(outcome: VariableId, max_units: usize) -> Self {
440 Self {
441 outcome,
442 unit_rows: None,
443 components: AttributionComponents::Inputs,
444 allocation: AllocationMethod::Shapley {
445 approximation: ShapleyConfig::monte_carlo(500),
446 },
447 max_units,
448 }
449 }
450
451 #[must_use]
453 pub fn with_unit_rows(mut self, rows: impl Into<Arc<[usize]>>) -> Self {
454 self.unit_rows = Some(rows.into());
455 self
456 }
457
458 #[must_use]
460 pub const fn with_components(mut self, components: AttributionComponents) -> Self {
461 self.components = components;
462 self
463 }
464
465 #[must_use]
467 pub fn with_allocation(mut self, allocation: AllocationMethod) -> Self {
468 self.allocation = allocation;
469 self
470 }
471
472 pub fn validate(&self) -> Result<(), QueryError> {
478 if self.max_units == 0 {
479 return Err(QueryError::NonPositiveAnomalyLimit);
480 }
481 self.allocation.validate()?;
482 Ok(())
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use crate::ComponentId;
490
491 #[test]
492 fn sequential_allocation_rejects_duplicate_components() {
493 let component = ComponentId::from_raw(7);
494 let allocation = AllocationMethod::Sequential { order: Arc::from([component, component]) };
495 assert_eq!(allocation.validate(), Err(QueryError::DuplicateAllocationComponent));
496 }
497}