File size: 6,435 Bytes
0e6887b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""相容性确定性回归基准(compatibility-engine-upgrade 任务 8.1)。

金标准用例覆盖经典相互作用 + 纠偏 + 别名,断言风险**档位**与命中**反应集合**。
确定性、离线运行(直接喂官能团 id,不依赖网络/LLM/RDKit),守护规则/SMARTS 改动
不致非预期回归(需求 1.4 / 2.2 / 2.3)。
"""

from __future__ import annotations

import pytest

from skills.compatibility import rules


def _assess(group_ids, excipients):
    return rules.evaluate([{"id": g} for g in group_ids], excipients)


def _excipient_reactions(assessment, idx=0):
    er = assessment.excipient_results[idx]
    return er.risk, {r.reaction for r in er.reactions}


# ---------------------------------------------------------------------------
# 经典相互作用金标准
# ---------------------------------------------------------------------------

@pytest.mark.parametrize(
    "group_ids,excipient,expected_overall,expected_reactions",
    [
        # 美拉德:伯胺 + 还原糖(乳糖)→ 高风险
        (["primary_amine"], "乳糖", rules.RISK_HIGH, {"maillard"}),
        # 碱催化水解:酯 + 偏碱(硬脂酸镁)→ 高风险
        (["ester"], "硬脂酸镁", rules.RISK_HIGH, {"hydrolysis"}),
        # 内酯 + 碱 → 高风险水解
        (["lactone"], "硬脂酸镁", rules.RISK_HIGH, {"hydrolysis"}),
        # 氧化:酚 + 过氧化物(聚维酮)→ 中风险
        (["phenol"], "聚维酮", rules.RISK_MEDIUM, {"oxidation"}),
        # 氧化:巯基 + 过氧化物 → 高风险
        (["thiol"], "聚维酮", rules.RISK_HIGH, {"oxidation"}),
        # 酸碱:羧基 + 偏碱 → 中风险
        (["carboxylic_acid"], "硬脂酸镁", rules.RISK_MEDIUM, {"acid_base"}),
        # 光降解:酚 + 二氧化钛(光催化剂 + 金属)→ 中风险(氧化 + 光降解)
        (["phenol"], "二氧化钛", rules.RISK_MEDIUM, {"oxidation", "photodegradation"}),
        # Michael:Michael 受体 + 亚硫酸盐 → 中风险
        (["michael_acceptor"], "亚硫酸氢钠", rules.RISK_MEDIUM, {"michael"}),
    ],
)
def test_classic_interactions(group_ids, excipient, expected_overall, expected_reactions):
    a = _assess(group_ids, [excipient])
    assert a.overall_risk == expected_overall, (group_ids, excipient)
    risk, reactions = _excipient_reactions(a)
    assert expected_reactions.issubset(reactions), (reactions, expected_reactions)


# ---------------------------------------------------------------------------
# 纠偏:淀粉 / MCC 假阳性(需求 2.2)
# ---------------------------------------------------------------------------

def test_starch_amine_not_high_maillard():
    """淀粉 + 伯胺 → 中风险(经还原糖杂质途径),而非默认高风险。"""
    a = _assess(["primary_amine"], ["淀粉"])
    risk, reactions = _excipient_reactions(a)
    assert "maillard" in reactions
    assert risk == rules.RISK_MEDIUM
    assert a.overall_risk == rules.RISK_MEDIUM


def test_mcc_amine_no_maillard():
    """微晶纤维素 + 伯胺 → 不触发美拉德(仅可能吸附低风险)。"""
    a = _assess(["primary_amine"], ["微晶纤维素"])
    risk, reactions = _excipient_reactions(a)
    assert "maillard" not in reactions


def test_lactose_still_high():
    """乳糖(真还原糖)+ 伯胺仍为高风险(纠偏不误伤真阳性)。"""
    a = _assess(["primary_amine"], ["乳糖"])
    assert a.overall_risk == rules.RISK_HIGH


def test_aromatic_amine_maillard_differentiated_from_aliphatic():
    """芳香胺 + 还原糖 → 低风险美拉德,区别于脂肪伯/仲胺的高风险(需求 2.1.3)。"""
    aromatic = _assess(["aromatic_amine"], ["乳糖"])
    arom_reactions = {r.reaction: r.risk for r in aromatic.excipient_results[0].reactions}
    assert arom_reactions.get("maillard") == rules.RISK_LOW

    aliphatic = _assess(["primary_amine"], ["乳糖"])
    ali_reactions = {r.reaction: r.risk for r in aliphatic.excipient_results[0].reactions}
    assert ali_reactions.get("maillard") == rules.RISK_HIGH

    # 差异化:脂肪胺风险严格高于芳香胺。
    assert rules.RISK_ORDER[ali_reactions["maillard"]] > rules.RISK_ORDER[arom_reactions["maillard"]]


def test_aromatic_amine_oxidation_prone():
    """芳香胺 + 过氧化物辅料 → 中风险氧化(苯胺类氧化敏感)。"""
    a = _assess(["aromatic_amine"], ["聚维酮"])
    reactions = {r.reaction: r.risk for r in a.excipient_results[0].reactions}
    assert reactions.get("oxidation") == rules.RISK_MEDIUM


# ---------------------------------------------------------------------------
# 最长别名(需求 2.3)
# ---------------------------------------------------------------------------

@pytest.mark.parametrize(
    "name,wrong_parent_tag_absent",
    [
        ("预胶化淀粉", "reducing_sugar_impulse"),  # 不应带淀粉的还原糖杂质途径标签
        ("交联聚维酮", None),
    ],
)
def test_longest_alias_not_parent(name, wrong_parent_tag_absent):
    a = _assess(["primary_amine"], [name])
    er = a.excipient_results[0]
    assert er.known is True
    if name == "预胶化淀粉":
        # 预胶化淀粉档案 tags=[high_moisture],不含 reducing_sugar_impurity → 不命中美拉德。
        assert "reducing_sugar_impurity" not in er.tags
        assert "maillard" not in {r.reaction for r in er.reactions}
    if name == "交联聚维酮":
        # 交联聚维酮 tags=[peroxide](无 high_moisture),区别于母体聚维酮。
        assert "high_moisture" not in er.tags


# ---------------------------------------------------------------------------
# 不确定 + 多辅料聚合
# ---------------------------------------------------------------------------

def test_no_groups_uncertain():
    a = _assess([], ["乳糖", "硬脂酸镁"])
    assert a.overall_risk == rules.RISK_UNCERTAIN
    assert a.overall_confidence == rules.CONF_LOW


def test_multi_excipient_worst_aggregation():
    """多辅料取最坏:乳糖(高) + 甘露醇(无) → 整体高。"""
    a = _assess(["primary_amine"], ["乳糖", "甘露醇"])
    assert a.overall_risk == rules.RISK_HIGH


def test_determinism_repeated_runs():
    first = _assess(["primary_amine", "ester"], ["乳糖", "硬脂酸镁"]).summary()
    again = _assess(["primary_amine", "ester"], ["乳糖", "硬脂酸镁"]).summary()
    assert first == again