303 lines
14 KiB
Python
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
|
|
)
|