Spaces:
Sleeping
Sleeping
File size: 7,037 Bytes
52a2a94 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | """
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
|