Files
reducers/backend/tests/test_cad_integration.py
T

303 lines
14 KiB
Python

from __future__ import annotations
import importlib
from pathlib import Path
import pytest
from src.assembly_reader import read_assembly_snapshot
from src.cad.ocp_interference import detect_interferences
from src.cad.simplecad_generator import generate_simplecad
from src.parameter_solver import load_requirement, solve_parameters
from src.topology import load_template
from src.validators import build_validation_report
ROOT = Path(__file__).resolve().parents[1]
REQ = ROOT / "input" / "requirements" / "reducers" / "simple_2k_h" / "simple_2kh_ratio_7p5.json"
TEMPLATE = ROOT / "src" / "configurations" / "simple_2k_h" / "topology.template.json"
def _simplecad_or_skip():
scad = pytest.importorskip("simplecadapi")
required = ["add_gear_constraint_rassembly", "add_belt_constraint_rassembly"]
missing = [name for name in required if not hasattr(scad, name)]
if missing:
pytest.skip("SimpleCADAPI lacks required gear/belt assembly constraints: " + ", ".join(missing))
required_gear = [
"make_spur_gear_rsolid",
"make_spur_ring_gear_rsolid",
"make_helical_gear_rsolid",
"make_helical_ring_gear_rsolid",
]
missing_gear = [name for name in required_gear if not hasattr(scad.std.gear, name)]
if missing_gear:
pytest.skip("SimpleCADAPI lacks required gear APIs: " + ", ".join(missing_gear))
return scad
def test_simplecad_generator_creates_step_model_session(tmp_path: Path) -> None:
_simplecad_or_skip()
instance = solve_parameters(requirement=load_requirement(REQ), template=load_template(TEMPLATE))[0]
manifest = generate_simplecad(instance, tmp_path)
for key in ["step", "model", "session", "manifest", "build_meta"]:
path = Path(manifest.exported_files[key])
assert path.exists()
assert path.stat().st_size > 0
snapshot = read_assembly_snapshot(
model_path=Path(manifest.exported_files["model"]),
manifest_path=Path(manifest.exported_files["manifest"]),
build_meta_path=Path(manifest.exported_files["build_meta"]),
)
assert {"sun", "ring", "carrier", "housing"} <= set(snapshot.components_from_model)
assert len(snapshot.gear_constraints) == instance.parameters.planet_count
assert len(snapshot.internal_mesh_constraints) == instance.parameters.planet_count
report = build_validation_report(instance, snapshot)
assert any(check.code == "ocp_boolean_interference" for check in report.checks)
assert report.overall_passed
def test_generated_validation_uses_topology_relations(tmp_path: Path) -> None:
_simplecad_or_skip()
instance = solve_parameters(requirement=load_requirement(REQ), template=load_template(TEMPLATE))[0]
manifest = generate_simplecad(instance, tmp_path)
snapshot = read_assembly_snapshot(
model_path=Path(manifest.exported_files["model"]),
manifest_path=Path(manifest.exported_files["manifest"]),
build_meta_path=Path(manifest.exported_files["build_meta"]),
)
report = build_validation_report(instance, snapshot)
codes = [check.code for check in report.checks]
assert any(code.startswith("external_mesh.") for code in codes)
assert any(code.startswith("internal_mesh.") for code in codes)
assert any(code.startswith("revolute_joint.") for code in codes)
assert any(code.startswith("coaxial.") for code in codes)
assert report.overall_passed
def test_ocp_interference_validator_detects_overlap(tmp_path: Path) -> None:
_simplecad_or_skip()
instance = solve_parameters(requirement=load_requirement(REQ), template=load_template(TEMPLATE))[0]
manifest = generate_simplecad(instance, tmp_path)
sun_step = Path(manifest.components["sun"].assembled_step_path)
metrics, results = detect_interferences(
{"sun_a": sun_step, "sun_b": sun_step},
volume_tolerance_mm3=1e-6,
)
assert len(metrics) == 2
assert results[0].interfering
assert results[0].common_volume_mm3 > 0
@pytest.mark.parametrize(
"requirement_name",
[
"simple_2kh_boundary_a_spur.json",
"simple_2kh_boundary_b_spur.json",
"simple_2kh_boundary_c_spur.json",
],
)
def test_spur_boundary_variants_create_and_validate(
tmp_path: Path,
requirement_name: str,
) -> None:
_simplecad_or_skip()
requirement = load_requirement(ROOT / "input" / "requirements" / "reducers" / "simple_2k_h" / requirement_name)
instance = solve_parameters(requirement=requirement, template=load_template(TEMPLATE))[0]
manifest = generate_simplecad(instance, tmp_path)
snapshot = read_assembly_snapshot(
model_path=Path(manifest.exported_files["model"]),
manifest_path=Path(manifest.exported_files["manifest"]),
build_meta_path=Path(manifest.exported_files["build_meta"]),
)
report = build_validation_report(instance, snapshot)
assert report.overall_passed
def test_helical_generator_creates_real_outputs(tmp_path: Path) -> None:
_simplecad_or_skip()
requirement = load_requirement(ROOT / "input" / "requirements" / "reducers" / "simple_2k_h" / "simple_2kh_ratio_3_helical.json")
instance = solve_parameters(requirement=requirement, template=load_template(TEMPLATE))[0]
manifest = generate_simplecad(instance, tmp_path)
for key in ["step", "model", "session", "manifest", "build_meta"]:
path = Path(manifest.exported_files[key])
assert path.exists()
assert path.stat().st_size > 0
snapshot = read_assembly_snapshot(
model_path=Path(manifest.exported_files["model"]),
manifest_path=Path(manifest.exported_files["manifest"]),
build_meta_path=Path(manifest.exported_files["build_meta"]),
)
report = build_validation_report(instance, snapshot)
if not report.overall_passed:
assert any(
check.code == "ocp_boolean_interference" and not check.passed
for check in report.checks
)
def test_industrial_simple_2kh_spur_creates_complete_core_and_validates(tmp_path: Path) -> None:
_simplecad_or_skip()
requirement = load_requirement(ROOT / "input" / "requirements" / "reducers" / "simple_2k_h" / "simple_2kh_industrial_ratio_7p5_spur.json")
instance = solve_parameters(requirement=requirement, template=load_template(TEMPLATE))[0]
manifest = generate_simplecad(instance, tmp_path)
for key in ["step", "model", "session", "manifest", "build_meta", "industrial_parameters", "assembly_topology"]:
path = Path(manifest.exported_files[key])
assert path.exists()
assert path.stat().st_size > 0
snapshot = read_assembly_snapshot(
model_path=Path(manifest.exported_files["model"]),
manifest_path=Path(manifest.exported_files["manifest"]),
build_meta_path=Path(manifest.exported_files["build_meta"]),
)
expected = {
"carrier",
"carrier_front_plate",
"output_shaft",
"sun_input_shaft",
"sun_input_key",
"output_bearing_1",
"output_bearing_2",
"front_cover",
"rear_cover",
"seal_input",
"seal_output",
}
for index in range(1, instance.parameters.planet_count + 1):
expected.update({f"planet_pin_{index}", f"planet_bearing_{index}", f"planet_spacer_{index}"})
assert expected <= set(snapshot.components_from_model)
assert "input_bearing" not in set(snapshot.components_from_model)
assert manifest.components["planet_bearing_1"].details["bearing_type"] == "radial_ball_bearing"
assert manifest.components["planet_bearing_1"].details["ball_count"] >= 6
assert manifest.components["output_bearing_1"].details["bearing_type"] == "radial_ball_bearing"
report = build_validation_report(instance, snapshot)
assert any(check.code == "industrial_all_physical_components_exist" for check in report.checks)
assert any(check.code == "industrial_component_steps_exist" for check in report.checks)
check_codes = {check.code for check in report.checks}
for relation in instance.assembly_topology.relations:
assert f"industrial.{relation.relation_type}.{relation.relation_id}.component_exists" in check_codes
assert report.overall_passed
def test_industrial_simple_2kh_helical_creates_complete_core_and_validates(tmp_path: Path) -> None:
_simplecad_or_skip()
requirement = load_requirement(ROOT / "input" / "requirements" / "reducers" / "simple_2k_h" / "simple_2kh_industrial_ratio_7p5_helical.json")
instance = solve_parameters(requirement=requirement, template=load_template(TEMPLATE))[0]
manifest = generate_simplecad(instance, tmp_path)
for key in ["step", "model", "session", "manifest", "build_meta", "industrial_parameters", "assembly_topology"]:
path = Path(manifest.exported_files[key])
assert path.exists()
assert path.stat().st_size > 0
assert manifest.components["sun"].tooth_form == "helical"
assert manifest.components["sun"].helix_hand == "right"
assert manifest.components["planet_1"].helix_hand == "left"
assert manifest.components["ring"].helix_hand == "left"
snapshot = read_assembly_snapshot(
model_path=Path(manifest.exported_files["model"]),
manifest_path=Path(manifest.exported_files["manifest"]),
build_meta_path=Path(manifest.exported_files["build_meta"]),
)
report = build_validation_report(instance, snapshot)
codes = {check.code for check in report.checks}
assert any(code.startswith("gear_mesh.") for code in codes)
assert any(code.startswith("industrial.keyed.") for code in codes)
assert any(code.endswith(".internal_ring_half_tooth_phase") for code in codes)
assert "input_bearing" not in set(snapshot.components_from_model)
assert manifest.components["planet_bearing_1"].details["bearing_type"] == "radial_ball_bearing"
assert report.overall_passed
def test_cascade_spur_creates_and_validates(tmp_path: Path) -> None:
_simplecad_or_skip()
requirement = load_requirement(ROOT / "input" / "requirements" / "reducers" / "simple_2k_h_cascade" / "simple_2kh_cascade_ratio_9_spur.json")
template = load_template(ROOT / "src" / "configurations" / "simple_2k_h_cascade" / "topology.template.json")
instance = solve_parameters(requirement=requirement, template=template)[0]
manifest = generate_simplecad(instance, tmp_path)
interstage_components = {
"interstage_shaft",
"interstage_coupler",
"interstage_bearing_1",
"interstage_bearing_2",
"interstage_support_1",
"interstage_support_2",
}
assert interstage_components <= set(manifest.components)
assert manifest.components["interstage_bearing_1"].details["bearing_type"] == "radial_ball_bearing"
assert manifest.components["interstage_bearing_1"].details["ball_count"] >= 6
assert {
"s1_carrier_rigid_to_interstage_shaft",
"interstage_shaft_rigid_to_s2_sun",
"interstage_shaft_supported_by_housing",
} <= {constraint.constraint_id for constraint in manifest.constraints}
snapshot = read_assembly_snapshot(
model_path=Path(manifest.exported_files["model"]),
manifest_path=Path(manifest.exported_files["manifest"]),
build_meta_path=Path(manifest.exported_files["build_meta"]),
)
report = build_validation_report(instance, snapshot)
codes = {check.code: check for check in report.checks}
assert interstage_components <= set(snapshot.components_from_model)
assert "s1_carrier_rigid_to_s2_sun" not in snapshot.fixed_constraints
assert codes["cascade_interstage_components_exist"].passed
assert codes["cascade_interstage_constraints_exist"].passed
assert codes["cascade_topology_rigid_realized_by_physical_chain"].passed
assert codes["cascade_interstage_bearings_are_ball_bearings"].passed
assert codes["cascade_interstage_dimension_stack"].passed
assert codes["cascade_interstage_component_steps_exist"].passed
assert report.overall_passed
assert manifest.components["s1_sun"].stage_id == "s1"
assert manifest.components["s2_sun"].stage_id == "s2"
assert instance.derived.axial_stack["stage_offsets_mm"] == {"s1": 0.0, "s2": 10.0}
missing_shaft_snapshot = snapshot.model_copy(
update={
"components_from_model": [
component_id
for component_id in snapshot.components_from_model
if component_id != "interstage_shaft"
]
}
)
missing_shaft_report = build_validation_report(instance, missing_shaft_snapshot)
assert not missing_shaft_report.overall_passed
assert any(
check.code == "cascade_interstage_components_exist"
and not check.passed
and "interstage_shaft" in check.actual["missing_from_model"]
for check in missing_shaft_report.checks
)
def test_cascade_helical_creates_real_outputs(tmp_path: Path) -> None:
_simplecad_or_skip()
requirement = load_requirement(ROOT / "input" / "requirements" / "reducers" / "simple_2k_h_cascade" / "simple_2kh_cascade_ratio_9_helical.json")
template = load_template(ROOT / "src" / "configurations" / "simple_2k_h_cascade" / "topology.template.json")
instance = solve_parameters(requirement=requirement, template=template)[0]
manifest = generate_simplecad(instance, tmp_path)
for key in ["step", "model", "session", "manifest", "build_meta"]:
path = Path(manifest.exported_files[key])
assert path.exists()
assert path.stat().st_size > 0
snapshot = read_assembly_snapshot(
model_path=Path(manifest.exported_files["model"]),
manifest_path=Path(manifest.exported_files["manifest"]),
build_meta_path=Path(manifest.exported_files["build_meta"]),
)
report = build_validation_report(instance, snapshot)
if not report.overall_passed:
assert any(
check.code == "ocp_boolean_interference" and not check.passed
for check in report.checks
)