# pylint: disable=missing-docstring """Basic tests for the PCPT prostate cancer risk model. Web calculator available at: https://riskcalc.org/PCPTRC/ """ import pytest from sentinel.risk_models import PCPTRiskModel from sentinel.user_input import ( Anthropometrics, CancerType, ClinicalTests, Demographics, DREResult, DRETest, Ethnicity, FamilyMemberCancer, FamilyRelation, FamilySide, Lifestyle, PCA3Test, PercentFreePSATest, PersonalMedicalHistory, PSATest, RelationshipDegree, Sex, SmokingHistory, SmokingStatus, T2ERGTest, UserInput, ) # Ground-truth regression fixtures collected from the official PCPT web calculator. GROUND_TRUTH_CASES = [ { "name": "low_risk", "input": UserInput( demographics=Demographics( age_years=70, sex=Sex.MALE, ethnicity=Ethnicity.WHITE, anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), ), personal_medical_history=PersonalMedicalHistory( prior_negative_prostate_biopsy=False, ), family_history=[], clinical_tests=ClinicalTests( psa=PSATest(value_ng_ml=10.0), dre=DRETest(result=DREResult.NORMAL), ), ), "expected_high_grade": 15.0, "expected_low_grade": 23.0, "expected_no_cancer": 62.0, }, { "name": "medium_high_risk", "input": UserInput( demographics=Demographics( age_years=80, sex=Sex.MALE, ethnicity=Ethnicity.WHITE, anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), ), personal_medical_history=PersonalMedicalHistory( prior_negative_prostate_biopsy=False, ), family_history=[ FamilyMemberCancer( relation=FamilyRelation.FATHER, side=FamilySide.PATERNAL, degree=RelationshipDegree.FIRST, cancer_type=CancerType.PROSTATE, age_at_diagnosis=70, ) ], clinical_tests=ClinicalTests( psa=PSATest(value_ng_ml=25.0), ), ), "expected_high_grade": 42.0, "expected_low_grade": 26.0, "expected_no_cancer": 32.0, }, { "name": "high_risk", "input": UserInput( demographics=Demographics( age_years=65, sex=Sex.MALE, ethnicity=Ethnicity.BLACK, anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), ), personal_medical_history=PersonalMedicalHistory( prior_negative_prostate_biopsy=False, ), family_history=[], clinical_tests=ClinicalTests( psa=PSATest(value_ng_ml=36.0), dre=DRETest(result=DREResult.ABNORMAL), ), ), "expected_high_grade": 66.0, "expected_low_grade": 13.0, "expected_no_cancer": 21.0, }, ] class TestPCPTRiskModel: def setup_method(self) -> None: self.model = PCPTRiskModel() def test_metadata(self) -> None: assert self.model.name == "pcpt" assert self.model.cancer_type() == "prostate" assert "PCPT" in self.model.description() assert "percent" in self.model.interpretation().lower() assert len(self.model.references()) > 0 def test_absolute_risk_basic(self) -> None: user = UserInput( demographics=Demographics( age_years=60, sex=Sex.MALE, ethnicity=Ethnicity.WHITE, anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), ), personal_medical_history=PersonalMedicalHistory( prior_negative_prostate_biopsy=False, ), family_history=[], clinical_tests=ClinicalTests( psa=PSATest(value_ng_ml=2.5), dre=DRETest(result=DREResult.NORMAL), ), ) risks = self.model.absolute_risk(user) assert risks["no_cancer"] > 0 assert risks["low_grade"] > 0 assert risks["high_grade"] > 0 total = risks["no_cancer"] + risks["low_grade"] + risks["high_grade"] assert 99.5 <= total <= 100.5 @pytest.mark.parametrize("case", GROUND_TRUTH_CASES, ids=lambda case: case["name"]) def test_ground_truth_cases(self, case) -> None: risks = self.model.absolute_risk(case["input"]) assert risks["high_grade"] == pytest.approx( case["expected_high_grade"], abs=2.0 ) assert risks["low_grade"] == pytest.approx(case["expected_low_grade"], abs=2.0) assert risks["no_cancer"] == pytest.approx(case["expected_no_cancer"], abs=2.0) def test_compute_score_with_male_user_input(self) -> None: user = UserInput( demographics=Demographics( age_years=60, sex=Sex.MALE, ethnicity=Ethnicity.WHITE, anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), ), personal_medical_history=PersonalMedicalHistory( prior_negative_prostate_biopsy=False, ), family_history=[], clinical_tests=ClinicalTests( psa=PSATest(value_ng_ml=2.5), percent_free_psa=PercentFreePSATest(value_percent=18.0), dre=DRETest(result=DREResult.NORMAL), pca3=PCA3Test(score=25.0), t2erg=T2ERGTest(score=10.0), ), ) score = self.model.compute_score(user) assert "No Cancer" in score assert "Low Grade" in score assert "High Grade" in score def test_compute_score_rejects_female_user(self) -> None: user = UserInput( demographics=Demographics( age_years=60, sex=Sex.FEMALE, ethnicity=Ethnicity.WHITE, anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), ), personal_medical_history=PersonalMedicalHistory(), family_history=[], clinical_tests=ClinicalTests( psa=PSATest(value_ng_ml=2.5), ), ) score = self.model.compute_score(user) assert score == "N/A: PCPT applies to male patients only." def test_validation_errors(self) -> None: """Test validation errors for missing required fields.""" user = UserInput( demographics=Demographics( age_years=60, sex=Sex.MALE, ethnicity=Ethnicity.WHITE, anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), ), personal_medical_history=PersonalMedicalHistory(), family_history=[], clinical_tests=ClinicalTests(), # Missing PSA ) with pytest.raises(ValueError, match=r"Invalid inputs for PCPT:"): self.model.compute_score(user) def test_age_out_of_range(self) -> None: """Test age outside validated range raises ValueError.""" user = UserInput( demographics=Demographics( age_years=50, # Below minimum sex=Sex.MALE, ethnicity=Ethnicity.WHITE, anthropometrics=Anthropometrics(height_cm=175.0, weight_kg=80.0), ), lifestyle=Lifestyle( smoking=SmokingHistory(status=SmokingStatus.NEVER, pack_years=0.0), ), personal_medical_history=PersonalMedicalHistory(), family_history=[], clinical_tests=ClinicalTests( psa=PSATest(value_ng_ml=2.5), ), ) with pytest.raises(ValueError, match=r"Invalid inputs for PCPT:"): self.model.compute_score(user)