Files
reducers/backend/src/cad/simplecad_industrial_generator.py
T

1401 lines
60 KiB
Python

from __future__ import annotations
import json
import math
from pathlib import Path
from typing import Any
from ..industrial import bolt_xy_positions, compute_industrial_parameters, build_industrial_assembly_topology
from ..models import (
AssemblyManifest,
ComponentManifest,
ConstraintManifest,
IndustrialDesignParameters,
ReducerInstance,
sha256_json,
utc_now_iso,
write_json,
write_model_json,
)
from ..placement import PlacementPlan, solve_reducer_placements
from .gear_factory import gear_spec_for_component, make_external_gear
from .simplecad_assembly import add_forwarded_connector, cref, require_assembly_api
from .simplecad_generator import (
CadGenerationError,
_export_assembled_component_steps,
export_model_session_json,
import_simplecadapi,
)
from .simplecad_parts import (
add_axis_connector,
internal_ring_phase_correction_degrees,
make_axis_part,
make_materials,
make_planet,
make_planet_placement,
make_ring,
make_z_rotation_placement,
planet_center_xy,
role_for_component,
)
def _ring_solid(
scad: Any,
*,
outer_radius: float,
inner_radius: float,
height: float,
bottom_z: float,
) -> Any:
outer = scad.make_cylinder_rsolid(
radius=outer_radius,
height=height,
bottom_face_center=(0.0, 0.0, bottom_z),
axis=(0.0, 0.0, 1.0),
)
bore = scad.make_cylinder_rsolid(
radius=inner_radius,
height=height + 2.0,
bottom_face_center=(0.0, 0.0, bottom_z - 1.0),
axis=(0.0, 0.0, 1.0),
)
return scad.cut_rsolid(outer, bore, skip_non_intersecting=False)
def _tag(scad: Any, *, shape: Any, instance: ReducerInstance, component_id: str, formula_ref: str) -> Any:
tagged = scad.apply_tag(shape=shape, tag=f"formula.run_id.{instance.run_id}")
tagged = scad.apply_tag(shape=tagged, tag=f"formula.node.{component_id}")
tagged = scad.apply_tag(shape=tagged, tag=f"formula.ref.{formula_ref}")
return tagged
def _cut_bolt_holes(
scad: Any,
*,
shape: Any,
params: IndustrialDesignParameters,
bottom_z: float,
height: float,
) -> Any:
result = shape
hole_radius = params.bolt_diameter_mm / 2.0 + params.bearing_seat_clearance_mm
for x, y in bolt_xy_positions(params):
hole = scad.make_cylinder_rsolid(
radius=hole_radius,
height=height + 2.0,
bottom_face_center=(x, y, bottom_z - 1.0),
axis=(0.0, 0.0, 1.0),
)
result = scad.cut_rsolid(result, hole, skip_non_intersecting=False)
return result
def _carrier_plate_shape(
scad: Any,
*,
instance: ReducerInstance,
params: IndustrialDesignParameters,
thickness: float,
bottom_z: float,
) -> Any:
p = instance.parameters
pin_clearance_radius = params.planet_pin_diameter_mm / 2.0 + params.bearing_seat_clearance_mm
sun_bore_radius = max(0.8, min(2.2, instance.derived.sun_pitch_radius_mm * 0.35)) + 0.35
center_bore_radius = max(
sun_bore_radius,
params.output_bearing_outer_diameter_mm / 2.0 + params.bearing_seat_clearance_mm,
)
disk_radius = (
instance.derived.planet_orbit_radius_mm
+ params.planet_bearing_outer_diameter_mm / 2.0
+ params.radial_clearance_mm
)
plate = scad.make_cylinder_rsolid(
radius=disk_radius,
height=thickness,
bottom_face_center=(0.0, 0.0, bottom_z),
axis=(0.0, 0.0, 1.0),
)
center_bore = scad.make_cylinder_rsolid(
radius=center_bore_radius,
height=thickness + 2.0,
bottom_face_center=(0.0, 0.0, bottom_z - 1.0),
axis=(0.0, 0.0, 1.0),
)
plate = scad.cut_rsolid(plate, center_bore, skip_non_intersecting=False)
for index in range(p.planet_count):
x, y = planet_center_xy(instance, index)
pin_hole = scad.make_cylinder_rsolid(
radius=pin_clearance_radius,
height=thickness + 2.0,
bottom_face_center=(x, y, bottom_z - 1.0),
axis=(0.0, 0.0, 1.0),
)
plate = scad.cut_rsolid(plate, pin_hole, skip_non_intersecting=False)
return plate
def _sun_bore_radius(instance: ReducerInstance) -> float:
return max(0.8, min(2.2, instance.derived.sun_pitch_radius_mm * 0.35))
def make_industrial_sun(
scad: Any,
*,
instance: ReducerInstance,
params: IndustrialDesignParameters,
material: Any,
) -> Any:
p = instance.parameters
spec = gear_spec_for_component(instance, "sun")
body = make_external_gear(scad, spec)
bore_radius = _sun_bore_radius(instance)
bore = scad.make_cylinder_rsolid(
radius=bore_radius,
height=p.face_width_mm + 2.0,
bottom_face_center=(0.0, 0.0, -1.0),
axis=(0.0, 0.0, 1.0),
)
body = scad.cut_rsolid(body, bore, skip_non_intersecting=False)
slot_height = params.sun_key_height_mm + 2.0 * params.bearing_seat_clearance_mm
slot_width = params.sun_key_width_mm + 2.0 * params.bearing_seat_clearance_mm
keyway = scad.make_box_rsolid(
width=slot_height + 0.20,
height=slot_width,
depth=p.face_width_mm + 2.0,
bottom_face_center=(
bore_radius + slot_height / 2.0,
0.0,
-1.0,
),
)
body = scad.cut_rsolid(body, keyway, skip_non_intersecting=False)
body = _tag(scad, shape=body, instance=instance, component_id="sun", formula_ref="kinematic.sun.keyway")
return make_axis_part(
scad,
part_id="sun_gear",
body=body,
name=f"Input {p.tooth_form} sun gear with keyed bore {p.z_sun}t m={p.module_mm}",
material=material,
connector_specs=(("axis", (0.0, 0.0, p.face_width_mm / 2.0), "Sun keyed bore axis"),),
)
def make_input_key(
scad: Any,
*,
instance: ReducerInstance,
params: IndustrialDesignParameters,
material: Any,
) -> Any:
p = instance.parameters
shaft_radius = params.input_shaft_diameter_mm / 2.0
key_bottom_z = (p.face_width_mm - params.sun_key_length_mm) / 2.0
body = scad.make_box_rsolid(
width=params.sun_key_height_mm,
height=params.sun_key_width_mm,
depth=params.sun_key_length_mm,
bottom_face_center=(
shaft_radius + params.sun_key_height_mm / 2.0 + params.bearing_seat_clearance_mm,
0.0,
key_bottom_z,
),
)
body = _tag(scad, shape=body, instance=instance, component_id="sun_input_key", formula_ref="industrial.sun_input_key")
return make_axis_part(
scad,
part_id="sun_input_key",
body=body,
name="Input shaft parallel key for sun gear",
material=material,
connector_specs=(("axis", (0.0, 0.0, p.face_width_mm / 2.0), "Input key reference axis"),),
)
def make_industrial_carrier_body(
scad: Any,
*,
instance: ReducerInstance,
params: IndustrialDesignParameters,
material: Any,
) -> Any:
bottom_z = -params.axial_clearance_mm - params.carrier_plate_thickness_mm
body = _carrier_plate_shape(
scad,
instance=instance,
params=params,
thickness=params.carrier_plate_thickness_mm,
bottom_z=bottom_z,
)
body = _tag(scad, shape=body, instance=instance, component_id="carrier", formula_ref="industrial.carrier_body")
connector_specs = [
("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), "Carrier motion axis"),
("output_axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), "Carrier output axis"),
]
connector_specs.extend(
(
f"planet_{index + 1}_axis",
(*planet_center_xy(instance, index), instance.parameters.face_width_mm / 2.0),
f"Planet {index + 1} pin axis",
)
for index in range(instance.parameters.planet_count)
)
return make_axis_part(
scad,
part_id="carrier",
body=body,
name="Industrial carrier rear plate/body",
material=material,
connector_specs=tuple(connector_specs),
)
def make_carrier_front_plate(
scad: Any,
*,
instance: ReducerInstance,
params: IndustrialDesignParameters,
material: Any,
) -> Any:
bottom_z = instance.parameters.face_width_mm + params.axial_clearance_mm
body = _carrier_plate_shape(
scad,
instance=instance,
params=params,
thickness=params.carrier_front_plate_thickness_mm,
bottom_z=bottom_z,
)
body = _tag(scad, shape=body, instance=instance, component_id="carrier_front_plate", formula_ref="industrial.carrier_front_plate")
return make_axis_part(
scad,
part_id="carrier_front_plate",
body=body,
name="Industrial carrier front retention plate",
material=material,
connector_specs=(("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), "Carrier front plate axis"),),
)
def make_pin(
scad: Any,
*,
instance: ReducerInstance,
params: IndustrialDesignParameters,
index: int,
material: Any,
) -> Any:
bottom_z = -params.axial_clearance_mm - params.carrier_plate_thickness_mm + 0.05
top_z = instance.parameters.face_width_mm + params.axial_clearance_mm + params.carrier_front_plate_thickness_mm - 0.05
body = scad.make_cylinder_rsolid(
radius=params.planet_pin_diameter_mm / 2.0,
height=top_z - bottom_z,
bottom_face_center=(0.0, 0.0, bottom_z),
axis=(0.0, 0.0, 1.0),
)
component_id = f"planet_pin_{index}"
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=f"industrial.{component_id}")
return make_axis_part(
scad,
part_id=component_id,
body=body,
name=f"Planet pin {index}",
material=material,
connector_specs=(("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), f"Planet pin {index} axis"),),
)
def make_bearing_ring(
scad: Any,
*,
instance: ReducerInstance,
params: IndustrialDesignParameters,
component_id: str,
inner_diameter: float,
outer_diameter: float,
width: float,
center_z: float,
material: Any,
formula_ref: str,
) -> Any:
body = _ring_solid(
scad,
outer_radius=outer_diameter / 2.0,
inner_radius=inner_diameter / 2.0,
height=width,
bottom_z=center_z - width / 2.0,
)
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=formula_ref)
return make_axis_part(
scad,
part_id=component_id,
body=body,
name=component_id.replace("_", " ").title(),
material=material,
connector_specs=(("axis", (0.0, 0.0, center_z), f"{component_id} axis"),),
)
def make_ball_bearing_part(
scad: Any,
*,
instance: ReducerInstance,
params: IndustrialDesignParameters,
component_id: str,
bore_diameter: float,
outer_diameter: float,
width: float,
center_z: float,
ball_count: int,
material: Any,
formula_ref: str,
) -> Any:
bore_radius = bore_diameter / 2.0
outer_radius = outer_diameter / 2.0
ball_radius = params.bearing_ball_diameter_mm / 2.0
raceway_clearance = max(0.01, params.bearing_seat_clearance_mm * 0.3)
ball_pitch_radius = (bore_radius + outer_radius) / 2.0
inner_ring_outer_radius = ball_pitch_radius - ball_radius - raceway_clearance
outer_ring_inner_radius = ball_pitch_radius + ball_radius + raceway_clearance
if inner_ring_outer_radius <= bore_radius:
raise CadGenerationError(f"{component_id}: bearing balls leave no inner-ring wall thickness")
if outer_ring_inner_radius >= outer_radius:
raise CadGenerationError(f"{component_id}: bearing balls leave no outer-ring wall thickness")
chord_spacing = 2.0 * ball_pitch_radius * math.sin(math.pi / ball_count)
if chord_spacing <= params.bearing_ball_diameter_mm:
raise CadGenerationError(f"{component_id}: bearing ball_count too high for ball diameter")
if width <= params.bearing_ball_diameter_mm + 2.0 * raceway_clearance:
raise CadGenerationError(f"{component_id}: bearing width too small for rolling balls")
assembly = scad.make_assembly_rassembly(
assembly_id=f"{component_id}_radial_ball_bearing",
name=f"{component_id.replace('_', ' ').title()} radial ball bearing",
)
identity = scad.identity_placement_rplacement()
def bearing_axis_part(part_id: str, body: Any, name: str) -> Any:
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=f"{formula_ref}.{part_id}")
return make_axis_part(
scad,
part_id=part_id,
body=body,
name=name,
material=material,
connector_specs=(("axis", (0.0, 0.0, 0.0), f"{name} axis"),),
)
outer_ring_id = f"{component_id}_outer_ring"
inner_ring_id = f"{component_id}_inner_ring"
ball_part_id = f"{component_id}_ball"
cage_front_id = f"{component_id}_cage_front"
cage_rear_id = f"{component_id}_cage_rear"
outer_ring = _ring_solid(
scad,
outer_radius=outer_radius,
inner_radius=outer_ring_inner_radius,
height=width,
bottom_z=-width / 2.0,
)
inner_ring = _ring_solid(
scad,
outer_radius=inner_ring_outer_radius,
inner_radius=bore_radius,
height=width,
bottom_z=-width / 2.0,
)
cage_thickness = min(width * 0.10, max(0.08, params.bearing_ball_diameter_mm * 0.18))
cage_radius_half_width = max(0.04, ball_radius * 0.30)
cage_z_offset = ball_radius + raceway_clearance + cage_thickness / 2.0
cage_inner_radius = max(bore_radius, ball_pitch_radius - cage_radius_half_width)
cage_outer_radius = min(outer_radius, ball_pitch_radius + cage_radius_half_width)
if cage_z_offset + cage_thickness / 2.0 >= width / 2.0:
cage_z_offset = width / 2.0 - cage_thickness / 2.0 - raceway_clearance
cage_front = _ring_solid(
scad,
outer_radius=cage_outer_radius,
inner_radius=cage_inner_radius,
height=cage_thickness,
bottom_z=cage_z_offset - cage_thickness / 2.0,
)
cage_rear = _ring_solid(
scad,
outer_radius=cage_outer_radius,
inner_radius=cage_inner_radius,
height=cage_thickness,
bottom_z=-cage_z_offset - cage_thickness / 2.0,
)
outer_part = bearing_axis_part(outer_ring_id, outer_ring, f"{component_id} outer ring")
inner_part = bearing_axis_part(inner_ring_id, inner_ring, f"{component_id} inner ring")
cage_front_part = bearing_axis_part(cage_front_id, cage_front, f"{component_id} front cage ring")
cage_rear_part = bearing_axis_part(cage_rear_id, cage_rear, f"{component_id} rear cage ring")
ball = scad.make_sphere_rsolid(radius=ball_radius, center=(0.0, 0.0, 0.0))
ball = _tag(scad, shape=ball, instance=instance, component_id=component_id, formula_ref=f"{formula_ref}.{ball_part_id}")
ball_part = scad.make_part_rpart(part_id=ball_part_id, body=ball, name=f"{component_id} rolling ball")
ball_part = scad.assign_material_rpart(part=ball_part, material=material)
for subcomponent_id, item in [
(outer_ring_id, outer_part),
(inner_ring_id, inner_part),
(cage_front_id, cage_front_part),
(cage_rear_id, cage_rear_part),
]:
assembly = scad.add_component_rassembly(
assembly=assembly,
item=item,
component_id=subcomponent_id,
placement=identity,
name=subcomponent_id.replace("_", " ").title(),
)
ball_component_ids: list[str] = []
ball_angles: dict[str, float] = {}
digits = max(2, len(str(ball_count - 1)))
for index in range(ball_count):
angle_degrees = 360.0 * index / ball_count
angle = math.radians(angle_degrees)
ball_component_id = f"{component_id}_ball_{index:0{digits}d}"
ball_component_ids.append(ball_component_id)
ball_angles[ball_component_id] = angle_degrees
assembly = scad.add_component_rassembly(
assembly=assembly,
item=ball_part,
component_id=ball_component_id,
placement=scad.make_placement_rplacement(
origin=(
ball_pitch_radius * math.cos(angle),
ball_pitch_radius * math.sin(angle),
0.0,
),
x_axis=(math.cos(angle), math.sin(angle), 0.0),
y_axis=(-math.sin(angle), math.cos(angle), 0.0),
),
name=f"{component_id} ball {index}",
)
assembly = scad.add_revolute_constraint_rassembly(
assembly=assembly,
constraint_id=f"{component_id}_inner_outer_revolute",
connector_a=scad.make_connector_ref_rconnectorref(outer_ring_id, "axis"),
connector_b=scad.make_connector_ref_rconnectorref(inner_ring_id, "axis"),
drive_angle_degrees=None,
angle_limit=None,
name=f"{component_id} inner ring spins in outer ring",
)
assembly = add_forwarded_connector(
scad,
assembly=assembly,
connector_id="outer_axis",
source_component_id=outer_ring_id,
source_connector_id="axis",
name=f"{component_id} outer ring public axis",
)
assembly = add_forwarded_connector(
scad,
assembly=assembly,
connector_id="inner_axis",
source_component_id=inner_ring_id,
source_connector_id="axis",
name=f"{component_id} inner ring public axis",
)
assembly.set_metadata(
"planetary_reducer.ball_bearing",
{
"component_id": component_id,
"bore_diameter_mm": bore_diameter,
"outer_diameter_mm": outer_diameter,
"width_mm": width,
"ball_diameter_mm": params.bearing_ball_diameter_mm,
"ball_count": ball_count,
"ball_pitch_radius_mm": ball_pitch_radius,
"raceway_clearance_mm": raceway_clearance,
"outer_ring_component_id": outer_ring_id,
"inner_ring_component_id": inner_ring_id,
"cage_component_ids": [cage_front_id, cage_rear_id],
"ball_component_ids": ball_component_ids,
"ball_angles_degrees": ball_angles,
"local_center_z_mm": center_z,
},
)
return assembly
def make_spacer(
scad: Any,
*,
instance: ReducerInstance,
params: IndustrialDesignParameters,
index: int,
material: Any,
) -> Any:
center_z = -params.axial_clearance_mm / 2.0
component_id = f"planet_spacer_{index}"
body = _ring_solid(
scad,
outer_radius=params.planet_bearing_outer_diameter_mm / 2.0,
inner_radius=params.planet_pin_diameter_mm / 2.0 + params.bearing_seat_clearance_mm,
height=params.planet_spacer_thickness_mm,
bottom_z=center_z - params.planet_spacer_thickness_mm / 2.0,
)
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=f"industrial.{component_id}")
return make_axis_part(
scad,
part_id=component_id,
body=body,
name=f"Planet spacer/retainer {index}",
material=material,
connector_specs=(("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), f"Planet spacer {index} axis"),),
)
def make_shaft(
scad: Any,
*,
instance: ReducerInstance,
component_id: str,
radius: float,
bottom_z: float,
height: float,
material: Any,
formula_ref: str,
) -> Any:
body = scad.make_cylinder_rsolid(
radius=radius,
height=height,
bottom_face_center=(0.0, 0.0, bottom_z),
axis=(0.0, 0.0, 1.0),
)
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=formula_ref)
return make_axis_part(
scad,
part_id=component_id,
body=body,
name=component_id.replace("_", " ").title(),
material=material,
connector_specs=(("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), f"{component_id} axis"),),
)
def make_housing_shell(
scad: Any,
*,
instance: ReducerInstance,
params: IndustrialDesignParameters,
material: Any,
) -> Any:
bottom_z = -params.carrier_plate_thickness_mm - params.cover_thickness_mm - 2.0 * params.axial_clearance_mm
top_z = instance.parameters.face_width_mm + params.carrier_front_plate_thickness_mm + params.cover_thickness_mm + 2.0 * params.axial_clearance_mm
body = _ring_solid(
scad,
outer_radius=params.housing_outer_diameter_mm / 2.0,
inner_radius=params.housing_inner_diameter_mm / 2.0,
height=top_z - bottom_z,
bottom_z=bottom_z,
)
body = _cut_bolt_holes(scad, shape=body, params=params, bottom_z=bottom_z, height=top_z - bottom_z)
body = _tag(scad, shape=body, instance=instance, component_id="housing", formula_ref="industrial.housing_body")
connector_specs = [
("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), "Housing axis"),
("front_cover_axis", (0.0, 0.0, top_z + params.cover_thickness_mm / 2.0), "Front cover axis"),
("rear_cover_axis", (0.0, 0.0, bottom_z - params.cover_thickness_mm / 2.0), "Rear cover axis"),
("ring_mount_axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), "Ring mount axis"),
("output_bearing_1_axis", (0.0, 0.0, bottom_z + params.cover_thickness_mm * 0.75), "Output bearing 1 axis"),
("output_bearing_2_axis", (0.0, 0.0, bottom_z + params.cover_thickness_mm * 1.75), "Output bearing 2 axis"),
("seal_input_axis", (0.0, 0.0, top_z + params.cover_thickness_mm * 0.75), "Input seal axis"),
("seal_output_axis", (0.0, 0.0, bottom_z - params.cover_thickness_mm * 0.75), "Output seal axis"),
]
connector_specs.extend(
(
f"bolt_{index + 1}_axis",
(x, y, instance.parameters.face_width_mm / 2.0),
f"Housing bolt {index + 1} axis",
)
for index, (x, y) in enumerate(bolt_xy_positions(params))
)
return make_axis_part(
scad,
part_id="housing",
body=body,
name="Industrial housing shell with bolt holes",
material=material,
connector_specs=tuple(connector_specs),
)
def make_cover(
scad: Any,
*,
instance: ReducerInstance,
params: IndustrialDesignParameters,
component_id: str,
center_z: float,
shaft_clearance_radius: float,
material: Any,
) -> Any:
body = _ring_solid(
scad,
outer_radius=params.housing_outer_diameter_mm / 2.0,
inner_radius=shaft_clearance_radius,
height=params.cover_thickness_mm,
bottom_z=center_z - params.cover_thickness_mm / 2.0,
)
body = _cut_bolt_holes(
scad,
shape=body,
params=params,
bottom_z=center_z - params.cover_thickness_mm / 2.0,
height=params.cover_thickness_mm,
)
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=f"industrial.{component_id}")
return make_axis_part(
scad,
part_id=component_id,
body=body,
name=component_id.replace("_", " ").title(),
material=material,
connector_specs=(("axis", (0.0, 0.0, center_z), f"{component_id} axis"),),
)
def make_bolt(
scad: Any,
*,
instance: ReducerInstance,
params: IndustrialDesignParameters,
index: int,
material: Any,
) -> Any:
component_id = f"housing_bolt_{index}"
height = params.cover_thickness_mm * 2.0
body = scad.make_cylinder_rsolid(
radius=params.bolt_diameter_mm / 2.0,
height=height,
bottom_face_center=(0.0, 0.0, -height / 2.0),
axis=(0.0, 0.0, 1.0),
)
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=f"industrial.{component_id}")
return make_axis_part(
scad,
part_id=component_id,
body=body,
name=f"Housing bolt {index}",
material=material,
connector_specs=(("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), f"Housing bolt {index} axis"),),
)
def make_industrial_parts(scad: Any, *, instance: ReducerInstance) -> dict[str, Any]:
if instance.industrial_parameters is None:
raise CadGenerationError("industrial_core_missing_industrial_parameters")
params = instance.industrial_parameters
p = instance.parameters
materials = make_materials(scad)
shaft_material = materials["gear"]
bearing_material = materials["ring"]
output_shaft_extension = max(12.0, p.face_width_mm * 1.50)
parts: dict[str, Any] = {
"sun": make_industrial_sun(scad, instance=instance, params=params, material=materials["gear"]),
"planet": make_planet(scad, instance=instance, material=materials["gear"]),
"ring": make_ring(scad, instance=instance, material=materials["ring"]),
"carrier": make_industrial_carrier_body(scad, instance=instance, params=params, material=materials["carrier"]),
"carrier_front_plate": make_carrier_front_plate(scad, instance=instance, params=params, material=materials["carrier"]),
"housing": make_housing_shell(scad, instance=instance, params=params, material=materials["housing"]),
"sun_input_key": make_input_key(scad, instance=instance, params=params, material=shaft_material),
"sun_input_shaft": make_shaft(
scad,
instance=instance,
component_id="sun_input_shaft",
radius=params.input_shaft_diameter_mm / 2.0,
bottom_z=-params.axial_clearance_mm,
height=(
p.face_width_mm
+ params.carrier_front_plate_thickness_mm
+ 3.0 * params.axial_clearance_mm
+ 3.0 * params.cover_thickness_mm
),
material=shaft_material,
formula_ref="industrial.sun_input_shaft",
),
"output_shaft": make_shaft(
scad,
instance=instance,
component_id="output_shaft",
radius=params.output_shaft_diameter_mm / 2.0,
bottom_z=(
-params.carrier_plate_thickness_mm
- 3.0 * params.cover_thickness_mm
- 2.0 * params.axial_clearance_mm
- output_shaft_extension
),
height=(
params.carrier_plate_thickness_mm
+ 3.0 * params.cover_thickness_mm
+ params.axial_clearance_mm
+ output_shaft_extension
),
material=shaft_material,
formula_ref="industrial.output_shaft",
),
"output_bearing_1": make_ball_bearing_part(
scad,
instance=instance,
params=params,
component_id="output_bearing_1",
bore_diameter=params.output_bearing_inner_diameter_mm,
outer_diameter=params.output_bearing_outer_diameter_mm,
width=max(0.8, params.cover_thickness_mm * 0.7),
center_z=-params.carrier_plate_thickness_mm - params.axial_clearance_mm - params.cover_thickness_mm * 0.35,
ball_count=params.output_bearing_ball_count,
material=bearing_material,
formula_ref="industrial.output_bearing_1",
),
"output_bearing_2": make_ball_bearing_part(
scad,
instance=instance,
params=params,
component_id="output_bearing_2",
bore_diameter=params.output_bearing_inner_diameter_mm,
outer_diameter=params.output_bearing_outer_diameter_mm,
width=max(0.8, params.cover_thickness_mm * 0.7),
center_z=-params.carrier_plate_thickness_mm - params.axial_clearance_mm - params.cover_thickness_mm * 1.35,
ball_count=params.output_bearing_ball_count,
material=bearing_material,
formula_ref="industrial.output_bearing_2",
),
"ring_mount": make_bearing_ring(
scad,
instance=instance,
params=params,
component_id="ring_mount",
inner_diameter=instance.derived.ring_outer_diameter_mm + 2.0 * params.bearing_seat_clearance_mm,
outer_diameter=params.housing_inner_diameter_mm - 2.0 * params.bearing_seat_clearance_mm,
width=max(0.8, p.face_width_mm * 0.18),
center_z=p.face_width_mm / 2.0,
material=materials["housing"],
formula_ref="industrial.ring_mount",
),
"seal_input": make_bearing_ring(
scad,
instance=instance,
params=params,
component_id="seal_input",
inner_diameter=params.input_shaft_diameter_mm + 2.0 * params.bearing_seat_clearance_mm,
outer_diameter=params.input_shaft_diameter_mm + 2.0 * max(params.radial_clearance_mm, params.bearing_seat_clearance_mm * 2.0),
width=max(0.5, p.module_mm),
center_z=p.face_width_mm + params.carrier_front_plate_thickness_mm + params.cover_thickness_mm + 2.0 * params.axial_clearance_mm,
material=materials["housing"],
formula_ref="industrial.seal_input",
),
"seal_output": make_bearing_ring(
scad,
instance=instance,
params=params,
component_id="seal_output",
inner_diameter=params.output_shaft_diameter_mm + 2.0 * params.bearing_seat_clearance_mm,
outer_diameter=params.output_bearing_outer_diameter_mm,
width=max(0.5, p.module_mm),
center_z=-params.carrier_plate_thickness_mm - params.cover_thickness_mm - 2.0 * params.axial_clearance_mm,
material=materials["housing"],
formula_ref="industrial.seal_output",
),
}
housing_bottom = -params.carrier_plate_thickness_mm - params.cover_thickness_mm - 2.0 * params.axial_clearance_mm
housing_top = p.face_width_mm + params.carrier_front_plate_thickness_mm + params.cover_thickness_mm + 2.0 * params.axial_clearance_mm
parts["front_cover"] = make_cover(
scad,
instance=instance,
params=params,
component_id="front_cover",
center_z=housing_top + params.cover_thickness_mm / 2.0,
shaft_clearance_radius=params.input_shaft_diameter_mm / 2.0 + params.radial_clearance_mm,
material=materials["housing"],
)
parts["rear_cover"] = make_cover(
scad,
instance=instance,
params=params,
component_id="rear_cover",
center_z=housing_bottom - params.cover_thickness_mm / 2.0,
shaft_clearance_radius=params.output_bearing_outer_diameter_mm / 2.0 + params.bearing_seat_clearance_mm,
material=materials["housing"],
)
for index in range(1, p.planet_count + 1):
parts[f"planet_pin_{index}"] = make_pin(scad, instance=instance, params=params, index=index, material=shaft_material)
parts[f"planet_bearing_{index}"] = make_ball_bearing_part(
scad,
instance=instance,
params=params,
component_id=f"planet_bearing_{index}",
bore_diameter=params.planet_bearing_inner_diameter_mm,
outer_diameter=params.planet_bearing_outer_diameter_mm,
width=params.planet_bearing_width_mm,
center_z=p.face_width_mm / 2.0,
ball_count=params.planet_bearing_ball_count,
material=bearing_material,
formula_ref=f"industrial.planet_bearing_{index}",
)
parts[f"planet_spacer_{index}"] = make_spacer(scad, instance=instance, params=params, index=index, material=materials["carrier"])
for index in range(1, params.bolt_count + 1):
parts[f"housing_bolt_{index}"] = make_bolt(scad, instance=instance, params=params, index=index, material=shaft_material)
return parts
def _add_component(
scad: Any,
*,
assembly: Any,
parts: dict[str, Any],
component_id: str,
placement: Any,
name: str,
) -> Any:
return scad.add_component_rassembly(
assembly=assembly,
item=parts[component_id],
component_id=component_id,
placement=placement,
name=name,
)
def build_industrial_assembly(scad: Any, *, instance: ReducerInstance, parts: dict[str, Any]) -> Any:
require_assembly_api(scad)
p = instance.parameters
d = instance.derived
params = instance.industrial_parameters
if params is None:
raise CadGenerationError("industrial_core_missing_industrial_parameters")
fixed_member = instance.boundary.fixed
if fixed_member is None:
raise CadGenerationError("industrial_core_requires_fixed_member")
placement_plan = solve_reducer_placements(instance)
def placement_for(component_id: str) -> Any:
pose = placement_plan.require_pose(component_id)
return make_z_rotation_placement(
scad,
origin=pose.translation_mm,
angle_degrees=pose.rotation_axis_angle_deg[3],
)
assembly = scad.make_assembly_rassembly(
assembly_id=f"{instance.run_id}_simple_2kh_industrial_{p.tooth_form}_reducer",
name=f"Formula-generated industrial-core simple 2K-H {p.tooth_form} planetary reducer",
)
identity = scad.identity_placement_rplacement()
for component_id in [
"housing",
"ring",
"sun",
"carrier",
"carrier_front_plate",
"sun_input_shaft",
"sun_input_key",
"output_shaft",
"front_cover",
"rear_cover",
"ring_mount",
"seal_input",
"seal_output",
]:
assembly = _add_component(
scad,
assembly=assembly,
parts=parts,
component_id=component_id,
placement=placement_for(component_id),
name=component_id.replace("_", " ").title(),
)
for component_id in ["output_bearing_1", "output_bearing_2"]:
assembly = _add_component(
scad,
assembly=assembly,
parts=parts,
component_id=component_id,
placement=placement_for(component_id),
name=component_id.replace("_", " ").title(),
)
for index in range(p.planet_count):
planet_component = f"planet_{index + 1}"
assembly = scad.add_component_rassembly(
assembly=assembly,
item=parts["planet"],
component_id=planet_component,
placement=placement_for(planet_component),
name=f"Planet gear {index + 1}",
)
for local in ["pin", "spacer"]:
component_id = f"planet_{local}_{index + 1}"
assembly = _add_component(
scad,
assembly=assembly,
parts=parts,
component_id=component_id,
placement=placement_for(component_id),
name=component_id.replace("_", " ").title(),
)
bearing_id = f"planet_bearing_{index + 1}"
assembly = _add_component(
scad,
assembly=assembly,
parts=parts,
component_id=bearing_id,
placement=placement_for(bearing_id),
name=bearing_id.replace("_", " ").title(),
)
for index in range(1, params.bolt_count + 1):
assembly = _add_component(
scad,
assembly=assembly,
parts=parts,
component_id=f"housing_bolt_{index}",
placement=placement_for(f"housing_bolt_{index}"),
name=f"Housing bolt {index}",
)
assembly = scad.ground_component_rassembly(assembly=assembly, component_id=fixed_member)
assembly = scad.add_fixed_constraint_rassembly(
assembly=assembly,
constraint_id=f"housing_fixed_to_{fixed_member}",
connector_a=cref(scad, component_id=fixed_member, connector_id="axis"),
connector_b=cref(scad, component_id="housing", connector_id="axis"),
name=f"Housing fixed to {fixed_member}",
)
for member in ("sun", "ring", "carrier"):
if member == fixed_member:
continue
role = role_for_component(instance, member)
assembly = scad.add_revolute_constraint_rassembly(
assembly=assembly,
constraint_id=f"{member}_{role}_revolute",
connector_a=cref(scad, component_id=fixed_member, connector_id="axis"),
connector_b=cref(scad, component_id=member, connector_id="axis"),
drive_angle_degrees=0.0 if role == "input" else None,
angle_limit=None,
name=f"{member.title()} {role} revolute",
)
assembly = scad.add_fixed_constraint_rassembly(
assembly=assembly,
constraint_id="sun_input_shaft_keyed_to_sun",
connector_a=cref(scad, component_id="sun", connector_id="axis"),
connector_b=cref(scad, component_id="sun_input_shaft", connector_id="axis"),
name="Sun input shaft keyed to sun gear",
)
assembly = scad.add_fixed_constraint_rassembly(
assembly=assembly,
constraint_id="sun_input_key_fixed_to_sun_input_shaft",
connector_a=cref(scad, component_id="sun_input_shaft", connector_id="axis"),
connector_b=cref(scad, component_id="sun_input_key", connector_id="axis"),
name="Input key fixed to sun input shaft",
)
for constraint_id, component_id, housing_connector in [
("carrier_front_plate_rigid_to_carrier", "carrier_front_plate", "axis"),
("carrier_body_rigid_to_output_shaft", "output_shaft", "axis"),
("output_bearing_1_fixed_to_housing", "output_bearing_1", "output_bearing_1_axis"),
("output_bearing_2_fixed_to_housing", "output_bearing_2", "output_bearing_2_axis"),
("front_cover_bolted_to_housing", "front_cover", "front_cover_axis"),
("rear_cover_bolted_to_housing", "rear_cover", "rear_cover_axis"),
("ring_mount_bolted_to_housing", "ring_mount", "ring_mount_axis"),
("seal_input_fixed_to_housing", "seal_input", "seal_input_axis"),
("seal_output_fixed_to_housing", "seal_output", "seal_output_axis"),
]:
base_component = "carrier" if component_id in {"carrier_front_plate", "output_shaft"} else "housing"
base_connector = "axis" if base_component == "carrier" else housing_connector
child_connector = "outer_axis" if component_id.startswith("output_bearing_") else "axis"
assembly = scad.add_fixed_constraint_rassembly(
assembly=assembly,
constraint_id=constraint_id,
connector_a=cref(scad, component_id=base_component, connector_id=base_connector),
connector_b=cref(scad, component_id=component_id, connector_id=child_connector),
name=constraint_id.replace("_", " ").title(),
)
for index in range(p.planet_count):
planet_id = f"planet_{index + 1}"
pin_id = f"planet_pin_{index + 1}"
bearing_id = f"planet_bearing_{index + 1}"
spacer_id = f"planet_spacer_{index + 1}"
carrier_connector = f"planet_{index + 1}_axis"
assembly = scad.add_revolute_constraint_rassembly(
assembly=assembly,
constraint_id=f"{planet_id}_pin_revolute",
connector_a=cref(scad, component_id="carrier", connector_id=carrier_connector),
connector_b=cref(scad, component_id=planet_id, connector_id="axis"),
drive_angle_degrees=None,
angle_limit=None,
name=f"Planet {index + 1} bearing revolute",
)
assembly = scad.add_fixed_constraint_rassembly(
assembly=assembly,
constraint_id=f"{pin_id}_press_fit_to_carrier",
connector_a=cref(scad, component_id="carrier", connector_id=carrier_connector),
connector_b=cref(scad, component_id=pin_id, connector_id="axis"),
name=f"Planet pin {index + 1} press fit to carrier",
)
for child_id, label in [(bearing_id, "bearing"), (spacer_id, "spacer")]:
child_connector = "inner_axis" if child_id == bearing_id else "axis"
assembly = scad.add_fixed_constraint_rassembly(
assembly=assembly,
constraint_id=f"{child_id}_fixed_to_{pin_id}",
connector_a=cref(scad, component_id=pin_id, connector_id="axis"),
connector_b=cref(scad, component_id=child_id, connector_id=child_connector),
name=f"Planet {index + 1} {label} fixed to pin reference",
)
assembly = scad.add_gear_constraint_rassembly(
assembly=assembly,
constraint_id=f"sun_to_{planet_id}_external_mesh",
connector_a=cref(scad, component_id="sun", connector_id="axis"),
connector_b=cref(scad, component_id=planet_id, connector_id="axis"),
pitch_radius_a=d.sun_pitch_radius_mm,
pitch_radius_b=d.planet_pitch_radius_mm,
phase_offset=None,
name=f"Sun external mesh to planet {index + 1}",
)
assembly = scad.add_belt_constraint_rassembly(
assembly=assembly,
constraint_id=f"ring_to_{planet_id}_internal_mesh",
connector_a=cref(scad, component_id="ring", connector_id="axis"),
connector_b=cref(scad, component_id=planet_id, connector_id="axis"),
pulley_radius_a=d.ring_pitch_radius_mm,
pulley_radius_b=d.planet_pitch_radius_mm,
phase_offset=None,
name=f"Ring internal mesh to planet {index + 1}",
)
for index in range(1, params.bolt_count + 1):
assembly = scad.add_fixed_constraint_rassembly(
assembly=assembly,
constraint_id=f"housing_bolt_{index}_fixed_to_housing",
connector_a=cref(scad, component_id="housing", connector_id=f"bolt_{index}_axis"),
connector_b=cref(scad, component_id=f"housing_bolt_{index}", connector_id="axis"),
name=f"Housing bolt {index} fixed to housing",
)
return scad.solve_assembly_constraints_rassembly(assembly=assembly, strict=True)
def _component_center(instance: ReducerInstance, component_id: str) -> tuple[float, float, float]:
return solve_reducer_placements(instance).require_pose(component_id).center_xyz_mm
def _industrial_component_manifests(
instance: ReducerInstance,
*,
placement_plan: PlacementPlan,
component_step_paths: dict[str, str] | None = None,
) -> dict[str, ComponentManifest]:
if instance.assembly_topology is None or instance.industrial_parameters is None:
raise CadGenerationError("industrial_core_missing_assembly_topology")
p = instance.parameters
d = instance.derived
params = instance.industrial_parameters
component_step_paths = component_step_paths or {}
sun_spec = gear_spec_for_component(instance, "sun")
planet_spec = gear_spec_for_component(instance, "planet")
ring_spec = gear_spec_for_component(instance, "ring")
physical_by_id = {component.component_id: component for component in instance.assembly_topology.components}
components: dict[str, ComponentManifest] = {}
for component_id, physical in physical_by_id.items():
gear_type = None
tooth_count = None
pitch_radius = None
tooth_form = None
helix_angle = None
helix_hand = None
module = None
pressure_angle = None
face_width = None
if component_id == "sun":
gear_type = "external"
tooth_count = p.z_sun
pitch_radius = d.sun_pitch_radius_mm
spec = sun_spec
elif component_id == "ring":
gear_type = "internal"
tooth_count = p.z_ring
pitch_radius = d.ring_pitch_radius_mm
spec = ring_spec
elif component_id.startswith("planet_") and component_id.split("_")[-1].isdigit() and physical.kind == "gear":
gear_type = "external"
tooth_count = p.z_planet
pitch_radius = d.planet_pitch_radius_mm
spec = planet_spec
else:
spec = None
if spec is not None:
tooth_form = p.tooth_form
helix_angle = spec.helix_angle_deg
helix_hand = spec.helix_hand
module = p.module_mm
pressure_angle = p.pressure_angle_deg
face_width = p.face_width_mm
role = role_for_component(instance, component_id) if component_id in {"sun", "ring", "carrier"} else physical.role
tags = [
f"formula.run_id.{instance.run_id}",
f"formula.node.{component_id}",
f"formula.ref.{physical.formula_ref}",
]
if role in {"input", "fixed", "output"}:
tags.append(f"formula.role.{role}")
details: dict[str, Any] = {}
if physical.kind == "bearing":
if component_id.startswith("planet_bearing_"):
details = {
"bearing_type": "radial_ball_bearing",
"standard_series": "parametric_deep_groove",
"inner_diameter_mm": params.planet_bearing_inner_diameter_mm,
"outer_diameter_mm": params.planet_bearing_outer_diameter_mm,
"width_mm": params.planet_bearing_width_mm,
"ball_diameter_mm": params.bearing_ball_diameter_mm,
"ball_count": params.planet_bearing_ball_count,
}
elif component_id.startswith("output_bearing_"):
details = {
"bearing_type": "radial_ball_bearing",
"standard_series": "parametric_deep_groove",
"inner_diameter_mm": params.output_bearing_inner_diameter_mm,
"outer_diameter_mm": params.output_bearing_outer_diameter_mm,
"width_mm": max(0.8, params.cover_thickness_mm * 0.7),
"ball_diameter_mm": params.bearing_ball_diameter_mm,
"ball_count": params.output_bearing_ball_count,
}
elif component_id == "sun_input_key":
details = {
"coupling_type": "parallel_key",
"width_mm": params.sun_key_width_mm,
"height_mm": params.sun_key_height_mm,
"length_mm": params.sun_key_length_mm,
"mates_with": ["sun_keyway", "sun_input_shaft_keyseat"],
}
components[component_id] = ComponentManifest(
component_id=component_id,
formula_node=physical.formula_ref,
role=role,
kind=physical.kind,
gear_type=gear_type,
tooth_count=tooth_count,
tooth_form=tooth_form,
helix_angle_deg=helix_angle,
helix_hand=helix_hand,
module_mm=module,
pressure_angle_deg=pressure_angle,
face_width_mm=face_width,
pitch_radius_mm=pitch_radius,
center_xyz_mm=placement_plan.require_pose(component_id).center_xyz_mm,
axis_xyz=placement_plan.require_pose(component_id).axis_xyz,
tags=tags,
assembled_step_path=component_step_paths.get(component_id),
details=details,
)
return components
def _industrial_constraint_manifests(instance: ReducerInstance) -> list[ConstraintManifest]:
p = instance.parameters
params = instance.industrial_parameters
if params is None or instance.assembly_topology is None:
raise CadGenerationError("industrial_core_missing_assembly_topology")
fixed_member = instance.boundary.fixed
if fixed_member is None:
raise CadGenerationError("industrial_core_requires_fixed_member")
constraints: list[ConstraintManifest] = [
ConstraintManifest(
constraint_id=f"housing_fixed_to_{fixed_member}",
relation_type="fixed",
component_a=fixed_member,
component_b="housing",
source="simplecadapi_constraint",
cad_constraint_kind="fixed",
)
]
for member in ("sun", "ring", "carrier"):
if member == fixed_member:
continue
role = role_for_component(instance, member)
constraints.append(
ConstraintManifest(
constraint_id=f"{member}_{role}_revolute",
relation_type="revolute_joint",
component_a=fixed_member,
component_b=member,
source="simplecadapi_constraint",
cad_constraint_kind="revolute",
)
)
constraints.extend(
[
ConstraintManifest(constraint_id="sun_input_shaft_keyed_to_sun", relation_type="keyed", component_a="sun_input_shaft", component_b="sun", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
ConstraintManifest(constraint_id="sun_input_key_fixed_to_sun_input_shaft", relation_type="rigid", component_a="sun_input_key", component_b="sun_input_shaft", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
ConstraintManifest(constraint_id="carrier_body_rigid_to_output_shaft", relation_type="rigid", component_a="carrier", component_b="output_shaft", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
ConstraintManifest(constraint_id="carrier_front_plate_rigid_to_carrier", relation_type="rigid", component_a="carrier_front_plate", component_b="carrier", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
ConstraintManifest(constraint_id="output_bearing_1_fixed_to_housing", relation_type="bearing_support", component_a="output_bearing_1", component_b="housing", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
ConstraintManifest(constraint_id="output_bearing_2_fixed_to_housing", relation_type="bearing_support", component_a="output_bearing_2", component_b="housing", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
ConstraintManifest(constraint_id="front_cover_bolted_to_housing", relation_type="bolted", component_a="front_cover", component_b="housing", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
ConstraintManifest(constraint_id="rear_cover_bolted_to_housing", relation_type="bolted", component_a="rear_cover", component_b="housing", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
ConstraintManifest(constraint_id="ring_mount_bolted_to_housing", relation_type="bolted", component_a="ring_mount", component_b="housing", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
ConstraintManifest(constraint_id="seal_input_fixed_to_housing", relation_type="coaxial", component_a="seal_input", component_b="sun_input_shaft", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
ConstraintManifest(constraint_id="seal_output_fixed_to_housing", relation_type="coaxial", component_a="seal_output", component_b="output_shaft", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
]
)
for index in range(1, p.planet_count + 1):
planet_id = f"planet_{index}"
pin_id = f"planet_pin_{index}"
bearing_id = f"planet_bearing_{index}"
spacer_id = f"planet_spacer_{index}"
constraints.extend(
[
ConstraintManifest(constraint_id=f"{planet_id}_pin_revolute", relation_type="revolute_joint", component_a="carrier", component_b=planet_id, source="simplecadapi_constraint", cad_constraint_kind="revolute"),
ConstraintManifest(constraint_id=f"{pin_id}_press_fit_to_carrier", relation_type="press_fit", component_a=pin_id, component_b="carrier", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
ConstraintManifest(constraint_id=f"{bearing_id}_fixed_to_{pin_id}", relation_type="slip_fit", component_a=bearing_id, component_b=pin_id, source="simplecadapi_constraint", cad_constraint_kind="fixed"),
ConstraintManifest(constraint_id=f"{spacer_id}_fixed_to_{pin_id}", relation_type="axial_retention", component_a=spacer_id, component_b=pin_id, source="simplecadapi_constraint", cad_constraint_kind="fixed"),
ConstraintManifest(constraint_id=f"sun_to_{planet_id}_external_mesh", relation_type="external_mesh", component_a="sun", component_b=planet_id, source="simplecadapi_constraint", cad_constraint_kind="gear"),
ConstraintManifest(constraint_id=f"ring_to_{planet_id}_internal_mesh", relation_type="internal_mesh", component_a="ring", component_b=planet_id, source="simplecadapi_constraint", cad_constraint_kind="belt"),
]
)
for relation in instance.assembly_topology.relations:
if not any(existing.constraint_id == relation.relation_id for existing in constraints):
constraints.append(
ConstraintManifest(
constraint_id=relation.relation_id,
relation_type=relation.relation_type,
component_a=relation.source_component_id,
component_b=relation.target_component_id,
source="formula_semantic",
cad_constraint_kind=None,
relation_id=relation.relation_id,
)
)
for index in range(1, params.bolt_count + 1):
constraints.append(
ConstraintManifest(
constraint_id=f"housing_bolt_{index}_fixed_to_housing",
relation_type="bolted",
component_a=f"housing_bolt_{index}",
component_b="housing",
source="simplecadapi_constraint",
cad_constraint_kind="fixed",
)
)
return constraints
def generate_simplecad_industrial(instance: ReducerInstance, out_dir: Path) -> AssemblyManifest:
if instance.topology_family != "simple_2k_h":
raise CadGenerationError("industrial_core supports only simple_2k_h in this implementation")
if instance.parameters.tooth_form not in {"spur", "helical"}:
raise CadGenerationError(f"unsupported_tooth_form: {instance.parameters.tooth_form}")
if instance.industrial_parameters is None:
instance = instance.model_copy(
update={
"industrial_parameters": compute_industrial_parameters(
parameters=instance.parameters,
derived=instance.derived,
)
}
)
if instance.assembly_topology is None:
instance = instance.model_copy(update={"assembly_topology": build_industrial_assembly_topology(instance)})
scad = import_simplecadapi()
out_dir.mkdir(parents=True, exist_ok=True)
step_path = out_dir / "reducer.step"
model_path = out_dir / "reducer.model.json"
session_path = out_dir / "reducer.session.json"
manifest_path = out_dir / "assembly_manifest.json"
build_meta_path = out_dir / "build_meta.json"
industrial_parameters_path = out_dir / "industrial_parameters.json"
assembly_topology_path = out_dir / "assembly_topology.json"
placement_plan_path = out_dir / "placement_plan.json"
placement_plan = solve_reducer_placements(instance)
graph_id = f"{instance.run_id}_simple_2kh_industrial_{instance.parameters.tooth_form}_generation"
with scad.GraphSession(graph_id=graph_id) as session:
parts = make_industrial_parts(scad, instance=instance)
assembly = build_industrial_assembly(scad, instance=instance, parts=parts)
preview = scad.make_compound_from_assembly_rcompound(assembly=assembly)
component_step_paths = _export_assembled_component_steps(scad, assembly=assembly, out_dir=out_dir)
model_json, session_json = export_model_session_json(scad, session=session)
model_path.write_text(model_json, encoding="utf-8")
session_path.write_text(session_json, encoding="utf-8")
scad.export_step(shapes=preview, filename=str(step_path))
write_model_json(industrial_parameters_path, instance.industrial_parameters)
write_model_json(assembly_topology_path, instance.assembly_topology)
write_model_json(placement_plan_path, placement_plan)
model_payload = json.loads(model_json)
preview_solids = len(preview.get_solids())
preview_volume = float(preview.get_volume())
exported_files = {
"step": str(step_path.resolve()),
"model": str(model_path.resolve()),
"session": str(session_path.resolve()),
"manifest": str(manifest_path.resolve()),
"build_meta": str(build_meta_path.resolve()),
"component_steps_dir": str((out_dir / "component_steps").resolve()),
"industrial_parameters": str(industrial_parameters_path.resolve()),
"assembly_topology": str(assembly_topology_path.resolve()),
"placement_plan": str(placement_plan_path.resolve()),
}
manifest = AssemblyManifest(
run_id=instance.run_id,
formula_instance_hash=sha256_json(instance.model_dump(mode="json")),
generated_at=utc_now_iso(),
generator=f"simplecadapi_{instance.parameters.tooth_form}_simple_2kh_industrial_core_v0.2.0",
components=_industrial_component_manifests(instance, placement_plan=placement_plan, component_step_paths=component_step_paths),
constraints=_industrial_constraint_manifests(instance),
exported_files=exported_files,
geometry_summary={
"preview_solids": preview_solids,
"preview_volume": preview_volume,
"model_graph_nodes": len(model_payload.get("graph", {}).get("nodes", [])),
"assembled_component_step_count": len(component_step_paths),
"physical_component_count": len(instance.assembly_topology.components if instance.assembly_topology else []),
"physical_relation_count": len(instance.assembly_topology.relations if instance.assembly_topology else []),
"detail_level": instance.detail_level,
"tooth_form": instance.parameters.tooth_form,
"input_coupling": "sun_input_shaft_keyed_to_sun",
"bearing_representation": "radial_ball_bearing_with_inner_ring_outer_ring_balls_and_cage",
"internal_ring_phase_correction_deg": internal_ring_phase_correction_degrees(
z_planet=instance.parameters.z_planet,
z_ring=instance.parameters.z_ring,
),
"planet_phase_formula": "180 - 180 / z_planet + planet_center_angle_deg * (1 + z_sun / z_planet)",
"placement_solver": placement_plan.solver_version,
},
)
write_model_json(manifest_path, manifest)
build_meta = {
"run_id": instance.run_id,
"formula_instance_hash": manifest.formula_instance_hash,
"generator": manifest.generator,
"simplecadapi_file": getattr(scad, "__file__", "unknown"),
"graph_id": graph_id,
"detail_level": instance.detail_level,
"tooth_form": instance.parameters.tooth_form,
"input_coupling": "sun_input_shaft_keyed_to_sun",
"bearing_representation": "radial_ball_bearing_with_inner_ring_outer_ring_balls_and_cage",
"internal_ring_phase_correction_deg": manifest.geometry_summary["internal_ring_phase_correction_deg"],
"planet_phase_formula": "180 - 180 / z_planet + planet_center_angle_deg * (1 + z_sun / z_planet)",
"boundary": instance.boundary.model_dump(mode="json"),
"industrial_parameters": instance.industrial_parameters.model_dump(mode="json") if instance.industrial_parameters else None,
"assembly_topology_hash": sha256_json(instance.assembly_topology.model_dump(mode="json")) if instance.assembly_topology else None,
"placement_plan_hash": sha256_json(placement_plan.model_dump(mode="json")),
"assembly_id": getattr(assembly, "assembly_id", None),
"component_ids": assembly.component_ids(),
"constraint_ids": assembly.constraint_ids(),
"preview_solids": preview_solids,
"preview_volume": preview_volume,
"model_graph_nodes": manifest.geometry_summary["model_graph_nodes"],
"exported_files": exported_files,
"file_mtime_ns": {
"step": step_path.stat().st_mtime_ns,
"model": model_path.stat().st_mtime_ns,
"session": session_path.stat().st_mtime_ns,
"manifest": manifest_path.stat().st_mtime_ns,
"placement_plan": placement_plan_path.stat().st_mtime_ns,
},
}
write_json(build_meta_path, build_meta)
return manifest