File size: 3,538 Bytes
b42dbeb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
"""Local DDI and guideline simulation using CSV lookup data."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple

from .data_loader import (
    BeersCriterion,
    DDIRule,
    DrugMeta,
    load_beers_criteria,
    load_ddi_rules,
    load_drug_metadata,
)


@dataclass(frozen=True)
class DDIResult:
    severity: str
    recommendation: str
    base_risk_score: float


_NO_INTERACTION = DDIResult(severity="none", recommendation="no_action", base_risk_score=0.0)


class DDISimulator:
    """Provides drug–drug interaction and Beers-criteria lookups."""

    def __init__(self) -> None:
        self._ddi_rules: Dict[Tuple[str, str], DDIRule] = load_ddi_rules()
        self._drug_meta: Dict[str, DrugMeta] = load_drug_metadata()
        self._beers: List[BeersCriterion] = load_beers_criteria()

    @staticmethod
    def _normalise_pair(a: str, b: str) -> Tuple[str, str]:
        return (a, b) if a < b else (b, a)

    def lookup_ddi(self, drug_id_1: str, drug_id_2: str) -> DDIResult:
        key = self._normalise_pair(drug_id_1, drug_id_2)
        rule = self._ddi_rules.get(key)
        if rule is None:
            return _NO_INTERACTION
        return DDIResult(
            severity=rule.severity,
            recommendation=rule.recommendation,
            base_risk_score=rule.base_risk_score,
        )

    def get_beers_flags(
        self,
        drug_id: str,
        patient_conditions: List[str],
    ) -> List[str]:
        """Return list of Beers flags applicable to *drug_id* given patient conditions."""
        flags: List[str] = []
        for bc in self._beers:
            if bc.drug_id != drug_id:
                continue
            if bc.condition is None:
                flags.append(bc.criterion_type)
            elif bc.condition in patient_conditions:
                flags.append(f"{bc.criterion_type}_{bc.condition}")
        return flags

    def get_drug_meta(self, drug_id: str) -> Optional[DrugMeta]:
        return self._drug_meta.get(drug_id)

    def find_substitute(
        self,
        drug_id: str,
        current_drug_ids: List[str],
    ) -> Optional[str]:
        """Find a safer same-class substitute not already in the regimen."""
        meta = self._drug_meta.get(drug_id)
        if meta is None:
            return None
        candidates = [
            dm
            for dm in self._drug_meta.values()
            if (
                dm.atc_class == meta.atc_class
                and dm.drug_id != drug_id
                and dm.drug_id not in current_drug_ids
                and not dm.is_high_risk_elderly
            )
        ]
        if not candidates:
            return None
        # Pick the candidate with fewest severe DDIs with current regimen
        def _severe_count(cand: DrugMeta) -> int:
            count = 0
            for did in current_drug_ids:
                if did == drug_id:
                    continue
                r = self.lookup_ddi(cand.drug_id, did)
                if r.severity == "severe":
                    count += 1
            return count

        candidates.sort(key=lambda c: (_severe_count(c), c.drug_id))
        return candidates[0].drug_id

    @property
    def drug_metadata(self) -> Dict[str, DrugMeta]:
        return self._drug_meta

    @property
    def ddi_rules(self) -> Dict[Tuple[str, str], DDIRule]:
        return self._ddi_rules

    @property
    def beers_criteria(self) -> List[BeersCriterion]:
        return self._beers