"""Tests for the Claus Breast Cancer Risk Model. Ground truth values will be validated using web calculator. References: - https://github.com/ColorGenomics/risk-models - https://www.princetonradiology.com/service/mammography/breast-cancer-risk-assessment/ """ import pytest from sentinel.risk_models.claus import ClausRiskModel from sentinel.user_input import ( Anthropometrics, CancerType, Demographics, Ethnicity, FamilyMemberCancer, FamilyRelation, FamilySide, Lifestyle, PersonalMedicalHistory, RelationshipDegree, Sex, SmokingHistory, SmokingStatus, UserInput, ) GROUND_TRUTH_CASES = [ { "name": "no_family_history", "input": UserInput( demographics=Demographics( age_years=49, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[], ), "expected": None, }, { "name": "mother_only", "input": UserInput( demographics=Demographics( age_years=50, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ), "expected": 8.7, }, { "name": "multiple_first_degree", "input": UserInput( demographics=Demographics( age_years=40, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=45, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.BREAST, age_at_diagnosis=52, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), ], ), "expected": 26.7, }, { "name": "mother_maternal_aunt", "input": UserInput( demographics=Demographics( age_years=35, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=50, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=60, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), ], ), "expected": 17.6, }, { "name": "complex_family_history", "input": UserInput( demographics=Demographics( age_years=45, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=40, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=65, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), ], ), "expected": 23.5, }, ] class TestClausModel: """Test suite for ClausRiskModel.""" def setup_method(self): """Initialize ClausRiskModel instance for testing.""" self.model = ClausRiskModel() @pytest.mark.parametrize("case", GROUND_TRUTH_CASES, ids=lambda x: x["name"]) def test_ground_truth_validation(self, case): """Test against reference implementation ground truth results. These values are validated against the Color Genomics reference implementation of the Claus model for the exact ages specified. Args: case: Parameterized ground truth case dict. """ calculated_risk = self.model.calculate_risk(case["input"]) if calculated_risk is None: assert case["expected"] is None else: calculated_pct = calculated_risk * 100 assert calculated_pct == pytest.approx(case["expected"], abs=0.1) def test_user_input_integration(self): """Test integration with UserInput model.""" user = UserInput( demographics=Demographics( age_years=45, sex=Sex.FEMALE, ethnicity=Ethnicity.WHITE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=60, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), ], ) score = self.model.compute_score(user) assert "N/A" not in score assert "%" in score risk_value = float(score.replace("%", "")) assert risk_value > 0 def test_male_patient_handling(self): """Test that male patients receive N/A response.""" male_user = UserInput( demographics=Demographics( age_years=45, sex=Sex.MALE, anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=75.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), ) score = self.model.compute_score(male_user) assert score == "N/A: Score available only for female patients." def test_age_validation_lower_bound(self): """Test age validation at lower boundary.""" young_user = UserInput( demographics=Demographics( age_years=19, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=45, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ) with pytest.raises( ValueError, match=r"Invalid inputs for Claus.*age_years.*greater than or equal to 20", ): self.model.compute_score(young_user) valid_age_user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=45, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ) score = self.model.compute_score(valid_age_user) assert "Age is outside" not in score def test_age_validation_upper_bound(self): """Test age validation at upper boundary.""" old_user = UserInput( demographics=Demographics( age_years=80, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ) with pytest.raises( ValueError, match=r"Invalid inputs for Claus.*age_years.*less than or equal to 79", ): self.model.compute_score(old_user) valid_age_user = UserInput( demographics=Demographics( age_years=79, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ) score = self.model.compute_score(valid_age_user) assert "Age is outside" not in score def test_no_family_history(self): """Test handling of no family history.""" user = UserInput( demographics=Demographics( age_years=45, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[], ) assert ( self.model.compute_score(user) == "N/A: No breast cancer family history available." ) def test_non_breast_cancer_family_history(self): """Test that non-breast cancer family history is ignored.""" user = UserInput( demographics=Demographics( age_years=45, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.LUNG, age_at_diagnosis=60, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.OVARIAN, age_at_diagnosis=50, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), ], ) assert ( self.model.compute_score(user) == "N/A: No breast cancer family history available." ) def test_relationship_mapping(self): """Test proper mapping of different relationship types.""" # Mapping from string names to enum values relationship_map = { "mother": FamilyRelation.MOTHER, "daughter": FamilyRelation.DAUGHTER, "sister": FamilyRelation.SISTER, "maternal_aunt": FamilyRelation.MATERNAL_AUNT, "paternal_aunt": FamilyRelation.PATERNAL_AUNT, "maternal_grandmother": FamilyRelation.MATERNAL_GRANDMOTHER, "paternal_grandmother": FamilyRelation.PATERNAL_GRANDMOTHER, } relationships = [ ("mother", "mother_onset_age"), ("daughter", "daughter_onset_ages"), ("sister", "full_sister_onset_ages"), ("maternal_aunt", "maternal_aunt_onset_ages"), ("paternal_aunt", "paternal_aunt_onset_ages"), ("maternal_grandmother", "maternal_grandmother_onset_ages"), ("paternal_grandmother", "paternal_grandmother_onset_ages"), ] for relative_name, _expected_field in relationships: user = UserInput( demographics=Demographics( age_years=45, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=relationship_map[relative_name], cancer_type=CancerType.BREAST, age_at_diagnosis=50, degree=RelationshipDegree.FIRST if relative_name in ["mother", "daughter", "sister"] else RelationshipDegree.SECOND, side=FamilySide.MATERNAL if "maternal" in relative_name else FamilySide.PATERNAL if "paternal" in relative_name else FamilySide.MATERNAL, ) ], ) score = self.model.compute_score(user) assert "N/A" not in score, f"Failed for {relative_name}" def test_family_member_age_filtering(self): """Test that family members outside age range are filtered.""" user = UserInput( demographics=Demographics( age_years=45, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=15, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.BREAST, age_at_diagnosis=85, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), ], ) assert ( self.model.compute_score(user) == "N/A: No breast cancer family history available." ) user_with_valid = UserInput( demographics=Demographics( age_years=45, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=15, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.BREAST, age_at_diagnosis=50, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), ], ) score = self.model.compute_score(user_with_valid) assert "N/A" not in score def test_model_metadata(self): """Test model metadata methods.""" assert self.model.name == "claus" assert self.model.cancer_type() == "breast" assert "Claus" in self.model.description() assert "lifetime risk" in self.model.interpretation().lower() assert isinstance(self.model.references(), list) assert len(self.model.references()) > 0 assert any("Claus" in ref for ref in self.model.references()) def test_calculate_risk_mother_only(self): """Test risk calculation with only mother's history.""" user = UserInput( demographics=Demographics( age_years=50, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ) risk = self.model.calculate_risk(user) assert risk is not None assert 0 < risk < 1 def test_calculate_risk_multiple_relatives(self): """Test risk calculation with multiple relatives.""" user = UserInput( demographics=Demographics( age_years=40, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=45, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=50, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=60, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), ], ) risk = self.model.calculate_risk(user) assert risk is not None assert 0 < risk < 1 def test_calculate_risk_no_history_returns_none(self): """Test that no family history returns None.""" user = UserInput( demographics=Demographics( age_years=45, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[], ) risk = self.model.calculate_risk(user) assert risk is None def test_output_format(self): """Test that output is properly formatted as percentage.""" user = UserInput( demographics=Demographics( age_years=45, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ) score = self.model.compute_score(user) assert "%" in score assert score.endswith("%") risk_str = score[:-1] risk_value = float(risk_str) assert 0 <= risk_value <= 100 def test_run_method_returns_risk_score(self): """Test that run() method returns proper RiskScore object.""" user = UserInput( demographics=Demographics( age_years=45, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ) risk_score = self.model.run(user) assert risk_score.name == "claus" assert risk_score.cancer_type == "breast" assert risk_score.description is not None assert risk_score.interpretation is not None assert risk_score.references is not None def test_sister_variations(self): """Test different ways of specifying sister relationship.""" variations = ["sister", "full sister", "full_sister"] for sister_variant in variations: user = UserInput( demographics=Demographics( age_years=45, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.SISTER, # All sister variants map to SISTER cancer_type=CancerType.BREAST, age_at_diagnosis=50, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ) score = self.model.compute_score(user) assert "N/A" not in score, f"Failed for variant: {sister_variant}" def test_half_sister_relationships(self): """Test maternal and paternal half-sister relationships.""" for half_sister_type in [ "maternal_half_sister", "maternal half-sister", "paternal_half_sister", "paternal half-sister", ]: # Determine side based on half-sister type side = ( FamilySide.MATERNAL if "maternal" in half_sister_type else FamilySide.PATERNAL ) user = UserInput( demographics=Demographics( age_years=45, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=50, degree=RelationshipDegree.SECOND, side=side, ) ], ) score = self.model.compute_score(user) assert "N/A" not in score, f"Failed for: {half_sister_type}" def test_two_first_degree_relatives(self): """Test scenario with two first-degree relatives.""" user = UserInput( demographics=Demographics( age_years=40, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=45, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.BREAST, age_at_diagnosis=50, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), ], ) risk = self.model.calculate_risk(user) assert risk is not None user_mother_daughter = UserInput( demographics=Demographics( age_years=50, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=45, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.DAUGHTER, cancer_type=CancerType.BREAST, age_at_diagnosis=30, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), ], ) risk_mother_daughter = self.model.calculate_risk(user_mother_daughter) assert risk_mother_daughter is not None def test_maximum_risk_selection(self): """Test that model selects maximum risk among applicable tables.""" user_complex = UserInput( demographics=Demographics( age_years=35, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=40, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.BREAST, age_at_diagnosis=45, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=50, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), ], ) risk_complex = self.model.calculate_risk(user_complex) user_mother_only = UserInput( demographics=Demographics( age_years=35, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=40, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), ], ) risk_mother = self.model.calculate_risk(user_mother_only) assert risk_complex is not None assert risk_mother is not None assert risk_complex >= risk_mother def test_web_calculator_validation(self): """Verify against web calculator using upper bounds of age ranges. Note: Web calculators often use the upper bound of age ranges (e.g., age 59 for "50-59" range), so these tests verify that our implementation matches at those boundary points. These values can be validated using a web calculator: - Test case 1: Patient 59, Mother 55 → Expected: 6.4% - Test case 2: Patient 49, Mother 45, Sister 52 → Expected: 22.6% - Test case 3: Patient 39, Mother 50, Aunt 60 → Expected: 17.1% - Test case 4: Patient 49, Mother 40, Aunts 55,65 → Expected: 21.7% """ # Case 1: Mother only (matches ground truth case 2 at upper bound) user1 = UserInput( demographics=Demographics( age_years=59, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ) risk1 = self.model.calculate_risk(user1) assert risk1 is not None assert risk1 * 100 == pytest.approx(6.4, abs=0.1) # Case 2: Multiple first degree (matches ground truth case 3 at upper bound) user2 = UserInput( demographics=Demographics( age_years=49, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=45, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.BREAST, age_at_diagnosis=52, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), ], ) risk2 = self.model.calculate_risk(user2) assert risk2 is not None assert risk2 * 100 == pytest.approx(22.6, abs=0.1) # Case 3: Mother + maternal aunt (matches ground truth case 4 at upper bound) user3 = UserInput( demographics=Demographics( age_years=39, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=50, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=60, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), ], ) risk3 = self.model.calculate_risk(user3) assert risk3 is not None assert risk3 * 100 == pytest.approx(17.1, abs=0.1) # Case 4: Complex family history (matches ground truth case 5 at upper bound) user4 = UserInput( demographics=Demographics( age_years=49, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=40, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=65, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), ], ) risk4 = self.model.calculate_risk(user4) assert risk4 is not None assert risk4 * 100 == pytest.approx(21.7, abs=0.1) class TestClausAlgorithm: """Test suite for core Claus algorithm logic from reference implementation.""" def setup_method(self): """Initialize ClausRiskModel instance for testing.""" self.model = ClausRiskModel() def test_direct_table_values(self): """Test against hardcoded values from the Claus paper tables. This verifies our tables match the published paper and that calculations produce mathematically correct conditional risks. """ from sentinel.risk_models.claus import ONE_FIRST_DEG_TABLE # Verify table values match the published Claus paper # ONE_FIRST_DEG_TABLE[patient_age_index][relative_age_index] assert ONE_FIRST_DEG_TABLE[5][2] == 0.132 # Lifetime (79), mother age 40-49 assert ONE_FIRST_DEG_TABLE[0][2] == 0.003 # Age 29, mother age 40-49 assert ONE_FIRST_DEG_TABLE[2][3] == 0.023 # Age 49, mother age 50-59 # Test: Patient at exact table boundary (age 29) # Patient age 29, mother age 44 (index 2 for 40-49) # Conditional risk = (lifetime - current) / (1 - current) user = UserInput( demographics=Demographics( age_years=29, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ) risk = self.model.calculate_risk(user) # Manual calculation from hardcoded table values lifetime = 0.132 # ONE_FIRST_DEG_TABLE[5][2] current = 0.003 # ONE_FIRST_DEG_TABLE[0][2] expected = round((lifetime - current) / (1 - current), 3) assert risk == expected # Should be 0.129 def test_one_first_degree_relative(self): """Test scenarios with one first-degree relative.""" from sentinel.risk_models.claus import ( ONE_FIRST_DEG_TABLE, _get_lifetime_risk, ) user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(ONE_FIRST_DEG_TABLE, 20, 2) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.DAUGHTER, cancer_type=CancerType.BREAST, age_at_diagnosis=23, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(ONE_FIRST_DEG_TABLE, 20, 0) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.BREAST, age_at_diagnosis=32, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(ONE_FIRST_DEG_TABLE, 20, 1) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.BREAST, age_at_diagnosis=11, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.BREAST, age_at_diagnosis=22, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(ONE_FIRST_DEG_TABLE, 20, 0) assert score == expected def test_one_second_degree_relative(self): """Test scenarios with one second-degree relative.""" from sentinel.risk_models.claus import ( ONE_SECOND_DEG_TABLE, _get_lifetime_risk, ) user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ) ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(ONE_SECOND_DEG_TABLE, 20, 2) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=54, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ) ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(ONE_SECOND_DEG_TABLE, 20, 3) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=77, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ) ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(ONE_SECOND_DEG_TABLE, 20, 5) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=67, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=12, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(ONE_SECOND_DEG_TABLE, 20, 4) assert score == expected def test_two_first_degree_relatives(self): """Test scenarios with two first-degree relatives.""" from sentinel.risk_models.claus import ( TWO_FIRST_DEG_TABLE, _get_lifetime_risk, ) user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.DAUGHTER, cancer_type=CancerType.BREAST, age_at_diagnosis=12, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.DAUGHTER, cancer_type=CancerType.BREAST, age_at_diagnosis=22, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=33, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(TWO_FIRST_DEG_TABLE, 20, 0, 2) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.DAUGHTER, cancer_type=CancerType.BREAST, age_at_diagnosis=12, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.DAUGHTER, cancer_type=CancerType.BREAST, age_at_diagnosis=22, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=33, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.BREAST, age_at_diagnosis=11, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.BREAST, age_at_diagnosis=23, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(TWO_FIRST_DEG_TABLE, 20, 0, 0) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.DAUGHTER, cancer_type=CancerType.BREAST, age_at_diagnosis=12, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.DAUGHTER, cancer_type=CancerType.BREAST, age_at_diagnosis=22, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=33, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.BREAST, age_at_diagnosis=11, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, cancer_type=CancerType.BREAST, age_at_diagnosis=34, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=22, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=23, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=24, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(TWO_FIRST_DEG_TABLE, 20, 0, 1) assert score == expected def test_mother_and_maternal_aunt(self): """Test scenarios with mother and maternal aunt.""" from sentinel.risk_models.claus import ( MOTHER_MATERNAL_AUNT, _get_lifetime_risk, ) user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=66, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(MOTHER_MATERNAL_AUNT, 20, 3, 4) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=66, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=52, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=43, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=54, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(MOTHER_MATERNAL_AUNT, 20, 3, 4) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=45, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=19, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=33, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=22, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(MOTHER_MATERNAL_AUNT, 20, 2, 1) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=66, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=22, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=88, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=34, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(MOTHER_MATERNAL_AUNT, 20, 3, 2) assert score == expected def test_mother_and_paternal_aunt(self): """Test scenarios with mother and paternal aunt.""" from sentinel.risk_models.claus import ( MOTHER_PATERNAL_AUNT, _get_lifetime_risk, ) user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=22, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(MOTHER_PATERNAL_AUNT, 20, 3, 0) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=45, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=99, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=63, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=22, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(MOTHER_PATERNAL_AUNT, 20, 2, 0) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=25, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=99, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=33, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=52, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=64, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=53, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=62, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(MOTHER_PATERNAL_AUNT, 20, 0, 1) assert score == expected def test_two_second_degree_different_sides(self): """Test scenarios with two second-degree relatives on different sides.""" from sentinel.risk_models.claus import ( TWO_SEC_DEG_DIFF_SIDE_TABLE, _get_lifetime_risk, ) user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.DAUGHTER, cancer_type=CancerType.BREAST, age_at_diagnosis=12, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=78, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(TWO_SEC_DEG_DIFF_SIDE_TABLE, 20, 2, 5) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.DAUGHTER, cancer_type=CancerType.BREAST, age_at_diagnosis=12, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=90, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(TWO_SEC_DEG_DIFF_SIDE_TABLE, 20, 2, 3) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=12, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=66, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(TWO_SEC_DEG_DIFF_SIDE_TABLE, 20, 3, 4) assert score == expected def test_two_second_degree_same_side(self): """Test scenarios with two second-degree relatives on same side.""" from sentinel.risk_models.claus import ( TWO_SEC_DEG_SAME_SIDE_TABLE, _get_lifetime_risk, ) user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=77, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(TWO_SEC_DEG_SAME_SIDE_TABLE, 20, 3, 5) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=12, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=77, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(TWO_SEC_DEG_SAME_SIDE_TABLE, 20, 2, 3) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=33, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=22, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(TWO_SEC_DEG_SAME_SIDE_TABLE, 20, 0, 3) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=33, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=77, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=22, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.SISTER, # Half-sister not in enum cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(TWO_SEC_DEG_SAME_SIDE_TABLE, 20, 0, 2) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=66, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=33, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(TWO_SEC_DEG_SAME_SIDE_TABLE, 20, 1, 2) assert score == expected user = UserInput( demographics=Demographics( age_years=20, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=22, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.MATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=77, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=44, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=55, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) score = self.model.calculate_risk(user) expected = _get_lifetime_risk(TWO_SEC_DEG_SAME_SIDE_TABLE, 20, 0, 5) assert score == expected def test_linear_interpolation_one_relative(self): """Test linear interpolation for patient's current age with one relative.""" from sentinel.risk_models.claus import ( ONE_FIRST_DEG_TABLE, _get_lifetime_risk, ) # Test linear interpolation with a UserInput user = UserInput( demographics=Demographics( age_years=32, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=50, degree=RelationshipDegree.FIRST, side=FamilySide.MATERNAL, ) ], ) computed_score = self.model.calculate_risk(user) # Manual calculation for verification expected_score = _get_lifetime_risk(ONE_FIRST_DEG_TABLE, 32, 3) current_age_risk = ( ONE_FIRST_DEG_TABLE[0][3] + (ONE_FIRST_DEG_TABLE[1][3] - ONE_FIRST_DEG_TABLE[0][3]) * 3 / 10 ) manual_expected = (ONE_FIRST_DEG_TABLE[5][3] - current_age_risk) / ( 1 - current_age_risk ) assert computed_score == round(manual_expected, 3) assert computed_score == expected_score def test_linear_interpolation_two_relatives(self): """Test linear interpolation for patient's current age with two relatives.""" from sentinel.risk_models.claus import ( TWO_SEC_DEG_DIFF_SIDE_TABLE, _get_lifetime_risk, ) # Test linear interpolation with two relatives using UserInput user = UserInput( demographics=Demographics( age_years=47, sex=Sex.FEMALE, anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER), ), personal_medical_history=PersonalMedicalHistory(), family_history=[ FamilyMemberCancer( relation=FamilyRelation.MATERNAL_GRANDMOTHER, cancer_type=CancerType.BREAST, age_at_diagnosis=50, degree=RelationshipDegree.SECOND, side=FamilySide.MATERNAL, ), FamilyMemberCancer( relation=FamilyRelation.PATERNAL_AUNT, cancer_type=CancerType.BREAST, age_at_diagnosis=30, degree=RelationshipDegree.SECOND, side=FamilySide.PATERNAL, ), ], ) computed_score = self.model.calculate_risk(user) # Manual calculation for verification - this should use TWO_SEC_DEG_DIFF_SIDE_TABLE # because we have one maternal second-degree (grandmother) and one paternal second-degree (aunt) expected_score = _get_lifetime_risk(TWO_SEC_DEG_DIFF_SIDE_TABLE, 47, 4, 1) current_age_risk = ( TWO_SEC_DEG_DIFF_SIDE_TABLE[1][4][1] + ( TWO_SEC_DEG_DIFF_SIDE_TABLE[2][4][1] - TWO_SEC_DEG_DIFF_SIDE_TABLE[1][4][1] ) * 8 / 10 ) manual_expected = (TWO_SEC_DEG_DIFF_SIDE_TABLE[5][4][1] - current_age_risk) / ( 1 - current_age_risk ) # The computed score should be reasonable (between 0 and 1) assert 0 <= computed_score <= 1 # The computed score should be close to the expected score from the function # (allowing for the model to select a different table if it gives higher risk) assert abs(computed_score - expected_score) < 0.01 # Allow for small rounding differences in manual calculation assert abs(computed_score - round(manual_expected, 3)) < 0.01