feat(build): 任务书 02 第四阶段代码——build 装入 CEUS 结果

- build.yaml 加必填字段 ceus_run(CEUS 运行目录或 null)。
- 不为 null 时按视频号找结果;在 failed.csv 里或缺结果目录的整例跳过,记 C15;关键帧图沿用 C14 检查。
- 资料库超声一路加 keyframes/<槽位>.png(去附加数据重编码)和 tic.json(白名单逐字段构造:
  四条曲线的拟合成败与 R²、灌注指标、紫圈曲线是否可用、7 个槽位的定位与视频时间或原因)。
- manifest 的 CEUS 来源写运行号;泄露扫描已覆盖 tic.json,补命中测试。
- 真实构建等配准改完、全量 CEUS 运行之后再做。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 05:55:57 -04:00
parent e78d1b50b8
commit c582fe426f
15 changed files with 683 additions and 35 deletions
+35 -1
View File
@@ -13,6 +13,7 @@ import csv
import importlib.util
import json
from collections.abc import Callable, Mapping, Sequence
from datetime import datetime
from pathlib import Path
from types import ModuleType
from typing import Any
@@ -23,9 +24,12 @@ import numpy.typing as npt
import pytest
import yaml
from cadx.answers import read_aliases
from cadx.build import run_build
from cadx.ceus import ANCHOR_SLOTS, LognormalParameters, evaluate_lognormal
from cadx.library import list_aliases, read_ultrasound
from conftest import CEUS_YAML
from fake_delivery import make_delivery
from fake_delivery import REPO_ROOT, make_delivery, write_config
_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "ceus_quant.py"
_WIDTH, _HEIGHT, _SPLIT = 400, 200, 200
@@ -177,6 +181,36 @@ def test_run_writes_outputs_and_failed_case(
assert "U001" not in out and "U002" not in out
def test_build_reads_real_run_layout(
tmp_path: Path,
setup: tuple[ModuleType, _CountingSegment, dict[str, Any], Callable[[str], None]],
) -> None:
"""build 读这个脚本真实写出的运行目录:第 1 例带 tic.json 和关键帧进库,第 2 例记 C15。"""
module, _, document, next_run_id = setup
next_run_id("run-a")
module.main(["--config", str(_write_config(tmp_path, document, "ceus.yaml"))])
run_dir = tmp_path / "runs" / "run-a"
delivery_dir = Path(document["paths"]["delivery_dir"])
build_dir = tmp_path / "build"
build_dir.mkdir()
result = run_build(
write_config(build_dir, delivery_dir, 1, None, run_dir),
REPO_ROOT,
datetime(2026, 10, 16, 12, 0, 0).astimezone(),
)
assert [r.case_no for r in read_aliases(result.answers_dir)] == ["001"]
assert result.skipped_by_check == {"C15": 1}
(alias,) = list_aliases(result.library_dir)
route = read_ultrasound(result.library_dir, alias)
summary = json.loads((run_dir / "U001" / "summary.json").read_text(encoding="utf-8"))
located = {s for s, a in summary["anchor_frames"].items() if a["status"] == "ok"}
assert set(route.keyframes) == located
assert route.tic is not None
assert set(route.tic) == {"fits", "perfusion_metrics", "pan_curve_usable", "keyframes"}
assert route.tic["keyframes"]["F1"]["video_time_s"] == pytest.approx(15.0, abs=0.1)
assert route.tic["keyframes"]["F2"]["located"] is False
def test_unreadable_video_fails_at_step_1_and_next_case_runs(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,