Files
massive-cloud/alg/delivery_v1.2.1/underframe_deflection/excel_report.py
lipan 29f1d45d66 feat: 算法升级、整车配准集成与性能分析文档
算法更新: delivery v1.2.1、平整度v1.5、测距v2.2、整车配准v1.3
适配器: vehicle-registration 新增、各算法 manifest 和 adapter 更新
后端: taskCadFiles/source 字段迁移、derivedPointClouds 注册、取消任务联动
前端: ElapsedTime 时长显示、两步算法流程、canRun 逻辑修复
文档: 新增5份算法性能分析报告
2026-06-25 08:22:20 +00:00

252 lines
9.1 KiB
Python

"""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"]