Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| # --------------------------------------------------------------------------- # | |
| class BoltPattern: | |
| """A rectangular bolt-hole pattern (4 corners).""" | |
| spacing_x_mm: float | |
| spacing_y_mm: float | |
| hole_dia_mm: float | |
| 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 | |