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
|