Preformu / tests /test_asap.py
Kevinshh's picture
Deploy Kernel+Skill architecture to HF Spaces; wire advanced stability features; remove deprecated entry points
19729e9
Raw
History Blame Contribute Delete
6.68 kB
"""ASAP(加速稳定性评估方案)单点交叉数据建模测试(用户场景:温湿度双因素)。
覆盖:
- ASAP 单点交叉数据的**检测与解析**(每行:温度/湿度/时间/降解)。
- 湿度修正 Arrhenius 双因素拟合:输出 Ea / lnA / B 及其不确定性。
- 目标条件(如 25°C/60%RH)下达到降解限度的货架期反算。
- 数据不足(温度/湿度水平 < 2 或点 < 4)时优雅拒绝。
- 端到端:compute 走 ASAP 分支并产出可渲染 summary(不再被 ICH 单点预检误拒)。
导入路径由 tests/conftest.py 设置(底座以顶层包导入;skills 为顶层包)。
"""
import pytest
from kernel.skill_base import ExtractedData, ReportSections
from skills.stability.skill import (
StabilitySkill,
_detect_asap_points,
_detect_time_unit,
_threshold_from_goal,
_build_intent_dict,
)
from skills.stability.asap import ASAPPoint, analyze_asap, rate_from_single_point
from services.report_service import ReportService
# 用户场景 1 的原始数据(解析后的文本形态)。
ASAP_TEXT = """Sheet1
\tBatch\tTemperature(C)\tHumidity(%RH)\tTime(Days)\tDegradation(%)
\tB1\t50\t75\t14\t0.153
\tB1\t60\t40\t14\t0.161
\tB1\t70\t5\t14\t0.156
\tB1\t70\t75\t2\t0.318
\tB1\t80\t15\t4\t0.222
\tB1\t80\t40\t2\t0.287
"""
GOAL = ("请导入上述 ASAP 加速数据,建立温湿度双因素动力学模型(Moisture-Modified "
"Arrhenius 方程)。请输出计算得到的活化能(Ea)、指前因子(lnA)和湿度敏感系数(B),"
"并预测 25°C/60%RH 条件下达到 0.5% 降解限度所需的时间。")
# ---------------------------------------------------------------------------
# 检测与解析
# ---------------------------------------------------------------------------
def test_detect_asap_points_parses_all_rows():
pts = _detect_asap_points(ASAP_TEXT)
assert len(pts) == 6
first = pts[0]
assert first["temperature_c"] == 50.0
assert first["humidity_rh"] == 75.0
assert first["time"] == 14.0
assert first["degradation_pct"] == pytest.approx(0.153)
def test_detect_time_unit_days():
assert _detect_time_unit(ASAP_TEXT) == "days"
def test_threshold_parsed_from_goal():
assert _threshold_from_goal(GOAL) == pytest.approx(0.5)
def test_non_asap_text_not_detected():
"""传统时间序列文本不应被误判为 ASAP。"""
text = "批次 B1,25C_60RH 长期;时间点 0 3 6 9 月;总杂质 0.1 0.13 0.16 0.19。"
assert _detect_asap_points(text) == []
def test_rate_from_single_point_zero_order():
assert rate_from_single_point(0.30, 2.0) == pytest.approx(0.15)
assert rate_from_single_point(0.0, 2.0) is None
assert rate_from_single_point(0.3, 0.0) is None
# ---------------------------------------------------------------------------
# 双因素拟合 + 货架期反算
# ---------------------------------------------------------------------------
def _points():
return [
ASAPPoint("B1", 50, 75, 14, 0.153),
ASAPPoint("B1", 60, 40, 14, 0.161),
ASAPPoint("B1", 70, 5, 14, 0.156),
ASAPPoint("B1", 70, 75, 2, 0.318),
ASAPPoint("B1", 80, 15, 4, 0.222),
ASAPPoint("B1", 80, 40, 2, 0.287),
]
def test_analyze_asap_fits_and_predicts():
res = analyze_asap(
_points(), threshold_pct=0.5,
target_temperature_c=25.0, target_humidity_rh=60.0, time_unit="days",
)
assert res.ok
assert res.model == "extended_arrhenius"
assert res.Ea_kj_per_mol is not None and res.Ea_kj_per_mol > 0
assert res.ln_A is not None
assert res.B_per_rh is not None # 湿度敏感系数已解析
# 参数不确定性(df = 6 - 3 = 3 ≥ 1)可估计。
assert res.Ea_se_kj_per_mol is not None
assert res.B_se is not None
# 目标条件货架期反算成功。
assert res.predicted_time_to_threshold is not None
assert res.predicted_time_months is not None
# 25°C 在实验温度(50–80°C)范围外 → 外推。
assert res.within_design_space is False
assert any("外推" in d for d in res.declarations)
def test_analyze_asap_insufficient_levels_refuses():
"""温度/湿度水平不足 → ok=False 并给出原因。"""
pts = [
ASAPPoint("B1", 50, 75, 14, 0.15),
ASAPPoint("B1", 50, 75, 7, 0.08),
ASAPPoint("B1", 50, 75, 2, 0.03),
] # 仅 1 个温度、1 个湿度水平
res = analyze_asap(pts, threshold_pct=0.5,
target_temperature_c=25.0, target_humidity_rh=60.0)
assert res.ok is False
assert "温度" in res.error or "湿度" in res.error
# ---------------------------------------------------------------------------
# 端到端:compute 走 ASAP 分支(不再被 ICH 单点预检误拒)
# ---------------------------------------------------------------------------
def _asap_extracted():
pts = _detect_asap_points(ASAP_TEXT)
data = {
"batches": [], "specification_limit": 0.5, "primary_cqa": "降解产物",
"target_timepoints": [24, 36],
"asap_points": pts, "asap_time_unit": "days", "spec_provided": True,
}
intent = _build_intent_dict(GOAL, data)
return ExtractedData(payload={"data": data, "intent": intent, "extraction_method": "llm"},
method="llm")
def test_compute_asap_branch_succeeds_not_refused():
"""ASAP 单点交叉数据应走双因素建模分支并成功,而非被 ICH 单点预检拒绝。"""
cr = StabilitySkill().compute(_asap_extracted())
assert cr.can_proceed is True, "ASAP 数据不应被『每条件 <3 点』预检误拒"
asap = cr.summary.get("asap")
assert asap is not None
assert asap["model"] == "extended_arrhenius"
assert asap["Ea_kJ_per_mol"] > 0
assert asap["B_per_rh"] is not None
assert asap["predicted_time_months"] is not None
def test_compute_asap_report_renders_requested_outputs():
"""报告应呈现 Ea / lnA / B 与货架期反算(用户请求的输出)。"""
cr = StabilitySkill().compute(_asap_extracted())
class _Meta:
id = "stability"; display_name = "稳定性预测"; version = "1.0.0"
html = ReportService().assemble(_Meta(), cr, ReportSections(sections={}), lang="zh")
assert "ASAP 湿度修正" in html
assert "活化能 Ea" in html
assert "指前因子 lnA" in html
assert "湿度敏感系数 B" in html
assert "降解限度所需时间" in html
# 目标条件超出实验范围 → 外推警示。
assert "外推" in html
if __name__ == "__main__": # pragma: no cover
import sys
sys.exit(pytest.main([__file__, "-v"]))