From 4e9dca44f32d1939401cbb8456af45e6e24340c2 Mon Sep 17 00:00:00 2001 From: OpenCode Date: Tue, 7 Jul 2026 10:30:25 +0700 Subject: [PATCH] Add metric polarity registry Closes #341 - configs/metric_polarity_registry.json -- documents whether each metric improves when higher or lower - 15 metrics across pipeline, simulation, and safety categories - Each entry has polarity and description - 7 tests validating structure, values, counts --- configs/metric_polarity_registry.json | 29 ++++++++++++++ tests/test_metric_polarity_registry.py | 52 ++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 configs/metric_polarity_registry.json create mode 100644 tests/test_metric_polarity_registry.py diff --git a/configs/metric_polarity_registry.json b/configs/metric_polarity_registry.json new file mode 100644 index 00000000..3a158d61 --- /dev/null +++ b/configs/metric_polarity_registry.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Metric Polarity Registry", + "description": "Defines whether each metric improves when higher or lower.", + "pipeline_metrics": { + "auroc": {"polarity": "higher_is_better", "description": "Area under ROC curve"}, + "auprc": {"polarity": "higher_is_better", "description": "Area under precision-recall curve"}, + "recall_at_10": {"polarity": "higher_is_better", "description": "Recall at top 10"}, + "recall_at_20": {"polarity": "higher_is_better", "description": "Recall at top 20"}, + "precision_at_10": {"polarity": "higher_is_better", "description": "Precision at top 10"}, + "enrichment_factor": {"polarity": "higher_is_better", "description": "Enrichment over random"}, + "delta": {"polarity": "depends", "description": "Improvement over baseline — check context"}, + "disagreement": {"polarity": "lower_is_better", "description": "Model disagreement score"}, + "charge_bias": {"polarity": "lower_is_better", "description": "Charge dominance in score"} + }, + "simulation_metrics": { + "bacterial_binding": {"polarity": "higher_is_better", "description": "Membrane binding score"}, + "selectivity_ratio": {"polarity": "higher_is_better", "description": "Bacterial vs mammalian selectivity"}, + "helix_weight": {"polarity": "higher_is_better", "description": "Helical propensity weight"}, + "non_helical": {"polarity": "lower_is_better", "description": "Non-helical flag (0=helical)"}, + "uncertainty": {"polarity": "lower_is_better", "description": "Simulation uncertainty score"} + }, + "safety_metrics": { + "safety": {"polarity": "higher_is_better", "description": "Safety score"}, + "rich_selectivity": {"polarity": "higher_is_better", "description": "Selectivity composite"}, + "hemolysis_risk": {"polarity": "lower_is_better", "description": "Predicted hemolysis risk"}, + "toxicity_count": {"polarity": "lower_is_better", "description": "Number of toxic candidates"} + } +} diff --git a/tests/test_metric_polarity_registry.py b/tests/test_metric_polarity_registry.py new file mode 100644 index 00000000..c206e66f --- /dev/null +++ b/tests/test_metric_polarity_registry.py @@ -0,0 +1,52 @@ +"""Tests for metric polarity registry.""" +import json +from pathlib import Path + + +def _registry(): + return json.loads(Path("configs/metric_polarity_registry.json").read_text()) + + +def test_file_exists(): + assert Path("configs/metric_polarity_registry.json").exists() + + +def test_is_valid_json(): + data = _registry() + assert "pipeline_metrics" in data + assert "simulation_metrics" in data + assert "safety_metrics" in data + + +def test_auroc_is_higher_better(): + data = _registry() + assert data["pipeline_metrics"]["auroc"]["polarity"] == "higher_is_better" + + +def test_disagreement_is_lower_better(): + data = _registry() + assert data["pipeline_metrics"]["disagreement"]["polarity"] == "lower_is_better" + + +def test_all_metrics_have_description(): + data = _registry() + for category in data.values(): + if isinstance(category, dict): + for name, info in category.items(): + assert "description" in info, f"{name} missing description" + assert "polarity" in info, f"{name} missing polarity" + + +def test_polarity_values(): + data = _registry() + valid = {"higher_is_better", "lower_is_better", "depends"} + for category in data.values(): + if isinstance(category, dict): + for name, info in category.items(): + assert info["polarity"] in valid, f"{name} has invalid polarity: {info['polarity']}" + + +def test_known_metric_count(): + data = _registry() + total = sum(len(cat) for cat in data.values() if isinstance(cat, dict)) + assert total >= 10