Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| from pathlib import Path | |
| from gcmd_classifier.classification import ( | |
| ClassificationCandidate, | |
| TermBranchSeed, | |
| VariableTerminalOutcome, | |
| candidate_from_term_branch, | |
| validate_candidate, | |
| validate_candidates, | |
| validate_terminal_outcomes, | |
| ) | |
| from gcmd_classifier.models import ClassificationFinalStatus, SupportType | |
| from gcmd_classifier.vocabulary import load_vocabulary | |
| FIXTURE_PATH = Path("tests/fixtures/gcmd_hierarchy_small.json") | |
| FULL_HIERARCHY_PATH = Path("data/gcmd_hierarchy.json") | |
| def _index(): | |
| return load_vocabulary(FIXTURE_PATH) | |
| def _candidate(uuid: str, *, branch_id: str | None = "branch:one") -> ClassificationCandidate: | |
| index = _index() | |
| record = index.get(uuid) | |
| topic = record if record.level == "Topic" else index.get(index.ancestors_of(uuid)[-1]) | |
| term = None | |
| if record.level == "Term": | |
| term = record | |
| elif record.level.startswith("Variable_Level_"): | |
| term = next( | |
| index.get(ancestor) | |
| for ancestor in index.ancestors_of(uuid) | |
| if index.get(ancestor).level == "Term" | |
| ) | |
| return ClassificationCandidate( | |
| final_uuid=record.UUID, | |
| final_name=record.name, | |
| final_level=record.level, | |
| final_canonical_path=record.canonical_path, | |
| path_components=record.path_components, | |
| topic_uuid=topic.UUID, | |
| topic_name=topic.name, | |
| term_uuid=None if term is None else term.UUID, | |
| term_name=None if term is None else term.name, | |
| branch_id=branch_id, | |
| evidence="Validated evidence.", | |
| support_type=SupportType.EXPLICIT, | |
| confidence=0.81, | |
| reason="Routing selected the concept.", | |
| candidate_id="candidate_0001", | |
| ) | |
| def _term_branch() -> TermBranchSeed: | |
| return TermBranchSeed( | |
| branch_id="topic:topic_0001/term:term_0001", | |
| parent_topic_uuid="topic-atmosphere", | |
| parent_topic_name="ATMOSPHERE", | |
| term_uuid="term-atmospheric-chemistry", | |
| term_name="ATMOSPHERIC CHEMISTRY", | |
| term_level="Term", | |
| term_canonical_path="ATMOSPHERE > ATMOSPHERIC CHEMISTRY", | |
| evidence="The article supports the Term.", | |
| support_type=SupportType.EXPLICIT, | |
| confidence=0.77, | |
| reason="Term was selected.", | |
| candidate_id="term_0001", | |
| parent_branch_id="topic:topic_0001", | |
| prompt_version="term-test", | |
| model_provider="fake", | |
| model_name="fake-model", | |
| retry_count=0, | |
| ) | |
| def _terminal( | |
| uuid: str, | |
| *, | |
| branch_id: str = "topic:t/term:x/variable:y", | |
| ) -> VariableTerminalOutcome: | |
| index = _index() | |
| record = index.get(uuid) | |
| return VariableTerminalOutcome( | |
| branch_id=branch_id, | |
| parent_branch_id="topic:t/term:x", | |
| topic_uuid="topic-atmosphere", | |
| topic_name="ATMOSPHERE", | |
| term_uuid="term-atmospheric-chemistry", | |
| term_name="ATMOSPHERIC CHEMISTRY", | |
| final_uuid=record.UUID, | |
| final_name=record.name, | |
| final_level=record.level, | |
| final_canonical_path=record.canonical_path, | |
| path_components=record.path_components, | |
| evidence="Final-stage evidence.", | |
| support_type=SupportType.MIXED, | |
| confidence=0.69, | |
| reason="Selected descendant.", | |
| stop_reason="Deepest supported concept.", | |
| candidate_id="variable_0001", | |
| prompt_version="variable-test", | |
| model_provider="fake", | |
| model_name="fake-model", | |
| retry_count=1, | |
| ) | |
| def _error_codes(result) -> set[str]: | |
| return {error.code for error in result.deterministic_validation.errors} | |
| def test_valid_candidate_accepted_and_populated_from_vocabulary_index() -> None: | |
| result = validate_candidate(_candidate("vl3-carbon-dioxide-profiles"), _index()) | |
| assert result.valid is True | |
| assert result.classification is not None | |
| classification = result.classification | |
| assert classification.UUID == "vl3-carbon-dioxide-profiles" | |
| assert classification.name == "CARBON DIOXIDE PROFILES" | |
| assert classification.level == "Variable_Level_3" | |
| assert classification.canonical_path.endswith("CARBON DIOXIDE PROFILES") | |
| assert classification.topic == "ATMOSPHERE" | |
| assert classification.term == "ATMOSPHERIC CHEMISTRY" | |
| assert classification.parent_uuid == "vl2-carbon-dioxide" | |
| assert classification.final_status is ClassificationFinalStatus.ACCEPTED | |
| assert classification.deterministic_validation.valid is True | |
| assert classification.confidence is not None | |
| assert classification.confidence.variable_level_3 == 0.81 | |
| assert classification.confidence.final == 0.81 | |
| assert classification.classifier_evidence == "Validated evidence." | |
| assert classification.support_type is SupportType.EXPLICIT | |
| def test_nonexistent_uuid_rejected() -> None: | |
| candidate = ClassificationCandidate(final_uuid="missing-uuid", final_level="Term") | |
| result = validate_candidate(candidate, _index()) | |
| assert result.valid is False | |
| assert result.classification is None | |
| assert _error_codes(result) == {"UNKNOWN_UUID"} | |
| def test_root_category_rejected() -> None: | |
| candidate = ClassificationCandidate( | |
| final_uuid="EARTH SCIENCE", | |
| final_name="EARTH SCIENCE", | |
| final_level="Category", | |
| ) | |
| result = validate_candidate(candidate, _index()) | |
| assert result.valid is False | |
| assert "ROOT_CATEGORY_NOT_ASSIGNABLE" in _error_codes(result) | |
| def test_uuid_name_mismatch_rejected() -> None: | |
| candidate = _candidate("vl2-carbon-dioxide").model_copy(update={"final_name": "METHANE"}) | |
| result = validate_candidate(candidate, _index()) | |
| assert "UUID_NAME_MISMATCH" in _error_codes(result) | |
| assert result.classification is None | |
| def test_uuid_level_mismatch_rejected() -> None: | |
| candidate = _candidate("vl2-carbon-dioxide").model_copy( | |
| update={"final_level": "Variable_Level_1"} | |
| ) | |
| result = validate_candidate(candidate, _index()) | |
| assert "UUID_LEVEL_MISMATCH" in _error_codes(result) | |
| def test_uuid_canonical_path_mismatch_rejected() -> None: | |
| candidate = _candidate("vl2-carbon-dioxide").model_copy( | |
| update={"final_canonical_path": "ATMOSPHERE > MADE UP"} | |
| ) | |
| result = validate_candidate(candidate, _index()) | |
| assert "UUID_CANONICAL_PATH_MISMATCH" in _error_codes(result) | |
| def test_invalid_path_components_rejected() -> None: | |
| candidate = _candidate("vl2-carbon-dioxide").model_copy( | |
| update={"path_components": ("ATMOSPHERE", "MADE UP")} | |
| ) | |
| result = validate_candidate(candidate, _index()) | |
| assert "PATH_COMPONENTS_MISMATCH" in _error_codes(result) | |
| def test_invalid_parent_child_chain_rejected() -> None: | |
| candidate = _candidate("vl2-carbon-dioxide").model_copy( | |
| update={"expected_parent_uuid": "term-weather-events"} | |
| ) | |
| result = validate_candidate(candidate, _index()) | |
| assert "INVALID_PARENT_CHILD_CHAIN" in _error_codes(result) | |
| def test_wrong_topic_membership_rejected() -> None: | |
| candidate = _candidate("vl2-carbon-dioxide").model_copy( | |
| update={"topic_uuid": "topic-oceans", "topic_name": "OCEANS"} | |
| ) | |
| result = validate_candidate(candidate, _index()) | |
| assert "WRONG_TOPIC_MEMBERSHIP" in _error_codes(result) | |
| assert "TOPIC_NAME_MISMATCH" in _error_codes(result) | |
| def test_wrong_term_membership_rejected() -> None: | |
| candidate = _candidate("vl2-carbon-dioxide").model_copy( | |
| update={"term_uuid": "term-weather-events", "term_name": "WEATHER EVENTS"} | |
| ) | |
| result = validate_candidate(candidate, _index()) | |
| assert "WRONG_TERM_MEMBERSHIP" in _error_codes(result) | |
| assert "TERM_NAME_MISMATCH" in _error_codes(result) | |
| def test_final_concept_under_expected_topic_and_term_succeeds() -> None: | |
| result = validate_candidate(_candidate("vl2-carbon-dioxide"), _index()) | |
| assert result.valid is True | |
| assert result.classification is not None | |
| assert result.classification.UUID == "vl2-carbon-dioxide" | |
| def test_topic_level_final_classification_succeeds() -> None: | |
| result = validate_candidate(_candidate("topic-atmosphere"), _index()) | |
| assert result.valid is True | |
| assert result.classification is not None | |
| assert result.classification.level == "Topic" | |
| assert result.classification.term is None | |
| def test_term_level_final_classification_succeeds_from_term_branch() -> None: | |
| result = validate_candidate(candidate_from_term_branch(_term_branch()), _index()) | |
| assert result.valid is True | |
| assert result.classification is not None | |
| assert result.classification.level == "Term" | |
| assert result.classification.UUID == "term-atmospheric-chemistry" | |
| def test_variable_level_1_final_classification_succeeds() -> None: | |
| assert validate_candidate(_candidate("vl1-atmosphere-carbon"), _index()).valid is True | |
| def test_variable_level_2_final_classification_succeeds() -> None: | |
| assert validate_candidate(_candidate("vl2-carbon-dioxide"), _index()).valid is True | |
| def test_variable_level_3_final_classification_succeeds() -> None: | |
| assert validate_candidate(_candidate("vl3-carbon-dioxide-profiles"), _index()).valid is True | |
| def test_rejected_invalid_candidates_do_not_appear_in_accepted_classifications() -> None: | |
| batch = validate_candidates( | |
| [ | |
| _candidate("vl2-carbon-dioxide"), | |
| ClassificationCandidate(final_uuid="missing-uuid", final_level="Term"), | |
| ], | |
| _index(), | |
| ) | |
| assert [record.UUID for record in batch.accepted] == ["vl2-carbon-dioxide"] | |
| assert batch.rejected_count == 1 | |
| assert batch.rejected[0].deterministic_validation.errors[0].details["field"] == "final_uuid" | |
| def test_stop_at_term_terminal_outcome_can_become_valid_term_classification() -> None: | |
| terminal = _terminal("term-atmospheric-chemistry", branch_id="topic:t/term:x") | |
| result = validate_terminal_outcomes([terminal], _index()) | |
| assert result.accepted_count == 1 | |
| assert result.accepted[0].level == "Term" | |
| def test_stop_at_variable_level_1_terminal_outcome_can_become_valid_classification() -> None: | |
| result = validate_terminal_outcomes([_terminal("vl1-atmosphere-carbon")], _index()) | |
| assert result.accepted[0].level == "Variable_Level_1" | |
| def test_selected_variable_level_3_terminal_outcome_can_become_valid_classification() -> None: | |
| result = validate_terminal_outcomes([_terminal("vl3-carbon-dioxide-profiles")], _index()) | |
| assert result.accepted[0].level == "Variable_Level_3" | |
| assert result.accepted[0].reason_for_stopping == "Deepest supported concept." | |
| def test_multiple_terminal_outcomes_validate_independently() -> None: | |
| result = validate_terminal_outcomes( | |
| [ | |
| _terminal("vl2-carbon-dioxide", branch_id="topic:t/term:x/variable:a"), | |
| _terminal("vl2-methane", branch_id="topic:t/term:x/variable:b"), | |
| ], | |
| _index(), | |
| ) | |
| assert [record.UUID for record in result.accepted] == ["vl2-carbon-dioxide", "vl2-methane"] | |
| def test_validation_does_not_require_model_call() -> None: | |
| result = validate_candidate(_candidate("vl2-methane"), _index()) | |
| assert result.valid is True | |
| def test_full_data_representative_records_pass_validation() -> None: | |
| index = load_vocabulary(FULL_HIERARCHY_PATH) | |
| representatives = [] | |
| for level in ["Topic", "Term", "Variable_Level_1", "Variable_Level_2", "Variable_Level_3"]: | |
| representatives.append(next(record for record in index.records if record.level == level)) | |
| for record in representatives: | |
| topic = ( | |
| record if record.level == "Topic" else index.get(index.ancestors_of(record.UUID)[-1]) | |
| ) | |
| term = None | |
| if record.level == "Term": | |
| term = record | |
| elif record.level.startswith("Variable_Level_"): | |
| term = next( | |
| index.get(ancestor) | |
| for ancestor in index.ancestors_of(record.UUID) | |
| if index.get(ancestor).level == "Term" | |
| ) | |
| candidate = ClassificationCandidate( | |
| final_uuid=record.UUID, | |
| final_name=record.name, | |
| final_level=record.level, | |
| final_canonical_path=record.canonical_path, | |
| path_components=record.path_components, | |
| topic_uuid=topic.UUID, | |
| topic_name=topic.name, | |
| term_uuid=None if term is None else term.UUID, | |
| term_name=None if term is None else term.name, | |
| ) | |
| assert validate_candidate(candidate, index).valid is True | |