"""This file contains tests for the OutputComposite masker.""" import tempfile import numpy as np import shap def test_output_composite_init(): """Test OutputComposite masker initialization.""" masker = shap.maskers.Fixed() def simple_model(x): return np.sum(x) output_composite = shap.maskers.OutputComposite(masker, simple_model) assert output_composite.masker is masker assert output_composite.model is simple_model def test_output_composite_attribute_propagation(): """Test that attributes from the underlying masker are propagated.""" masker = shap.maskers.Fixed() def simple_model(x): return x output_composite = shap.maskers.OutputComposite(masker, simple_model) # Check that shape is propagated assert hasattr(output_composite, "shape") assert output_composite.shape == masker.shape def test_output_composite_call_with_tuple_output(): """Test OutputComposite __call__ when model returns a tuple.""" masker = shap.maskers.Fixed() def model_with_tuple_output(x): return (np.sum(x), np.mean(x)) output_composite = shap.maskers.OutputComposite(masker, model_with_tuple_output) test_input = np.array([1, 2, 3, 4, 5]) mask = np.array([], dtype=bool) result = output_composite(mask, test_input) # Result should be masked input + model output assert isinstance(result, tuple) # Fixed masker returns 1 element, model returns 2 elements assert len(result) == 3 def test_output_composite_call_with_scalar_output(): """Test OutputComposite __call__ when model returns a scalar.""" masker = shap.maskers.Fixed() def model_with_scalar_output(x): return np.sum(x) output_composite = shap.maskers.OutputComposite(masker, model_with_scalar_output) test_input = np.array([1, 2, 3, 4, 5]) mask = np.array([], dtype=bool) result = output_composite(mask, test_input) # Result should be masked input + model output (wrapped in tuple) assert isinstance(result, tuple) # Fixed masker returns 1 element, model returns 1 element (wrapped) assert len(result) == 2 def test_output_composite_call_with_array_output(): """Test OutputComposite __call__ when model returns an array.""" masker = shap.maskers.Fixed() def model_with_array_output(x): return x * 2 output_composite = shap.maskers.OutputComposite(masker, model_with_array_output) test_input = np.array([1, 2, 3]) mask = np.array([], dtype=bool) result = output_composite(mask, test_input) assert isinstance(result, tuple) assert len(result) == 2 # Check model output is correct np.testing.assert_array_equal(result[1], test_input * 2) def test_output_composite_serialization(): """Test OutputComposite serialization and deserialization.""" masker = shap.maskers.Fixed() def simple_model(x): return np.sum(x) original_composite = shap.maskers.OutputComposite(masker, simple_model) with tempfile.TemporaryFile() as temp_file: # Serialize original_composite.save(temp_file) temp_file.seek(0) # Deserialize loaded_composite = shap.maskers.OutputComposite.load(temp_file) # Verify the loaded masker works test_input = np.array([1, 2, 3]) mask = np.array([], dtype=bool) original_result = original_composite(mask, test_input) loaded_result = loaded_composite(mask, test_input) # Check results are the same assert len(original_result) == len(loaded_result) def test_output_composite_with_multiple_args(): """Test OutputComposite with a model that takes multiple arguments.""" # Create a custom masker that takes 2 arguments class TwoArgMasker(shap.maskers.Masker): def __init__(self): self.shape = (None, 0) def __call__(self, mask, *args): # type: ignore[override] x, y = args return ([x], [y]) masker = TwoArgMasker() def model_two_args(x, y): return np.sum(x) + np.sum(y) output_composite = shap.maskers.OutputComposite(masker, model_two_args) test_input1 = np.array([1, 2, 3]) test_input2 = np.array([4, 5, 6]) mask = np.array([], dtype=bool) result = output_composite(mask, test_input1, test_input2) assert isinstance(result, tuple) # Masker returns 2 elements, model returns 1 (wrapped) assert len(result) == 3 assert result[2] == np.sum(test_input1) + np.sum(test_input2) def test_output_composite_attribute_none_handling(): """Test that OutputComposite handles None attributes correctly.""" # Create a minimal masker without all optional attributes class MinimalMasker(shap.maskers.Masker): def __init__(self): self.shape = (None, 0) # Don't set optional attributes def __call__(self, mask, *args): # type: ignore[override] x = args[0] return ([x],) masker = MinimalMasker() def simple_model(x): return x output_composite = shap.maskers.OutputComposite(masker, simple_model) # Should have shape but not necessarily other attributes assert hasattr(output_composite, "shape") # These attributes shouldn't be set if not present in masker assert not hasattr(output_composite, "invariants") or output_composite.invariants is None def test_output_composite_text_data_flag(): """Test that text_data flag propagates from underlying masker.""" class TextMasker(shap.maskers.Masker): def __init__(self): self.shape = (None, 0) self.text_data = True def __call__(self, mask, *args): # type: ignore[override] x = args[0] return ([x],) masker = TextMasker() def simple_model(x): return x output_composite = shap.maskers.OutputComposite(masker, simple_model) assert hasattr(output_composite, "text_data") assert output_composite.text_data is True def test_output_composite_image_data_flag(): """Test that image_data flag propagates from underlying masker.""" class ImageMasker(shap.maskers.Masker): def __init__(self): self.shape = (None, 0) self.image_data = True def __call__(self, mask, *args): # type: ignore[override] x = args[0] return ([x],) masker = ImageMasker() def simple_model(x): return x output_composite = shap.maskers.OutputComposite(masker, simple_model) assert hasattr(output_composite, "image_data") assert output_composite.image_data is True