Split project into frontend and backend

This commit is contained in:
2026-08-13 16:24:36 +08:00
commit f051146486
274 changed files with 953932 additions and 0 deletions
+353
View File
@@ -0,0 +1,353 @@
from __future__ import annotations
import importlib.util
import sys
import traceback
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from ..models import read_json, sha256_json, write_json
from ..parameter_solver import load_requirement
from .datum_inference import infer_step_datum_axis
from .generator import JointModuleError, run_joint_module
from .models import JointModuleManifest
PACKAGE_ROOT = Path(__file__).resolve().parents[2]
AUTO_CONTROLLED_OUTPUTS = [
"auto_design_input.json",
"auto_design_report.json",
"generated_housing.step",
"generated_reducer_requirement.json",
"generated_joint_requirement.json",
"auto_generation_error.json",
]
class AutoJointDesignError(RuntimeError):
pass
@dataclass(frozen=True)
class AutoJointEnvelope:
motor_radial_diameter_mm: float
motor_axis_length_mm: float
housing_inner_diameter_mm: float
housing_outer_diameter_mm: float
housing_axis_length_mm: float
reducer_max_outer_diameter_mm: float
target_ratio: float
module_candidates_mm: list[float]
planet_count_candidates: list[int]
def _patch_cadquery_visual_imports() -> None:
try:
import OCP.IVtkOCC as ivtkocc # type: ignore
class _Dummy:
def __init__(self, *args: object, **kwargs: object) -> None:
pass
def __getattr__(self, name: str) -> object:
raise RuntimeError("CadQuery VTK visualization bridge is unavailable")
for name in ["IVtkOCC_Shape", "IVtkOCC_ShapeMesher"]:
if not hasattr(ivtkocc, name):
setattr(ivtkocc, name, _Dummy)
try:
import OCP.IVtkVTK as ivtkvtk # type: ignore
if not hasattr(ivtkvtk, "IVtkVTK_ShapeData"):
setattr(ivtkvtk, "IVtkVTK_ShapeData", _Dummy)
except Exception: # noqa: BLE001
return
except Exception: # noqa: BLE001
return
def _load_housing_module(path: Path) -> Any:
_patch_cadquery_visual_imports()
spec = importlib.util.spec_from_file_location("auto_joint_housing_source", path)
if spec is None or spec.loader is None:
raise AutoJointDesignError(f"cannot_load_housing_python: {path}")
module = importlib.util.module_from_spec(spec)
sys.modules["auto_joint_housing_source"] = module
spec.loader.exec_module(module)
return module
def _resolve_path(path_text: str, *, requirement_path: Path | None = None) -> Path:
path = Path(path_text).expanduser()
if path.is_absolute():
return path
if requirement_path is not None:
relative_to_requirement = requirement_path.parent / path
if relative_to_requirement.exists():
return relative_to_requirement.resolve()
relative_to_package = PACKAGE_ROOT / path
if relative_to_package.exists():
return relative_to_package.resolve()
return path.resolve()
def _axis_extent(metrics: Any) -> float:
axis = metrics.inferred_axis
if axis is None:
raise AutoJointDesignError(f"{metrics.component_id}: missing inferred axis")
selected = str(axis.details.get("selected_axis_dimension", "z"))
xmin, ymin, zmin, xmax, ymax, zmax = metrics.bbox
extents = {
"x": xmax - xmin,
"y": ymax - ymin,
"z": zmax - zmin,
}
return float(extents[selected])
def _radial_diameter(metrics: Any) -> float:
axis = metrics.inferred_axis
if axis is None:
raise AutoJointDesignError(f"{metrics.component_id}: missing inferred axis")
selected = str(axis.details.get("selected_axis_dimension", "z"))
xmin, ymin, zmin, xmax, ymax, zmax = metrics.bbox
extents = {
"x": xmax - xmin,
"y": ymax - ymin,
"z": zmax - zmin,
}
radial_axes = [axis_name for axis_name in ["x", "y", "z"] if axis_name != selected]
return max(float(extents[radial_axes[0]]), float(extents[radial_axes[1]]))
def _ratio_from_envelope(envelope: AutoJointEnvelope) -> float:
if envelope.reducer_max_outer_diameter_mm < 46.0:
return 3.0
if envelope.reducer_max_outer_diameter_mm < 58.0:
return 5.0
if envelope.reducer_max_outer_diameter_mm < 76.0:
return 7.5
return 10.0
def _module_candidates_for_outer_diameter(max_outer_diameter_mm: float) -> list[float]:
if max_outer_diameter_mm < 45.0:
return [0.3, 0.35, 0.4, 0.5]
if max_outer_diameter_mm < 65.0:
return [0.35, 0.4, 0.5, 0.6, 0.75]
return [0.35, 0.4, 0.5, 0.75, 1.0]
def compute_auto_envelope(
*,
motor_step: Path,
housing_source: Path,
radial_clearance_mm: float,
reducer_axial_allowance_mm: float,
reducer_radial_clearance_mm: float,
) -> tuple[AutoJointEnvelope, dict[str, Any], Any]:
motor_metrics = infer_step_datum_axis(component_id="motor", step_path=motor_step, role="motor")
motor_radial_diameter = _radial_diameter(motor_metrics)
motor_axis_length = _axis_extent(motor_metrics)
housing_module = _load_housing_module(housing_source)
if not all(hasattr(housing_module, name) for name in ["scale_from_motor", "scaled_dimensions"]):
raise AutoJointDesignError(
"housing source must provide scale_from_motor(...) and scaled_dimensions(...)"
)
scale = housing_module.scale_from_motor(
motor_radial_diameter_mm=motor_radial_diameter,
motor_axis_length_mm=motor_axis_length,
radial_clearance_mm=radial_clearance_mm,
reducer_axial_allowance_mm=reducer_axial_allowance_mm,
)
dimensions = housing_module.scaled_dimensions(scale)
housing_inner_diameter = float(dimensions["inner_wall_radius"]) * 2.0
housing_outer_diameter = float(dimensions["top_outer_radius"]) * 2.0
housing_axis_length = float(dimensions["height"])
reducer_max_outer = max(24.0, housing_inner_diameter - 2.0 * reducer_radial_clearance_mm)
envelope = AutoJointEnvelope(
motor_radial_diameter_mm=motor_radial_diameter,
motor_axis_length_mm=motor_axis_length,
housing_inner_diameter_mm=housing_inner_diameter,
housing_outer_diameter_mm=housing_outer_diameter,
housing_axis_length_mm=housing_axis_length,
reducer_max_outer_diameter_mm=reducer_max_outer,
target_ratio=0.0,
module_candidates_mm=_module_candidates_for_outer_diameter(reducer_max_outer),
planet_count_candidates=[3, 4],
)
envelope = AutoJointEnvelope(
**{
**envelope.__dict__,
"target_ratio": _ratio_from_envelope(envelope),
}
)
return envelope, {
"motor_metrics": motor_metrics.model_dump(mode="json"),
"housing_scale": {
"radial_scale": float(scale.radial_scale),
"axial_scale": float(scale.axial_scale),
"source": str(scale.source),
},
"housing_dimensions": dimensions,
}, housing_module
def export_generated_housing(
*,
housing_module: Any,
scale_payload: dict[str, Any],
out_path: Path,
) -> None:
if not hasattr(housing_module, "HousingScale") or not hasattr(housing_module, "export_scaled"):
raise AutoJointDesignError("housing source must provide HousingScale and export_scaled")
scale = housing_module.HousingScale(
radial_scale=float(scale_payload["radial_scale"]),
axial_scale=float(scale_payload["axial_scale"]),
source=str(scale_payload.get("source", "computed_from_motor_bbox")),
)
out_path.parent.mkdir(parents=True, exist_ok=True)
housing_module.export_scaled(out_path, scale)
if not out_path.exists() or out_path.stat().st_size <= 0:
raise AutoJointDesignError(f"generated_housing_step_missing_or_empty: {out_path}")
def build_reducer_requirement(envelope: AutoJointEnvelope, *, tooth_form: str = "spur") -> dict[str, Any]:
return {
"target_ratio": envelope.target_ratio,
"ratio_tolerance": 0.000001,
"topology_family": "simple_2k_h",
"detail_level": "industrial_core",
"input": "sun",
"fixed": "ring",
"output": "carrier",
"tooth_form": tooth_form,
"planet_count_candidates": envelope.planet_count_candidates,
"module_candidates_mm": envelope.module_candidates_mm,
"pressure_angle_deg": 20.0,
"face_width_mm": min(8.0, max(5.0, envelope.housing_axis_length_mm * 0.10)),
"backlash_mm": 0.03,
"addendum_coeff": 1.0,
"clearance_coeff": 0.25,
"ring_rim_thickness_mm": max(1.8, envelope.reducer_max_outer_diameter_mm * 0.035),
"constraints": {
"max_outer_diameter_mm": envelope.reducer_max_outer_diameter_mm,
"min_planet_neighbor_clearance_mm": 0.3,
"min_sun_teeth": 18,
"max_sun_teeth": 96,
"min_planet_teeth": 18,
"max_planet_teeth": 96,
},
}
def build_joint_requirement(
*,
motor_step: Path,
housing_step: Path,
reducer_requirement: Path,
) -> dict[str, Any]:
return {
"module_type": "planetary_joint_module",
"motor_step": str(motor_step.resolve()),
"housing_step": str(housing_step.resolve()),
"reducer_requirement": str(reducer_requirement.resolve()),
"placement_policy": "infer_then_write_editable_json",
"motor_to_reducer_relation": "motor_output_to_sun_keyed_input",
"housing_relation": "ring_fixed_to_housing",
"joint_output": "carrier_output_shaft",
"input_shaft_insertion_depth_mm": 4.0,
"output_extension_mm": 8.0,
}
def prepare_auto_dir(out_dir: Path) -> None:
out_dir.mkdir(parents=True, exist_ok=True)
for name in AUTO_CONTROLLED_OUTPUTS:
path = out_dir / name
if path.exists():
path.unlink()
def run_auto_joint_design(
*,
motor_step: Path,
housing_source: Path,
out_dir: Path,
tooth_form: str = "spur",
radial_clearance_mm: float = 14.0,
reducer_axial_allowance_mm: float = 42.0,
reducer_radial_clearance_mm: float = 8.0,
) -> JointModuleManifest:
prepare_auto_dir(out_dir)
if not motor_step.exists():
raise AutoJointDesignError(f"missing_motor_step: {motor_step}")
if not housing_source.exists():
raise AutoJointDesignError(f"missing_housing_source: {housing_source}")
envelope, evidence, housing_module = compute_auto_envelope(
motor_step=motor_step,
housing_source=housing_source,
radial_clearance_mm=radial_clearance_mm,
reducer_axial_allowance_mm=reducer_axial_allowance_mm,
reducer_radial_clearance_mm=reducer_radial_clearance_mm,
)
housing_step = out_dir / "generated_housing.step"
export_generated_housing(
housing_module=housing_module,
scale_payload=evidence["housing_scale"],
out_path=housing_step,
)
reducer_requirement_path = out_dir / "generated_reducer_requirement.json"
joint_requirement_path = out_dir / "generated_joint_requirement.json"
reducer_requirement_payload = build_reducer_requirement(envelope, tooth_form=tooth_form)
write_json(reducer_requirement_path, reducer_requirement_payload)
# Validate before launching the heavy generation path.
load_requirement(reducer_requirement_path)
joint_requirement_payload = build_joint_requirement(
motor_step=motor_step,
housing_step=housing_step,
reducer_requirement=reducer_requirement_path,
)
write_json(joint_requirement_path, joint_requirement_payload)
write_json(
out_dir / "auto_design_input.json",
{
"motor_step": str(motor_step.resolve()),
"housing_source": str(housing_source.resolve()),
"tooth_form": tooth_form,
"radial_clearance_mm": radial_clearance_mm,
"reducer_axial_allowance_mm": reducer_axial_allowance_mm,
"reducer_radial_clearance_mm": reducer_radial_clearance_mm,
},
)
write_json(
out_dir / "auto_design_report.json",
{
"run_id": f"auto_{uuid.uuid4().hex[:12]}",
"requirement_hash": sha256_json(joint_requirement_payload),
"envelope": envelope.__dict__,
"evidence": evidence,
"generated_files": {
"housing_step": str(housing_step.resolve()),
"reducer_requirement": str(reducer_requirement_path.resolve()),
"joint_requirement": str(joint_requirement_path.resolve()),
},
},
)
try:
return run_joint_module(requirement_path=joint_requirement_path, out_dir=out_dir / "joint")
except Exception as exc: # noqa: BLE001
write_json(
out_dir / "auto_generation_error.json",
{
"stage": "joint",
"error_type": type(exc).__name__,
"message": str(exc),
"traceback": traceback.format_exc(),
},
)
raise