MVP / cad_engine.py
cigawa's picture
Upload 11 files
52a2a94 verified
Raw
History Blame Contribute Delete
7.04 kB
"""
Parametric vibration test fixture CAD engine (CadQuery).
Design philosophy: the LLM does not emit raw CAD code. Instead it emits a
STRUCTURED FIXTURE SPEC (a validated dict / JSON). This engine deterministically
turns that spec into real geometry and exports STEP, STL, and an SVG preview.
That keeps geometry generation reliable and reviewable, and the spec itself is a
clean artifact for test engineers and for the RLHF feedback loop.
A typical vibration fixture (for mounting a test article to a shaker/slip table):
- a base plate that bolts to the shaker head expander / slip table grid
- a raised interface/boss pattern that the test article bolts onto
- through-holes for both bolt patterns
Optional: stiffening ribs, a cavity to reduce mass, chamfered edges.
If CadQuery is unavailable (e.g. won't compile on Windows ARM), import of this
module fails gracefully and the app degrades to spec-only mode.
"""
from dataclasses import dataclass, field, asdict
from typing import List, Optional, Tuple
import os
try:
import cadquery as cq
from cadquery import exporters
CADQUERY_AVAILABLE = True
CADQUERY_IMPORT_ERROR = None
except Exception as e: # noqa: BLE001
CADQUERY_AVAILABLE = False
CADQUERY_IMPORT_ERROR = str(e)
# --------------------------------------------------------------------------- #
# Structured fixture specification
# --------------------------------------------------------------------------- #
@dataclass
class BoltPattern:
"""A rectangular bolt-hole pattern (4 corners)."""
spacing_x_mm: float
spacing_y_mm: float
hole_dia_mm: float
@dataclass
class FixtureSpec:
"""Everything needed to build a parametric fixture. Units: mm."""
# Base plate (bolts to shaker table)
base_length_mm: float = 200.0
base_width_mm: float = 200.0
base_thickness_mm: float = 15.0
table_bolt_pattern: BoltPattern = field(
default_factory=lambda: BoltPattern(160, 160, 9.0)
)
# Raised interface boss (test article mounts here)
boss_length_mm: float = 100.0
boss_width_mm: float = 100.0
boss_height_mm: float = 25.0
article_bolt_pattern: BoltPattern = field(
default_factory=lambda: BoltPattern(80, 80, 5.5)
)
# Options
edge_chamfer_mm: float = 3.0
add_ribs: bool = True
rib_thickness_mm: float = 8.0
material: str = "6061-T6 Aluminum"
# Free-text design rationale from the LLM (for the report + review)
rationale: str = ""
def to_dict(self) -> dict:
d = asdict(self)
return d
def estimated_mass_kg(self) -> float:
"""Rough solid-volume mass estimate (ignores holes/cavities)."""
# density kg/mm^3 for common materials
density = {
"6061-T6 Aluminum": 2.70e-6,
"7075-T6 Aluminum": 2.81e-6,
"Steel": 7.85e-6,
"Stainless Steel": 8.00e-6,
"Magnesium": 1.74e-6,
}.get(self.material, 2.70e-6)
base_v = self.base_length_mm * self.base_width_mm * self.base_thickness_mm
boss_v = self.boss_length_mm * self.boss_width_mm * self.boss_height_mm
return round((base_v + boss_v) * density, 3)
# --------------------------------------------------------------------------- #
# Geometry builder
# --------------------------------------------------------------------------- #
def build_fixture(spec: FixtureSpec):
"""Return a CadQuery Workplane solid for the given spec."""
if not CADQUERY_AVAILABLE:
raise RuntimeError(
f"CadQuery not available: {CADQUERY_IMPORT_ERROR}"
)
# Base plate
fixture = (
cq.Workplane("XY")
.box(spec.base_length_mm, spec.base_width_mm, spec.base_thickness_mm)
)
# Table bolt holes (through the base plate)
tbp = spec.table_bolt_pattern
fixture = (
fixture.faces(">Z").workplane()
.rect(tbp.spacing_x_mm, tbp.spacing_y_mm, forConstruction=True)
.vertices()
.hole(tbp.hole_dia_mm)
)
# Raised interface boss, centered on top of the base plate
boss = (
cq.Workplane("XY")
.workplane(offset=spec.base_thickness_mm / 2.0)
.box(spec.boss_length_mm, spec.boss_width_mm, spec.boss_height_mm,
centered=(True, True, False))
)
fixture = fixture.union(boss)
# Article bolt holes (into the top of the boss)
abp = spec.article_bolt_pattern
top_z = spec.base_thickness_mm / 2.0 + spec.boss_height_mm
fixture = (
fixture.faces(">Z").workplane()
.rect(abp.spacing_x_mm, abp.spacing_y_mm, forConstruction=True)
.vertices()
.hole(abp.hole_dia_mm, depth=spec.boss_height_mm * 0.8)
)
# Optional stiffening ribs (two crossing ribs under the boss footprint)
if spec.add_ribs:
try:
rib_h = spec.boss_height_mm
rib_x = (
cq.Workplane("XY").workplane(offset=spec.base_thickness_mm / 2.0)
.box(spec.base_length_mm * 0.9, spec.rib_thickness_mm, rib_h,
centered=(True, True, False))
)
rib_y = (
cq.Workplane("XY").workplane(offset=spec.base_thickness_mm / 2.0)
.box(spec.rib_thickness_mm, spec.base_width_mm * 0.9, rib_h,
centered=(True, True, False))
)
fixture = fixture.union(rib_x).union(rib_y).union(boss)
# re-cut article holes after rib union (union can cover them)
fixture = (
fixture.faces(">Z").workplane()
.rect(abp.spacing_x_mm, abp.spacing_y_mm, forConstruction=True)
.vertices()
.hole(abp.hole_dia_mm, depth=spec.boss_height_mm * 0.8)
)
except Exception:
pass # ribs are a nicety; never fail the whole build over them
# Chamfer the outer top edges of the base plate for a finished look
if spec.edge_chamfer_mm and spec.edge_chamfer_mm > 0:
try:
fixture = fixture.edges("|Z").chamfer(spec.edge_chamfer_mm)
except Exception:
pass
return fixture
def export_fixture(spec: FixtureSpec, out_dir: str, basename: str = "fixture"
) -> dict:
"""
Build and export the fixture. Returns paths dict:
{"step": ..., "stl": ..., "svg": ..., "mass_kg": ...}
"""
os.makedirs(out_dir, exist_ok=True)
fixture = build_fixture(spec)
paths = {}
step_path = os.path.join(out_dir, f"{basename}.step")
stl_path = os.path.join(out_dir, f"{basename}.stl")
svg_path = os.path.join(out_dir, f"{basename}.svg")
exporters.export(fixture, step_path)
exporters.export(fixture, stl_path)
exporters.export(
fixture, svg_path,
opt={"width": 520, "height": 380, "showAxes": False,
"projectionDir": (1, 1, 1), "strokeWidth": 0.5},
)
paths["step"] = step_path
paths["stl"] = stl_path
paths["svg"] = svg_path
paths["mass_kg"] = spec.estimated_mass_kg()
return paths