Files
wehub-resource-sync 2c632336aa
CI / Viewer CI (push) Successful in 13m37s
CI / Core CI (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:38 +08:00

1279 lines
43 KiB
Python

from __future__ import annotations
import hashlib
import importlib
import inspect
import json
import logging
import math
import os
import re
import runpy
import statistics
import sys
import threading
import traceback
from contextlib import suppress
from pathlib import Path
from typing import Any
from agent.feedback import build_compile_signal_bundle
from agent.models import CompileReport, CompileSignalBundle
from agent.mp_utils import get_mp_context
from agent.prompts import normalize_sdk_package
from sdk._core.v0.assets import activate_asset_session, asset_session_for_script
logger = logging.getLogger(__name__)
_COMPILE_TARGETS = {"full", "visual"}
_EXCEPTION_PREFIX_RE = re.compile(r"^(?:[A-Za-z_][A-Za-z0-9_]*(?:Error|Exception)):\s*")
_VISUAL_OBJ_MESH_RE = re.compile(
r"<visual\b[\s\S]*?<mesh\b[^>]*filename=['\"][^'\"]+\.obj['\"]",
re.IGNORECASE,
)
_GEOMETRY_QC_MARKERS = (
"isolated parts detected",
"geometry overlap check reported overlaps",
"mesh connectivity check failed",
"visual connectivity check failed",
"fail_if_parts_overlap_in_sampled_poses(",
"expect_contact(",
"expect_gap(",
"expect_overlap(",
"expect_within(",
"expect_aabb_",
"expect_xy_distance",
"expect_above",
"expect_joint_motion_axis",
"urdf tests failed:",
)
_AUTOMATED_BASELINE_WARNING_CHECK_NAME = (
"warn_if_part_contains_disconnected_geometry_islands(tol=1e-06)"
)
_AUTOMATED_BASELINE_DEFAULT_CHECK_NAMES = frozenset(
{
"check_model_valid",
"check_single_root_part",
"check_mesh_assets_ready",
"fail_if_isolated_parts()",
_AUTOMATED_BASELINE_WARNING_CHECK_NAME,
"fail_if_parts_overlap_in_current_pose()",
}
)
_MODEL_EXECUTION_LOCK = threading.Lock()
def _import_sdk_module(sdk_package: str, module_suffix: str = "") -> Any:
package = normalize_sdk_package(sdk_package)
return importlib.import_module(f"{package}{module_suffix}")
def _env_float(name: str, default: float) -> float:
raw = os.environ.get(name)
if raw is None:
return default
try:
return float(raw.strip())
except Exception:
return default
def compile_urdf(
script_path: Path,
*,
sdk_package: str = "sdk",
run_checks: bool = True,
ignore_geom_qc: bool = False,
target: str = "full",
rewrite_visual_glb: bool | None = None,
) -> str:
"""Execute a generated script and return the exported XML payload."""
report = compile_urdf_report_maybe_timeout(
script_path,
sdk_package=sdk_package,
run_checks=run_checks,
ignore_geom_qc=ignore_geom_qc,
target=target,
rewrite_visual_glb=rewrite_visual_glb,
)
for warning in report.warnings:
logger.warning("%s", warning)
return report.urdf_xml
def _normalize_compile_target(target: str) -> str:
target_key = str(target).strip().lower()
if target_key not in _COMPILE_TARGETS:
supported = ", ".join(sorted(_COMPILE_TARGETS))
raise ValueError(f"Unsupported compile target {target!r}. Expected one of: {supported}")
return target_key
def _extract_urdf_xml(
globals_dict: dict,
*,
sdk_package: str = "sdk",
target: str = "full",
validate_export: bool = True,
suppress_exceptions: bool = False,
) -> str | None:
target_key = _normalize_compile_target(target)
object_model = globals_dict.get("object_model")
urdf_xml: str | None = None
export_exc: Exception | None = None
try:
if object_model is not None:
compile_object_to_urdf_xml = getattr(
_import_sdk_module(sdk_package, ".v0._urdf_export"),
"compile_object_to_urdf_xml",
)
script_dir = globals_dict.get("__file__")
asset_root = Path(script_dir).resolve().parent if isinstance(script_dir, str) else None
try:
params = inspect.signature(compile_object_to_urdf_xml).parameters
except Exception:
params = {}
if "asset_root" in params:
urdf_xml = compile_object_to_urdf_xml(
object_model,
asset_root=asset_root,
include_physical_collisions=target_key != "visual",
validate=validate_export,
)
else:
urdf_xml = compile_object_to_urdf_xml(object_model)
globals_dict["urdf_xml"] = urdf_xml
except Exception as exc:
export_exc = exc
urdf_xml = None
if not isinstance(urdf_xml, str):
maybe_urdf_xml = globals_dict.get("urdf_xml")
if isinstance(maybe_urdf_xml, str):
urdf_xml = maybe_urdf_xml
if not isinstance(urdf_xml, str) and export_exc is not None and not suppress_exceptions:
raise export_exc
return urdf_xml
def _attach_compiled_urdf_on_failure(
exc: BaseException,
*,
urdf_xml: str | None,
warnings: list[str],
signal_bundle: CompileSignalBundle,
) -> BaseException:
wrapped = RuntimeError(f"{type(exc).__name__}: {exc}")
if isinstance(urdf_xml, str) and urdf_xml.strip():
setattr(wrapped, "compiled_urdf_xml", urdf_xml)
setattr(wrapped, "warnings", list(warnings))
test_report = getattr(exc, "test_report", None)
if test_report is not None:
setattr(wrapped, "test_report", test_report)
setattr(wrapped, "compile_signal_bundle", signal_bundle)
return wrapped
def _strip_exception_prefixes(message: str) -> str:
stripped = message.strip()
while True:
match = _EXCEPTION_PREFIX_RE.match(stripped)
if match is None:
return stripped
stripped = stripped[match.end() :].lstrip()
def _nonblocking_geometry_qc_warning_from_exception(exc: BaseException) -> str | None:
compiled_urdf_xml = getattr(exc, "compiled_urdf_xml", None)
if not isinstance(compiled_urdf_xml, str) or not compiled_urdf_xml.strip():
return None
message = _strip_exception_prefixes(str(exc))
lowered = message.lower()
if not any(marker in lowered for marker in _GEOMETRY_QC_MARKERS):
return None
if message.startswith("URDF compile failure ("):
header_end = message.find("):")
if header_end != -1:
context = message[len("URDF compile failure (") : header_end]
detail = message[header_end + 2 :].lstrip(": ").lstrip()
geometry_label = context.split(",", 1)[0].strip() or "geometry"
return f"URDF compile warning ({geometry_label}, non-blocking): {detail}"
return f"URDF compile warning (non-blocking): {message}"
def compile_urdf_report(
script_path: Path,
*,
sdk_package: str = "sdk",
run_checks: bool = True,
ignore_geom_qc: bool = False,
target: str = "full",
rewrite_visual_glb: bool | None = None,
) -> CompileReport:
session = asset_session_for_script(script_path)
with activate_asset_session(session):
return _compile_urdf_report_impl(
script_path,
sdk_package=sdk_package,
run_checks=run_checks,
ignore_geom_qc=ignore_geom_qc,
target=target,
rewrite_visual_glb=rewrite_visual_glb,
)
def _compile_urdf_report_impl(
script_path: Path,
*,
sdk_package: str = "sdk",
run_checks: bool = True,
ignore_geom_qc: bool = False,
target: str = "full",
rewrite_visual_glb: bool | None = None,
) -> CompileReport:
"""Execute a generated script and return export XML plus non-blocking warnings."""
globals_dict = load_model_globals(script_path, sdk_package=sdk_package)
warnings: list[str] = []
test_report = None
target_key = _normalize_compile_target(target)
script_path = script_path.resolve()
if run_checks:
try:
if target_key == "full":
authored_report = _run_required_tests(
globals_dict,
sdk_package=sdk_package,
)
baseline_report = _run_compiler_owned_baseline_tests(
globals_dict,
script_path=script_path,
sdk_package=sdk_package,
authored_report=authored_report,
)
test_report = _merge_test_reports(
authored_report,
baseline_report,
sdk_package=sdk_package,
)
_raise_for_failed_test_report(test_report)
except Exception as exc:
urdf_xml = _extract_urdf_xml(
globals_dict,
sdk_package=sdk_package,
target=target_key,
suppress_exceptions=True,
)
signal_bundle = build_compile_signal_bundle(
status="failure",
warnings=warnings,
test_report=getattr(exc, "test_report", None),
exc=exc,
)
wrapped = _attach_compiled_urdf_on_failure(
exc,
urdf_xml=urdf_xml,
warnings=warnings,
signal_bundle=signal_bundle,
)
if ignore_geom_qc:
nonblocking_warning = _nonblocking_geometry_qc_warning_from_exception(wrapped)
compiled_urdf_xml = getattr(wrapped, "compiled_urdf_xml", None)
if (
nonblocking_warning is not None
and isinstance(compiled_urdf_xml, str)
and compiled_urdf_xml.strip()
):
warning_lines = list(warnings)
warning_lines.append(nonblocking_warning)
return CompileReport(
urdf_xml=compiled_urdf_xml,
warnings=warning_lines,
signal_bundle=build_compile_signal_bundle(
status="success",
warnings=warning_lines,
test_report=getattr(wrapped, "test_report", None),
),
)
raise wrapped from exc
warnings.extend(str(item) for item in getattr(test_report, "warnings", ()) or ())
urdf_xml = _extract_urdf_xml(
globals_dict,
sdk_package=sdk_package,
target=target_key,
validate_export=not (run_checks and target_key == "full"),
)
if not isinstance(urdf_xml, str):
raise ValueError("object_model must compile into an exportable XML payload")
if _should_rewrite_visual_meshes_to_glb(
sdk_package=sdk_package,
rewrite_visual_glb=rewrite_visual_glb,
):
urdf_xml = rewrite_visual_meshes_to_glb(
urdf_xml,
sdk_package=sdk_package,
asset_root=script_path.parent,
warnings=warnings,
)
signal_bundle = build_compile_signal_bundle(
status="success",
warnings=warnings,
test_report=test_report,
)
return CompileReport(
urdf_xml=urdf_xml,
warnings=warnings,
signal_bundle=signal_bundle,
)
def rewrite_visual_meshes_to_glb(
urdf_xml: str,
*,
sdk_package: str,
asset_root: Path,
warnings: list[str],
) -> str:
if _VISUAL_OBJ_MESH_RE.search(urdf_xml) is None:
return urdf_xml
try:
convert_urdf_visual_meshes_to_glb = getattr(
_import_sdk_module(sdk_package, ".v0.viewer_assets"),
"convert_urdf_visual_meshes_to_glb",
)
converted_xml, conversion_warnings = convert_urdf_visual_meshes_to_glb(
urdf_xml,
asset_root=asset_root,
)
except Exception as exc:
warnings.append(f"Viewer mesh conversion warning: {exc}")
return urdf_xml
warnings.extend(str(item) for item in conversion_warnings)
return converted_xml
def _should_rewrite_visual_meshes_to_glb(
*,
sdk_package: str,
rewrite_visual_glb: bool | None,
) -> bool:
if rewrite_visual_glb is not None:
return rewrite_visual_glb
return False
def load_model_globals(
script_path: Path,
*,
sdk_package: str = "sdk",
) -> dict[str, Any]:
"""Execute a model script and return its globals."""
normalize_sdk_package(sdk_package)
repo_root = Path(__file__).resolve().parents[1]
script_path = script_path.resolve()
with _MODEL_EXECUTION_LOCK:
prev_cwd = Path.cwd()
os.chdir(script_path.parent)
sys.path.insert(0, str(repo_root))
try:
globals_dict = runpy.run_path(script_path.name)
finally:
os.chdir(prev_cwd)
if sys.path and sys.path[0] == str(repo_root):
sys.path.pop(0)
return globals_dict
def _warn_cwd_relative_asset_paths(*, script_path: Path, warnings: list[str]) -> None:
"""
Emit a non-blocking warning for path anti-patterns that make outputs depend on current cwd.
These scripts are expected to write assets relative to script location, not process cwd.
"""
try:
source = script_path.read_text(encoding="utf-8")
except Exception:
return
findings: list[str] = []
if re.search(r'^\s*HERE\s*=\s*Path\(\s*["\']\.\s*["\']\s*\)', source, re.MULTILINE):
findings.append("Detected `HERE = Path('.')` assignment.")
if re.search(r'asset_root\s*=\s*["\']\.\s*["\']', source):
findings.append("Detected `asset_root='.'` usage.")
if re.search(r'asset_root\s*=\s*Path\(\s*["\']\.\s*["\']\s*\)', source):
findings.append("Detected `asset_root=Path('.')` usage.")
if not findings:
return
warnings.append(
"URDF compile warning (non-blocking): cwd-relative asset paths detected.\n"
+ "\n".join(f"- {item}" for item in findings)
+ "\nUse managed mesh helpers instead: `mesh_from_geometry(..., name='part_name')`, "
"`mesh_from_input('existing_mesh')`, `mesh_from_cadquery(..., name='part_name')`, "
"and `TestContext(object_model)`."
)
def _env_int(name: str, default: int) -> int:
raw = os.environ.get(name)
if raw is None:
return default
try:
return int(raw.strip())
except Exception:
return default
def _iter_model_links(object_model: object) -> list[object]:
links = getattr(object_model, "links", None)
if isinstance(links, list):
return links
parts = getattr(object_model, "parts", None)
if isinstance(parts, list):
return parts
return []
def _format_dims(dims: tuple[float, float, float]) -> str:
return "(" + ", ".join(f"{float(v):.4g}" for v in dims) + ")m"
def _warn_geometry_scale_anomalies(
globals_dict: dict,
*,
script_dir: Path,
warnings: list[str],
sdk_package: str = "sdk",
) -> None:
if os.environ.get("URDF_DISABLE_IMPORTANT_GEOMETRY_WARNINGS") in {"1", "true", "TRUE"}:
return
object_model = globals_dict.get("object_model")
if object_model is None:
return
links = _iter_model_links(object_model)
if not links:
return
absurd_dim_max = float(os.environ.get("URDF_IMPORTANT_GEOMETRY_MAX_DIM", "1000.0"))
outlier_ratio = float(os.environ.get("URDF_IMPORTANT_GEOMETRY_OUTLIER_RATIO", "100.0"))
outlier_abs_min = float(os.environ.get("URDF_IMPORTANT_GEOMETRY_OUTLIER_ABS_MIN", "10.0"))
max_findings = _env_int("URDF_IMPORTANT_GEOMETRY_MAX_FINDINGS", 10)
absurd_findings: list[str] = []
span_records: list[tuple[str, str, int, str, tuple[float, float, float], float]] = []
for link in links:
link_name = getattr(link, "name", None)
if not isinstance(link_name, str) or not link_name:
continue
for source_name in ("visuals", "collisions"):
items = getattr(link, source_name, None)
if not isinstance(items, list) or not items:
continue
for index, item in enumerate(items):
geometry = getattr(item, "geometry", None)
if geometry is None:
continue
try:
local_min, local_max = _geometry_local_aabb(
geometry,
script_dir=script_dir,
sdk_package=sdk_package,
)
except Exception:
continue
dims = (
float(local_max[0] - local_min[0]),
float(local_max[1] - local_min[1]),
float(local_max[2] - local_min[2]),
)
geom_type = type(geometry).__name__
max_dim = max(abs(d) for d in dims)
has_non_finite = any(not math.isfinite(v) for v in (*local_min, *local_max, *dims))
if has_non_finite or max_dim > absurd_dim_max:
reason = (
"non-finite dimensions" if has_non_finite else f"max_dim={max_dim:.4g}m"
)
absurd_findings.append(
f"- link={link_name!r} source={source_name[:-1]!r} index={index} "
f"geometry={geom_type!r} dims={_format_dims(dims)} reason={reason}"
)
if all(math.isfinite(v) for v in dims) and max_dim > 0.0:
span_records.append(
(link_name, source_name[:-1], index, geom_type, dims, max_dim)
)
if absurd_findings:
preview = "\n".join(absurd_findings[:max_findings])
more = (
""
if len(absurd_findings) <= max_findings
else f"\n... ({len(absurd_findings) - max_findings} more)"
)
warnings.append(
"IMPORTANT: URDF compile warning (non-blocking): non-finite or absurd geometry dimensions detected.\n"
f"{preview}{more}\n"
"These dimensions are likely numerically broken and will likely look wrong in the viewer. "
"Check for sign errors, bad normalization denominators, unit mistakes, or runaway procedural geometry."
)
if not span_records:
return
spans = [record[5] for record in span_records]
median_span = float(statistics.median(spans))
if not math.isfinite(median_span) or median_span <= 0.0:
return
outlier_findings: list[str] = []
for link_name, source_name, index, geom_type, dims, max_dim in span_records:
if max_dim < outlier_abs_min:
continue
ratio = max_dim / max(median_span, 1e-9)
if ratio < outlier_ratio:
continue
outlier_findings.append(
f"- link={link_name!r} source={source_name!r} index={index} geometry={geom_type!r} "
f"dims={_format_dims(dims)} max_dim={max_dim:.4g}m median_dim={median_span:.4g}m ratio={ratio:.4g}x"
)
if outlier_findings:
preview = "\n".join(outlier_findings[:max_findings])
more = (
""
if len(outlier_findings) <= max_findings
else f"\n... ({len(outlier_findings) - max_findings} more)"
)
warnings.append(
"IMPORTANT: URDF compile warning (non-blocking): geometry outlier dimensions detected.\n"
f"{preview}{more}\n"
"One or more members are dramatically larger than the rest of the object and are likely malformed. "
"Check for bad interpolation, reversed spans, or accidental scale explosions in authored geometry."
)
def _compile_worker(
script_path_str: str,
sdk_package: str,
run_checks: bool,
ignore_geom_qc: bool,
target: str,
rewrite_visual_glb: bool,
conn: object,
) -> None:
try:
report = compile_urdf_report(
Path(script_path_str),
sdk_package=sdk_package,
run_checks=run_checks,
ignore_geom_qc=ignore_geom_qc,
target=target,
rewrite_visual_glb=rewrite_visual_glb,
)
payload = {
"ok": True,
"urdf_xml": report.urdf_xml,
"warnings": report.warnings,
"signal_bundle": report.signal_bundle.to_dict(),
}
conn.send(payload) # type: ignore[attr-defined]
except BaseException as exc:
payload = {
"ok": False,
"error": f"{type(exc).__name__}: {exc}",
"error_type": type(exc).__name__,
"traceback": traceback.format_exc(),
}
compiled_urdf_xml = getattr(exc, "compiled_urdf_xml", None)
if isinstance(compiled_urdf_xml, str) and compiled_urdf_xml.strip():
payload["compiled_urdf_xml"] = compiled_urdf_xml
warnings = getattr(exc, "warnings", None)
if isinstance(warnings, list):
payload["warnings"] = [str(w) for w in warnings]
signal_bundle = getattr(exc, "compile_signal_bundle", None)
if isinstance(signal_bundle, CompileSignalBundle):
payload["signal_bundle"] = signal_bundle.to_dict()
conn.send(payload) # type: ignore[attr-defined]
finally:
with suppress(Exception):
conn.close() # type: ignore[attr-defined]
def compile_urdf_report_maybe_timeout(
script_path: Path,
*,
sdk_package: str = "sdk",
run_checks: bool = True,
ignore_geom_qc: bool = False,
target: str = "full",
rewrite_visual_glb: bool | None = None,
) -> CompileReport:
"""
Run `compile_urdf_report` with a hard timeout to prevent indefinite hangs.
Controlled by `URDF_COMPILE_TIMEOUT_SECONDS` (default: 300). Set to 0 to disable.
"""
timeout_seconds = float(_env_float("URDF_COMPILE_TIMEOUT_SECONDS", 300.0))
if timeout_seconds <= 0:
return compile_urdf_report(
script_path,
sdk_package=sdk_package,
run_checks=run_checks,
ignore_geom_qc=ignore_geom_qc,
target=target,
rewrite_visual_glb=rewrite_visual_glb,
)
ctx = get_mp_context()
parent_conn, child_conn = ctx.Pipe(duplex=False)
proc = ctx.Process(
target=_compile_worker,
args=(
str(script_path),
sdk_package,
run_checks,
ignore_geom_qc,
target,
rewrite_visual_glb,
child_conn,
),
daemon=True,
)
proc.start()
try:
with suppress(Exception):
child_conn.close()
if parent_conn.poll(timeout_seconds):
msg = parent_conn.recv()
else:
with suppress(Exception):
proc.terminate()
proc.join(timeout=2.0)
raise TimeoutError(
f"URDF compile timed out after {timeout_seconds:.0f}s. "
"This can happen if the generated script contains a long-running loop, "
"expensive mesh processing, or very slow overlap checks. "
"To adjust: set URDF_COMPILE_TIMEOUT_SECONDS, or reduce/disable overlap checks "
"(URDF_GEOMETRY_OVERLAP_MAX_SAMPLES / URDF_DISABLE_GEOMETRY_OVERLAP_CHECK=1)."
)
finally:
with suppress(Exception):
parent_conn.close()
proc.join(timeout=2.0)
if proc.is_alive():
with suppress(Exception):
proc.terminate()
proc.join(timeout=2.0)
if not isinstance(msg, dict):
raise RuntimeError(
f"URDF compile failed: worker returned non-dict payload ({type(msg).__name__})"
)
if msg.get("ok") is True:
urdf_xml = msg.get("urdf_xml")
warnings = msg.get("warnings")
signal_bundle_payload = msg.get("signal_bundle")
if not isinstance(urdf_xml, str):
raise RuntimeError("URDF compile failed: missing urdf_xml from worker")
if not isinstance(warnings, list):
warnings = []
if isinstance(signal_bundle_payload, dict):
signal_bundle = CompileSignalBundle.from_dict(signal_bundle_payload)
else:
signal_bundle = build_compile_signal_bundle(status="success", warnings=warnings)
return CompileReport(
urdf_xml=urdf_xml,
warnings=[str(w) for w in warnings],
signal_bundle=signal_bundle,
)
error_text = str(msg.get("error", "Unknown compile worker error")).strip()
error_type = str(msg.get("error_type", "")).strip()
tb_text = str(msg.get("traceback", "")).strip()
if tb_text:
logger.debug("Compile worker traceback:\n%s", tb_text)
exc = RuntimeError(error_text or "Unknown compile worker error")
if error_type:
setattr(exc, "remote_error_type", error_type)
if tb_text:
setattr(exc, "remote_traceback", tb_text)
compiled_urdf_xml = msg.get("compiled_urdf_xml")
if isinstance(compiled_urdf_xml, str) and compiled_urdf_xml.strip():
setattr(exc, "compiled_urdf_xml", compiled_urdf_xml)
warnings = msg.get("warnings")
if isinstance(warnings, list):
setattr(exc, "warnings", [str(w) for w in warnings])
signal_bundle_payload = msg.get("signal_bundle")
if isinstance(signal_bundle_payload, dict):
setattr(
exc,
"compile_signal_bundle",
CompileSignalBundle.from_dict(signal_bundle_payload),
)
raise exc
def _validate_mesh_connectivity(
globals_dict: dict,
*,
script_dir: Path,
sdk_package: str = "sdk",
) -> None:
if os.environ.get("URDF_DISABLE_MESH_CONNECTIVITY_CHECK") in {"1", "true", "TRUE"}:
return
tol = float(os.environ.get("URDF_MESH_CONNECTIVITY_TOL", "0.02"))
object_model = globals_dict.get("object_model")
if object_model is None:
return
find_joint_origin_distance_findings = getattr(
_import_sdk_module("sdk", "._core.v0.geometry_qc"),
"find_joint_origin_distance_findings",
)
findings = find_joint_origin_distance_findings(
object_model,
asset_root=script_dir,
tol=tol,
)
if not findings:
return
first = findings[0]
raise ValueError(
"Visual connectivity check failed: articulation origin is far from exact visual geometry. "
f"joint={first.joint!r} parent={first.parent!r} child={first.child!r} "
f"dist_parent={first.parent_distance:.4g} dist_child={first.child_distance:.4g} "
f"tol={first.tol:.4g}. "
"Fix by moving the joint origin and/or adjusting authored visuals so the parts touch where the joint is mounted."
)
def _geometry_local_aabb(
geometry: object,
*,
script_dir: Path,
sdk_package: str = "sdk",
) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
sdk_mod = _import_sdk_module(sdk_package)
box = getattr(sdk_mod, "Box")
cylinder = getattr(sdk_mod, "Cylinder")
mesh = getattr(sdk_mod, "Mesh")
sphere = getattr(sdk_mod, "Sphere")
if isinstance(geometry, box):
sx, sy, sz = geometry.size
return (-sx / 2.0, -sy / 2.0, -sz / 2.0), (sx / 2.0, sy / 2.0, sz / 2.0)
if isinstance(geometry, cylinder):
radius = float(geometry.radius)
length = float(geometry.length)
return (-radius, -radius, -length / 2.0), (radius, radius, length / 2.0)
if isinstance(geometry, sphere):
radius = float(geometry.radius)
return (-radius, -radius, -radius), (radius, radius, radius)
if isinstance(geometry, mesh):
filename = getattr(geometry, "filename", "")
if not isinstance(filename, str) or not filename:
raise ValueError("Mesh geometry filename is missing")
mesh_path = (script_dir / filename).resolve()
if not mesh_path.exists():
raise ValueError(f"Mesh file not found: {mesh_path}")
local_min, local_max = _obj_aabb(mesh_path)
scale = getattr(geometry, "scale", None)
if scale:
sx, sy, sz = scale
local_min = (local_min[0] * sx, local_min[1] * sy, local_min[2] * sz)
local_max = (local_max[0] * sx, local_max[1] * sy, local_max[2] * sz)
return local_min, local_max
raise ValueError(f"Unsupported geometry type for connectivity check: {type(geometry).__name__}")
def _obj_aabb(path: Path) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
min_x = min_y = min_z = float("inf")
max_x = max_y = max_z = float("-inf")
found = False
for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
if not line.startswith("v "):
continue
parts = line.split()
if len(parts) < 4:
continue
try:
x = float(parts[1])
y = float(parts[2])
z = float(parts[3])
except ValueError:
continue
found = True
min_x = min(min_x, x)
min_y = min(min_y, y)
min_z = min(min_z, z)
max_x = max(max_x, x)
max_y = max(max_y, y)
max_z = max(max_z, z)
if not found:
raise ValueError(f"OBJ contains no vertices: {path}")
return (min_x, min_y, min_z), (max_x, max_y, max_z)
def _transform_aabb(
local_min: tuple[float, float, float],
local_max: tuple[float, float, float],
*,
origin: object,
) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
xyz = getattr(origin, "xyz", (0.0, 0.0, 0.0)) if origin is not None else (0.0, 0.0, 0.0)
rpy = getattr(origin, "rpy", (0.0, 0.0, 0.0)) if origin is not None else (0.0, 0.0, 0.0)
ox, oy, oz = (float(xyz[0]), float(xyz[1]), float(xyz[2]))
rr, rp, ry = (float(rpy[0]), float(rpy[1]), float(rpy[2]))
rot = _rpy_matrix(rr, rp, ry)
corners = []
for x in (local_min[0], local_max[0]):
for y in (local_min[1], local_max[1]):
for z in (local_min[2], local_max[2]):
corners.append((x, y, z))
min_x = min_y = min_z = float("inf")
max_x = max_y = max_z = float("-inf")
for x, y, z in corners:
tx, ty, tz = _mat_vec(rot, (x, y, z))
tx += ox
ty += oy
tz += oz
min_x = min(min_x, tx)
min_y = min(min_y, ty)
min_z = min(min_z, tz)
max_x = max(max_x, tx)
max_y = max(max_y, ty)
max_z = max(max_z, tz)
return (min_x, min_y, min_z), (max_x, max_y, max_z)
def _rpy_matrix(roll: float, pitch: float, yaw: float) -> tuple[tuple[float, float, float], ...]:
cr = math.cos(roll)
sr = math.sin(roll)
cp = math.cos(pitch)
sp = math.sin(pitch)
cy = math.cos(yaw)
sy = math.sin(yaw)
return (
(cy * cp, cy * sp * sr - sy * cr, cy * sp * cr + sy * sr),
(sy * cp, sy * sp * sr + cy * cr, sy * sp * cr - cy * sr),
(-sp, cp * sr, cp * cr),
)
def _mat_vec(
mat: tuple[tuple[float, float, float], ...],
vec: tuple[float, float, float],
) -> tuple[float, float, float]:
x, y, z = vec
return (
mat[0][0] * x + mat[0][1] * y + mat[0][2] * z,
mat[1][0] * x + mat[1][1] * y + mat[1][2] * z,
mat[2][0] * x + mat[2][1] * y + mat[2][2] * z,
)
def _point_aabb_distance(
point: tuple[float, float, float],
aabb: tuple[tuple[float, float, float], tuple[float, float, float]],
) -> float:
(min_x, min_y, min_z), (max_x, max_y, max_z) = aabb
px, py, pz = point
dx = 0.0
if px < min_x:
dx = min_x - px
elif px > max_x:
dx = px - max_x
dy = 0.0
if py < min_y:
dy = min_y - py
elif py > max_y:
dy = py - max_y
dz = 0.0
if pz < min_z:
dz = min_z - pz
elif pz > max_z:
dz = pz - max_z
return math.sqrt(dx * dx + dy * dy + dz * dz)
def _run_required_tests(
globals_dict: dict,
*,
sdk_package: str = "sdk",
) -> object:
run_tests = globals_dict.get("run_tests")
if not callable(run_tests):
raise ValueError(
"Missing required `run_tests()` in generated script. "
"Add a top-level `def run_tests() -> TestReport:` and return `ctx.report()`."
)
try:
test_report_type = getattr(_import_sdk_module(sdk_package), "TestReport")
except Exception as exc: # pragma: no cover
raise ValueError(f"Failed to import {sdk_package}.TestReport: {exc}") from exc
report = run_tests()
if not isinstance(report, test_report_type):
raise ValueError(
f"run_tests() must return {sdk_package}.TestReport (got {type(report).__name__})"
)
return report
def _report_warning_check_name(text: object) -> str | None:
warning_text = str(text).strip()
if not warning_text:
return None
check_name, has_separator, _detail = warning_text.partition(":")
if not has_separator:
return None
return check_name.strip() or None
def _build_test_report(
report_type: type,
*,
checks: tuple[str, ...],
failures: tuple[object, ...],
warnings: tuple[str, ...],
allowances: tuple[str, ...],
allowed_isolated_parts: tuple[str, ...],
allowed_overlaps: tuple[object, ...],
) -> object:
return report_type(
passed=not failures,
checks_run=len(checks),
checks=checks,
failures=failures,
warnings=warnings,
allowances=allowances,
allowed_isolated_parts=allowed_isolated_parts,
allowed_overlaps=allowed_overlaps,
)
def _filter_duplicate_automated_baseline_results(
authored_report: object, baseline_report: object
) -> object:
authored_check_names = {
str(name)
for name in getattr(authored_report, "checks", ())
if str(name) in _AUTOMATED_BASELINE_DEFAULT_CHECK_NAMES
}
if not authored_check_names:
return baseline_report
baseline_checks = tuple(
str(name)
for name in getattr(baseline_report, "checks", ())
if str(name) not in authored_check_names
)
baseline_failures = tuple(
failure
for failure in getattr(baseline_report, "failures", ())
if str(getattr(failure, "name", "")) not in authored_check_names
)
baseline_warnings = tuple(
str(warning)
for warning in getattr(baseline_report, "warnings", ())
if _report_warning_check_name(warning) not in authored_check_names
)
return _build_test_report(
type(baseline_report),
checks=baseline_checks,
failures=baseline_failures,
warnings=baseline_warnings,
allowances=tuple(str(item) for item in getattr(baseline_report, "allowances", ())),
allowed_isolated_parts=tuple(
str(item) for item in getattr(baseline_report, "allowed_isolated_parts", ())
),
allowed_overlaps=tuple(getattr(baseline_report, "allowed_overlaps", ())),
)
def _merge_test_reports(
authored_report: object, baseline_report: object, *, sdk_package: str
) -> object:
filtered_baseline_report = _filter_duplicate_automated_baseline_results(
authored_report,
baseline_report,
)
try:
test_report_type = getattr(_import_sdk_module(sdk_package), "TestReport")
except Exception as exc: # pragma: no cover
raise ValueError(f"Failed to import {sdk_package}.TestReport: {exc}") from exc
check_names: list[str] = []
seen_checks: set[str] = set()
for report in (authored_report, filtered_baseline_report):
for check_name in getattr(report, "checks", ()):
normalized = str(check_name)
if normalized in seen_checks:
continue
seen_checks.add(normalized)
check_names.append(normalized)
failures: list[object] = []
seen_failures: set[tuple[str, str]] = set()
for report in (authored_report, filtered_baseline_report):
for failure in getattr(report, "failures", ()):
key = (str(getattr(failure, "name", "")), str(getattr(failure, "details", "")))
if key in seen_failures:
continue
seen_failures.add(key)
failures.append(failure)
warnings: list[str] = []
seen_warnings: set[str] = set()
for report in (authored_report, filtered_baseline_report):
for warning in getattr(report, "warnings", ()):
text = str(warning)
if text in seen_warnings:
continue
seen_warnings.add(text)
warnings.append(text)
allowances: list[str] = []
seen_allowances: set[str] = set()
for report in (authored_report, filtered_baseline_report):
for allowance in getattr(report, "allowances", ()):
text = str(allowance)
if text in seen_allowances:
continue
seen_allowances.add(text)
allowances.append(text)
allowed_isolated_parts: list[str] = []
seen_isolated_parts: set[str] = set()
for report in (authored_report, filtered_baseline_report):
for part_name in getattr(report, "allowed_isolated_parts", ()):
normalized = str(part_name)
if normalized in seen_isolated_parts:
continue
seen_isolated_parts.add(normalized)
allowed_isolated_parts.append(normalized)
allowed_overlaps: list[object] = []
seen_overlaps: set[tuple[str, str, str | None, str | None, str]] = set()
for report in (authored_report, filtered_baseline_report):
for overlap in getattr(report, "allowed_overlaps", ()):
key = (
str(getattr(overlap, "link_a", "")),
str(getattr(overlap, "link_b", "")),
(
None
if getattr(overlap, "elem_a", None) is None
else str(getattr(overlap, "elem_a"))
),
(
None
if getattr(overlap, "elem_b", None) is None
else str(getattr(overlap, "elem_b"))
),
str(getattr(overlap, "reason", "")),
)
if key in seen_overlaps:
continue
seen_overlaps.add(key)
allowed_overlaps.append(overlap)
return _build_test_report(
test_report_type,
checks=tuple(check_names),
failures=tuple(failures),
warnings=tuple(warnings),
allowances=tuple(allowances),
allowed_isolated_parts=tuple(allowed_isolated_parts),
allowed_overlaps=tuple(allowed_overlaps),
)
def _check_model_has_single_root_part(ctx: object) -> bool:
root_parts = getattr(ctx.model, "root_parts", None)
if not callable(root_parts):
return bool(ctx.check("check_single_root_part", False, "model has no .root_parts()"))
try:
roots = root_parts()
except Exception as exc:
return bool(ctx.check("check_single_root_part", False, f"{type(exc).__name__}: {exc}"))
root_names = tuple(
str(getattr(part, "name", "")).strip()
for part in roots
if str(getattr(part, "name", "")).strip()
)
if len(root_names) != 1:
return bool(
ctx.check(
"check_single_root_part",
False,
f"Expected exactly one root part, found {len(root_names)}: {list(root_names)!r}",
)
)
return bool(ctx.check("check_single_root_part", True))
def _apply_authored_allowances_to_baseline_context(ctx: object, authored_report: object) -> None:
for part_name in getattr(authored_report, "allowed_isolated_parts", ()):
ctx.allow_isolated_part(
str(part_name),
reason="carried over from authored run_tests() allowance",
)
for overlap in getattr(authored_report, "allowed_overlaps", ()):
ctx.allow_overlap(
str(getattr(overlap, "link_a", "")),
str(getattr(overlap, "link_b", "")),
reason=str(getattr(overlap, "reason", "")),
elem_a=(
None
if getattr(overlap, "elem_a", None) is None
else str(getattr(overlap, "elem_a"))
),
elem_b=(
None
if getattr(overlap, "elem_b", None) is None
else str(getattr(overlap, "elem_b"))
),
)
def _run_compiler_owned_baseline_tests(
globals_dict: dict,
*,
script_path: Path,
sdk_package: str,
authored_report: object,
) -> object:
object_model = globals_dict.get("object_model")
if object_model is None:
raise ValueError("Generated script must define top-level `object_model`")
sdk_module = _import_sdk_module(sdk_package)
test_context_type = getattr(sdk_module, "TestContext")
ctx = test_context_type(object_model, asset_root=script_path.parent)
_apply_authored_allowances_to_baseline_context(ctx, authored_report)
ctx.check_model_valid()
_check_model_has_single_root_part(ctx)
preliminary_report = ctx.report()
if not bool(getattr(preliminary_report, "passed", False)):
return _build_test_report(
type(preliminary_report),
checks=tuple(str(name) for name in getattr(preliminary_report, "checks", ())),
failures=tuple(getattr(preliminary_report, "failures", ())),
warnings=tuple(str(item) for item in getattr(preliminary_report, "warnings", ())),
allowances=(),
allowed_isolated_parts=(),
allowed_overlaps=(),
)
ctx.check_mesh_assets_ready()
ctx.fail_if_isolated_parts()
ctx.warn_if_part_contains_disconnected_geometry_islands()
ctx.fail_if_parts_overlap_in_current_pose()
baseline_report = ctx.report()
return _build_test_report(
type(baseline_report),
checks=tuple(str(name) for name in getattr(baseline_report, "checks", ())),
failures=tuple(getattr(baseline_report, "failures", ())),
warnings=tuple(str(item) for item in getattr(baseline_report, "warnings", ())),
allowances=(),
allowed_isolated_parts=(),
allowed_overlaps=(),
)
def _raise_for_failed_test_report(report: object) -> None:
if bool(getattr(report, "passed", False)):
return
failures = getattr(report, "failures", ())
lines = ["URDF tests failed:"]
for failure in list(failures)[:10]:
name = getattr(failure, "name", "unknown")
details = getattr(failure, "details", "")
lines.append(f"- {name}: {details}")
if len(list(failures)) > 10:
lines.append(f"... ({len(list(failures)) - 10} more)")
exc = ValueError("\n".join(lines))
setattr(exc, "test_report", report)
raise exc
def update_manifest(outputs_root: Path) -> None:
entries = []
for path in outputs_root.rglob("*.urdf"):
if "viewer" in path.parts:
continue
rel = path.relative_to(outputs_root).as_posix()
name = path.stem
entries.append({"name": name, "path": rel})
entries.sort(key=lambda item: item["name"])
manifest_path = outputs_root / "manifest.json"
manifest_path.write_text(json.dumps({"generated": entries}, indent=2))
def persist_compile_success_artifacts(
*,
urdf_xml: str,
urdf_out: Path | None,
outputs_root: Path | None,
previous_sig: str | None = None,
) -> str | None:
"""
Persist the latest compile-success URDF and update manifest opportunistically.
Returns:
Content signature for deduping repeated writes.
"""
if not isinstance(urdf_xml, str):
return previous_sig
sig = hashlib.sha1(urdf_xml.encode("utf-8")).hexdigest()
if previous_sig and sig == previous_sig:
return previous_sig
if urdf_out is not None:
out_path = Path(urdf_out).resolve()
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(urdf_xml, encoding="utf-8")
logger.info("Wrote checkpoint URDF to %s", out_path)
if outputs_root is not None:
update_manifest(Path(outputs_root).resolve())
return sig