1use crate::{Background, Result, ShapError};
2use ndarray::{Array1, Array2, ArrayView1, Axis};
3pub trait Masker {
4 fn n_features(&self) -> usize;
6 fn n_input_features(&self) -> usize {
10 self.n_features()
11 }
12 fn attribution_data(&self, samples: ndarray::ArrayView2<'_, f64>) -> Result<Array2<f64>> {
14 if samples.ncols() != self.n_input_features() {
15 return Err(ShapError::DimensionMismatch {
16 expected: format!("{} input features", self.n_input_features()),
17 found: format!("{}", samples.ncols()),
18 });
19 }
20 Ok(samples.to_owned())
21 }
22 fn mask(&self, sample: ArrayView1<'_, f64>, present: &[bool]) -> Result<Array2<f64>>;
23
24 fn for_each_masked_batch(
31 &self,
32 sample: ArrayView1<'_, f64>,
33 present: &[bool],
34 visitor: &mut dyn FnMut(Array2<f64>) -> Result<()>,
35 ) -> Result<()> {
36 visitor(self.mask(sample, present)?)
37 }
38
39 fn streams_masked_batches(&self) -> bool {
42 false
43 }
44}
45impl<T: Masker + ?Sized> Masker for &T {
46 fn n_features(&self) -> usize {
47 (**self).n_features()
48 }
49 fn n_input_features(&self) -> usize {
50 (**self).n_input_features()
51 }
52 fn attribution_data(&self, samples: ndarray::ArrayView2<'_, f64>) -> Result<Array2<f64>> {
53 (**self).attribution_data(samples)
54 }
55 fn mask(&self, sample: ArrayView1<'_, f64>, present: &[bool]) -> Result<Array2<f64>> {
56 (**self).mask(sample, present)
57 }
58 fn for_each_masked_batch(
59 &self,
60 sample: ArrayView1<'_, f64>,
61 present: &[bool],
62 visitor: &mut dyn FnMut(Array2<f64>) -> Result<()>,
63 ) -> Result<()> {
64 (**self).for_each_masked_batch(sample, present, visitor)
65 }
66 fn streams_masked_batches(&self) -> bool {
67 (**self).streams_masked_batches()
68 }
69}
70
71pub struct FnStreamingMasker<F> {
80 n_features: usize,
81 batch_fn: F,
82}
83
84impl<F> FnStreamingMasker<F> {
85 pub fn new(n_features: usize, batch_fn: F) -> Result<Self> {
86 if n_features == 0 {
87 return Err(ShapError::InvalidConfiguration(
88 "streaming masker feature count must be positive".into(),
89 ));
90 }
91 Ok(Self {
92 n_features,
93 batch_fn,
94 })
95 }
96}
97
98impl<F> Masker for FnStreamingMasker<F>
99where
100 F: Fn(ArrayView1<'_, f64>, &[bool], &mut dyn FnMut(Array2<f64>) -> Result<()>) -> Result<()>,
101{
102 fn n_features(&self) -> usize {
103 self.n_features
104 }
105
106 fn mask(&self, sample: ArrayView1<'_, f64>, present: &[bool]) -> Result<Array2<f64>> {
107 let mut batches = Vec::new();
108 self.for_each_masked_batch(sample, present, &mut |batch| {
109 batches.push(batch);
110 Ok(())
111 })?;
112 let rows = batches.iter().try_fold(0usize, |rows, batch| {
113 rows.checked_add(batch.nrows()).ok_or_else(|| {
114 ShapError::InvalidConfiguration("streaming masker row count overflow".into())
115 })
116 })?;
117 crate::error::checked_f64_shape(
118 &[rows, self.n_features],
119 "collected streaming masker output",
120 )?;
121 let mut output = Array2::zeros((rows, self.n_features));
122 let mut offset = 0;
123 for batch in batches {
124 let end = offset + batch.nrows();
125 output
126 .slice_axis_mut(Axis(0), ndarray::Slice::from(offset..end))
127 .assign(&batch);
128 offset = end;
129 }
130 Ok(output)
131 }
132
133 fn for_each_masked_batch(
134 &self,
135 sample: ArrayView1<'_, f64>,
136 present: &[bool],
137 visitor: &mut dyn FnMut(Array2<f64>) -> Result<()>,
138 ) -> Result<()> {
139 if sample.len() != self.n_features || present.len() != self.n_features {
140 return Err(ShapError::DimensionMismatch {
141 expected: format!("{} features", self.n_features),
142 found: format!("sample {}, mask {}", sample.len(), present.len()),
143 });
144 }
145 let mut emitted = false;
146 let mut checked_visitor = |batch: Array2<f64>| {
147 if batch.nrows() == 0 || batch.ncols() != self.n_features {
148 return Err(ShapError::DimensionMismatch {
149 expected: format!("(rows>0, {}) streaming batch", self.n_features),
150 found: format!("{:?}", batch.dim()),
151 });
152 }
153 emitted = true;
154 visitor(batch)
155 };
156 (self.batch_fn)(sample, present, &mut checked_visitor)?;
157 if !emitted {
158 return Err(ShapError::MaskerError(
159 "streaming masker returned no rows".into(),
160 ));
161 }
162 Ok(())
163 }
164
165 fn streams_masked_batches(&self) -> bool {
166 true
167 }
168}
169
170#[derive(Debug, Clone)]
176pub struct GroupedMasker<K> {
177 inner: K,
178 groups: Vec<Vec<usize>>,
179}
180
181impl<K: Masker> GroupedMasker<K> {
182 pub fn new(inner: K, groups: Vec<Vec<usize>>) -> Result<Self> {
183 let n = inner.n_features();
184 if n != inner.n_input_features() {
185 return Err(ShapError::InvalidConfiguration(
186 "nested grouped maskers are not supported".into(),
187 ));
188 }
189 if groups.is_empty() || groups.iter().any(Vec::is_empty) {
190 return Err(ShapError::InvalidConfiguration(
191 "feature groups must be non-empty".into(),
192 ));
193 }
194 let mut seen = vec![false; n];
195 for &column in groups.iter().flatten() {
196 if column >= n {
197 return Err(ShapError::InvalidFeatureIndex {
198 index: column,
199 n_features: n,
200 });
201 }
202 if std::mem::replace(&mut seen[column], true) {
203 return Err(ShapError::InvalidConfiguration(format!(
204 "source column {column} belongs to more than one feature group"
205 )));
206 }
207 }
208 if seen.iter().any(|included| !included) {
209 return Err(ShapError::InvalidConfiguration(
210 "feature groups must cover every source column exactly once".into(),
211 ));
212 }
213 Ok(Self { inner, groups })
214 }
215
216 pub fn inner(&self) -> &K {
217 &self.inner
218 }
219
220 pub fn groups(&self) -> &[Vec<usize>] {
221 &self.groups
222 }
223}
224
225impl<K: Masker> Masker for GroupedMasker<K> {
226 fn n_features(&self) -> usize {
227 self.groups.len()
228 }
229
230 fn n_input_features(&self) -> usize {
231 self.inner.n_input_features()
232 }
233
234 fn attribution_data(&self, samples: ndarray::ArrayView2<'_, f64>) -> Result<Array2<f64>> {
235 if samples.ncols() != self.n_input_features() {
236 return Err(ShapError::DimensionMismatch {
237 expected: format!("{} input features", self.n_input_features()),
238 found: format!("{}", samples.ncols()),
239 });
240 }
241 let mut grouped = Array2::zeros((samples.nrows(), self.groups.len()));
242 for (group_index, group) in self.groups.iter().enumerate() {
243 for row in 0..samples.nrows() {
244 grouped[[row, group_index]] = group
245 .iter()
246 .map(|&column| samples[[row, column]])
247 .sum::<f64>()
248 / group.len() as f64;
249 }
250 }
251 Ok(grouped)
252 }
253
254 fn mask(&self, sample: ArrayView1<'_, f64>, present: &[bool]) -> Result<Array2<f64>> {
255 if sample.len() != self.n_input_features() || present.len() != self.n_features() {
256 return Err(ShapError::DimensionMismatch {
257 expected: format!(
258 "{} input columns and {} feature groups",
259 self.n_input_features(),
260 self.n_features()
261 ),
262 found: format!("sample {}, mask {}", sample.len(), present.len()),
263 });
264 }
265 let mut expanded = vec![false; self.inner.n_features()];
266 for (group, &on) in self.groups.iter().zip(present) {
267 for &column in group {
268 expanded[column] = on;
269 }
270 }
271 self.inner.mask(sample, &expanded)
272 }
273
274 fn for_each_masked_batch(
275 &self,
276 sample: ArrayView1<'_, f64>,
277 present: &[bool],
278 visitor: &mut dyn FnMut(Array2<f64>) -> Result<()>,
279 ) -> Result<()> {
280 if sample.len() != self.n_input_features() || present.len() != self.n_features() {
281 return Err(ShapError::DimensionMismatch {
282 expected: format!(
283 "{} input columns and {} feature groups",
284 self.n_input_features(),
285 self.n_features()
286 ),
287 found: format!("sample {}, mask {}", sample.len(), present.len()),
288 });
289 }
290 let mut expanded = vec![false; self.inner.n_features()];
291 for (group, &on) in self.groups.iter().zip(present) {
292 for &column in group {
293 expanded[column] = on;
294 }
295 }
296 self.inner.for_each_masked_batch(sample, &expanded, visitor)
297 }
298
299 fn streams_masked_batches(&self) -> bool {
300 self.inner.streams_masked_batches()
301 }
302}
303pub struct FnMasker<F> {
306 n_features: usize,
307 mask_fn: F,
308}
309impl<F> FnMasker<F> {
310 pub fn new(n_features: usize, mask_fn: F) -> Result<Self> {
311 if n_features == 0 {
312 return Err(ShapError::InvalidConfiguration(
313 "masker must expose at least one feature".into(),
314 ));
315 }
316 Ok(Self {
317 n_features,
318 mask_fn,
319 })
320 }
321}
322impl<F> Masker for FnMasker<F>
323where
324 F: Fn(ArrayView1<'_, f64>, &[bool]) -> Result<Array2<f64>>,
325{
326 fn n_features(&self) -> usize {
327 self.n_features
328 }
329 fn mask(&self, sample: ArrayView1<'_, f64>, present: &[bool]) -> Result<Array2<f64>> {
330 if sample.len() != self.n_features || present.len() != self.n_features {
331 return Err(ShapError::DimensionMismatch {
332 expected: format!("{} features", self.n_features),
333 found: format!("sample {}, mask {}", sample.len(), present.len()),
334 });
335 }
336 (self.mask_fn)(sample, present)
337 }
338}
339#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
341#[serde(try_from = "FixedMaskerPayload")]
342pub struct FixedMasker {
343 reference: Array1<f64>,
344}
345#[derive(serde::Deserialize)]
346struct FixedMaskerPayload {
347 reference: Array1<f64>,
348}
349impl TryFrom<FixedMaskerPayload> for FixedMasker {
350 type Error = ShapError;
351 fn try_from(payload: FixedMaskerPayload) -> Result<Self> {
352 Self::new(payload.reference)
353 }
354}
355impl FixedMasker {
356 pub fn new(reference: Array1<f64>) -> Result<Self> {
357 if reference.is_empty() {
358 return Err(ShapError::InvalidConfiguration(
359 "fixed masker reference cannot be empty".into(),
360 ));
361 }
362 Ok(Self { reference })
363 }
364 pub fn reference(&self) -> ndarray::ArrayView1<'_, f64> {
365 self.reference.view()
366 }
367 pub fn validate(&self) -> Result<()> {
368 if self.reference.is_empty() {
369 return Err(ShapError::InvalidConfiguration(
370 "fixed masker reference cannot be empty".into(),
371 ));
372 }
373 Ok(())
374 }
375}
376impl Masker for FixedMasker {
377 fn n_features(&self) -> usize {
378 self.reference.len()
379 }
380 fn mask(&self, sample: ArrayView1<'_, f64>, present: &[bool]) -> Result<Array2<f64>> {
381 self.validate()?;
382 if sample.len() != self.n_features() || present.len() != self.n_features() {
383 return Err(ShapError::DimensionMismatch {
384 expected: format!("{} features", self.n_features()),
385 found: format!("sample {}, mask {}", sample.len(), present.len()),
386 });
387 }
388 let mut row = self.reference.clone();
389 for (j, &on) in present.iter().enumerate() {
390 if on {
391 row[j] = sample[j]
392 }
393 }
394 Array2::from_shape_vec((1, row.len()), row.to_vec())
395 .map_err(|e| ShapError::MaskerError(e.to_string()))
396 }
397}
398
399#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
402#[serde(try_from = "TextMaskerPayload")]
403pub struct TextMasker {
404 n_tokens: usize,
405 mask_token: f64,
406 immutable: Vec<bool>,
407}
408#[derive(serde::Deserialize)]
409struct TextMaskerPayload {
410 n_tokens: usize,
411 mask_token: f64,
412 immutable: Vec<bool>,
413}
414impl TryFrom<TextMaskerPayload> for TextMasker {
415 type Error = ShapError;
416 fn try_from(payload: TextMaskerPayload) -> Result<Self> {
417 let masker = Self {
418 n_tokens: payload.n_tokens,
419 mask_token: payload.mask_token,
420 immutable: payload.immutable,
421 };
422 masker.validate()?;
423 Ok(masker)
424 }
425}
426impl TextMasker {
427 pub fn new(n_tokens: usize, mask_token: f64) -> Result<Self> {
428 if n_tokens == 0 || !mask_token.is_finite() {
429 return Err(ShapError::InvalidConfiguration(
430 "text masker requires tokens and a finite mask token".into(),
431 ));
432 }
433 Ok(Self {
434 n_tokens,
435 mask_token,
436 immutable: vec![false; n_tokens],
437 })
438 }
439 pub fn with_immutable_positions(mut self, positions: &[usize]) -> Result<Self> {
440 for &j in positions {
441 if j >= self.n_tokens {
442 return Err(ShapError::InvalidFeatureIndex {
443 index: j,
444 n_features: self.n_tokens,
445 });
446 }
447 self.immutable[j] = true
448 }
449 Ok(self)
450 }
451 pub fn validate(&self) -> Result<()> {
452 if self.n_tokens == 0
453 || !self.mask_token.is_finite()
454 || self.immutable.len() != self.n_tokens
455 {
456 return Err(ShapError::InvalidConfiguration(
457 "text masker has invalid token metadata".into(),
458 ));
459 }
460 Ok(())
461 }
462}
463impl Masker for TextMasker {
464 fn n_features(&self) -> usize {
465 self.n_tokens
466 }
467 fn mask(&self, sample: ArrayView1<'_, f64>, present: &[bool]) -> Result<Array2<f64>> {
468 self.validate()?;
469 if sample.len() != self.n_tokens || present.len() != self.n_tokens {
470 return Err(ShapError::DimensionMismatch {
471 expected: format!("{} tokens", self.n_tokens),
472 found: format!("sample {}, mask {}", sample.len(), present.len()),
473 });
474 }
475 let row = (0..self.n_tokens)
476 .map(|j| {
477 if present[j] || self.immutable[j] {
478 sample[j]
479 } else {
480 self.mask_token
481 }
482 })
483 .collect::<Vec<_>>();
484 Array2::from_shape_vec((1, self.n_tokens), row)
485 .map_err(|e| ShapError::MaskerError(e.to_string()))
486 }
487}
488
489#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
491#[serde(try_from = "TextTokenPayload")]
492pub struct TextToken {
493 id: f64,
494 text: String,
495 special: bool,
496 group: usize,
497}
498
499#[derive(serde::Deserialize)]
500struct TextTokenPayload {
501 id: f64,
502 text: String,
503 special: bool,
504 group: usize,
505}
506
507impl TryFrom<TextTokenPayload> for TextToken {
508 type Error = ShapError;
509
510 fn try_from(payload: TextTokenPayload) -> Result<Self> {
511 Ok(Self::new(payload.id, payload.text, payload.group)?.special(payload.special))
512 }
513}
514
515impl TextToken {
516 pub fn new(id: f64, text: impl Into<String>, group: usize) -> Result<Self> {
517 if !id.is_finite() {
518 return Err(ShapError::InvalidConfiguration(
519 "text token ID must be finite".into(),
520 ));
521 }
522 Ok(Self {
523 id,
524 text: text.into(),
525 special: false,
526 group,
527 })
528 }
529
530 pub fn special(mut self, special: bool) -> Self {
531 self.special = special;
532 self
533 }
534
535 pub fn id(&self) -> f64 {
536 self.id
537 }
538
539 pub fn text(&self) -> &str {
540 &self.text
541 }
542
543 pub fn is_special(&self) -> bool {
544 self.special
545 }
546
547 pub fn group(&self) -> usize {
548 self.group
549 }
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
554pub enum SpecialTokenPolicy {
555 Preserve,
557 Mask,
559}
560
561#[derive(Debug, Clone, PartialEq)]
568pub struct TokenizedTextMasker {
569 tokens: Vec<TextToken>,
570 position_groups: Vec<Option<usize>>,
571 group_count: usize,
572 mask_token_id: f64,
573 mask_text: String,
574 separator: String,
575 special_policy: SpecialTokenPolicy,
576}
577
578impl TokenizedTextMasker {
579 pub fn new(
580 tokens: Vec<TextToken>,
581 mask_token_id: f64,
582 mask_text: impl Into<String>,
583 special_policy: SpecialTokenPolicy,
584 ) -> Result<Self> {
585 if tokens.is_empty() || !mask_token_id.is_finite() {
586 return Err(ShapError::InvalidConfiguration(
587 "tokenized text masker requires tokens and a finite mask token ID".into(),
588 ));
589 }
590 if tokens.iter().any(|token| !token.id.is_finite()) {
591 return Err(ShapError::InvalidConfiguration(
592 "text token IDs must be finite".into(),
593 ));
594 }
595 let mut groups = std::collections::HashMap::new();
596 let mut position_groups = Vec::with_capacity(tokens.len());
597 for token in &tokens {
598 if token.special && special_policy == SpecialTokenPolicy::Preserve {
599 position_groups.push(None);
600 } else {
601 let next = groups.len();
602 let group = *groups.entry(token.group).or_insert(next);
603 position_groups.push(Some(group));
604 }
605 }
606 if groups.is_empty() {
607 return Err(ShapError::InvalidConfiguration(
608 "tokenized text masker must expose at least one non-preserved token group".into(),
609 ));
610 }
611 Ok(Self {
612 tokens,
613 position_groups,
614 group_count: groups.len(),
615 mask_token_id,
616 mask_text: mask_text.into(),
617 separator: String::new(),
618 special_policy,
619 })
620 }
621
622 pub fn with_separator(mut self, separator: impl Into<String>) -> Self {
624 self.separator = separator.into();
625 self
626 }
627
628 pub fn tokens(&self) -> &[TextToken] {
629 &self.tokens
630 }
631
632 pub fn special_token_policy(&self) -> SpecialTokenPolicy {
633 self.special_policy
634 }
635
636 pub fn token_ids(&self) -> Array1<f64> {
638 Array1::from_iter(self.tokens.iter().map(TextToken::id))
639 }
640
641 pub fn reconstruct(&self, present: &[bool]) -> Result<String> {
643 if present.len() != self.group_count {
644 return Err(ShapError::DimensionMismatch {
645 expected: format!("{} token groups", self.group_count),
646 found: format!("{}", present.len()),
647 });
648 }
649 Ok(self
650 .tokens
651 .iter()
652 .zip(&self.position_groups)
653 .map(|(token, group)| match group {
654 None => token.text.as_str(),
655 Some(group) if present[*group] => token.text.as_str(),
656 Some(_) => self.mask_text.as_str(),
657 })
658 .collect::<Vec<_>>()
659 .join(&self.separator))
660 }
661}
662
663impl Masker for TokenizedTextMasker {
664 fn n_features(&self) -> usize {
665 self.group_count
666 }
667
668 fn n_input_features(&self) -> usize {
669 self.tokens.len()
670 }
671
672 fn attribution_data(&self, samples: ndarray::ArrayView2<'_, f64>) -> Result<Array2<f64>> {
673 if samples.ncols() != self.tokens.len() {
674 return Err(ShapError::DimensionMismatch {
675 expected: format!("{} token IDs", self.tokens.len()),
676 found: format!("{}", samples.ncols()),
677 });
678 }
679 let mut grouped = Array2::zeros((samples.nrows(), self.group_count));
680 let mut counts = vec![0usize; self.group_count];
681 for group in self.position_groups.iter().flatten() {
682 counts[*group] += 1;
683 }
684 for row in 0..samples.nrows() {
685 for (position, group) in self.position_groups.iter().enumerate() {
686 if let Some(group) = group {
687 grouped[[row, *group]] += samples[[row, position]];
688 }
689 }
690 for group in 0..self.group_count {
691 grouped[[row, group]] /= counts[group] as f64;
692 }
693 }
694 Ok(grouped)
695 }
696
697 fn mask(&self, sample: ArrayView1<'_, f64>, present: &[bool]) -> Result<Array2<f64>> {
698 if sample.len() != self.tokens.len() || present.len() != self.group_count {
699 return Err(ShapError::DimensionMismatch {
700 expected: format!(
701 "{} token IDs and {} token groups",
702 self.tokens.len(),
703 self.group_count
704 ),
705 found: format!("sample {}, mask {}", sample.len(), present.len()),
706 });
707 }
708 let row = self
709 .position_groups
710 .iter()
711 .enumerate()
712 .map(|(position, group)| match group {
713 None => sample[position],
714 Some(group) if present[*group] => sample[position],
715 Some(_) => self.mask_token_id,
716 })
717 .collect::<Vec<_>>();
718 Array2::from_shape_vec((1, row.len()), row)
719 .map_err(|error| ShapError::MaskerError(error.to_string()))
720 }
721}
722
723#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
726#[serde(try_from = "ImageMaskerPayload")]
727pub struct ImageMasker {
728 width: usize,
729 height: usize,
730 channels: usize,
731 reference: Array1<f64>,
732}
733
734#[derive(Debug, Clone, PartialEq)]
736pub enum ImageBaseline {
737 Reference(Array1<f64>),
739 Blur { radius: usize },
741}
742
743#[derive(Debug, Clone, PartialEq)]
746pub struct SegmentedImageMasker {
747 width: usize,
748 height: usize,
749 channels: usize,
750 segments: Vec<usize>,
751 segment_count: usize,
752 baseline: ImageBaseline,
753}
754
755impl SegmentedImageMasker {
756 pub fn new(
757 width: usize,
758 height: usize,
759 channels: usize,
760 segments: Vec<usize>,
761 baseline: ImageBaseline,
762 ) -> Result<Self> {
763 let pixels = width
764 .checked_mul(height)
765 .ok_or_else(|| ShapError::InvalidConfiguration("image dimensions overflow".into()))?;
766 let values = pixels
767 .checked_mul(channels)
768 .ok_or_else(|| ShapError::InvalidConfiguration("image dimensions overflow".into()))?;
769 if values == 0 || segments.len() != pixels {
770 return Err(ShapError::DimensionMismatch {
771 expected: format!("{pixels} segment labels"),
772 found: format!("{}", segments.len()),
773 });
774 }
775 let mut labels = std::collections::HashMap::new();
776 let mut normalized = Vec::with_capacity(pixels);
777 for label in segments {
778 let next = labels.len();
779 let index = *labels.entry(label).or_insert(next);
780 normalized.push(index);
781 }
782 match &baseline {
783 ImageBaseline::Reference(reference) if reference.len() != values => {
784 return Err(ShapError::DimensionMismatch {
785 expected: format!("{values} reference values"),
786 found: format!("{}", reference.len()),
787 });
788 }
789 ImageBaseline::Blur { radius: 0 } => {
790 return Err(ShapError::InvalidConfiguration(
791 "image blur radius must be positive".into(),
792 ));
793 }
794 _ => {}
795 }
796 Ok(Self {
797 width,
798 height,
799 channels,
800 segment_count: labels.len(),
801 segments: normalized,
802 baseline,
803 })
804 }
805
806 pub fn dimensions(&self) -> (usize, usize, usize) {
807 (self.width, self.height, self.channels)
808 }
809
810 pub fn segments(&self) -> &[usize] {
811 &self.segments
812 }
813
814 pub fn baseline(&self) -> &ImageBaseline {
815 &self.baseline
816 }
817
818 fn blurred(&self, sample: ArrayView1<'_, f64>, radius: usize) -> Array1<f64> {
819 let mut output = Array1::zeros(sample.len());
820 for y in 0..self.height {
821 let y0 = y.saturating_sub(radius);
822 let y1 = y.saturating_add(radius).min(self.height - 1);
823 for x in 0..self.width {
824 let x0 = x.saturating_sub(radius);
825 let x1 = x.saturating_add(radius).min(self.width - 1);
826 let count = (y1 - y0 + 1) * (x1 - x0 + 1);
827 for channel in 0..self.channels {
828 let sum = (y0..=y1)
829 .flat_map(|source_y| (x0..=x1).map(move |source_x| (source_y, source_x)))
830 .map(|(source_y, source_x)| {
831 sample[(source_y * self.width + source_x) * self.channels + channel]
832 })
833 .sum::<f64>();
834 output[(y * self.width + x) * self.channels + channel] = sum / count as f64;
835 }
836 }
837 }
838 output
839 }
840}
841
842impl Masker for SegmentedImageMasker {
843 fn n_features(&self) -> usize {
844 self.segment_count
845 }
846 fn n_input_features(&self) -> usize {
847 self.width * self.height * self.channels
848 }
849
850 fn attribution_data(&self, samples: ndarray::ArrayView2<'_, f64>) -> Result<Array2<f64>> {
851 if samples.ncols() != self.n_input_features() {
852 return Err(ShapError::DimensionMismatch {
853 expected: format!("{} image values", self.n_input_features()),
854 found: format!("{}", samples.ncols()),
855 });
856 }
857 let mut grouped = Array2::zeros((samples.nrows(), self.segment_count));
858 let mut counts = vec![0usize; self.segment_count];
859 for &segment in &self.segments {
860 counts[segment] += self.channels;
861 }
862 for row in 0..samples.nrows() {
863 for (pixel, &segment) in self.segments.iter().enumerate() {
864 for channel in 0..self.channels {
865 grouped[[row, segment]] += samples[[row, pixel * self.channels + channel]];
866 }
867 }
868 for segment in 0..self.segment_count {
869 grouped[[row, segment]] /= counts[segment] as f64;
870 }
871 }
872 Ok(grouped)
873 }
874
875 fn mask(&self, sample: ArrayView1<'_, f64>, present: &[bool]) -> Result<Array2<f64>> {
876 if sample.len() != self.n_input_features() || present.len() != self.segment_count {
877 return Err(ShapError::DimensionMismatch {
878 expected: format!(
879 "{} image values and {} segments",
880 self.n_input_features(),
881 self.segment_count
882 ),
883 found: format!("sample {}, mask {}", sample.len(), present.len()),
884 });
885 }
886 let mut output = match &self.baseline {
887 ImageBaseline::Reference(reference) => reference.clone(),
888 ImageBaseline::Blur { radius } => self.blurred(sample, *radius),
889 };
890 for (pixel, &segment) in self.segments.iter().enumerate() {
891 if present[segment] {
892 for channel in 0..self.channels {
893 let index = pixel * self.channels + channel;
894 output[index] = sample[index];
895 }
896 }
897 }
898 Array2::from_shape_vec((1, output.len()), output.to_vec())
899 .map_err(|error| ShapError::MaskerError(error.to_string()))
900 }
901}
902
903pub struct InpaintingImageMasker<F> {
905 segmented: SegmentedImageMasker,
906 inpaint: F,
907}
908
909impl<F> InpaintingImageMasker<F> {
910 pub fn new(segmented: SegmentedImageMasker, inpaint: F) -> Self {
911 Self { segmented, inpaint }
912 }
913 pub fn segmented(&self) -> &SegmentedImageMasker {
914 &self.segmented
915 }
916}
917
918impl<F> Masker for InpaintingImageMasker<F>
919where
920 F: Fn(ArrayView1<'_, f64>, &[bool], &[usize], (usize, usize, usize)) -> Result<Array2<f64>>,
921{
922 fn n_features(&self) -> usize {
923 self.segmented.n_features()
924 }
925 fn n_input_features(&self) -> usize {
926 self.segmented.n_input_features()
927 }
928 fn attribution_data(&self, samples: ndarray::ArrayView2<'_, f64>) -> Result<Array2<f64>> {
929 self.segmented.attribution_data(samples)
930 }
931 fn mask(&self, sample: ArrayView1<'_, f64>, present: &[bool]) -> Result<Array2<f64>> {
932 if sample.len() != self.n_input_features() || present.len() != self.n_features() {
933 return Err(ShapError::DimensionMismatch {
934 expected: format!(
935 "{} image values and {} segments",
936 self.n_input_features(),
937 self.n_features()
938 ),
939 found: format!("sample {}, mask {}", sample.len(), present.len()),
940 });
941 }
942 let output = (self.inpaint)(
943 sample,
944 present,
945 self.segmented.segments(),
946 self.segmented.dimensions(),
947 )?;
948 if output.nrows() == 0 || output.ncols() != self.n_input_features() {
949 return Err(ShapError::DimensionMismatch {
950 expected: format!(
951 "(rows>0, {}) inpainted image batch",
952 self.n_input_features()
953 ),
954 found: format!("{:?}", output.dim()),
955 });
956 }
957 Ok(output)
958 }
959}
960#[derive(serde::Deserialize)]
961struct ImageMaskerPayload {
962 width: usize,
963 height: usize,
964 channels: usize,
965 reference: Array1<f64>,
966}
967impl TryFrom<ImageMaskerPayload> for ImageMasker {
968 type Error = ShapError;
969 fn try_from(payload: ImageMaskerPayload) -> Result<Self> {
970 Self::new(
971 payload.width,
972 payload.height,
973 payload.channels,
974 payload.reference,
975 )
976 }
977}
978impl ImageMasker {
979 pub fn new(
980 width: usize,
981 height: usize,
982 channels: usize,
983 reference: Array1<f64>,
984 ) -> Result<Self> {
985 let expected = width
986 .checked_mul(height)
987 .and_then(|x| x.checked_mul(channels))
988 .ok_or_else(|| ShapError::InvalidConfiguration("image dimensions overflow".into()))?;
989 if expected == 0 || reference.len() != expected {
990 return Err(ShapError::DimensionMismatch {
991 expected: format!("{expected} image values"),
992 found: format!("{}", reference.len()),
993 });
994 }
995 Ok(Self {
996 width,
997 height,
998 channels,
999 reference,
1000 })
1001 }
1002 pub fn dimensions(&self) -> (usize, usize, usize) {
1003 (self.width, self.height, self.channels)
1004 }
1005 pub fn validate(&self) -> Result<()> {
1006 let expected = self
1007 .width
1008 .checked_mul(self.height)
1009 .and_then(|value| value.checked_mul(self.channels))
1010 .ok_or_else(|| ShapError::InvalidConfiguration("image dimensions overflow".into()))?;
1011 if expected == 0 || self.reference.len() != expected {
1012 return Err(ShapError::DimensionMismatch {
1013 expected: format!("{expected} image values"),
1014 found: format!("{}", self.reference.len()),
1015 });
1016 }
1017 Ok(())
1018 }
1019}
1020impl Masker for ImageMasker {
1021 fn n_features(&self) -> usize {
1022 self.reference.len()
1023 }
1024 fn mask(&self, sample: ArrayView1<'_, f64>, present: &[bool]) -> Result<Array2<f64>> {
1025 self.validate()?;
1026 if sample.len() != self.n_features() || present.len() != self.n_features() {
1027 return Err(ShapError::DimensionMismatch {
1028 expected: format!("{} image values", self.n_features()),
1029 found: format!("sample {}, mask {}", sample.len(), present.len()),
1030 });
1031 }
1032 let mut row = self.reference.clone();
1033 for (j, &on) in present.iter().enumerate() {
1034 if on {
1035 row[j] = sample[j]
1036 }
1037 }
1038 Array2::from_shape_vec((1, row.len()), row.to_vec())
1039 .map_err(|e| ShapError::MaskerError(e.to_string()))
1040 }
1041}
1042#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1043pub struct IndependentMasker {
1044 background: Background,
1045}
1046impl IndependentMasker {
1047 pub fn new(background: Background) -> Self {
1048 Self { background }
1049 }
1050 pub fn background(&self) -> &Background {
1051 &self.background
1052 }
1053 pub fn baseline(&self) -> Result<Array1<f64>> {
1054 self.background.validate()?;
1055 self.background
1056 .data()
1057 .mean_axis(Axis(0))
1058 .ok_or(ShapError::EmptyBackground)
1059 }
1060}
1061impl Masker for IndependentMasker {
1062 fn n_features(&self) -> usize {
1063 self.background.n_features()
1064 }
1065 fn mask(&self, sample: ArrayView1<'_, f64>, present: &[bool]) -> Result<Array2<f64>> {
1066 self.background.validate()?;
1067 if sample.len() != self.n_features() || present.len() != self.n_features() {
1068 return Err(ShapError::DimensionMismatch {
1069 expected: format!("{} features", self.n_features()),
1070 found: format!("sample {}, mask {}", sample.len(), present.len()),
1071 });
1072 }
1073 let mut out = self.background.data().to_owned();
1074 for (j, &on) in present.iter().enumerate() {
1075 if on {
1076 out.column_mut(j).fill(sample[j])
1077 }
1078 }
1079 Ok(out)
1080 }
1081}
1082
1083#[derive(Debug, Clone)]
1087pub struct ConditionalTabularMasker {
1088 background: Background,
1089 categorical: Vec<bool>,
1090 scales: Vec<f64>,
1091 neighbors: usize,
1092}
1093
1094impl ConditionalTabularMasker {
1095 pub fn new(
1096 background: Background,
1097 categorical_features: &[usize],
1098 neighbors: usize,
1099 ) -> Result<Self> {
1100 background.validate()?;
1101 if neighbors == 0 {
1102 return Err(ShapError::InvalidConfiguration(
1103 "conditional tabular neighbors must be positive".into(),
1104 ));
1105 }
1106 let mut categorical = vec![false; background.n_features()];
1107 for &feature in categorical_features {
1108 if feature >= categorical.len() {
1109 return Err(ShapError::InvalidFeatureIndex {
1110 index: feature,
1111 n_features: categorical.len(),
1112 });
1113 }
1114 if categorical[feature] {
1115 return Err(ShapError::InvalidConfiguration(
1116 "categorical feature indices must be unique".into(),
1117 ));
1118 }
1119 categorical[feature] = true;
1120 }
1121 let means = background.data().mean_axis(Axis(0)).unwrap();
1122 let scales = (0..background.n_features())
1123 .map(|feature| {
1124 let variance = background
1125 .data()
1126 .column(feature)
1127 .iter()
1128 .filter(|value| value.is_finite())
1129 .map(|value| (value - means[feature]).powi(2))
1130 .sum::<f64>()
1131 / background.n_samples() as f64;
1132 let scale = variance.sqrt();
1133 if scale.is_finite() && scale > f64::EPSILON {
1134 scale
1135 } else {
1136 1.0
1137 }
1138 })
1139 .collect();
1140 Ok(Self {
1141 background,
1142 categorical,
1143 scales,
1144 neighbors,
1145 })
1146 }
1147
1148 pub fn background(&self) -> &Background {
1149 &self.background
1150 }
1151
1152 pub fn neighbors(&self) -> usize {
1153 self.neighbors
1154 }
1155}
1156
1157impl Masker for ConditionalTabularMasker {
1158 fn n_features(&self) -> usize {
1159 self.background.n_features()
1160 }
1161
1162 fn mask(&self, sample: ArrayView1<'_, f64>, present: &[bool]) -> Result<Array2<f64>> {
1163 self.background.validate()?;
1164 if sample.len() != self.n_features() || present.len() != self.n_features() {
1165 return Err(ShapError::DimensionMismatch {
1166 expected: format!("{} features", self.n_features()),
1167 found: format!("sample {}, mask {}", sample.len(), present.len()),
1168 });
1169 }
1170 let selected = if present.iter().any(|value| *value) {
1171 let mut distances = self
1172 .background
1173 .data()
1174 .rows()
1175 .into_iter()
1176 .enumerate()
1177 .map(|(row, values)| {
1178 let distance = present
1179 .iter()
1180 .enumerate()
1181 .filter(|(_, enabled)| **enabled)
1182 .map(|(feature, _)| {
1183 let left = sample[feature];
1184 let right = values[feature];
1185 if left.is_nan() || right.is_nan() {
1186 if left.is_nan() && right.is_nan() {
1187 0.0
1188 } else {
1189 1.0
1190 }
1191 } else if self.categorical[feature] {
1192 if left == right {
1193 0.0
1194 } else {
1195 1.0
1196 }
1197 } else {
1198 ((left - right) / self.scales[feature]).powi(2)
1199 }
1200 })
1201 .sum::<f64>();
1202 (row, distance)
1203 })
1204 .collect::<Vec<_>>();
1205 distances.sort_by(|left, right| {
1206 left.1
1207 .total_cmp(&right.1)
1208 .then_with(|| left.0.cmp(&right.0))
1209 });
1210 distances
1211 .into_iter()
1212 .take(self.neighbors.min(self.background.n_samples()))
1213 .map(|(row, _)| row)
1214 .collect::<Vec<_>>()
1215 } else {
1216 (0..self.background.n_samples()).collect()
1217 };
1218 let mut output = self.background.select(&selected)?.data().to_owned();
1219 for (feature, enabled) in present.iter().copied().enumerate() {
1220 if enabled {
1221 output.column_mut(feature).fill(sample[feature]);
1222 }
1223 }
1224 Ok(output)
1225 }
1226}
1227#[cfg(test)]
1228mod structured_tests {
1229 use super::*;
1230 use ndarray::array;
1231 #[test]
1232 fn text_masker_preserves_special_tokens() {
1233 let m = TextMasker::new(3, 99.)
1234 .unwrap()
1235 .with_immutable_positions(&[0])
1236 .unwrap();
1237 let out = m
1238 .mask(array![1., 2., 3.].view(), &[false, false, true])
1239 .unwrap();
1240 assert_eq!(out, array![[1., 99., 3.]]);
1241 }
1242 #[test]
1243 fn tokenized_text_groups_pieces_and_reconstructs_text() {
1244 let masker = TokenizedTextMasker::new(
1245 vec![
1246 TextToken::new(101., "[CLS]", 99).unwrap().special(true),
1247 TextToken::new(10., "walk", 0).unwrap(),
1248 TextToken::new(11., "##ing", 0).unwrap(),
1249 TextToken::new(20., " home", 1).unwrap(),
1250 TextToken::new(102., "[SEP]", 100).unwrap().special(true),
1251 ],
1252 0.,
1253 "[MASK]",
1254 SpecialTokenPolicy::Preserve,
1255 )
1256 .unwrap();
1257 assert_eq!(masker.n_features(), 2);
1258 assert_eq!(masker.n_input_features(), 5);
1259 assert_eq!(
1260 masker
1261 .mask(masker.token_ids().view(), &[false, true])
1262 .unwrap(),
1263 array![[101., 0., 0., 20., 102.]]
1264 );
1265 assert_eq!(
1266 masker.reconstruct(&[false, true]).unwrap(),
1267 "[CLS][MASK][MASK] home[SEP]"
1268 );
1269 assert_eq!(
1270 masker
1271 .attribution_data(array![[101., 10., 12., 20., 102.]].view())
1272 .unwrap(),
1273 array![[11., 20.]]
1274 );
1275 }
1276
1277 #[test]
1278 fn tokenized_text_can_mask_special_tokens() {
1279 let masker = TokenizedTextMasker::new(
1280 vec![
1281 TextToken::new(101., "CLS", 0).unwrap().special(true),
1282 TextToken::new(5., "word", 1).unwrap(),
1283 ],
1284 -1.,
1285 "_",
1286 SpecialTokenPolicy::Mask,
1287 )
1288 .unwrap()
1289 .with_separator(" ");
1290 assert_eq!(masker.reconstruct(&[false, true]).unwrap(), "_ word");
1291 assert_eq!(
1292 masker
1293 .mask(masker.token_ids().view(), &[false, true])
1294 .unwrap(),
1295 array![[-1., 5.]]
1296 );
1297 }
1298 #[test]
1299 fn image_masker_uses_reference() {
1300 let m = ImageMasker::new(1, 1, 2, array![0.1, 0.2]).unwrap();
1301 let out = m.mask(array![0.8, 0.9].view(), &[true, false]).unwrap();
1302 assert_eq!(out, array![[0.8, 0.2]]);
1303 }
1304 #[test]
1305 fn segmented_image_masker_controls_pixels_and_channels_together() {
1306 let masker = SegmentedImageMasker::new(
1307 2,
1308 1,
1309 2,
1310 vec![7, 9],
1311 ImageBaseline::Reference(array![0.1, 0.2, 0.3, 0.4]),
1312 )
1313 .unwrap();
1314 let output = masker
1315 .mask(array![1., 2., 3., 4.].view(), &[true, false])
1316 .unwrap();
1317 assert_eq!(output, array![[1., 2., 0.3, 0.4]]);
1318 assert_eq!(masker.n_features(), 2);
1319 assert_eq!(
1320 masker
1321 .attribution_data(array![[1., 3., 5., 7.]].view())
1322 .unwrap(),
1323 array![[2., 6.]]
1324 );
1325 }
1326
1327 #[test]
1328 fn segmented_image_blur_is_channel_aware() {
1329 let masker =
1330 SegmentedImageMasker::new(3, 1, 1, vec![0, 1, 2], ImageBaseline::Blur { radius: 1 })
1331 .unwrap();
1332 let output = masker
1333 .mask(array![0., 3., 9.].view(), &[false, false, false])
1334 .unwrap();
1335 assert_eq!(output, array![[1.5, 4., 6.]]);
1336 }
1337
1338 #[test]
1339 fn inpainting_adapter_validates_callback_shape() {
1340 let segmented =
1341 SegmentedImageMasker::new(1, 1, 1, vec![0], ImageBaseline::Reference(array![0.]))
1342 .unwrap();
1343 let masker = InpaintingImageMasker::new(
1344 segmented,
1345 |_: ArrayView1<'_, f64>,
1346 _: &[bool],
1347 _: &[usize],
1348 _: (usize, usize, usize)|
1349 -> Result<Array2<f64>> { Ok(Array2::zeros((1, 0))) },
1350 );
1351 assert!(masker.mask(array![1.].view(), &[false]).is_err());
1352 }
1353 #[test]
1354 fn conditional_tabular_masker_uses_categorical_and_numeric_distance() {
1355 let masker = ConditionalTabularMasker::new(
1356 Background::new(array![[0., 0.], [1., 0.1], [1., 10.], [2., 0.2]]).unwrap(),
1357 &[0],
1358 1,
1359 )
1360 .unwrap();
1361 let conditioned = masker.mask(array![1., 9.].view(), &[true, true]).unwrap();
1362 assert_eq!(conditioned, array![[1., 9.]]);
1363 let categorical_only = masker.mask(array![1., 9.].view(), &[true, false]).unwrap();
1364 assert_eq!(categorical_only, array![[1., 0.1]]);
1365 assert_eq!(
1366 masker
1367 .mask(array![1., 9.].view(), &[false, false])
1368 .unwrap()
1369 .nrows(),
1370 4
1371 );
1372 }
1373 #[test]
1374 fn grouped_masker_expands_coalitions_and_preserves_source_mapping() {
1375 let inner = FixedMasker::new(array![0., 0., 0.]).unwrap();
1376 let masker = GroupedMasker::new(inner, vec![vec![0, 2], vec![1]]).unwrap();
1377 let masked = masker
1378 .mask(array![2., 4., 8.].view(), &[true, false])
1379 .unwrap();
1380 assert_eq!(masked, array![[2., 0., 8.]]);
1381 assert_eq!(masker.groups(), &[vec![0, 2], vec![1]]);
1382 assert_eq!(
1383 masker
1384 .attribution_data(array![[2., 4., 8.]].view())
1385 .unwrap(),
1386 array![[5., 4.]]
1387 );
1388 }
1389 #[test]
1390 fn rejects_invalid_deserialized_style_maskers_before_indexing() {
1391 let text = TextMasker {
1392 n_tokens: 2,
1393 mask_token: 0.,
1394 immutable: vec![false],
1395 };
1396 assert!(text.mask(array![1., 2.].view(), &[false, false]).is_err());
1397
1398 let image = ImageMasker {
1399 width: 2,
1400 height: 2,
1401 channels: 1,
1402 reference: array![0., 0.],
1403 };
1404 assert!(image.validate().is_err());
1405 }
1406
1407 #[cfg(feature = "json-adapters")]
1408 #[test]
1409 fn serializable_builtin_maskers_round_trip() {
1410 let fixed = FixedMasker::new(array![0., 1.]).unwrap();
1411 let decoded: FixedMasker =
1412 serde_json::from_str(&serde_json::to_string(&fixed).unwrap()).unwrap();
1413 assert_eq!(decoded, fixed);
1414
1415 let independent = IndependentMasker::new(Background::new(array![[0., 1.]]).unwrap());
1416 let decoded: IndependentMasker =
1417 serde_json::from_str(&serde_json::to_string(&independent).unwrap()).unwrap();
1418 assert_eq!(decoded, independent);
1419 }
1420}