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 )