feat: 升级算法包 delivery_v1.1 → v1.2,新增任务级 CAD 与测距面板
- 算法包替换为 delivery_v1.2(coupler_panel 新增 autotune/layer_select,underframe_deflection 新增 single_beam 管线),删除 delivery_v1.1 - manifests/adapters 的 PYTHONPATH 与版本说明同步更新到 v1.2 - 新增任务级 CAD 文件支持(task_cad_files 表、上传/读取路由、TaskManager 全选与加载显示) - 新增 train-measurement 自定义前端面板(YAML 编辑器 + 可视化 ROI 配置 + 3D 取点) - PointCloudViewer 支持 PLY 叠加层渲染与 ROI 拾取 - Dockerfile 改用 Miniforge 安装 pythonocc-core,修复 libgomp/libstdc++ 依赖
This commit is contained in:
251
alg/delivery_v1.2/underframe_deflection/excel_report.py
Normal file
251
alg/delivery_v1.2/underframe_deflection/excel_report.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""Excel error report writer (.xlsx).
|
||||
|
||||
Optional dependency: ``openpyxl``. When the package is missing the
|
||||
caller is expected to fall back gracefully (e.g. log a warning and
|
||||
continue without the xlsx artifact).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .api import DeflectionResult
|
||||
from .artifacts import _require_artifacts, _annotate_beam_track_rows
|
||||
from .beams import candidate_rows
|
||||
from .io import ensure_output_dir
|
||||
|
||||
|
||||
SUMMARY_SHEET = "Summary"
|
||||
CURVES_SHEET = "Deflection_Curves"
|
||||
TRACKS_SHEET = "Beam_Tracks"
|
||||
REFERENCE_SHEET = "Reference_Lines"
|
||||
REPRESENTATIVES_SHEET = "Representative_Points"
|
||||
|
||||
|
||||
def write_excel_report(
|
||||
result: DeflectionResult,
|
||||
path: str | Path,
|
||||
*,
|
||||
inputs: dict[str, Any] | None = None,
|
||||
registration_info: dict[str, Any] | None = None,
|
||||
) -> Path:
|
||||
"""Write a 5-sheet Excel error report next to the JSON artifacts.
|
||||
|
||||
``inputs`` is a free-form dict of input paths (scan / cad / roi) and
|
||||
profile; it is rendered verbatim into the Summary sheet.
|
||||
``registration_info`` may include ``rms_mm``, ``iterations``,
|
||||
``inlier_ratio``, ``converged``, ``initial_rms_mm``.
|
||||
"""
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
except ImportError as exc: # pragma: no cover - exercised at runtime only
|
||||
raise ImportError(
|
||||
"Writing Excel reports requires the 'openpyxl' package. "
|
||||
"Install with: pip install openpyxl"
|
||||
) from exc
|
||||
|
||||
output_path = Path(path)
|
||||
ensure_output_dir(output_path.parent)
|
||||
artifacts = _require_artifacts(result)
|
||||
|
||||
wb = Workbook()
|
||||
wb.remove(wb.active)
|
||||
|
||||
_write_summary(wb, result, artifacts, inputs or {}, registration_info or {})
|
||||
_write_curves(wb, artifacts)
|
||||
_write_tracks(wb, artifacts)
|
||||
_write_reference_lines(wb, result)
|
||||
_write_representatives(wb, result)
|
||||
|
||||
wb.save(output_path)
|
||||
return output_path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sheet builders
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _write_summary(
|
||||
wb,
|
||||
result: DeflectionResult,
|
||||
artifacts,
|
||||
inputs: dict[str, Any],
|
||||
reg: dict[str, Any],
|
||||
) -> None:
|
||||
ws = wb.create_sheet(SUMMARY_SHEET)
|
||||
ws.append(["key", "value"])
|
||||
rows: list[tuple[str, Any]] = []
|
||||
|
||||
rows.append(("schema_version", result.schema_version))
|
||||
rows.append(("algorithm_version", result.algorithm_version))
|
||||
rows.append(("profile", inputs.get("profile", "")))
|
||||
rows.append(("scan_path", inputs.get("scan_path", "")))
|
||||
rows.append(("cad_path", inputs.get("cad_path", "")))
|
||||
rows.append(("roi_path", inputs.get("roi_path", "")))
|
||||
rows.append(("config_path", inputs.get("config_path", "")))
|
||||
|
||||
rows.append(("registration_enabled", reg.get("enabled", False)))
|
||||
rows.append(("registration_rms_mm", reg.get("rms_mm", "")))
|
||||
rows.append(("registration_initial_rms_mm", reg.get("initial_rms_mm", "")))
|
||||
rows.append(("registration_iterations", reg.get("iterations", "")))
|
||||
rows.append(("registration_inlier_ratio", reg.get("inlier_ratio", "")))
|
||||
rows.append(("registration_converged", reg.get("converged", "")))
|
||||
|
||||
preprocess = result.report.get("preprocess", {}) or {}
|
||||
for key in (
|
||||
"raw_count",
|
||||
"after_passthrough",
|
||||
"passthrough_applied",
|
||||
"after_statistical_filter",
|
||||
"after_radius_filter",
|
||||
"after_bilateral",
|
||||
"bilateral_applied",
|
||||
"downsampled_count",
|
||||
"spacing_mm",
|
||||
"statistical_keep_ratio",
|
||||
"radius_keep_ratio",
|
||||
):
|
||||
if key in preprocess:
|
||||
rows.append((f"preprocess.{key}", preprocess[key]))
|
||||
|
||||
deflection = result.report.get("deflection", {}) or {}
|
||||
for side in ("left", "right"):
|
||||
side_data = deflection.get(side, {}) or {}
|
||||
selected = side_data.get("selected_measurement", {}) or {}
|
||||
rows.append((f"{side}.available", side_data.get("available", False)))
|
||||
rows.append((f"{side}.reference_available", side_data.get("reference_available", False)))
|
||||
rows.append((f"{side}.reference_confidence", side_data.get("reference_confidence", "")))
|
||||
rows.append((f"{side}.max_downward_deflection_mm", side_data.get("max_downward_deflection_mm", "")))
|
||||
rows.append((f"{side}.max_downward_x_mm", side_data.get("max_downward_x_mm", "")))
|
||||
rows.append((f"{side}.max_upward_deflection_mm", side_data.get("max_upward_deflection_mm", "")))
|
||||
rows.append((f"{side}.max_upward_x_mm", side_data.get("max_upward_x_mm", "")))
|
||||
rows.append((f"{side}.selected_x_mm", selected.get("x_mm", "")))
|
||||
rows.append((f"{side}.selected_signed_deflection_mm", selected.get("signed_deflection_mm", "")))
|
||||
rows.append((f"{side}.selected_absolute_deflection_mm", selected.get("absolute_deflection_mm", "")))
|
||||
rows.append((f"{side}.beam_fit_residual_rms_z", side_data.get("beam_fit_residual_rms_z", "")))
|
||||
|
||||
rows.append(("quality_flags", "; ".join(result.quality_flags)))
|
||||
|
||||
roi_payload = result.report.get("roi", {}) or {}
|
||||
for key in ("source", "x_min", "x_max", "y_min", "y_max", "z_min", "z_max", "z_ceiling", "point_count"):
|
||||
if key in roi_payload:
|
||||
rows.append((f"roi.{key}", roi_payload[key]))
|
||||
|
||||
for key, value in rows:
|
||||
ws.append([key, _coerce_cell(value)])
|
||||
|
||||
ws.column_dimensions["A"].width = 42
|
||||
ws.column_dimensions["B"].width = 32
|
||||
|
||||
|
||||
def _write_curves(wb, artifacts) -> None:
|
||||
ws = wb.create_sheet(CURVES_SHEET)
|
||||
rows = artifacts.deflection_rows
|
||||
if not rows:
|
||||
ws.append(["x"])
|
||||
return
|
||||
fieldnames = list(rows[0].keys())
|
||||
ws.append(fieldnames)
|
||||
for row in rows:
|
||||
ws.append([_coerce_cell(row.get(name)) for name in fieldnames])
|
||||
|
||||
|
||||
def _write_tracks(wb, artifacts) -> None:
|
||||
ws = wb.create_sheet(TRACKS_SHEET)
|
||||
rows = _annotate_beam_track_rows(
|
||||
candidate_rows(artifacts.slices),
|
||||
artifacts.left_tracks,
|
||||
artifacts.right_tracks,
|
||||
)
|
||||
if not rows:
|
||||
ws.append(["slice_index", "candidate_id", "side"])
|
||||
return
|
||||
fieldnames = list(rows[0].keys())
|
||||
ws.append(fieldnames)
|
||||
for row in rows:
|
||||
ws.append([_coerce_cell(row.get(name)) for name in fieldnames])
|
||||
|
||||
|
||||
def _write_reference_lines(wb, result: DeflectionResult) -> None:
|
||||
ws = wb.create_sheet(REFERENCE_SHEET)
|
||||
header = [
|
||||
"side",
|
||||
"available",
|
||||
"confidence",
|
||||
"residual",
|
||||
"front_x",
|
||||
"front_y",
|
||||
"front_z",
|
||||
"back_x",
|
||||
"back_y",
|
||||
"back_z",
|
||||
"dir_x",
|
||||
"dir_y",
|
||||
"dir_z",
|
||||
]
|
||||
ws.append(header)
|
||||
for side in ("left", "right"):
|
||||
ref = (result.report.get("reference_lines") or {}).get(side) or {}
|
||||
front = ref.get("point_front") or [None, None, None]
|
||||
back = ref.get("point_back") or [None, None, None]
|
||||
direction = ref.get("direction") or [None, None, None]
|
||||
ws.append(
|
||||
[
|
||||
side,
|
||||
_coerce_cell(ref.get("available")),
|
||||
_coerce_cell(ref.get("confidence")),
|
||||
_coerce_cell(ref.get("residual")),
|
||||
_coerce_cell(front[0] if len(front) > 0 else None),
|
||||
_coerce_cell(front[1] if len(front) > 1 else None),
|
||||
_coerce_cell(front[2] if len(front) > 2 else None),
|
||||
_coerce_cell(back[0] if len(back) > 0 else None),
|
||||
_coerce_cell(back[1] if len(back) > 1 else None),
|
||||
_coerce_cell(back[2] if len(back) > 2 else None),
|
||||
_coerce_cell(direction[0] if len(direction) > 0 else None),
|
||||
_coerce_cell(direction[1] if len(direction) > 1 else None),
|
||||
_coerce_cell(direction[2] if len(direction) > 2 else None),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _write_representatives(wb, result: DeflectionResult) -> None:
|
||||
ws = wb.create_sheet(REPRESENTATIVES_SHEET)
|
||||
header = ["side", "slice_index", "x", "y", "z", "width", "point_count", "residual", "confidence"]
|
||||
ws.append(header)
|
||||
rep_payload = result.report.get("representative_points") or {}
|
||||
for side in ("left", "right"):
|
||||
for item in rep_payload.get(side, []) or []:
|
||||
ws.append(
|
||||
[
|
||||
side,
|
||||
_coerce_cell(item.get("slice_index")),
|
||||
_coerce_cell(item.get("x")),
|
||||
_coerce_cell(item.get("y")),
|
||||
_coerce_cell(item.get("z")),
|
||||
_coerce_cell(item.get("width")),
|
||||
_coerce_cell(item.get("point_count")),
|
||||
_coerce_cell(item.get("residual")),
|
||||
_coerce_cell(item.get("confidence")),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _coerce_cell(value: Any) -> Any:
|
||||
"""Map non-primitive types to plain Python so openpyxl can write them."""
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, (str, int, float, bool)):
|
||||
return value
|
||||
if isinstance(value, (list, tuple)):
|
||||
return ",".join(str(_coerce_cell(v)) for v in value)
|
||||
return str(value)
|
||||
|
||||
|
||||
__all__ = ["write_excel_report"]
|
||||
Reference in New Issue
Block a user