145 lines
7.3 KiB
Python
145 lines
7.3 KiB
Python
from __future__ import annotations
|
|
|
|
import math
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from src.cli import main
|
|
from src.configurations.registry import CONFIGURATIONS, load_configuration_template
|
|
from src.parameter_solver import load_requirement, solve_parameters
|
|
from src.placement import solve_reducer_placements
|
|
from src.topology import load_template
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SIMPLE_REQ = ROOT / "input" / "requirements" / "reducers" / "simple_2k_h" / "simple_2kh_ratio_7p5.json"
|
|
INDUSTRIAL_REQ = ROOT / "input" / "requirements" / "reducers" / "simple_2k_h" / "simple_2kh_industrial_ratio_7p5_spur.json"
|
|
CASCADE_REQ = ROOT / "input" / "requirements" / "reducers" / "simple_2k_h_cascade" / "simple_2kh_cascade_ratio_9_spur.json"
|
|
FERGUSON_WOLFROM_REQ = ROOT / "input" / "requirements" / "reducers" / "ferguson_wolfrom" / "ferguson_wolfrom_ratio_531p25_spur.json"
|
|
SIMPLE_TEMPLATE = ROOT / "src" / "configurations" / "simple_2k_h" / "topology.template.json"
|
|
CASCADE_TEMPLATE = ROOT / "src" / "configurations" / "simple_2k_h_cascade" / "topology.template.json"
|
|
FERGUSON_WOLFROM_TEMPLATE = ROOT / "src" / "configurations" / "ferguson_wolfrom" / "topology.template.json"
|
|
|
|
|
|
def test_configuration_registry_loads_all_families() -> None:
|
|
assert {"simple_2k_h", "simple_2k_h_cascade", "ferguson_wolfrom"} <= set(CONFIGURATIONS)
|
|
assert load_configuration_template("simple_2k_h").family == "simple_2k_h"
|
|
assert load_configuration_template("simple_2k_h_cascade").family == "simple_2k_h_cascade"
|
|
assert load_configuration_template("ferguson_wolfrom").family == "ferguson_wolfrom"
|
|
|
|
|
|
def test_all_requirements_live_under_input() -> None:
|
|
requirement_files = sorted((ROOT / "input" / "requirements").rglob("*.json"))
|
|
assert requirement_files
|
|
assert all("input/requirements" in path.as_posix() for path in requirement_files)
|
|
|
|
|
|
def test_cli_rejects_input_outside_input_dir(tmp_path: Path) -> None:
|
|
bad_input = tmp_path / "bad.json"
|
|
bad_input.write_text("{}", encoding="utf-8")
|
|
output = ROOT / "output" / "runs" / "bad_input_rejected"
|
|
with pytest.raises(ValueError, match="input_must_be_under_input"):
|
|
main(["solve", "--input", str(bad_input), "--output", str(output)])
|
|
|
|
|
|
def test_cli_rejects_output_outside_output_dir(tmp_path: Path) -> None:
|
|
with pytest.raises(ValueError, match="output_must_be_under_output"):
|
|
main(["solve", "--input", str(SIMPLE_REQ), "--output", str(tmp_path / "outside")])
|
|
|
|
|
|
def test_simple_2kh_placement_solver_planet_positions() -> None:
|
|
instance = solve_parameters(requirement=load_requirement(SIMPLE_REQ), template=load_template(SIMPLE_TEMPLATE))[0]
|
|
plan = solve_reducer_placements(instance)
|
|
for index in range(instance.parameters.planet_count):
|
|
pose = plan.require_pose(f"planet_{index + 1}")
|
|
radius = math.hypot(pose.center_xyz_mm[0], pose.center_xyz_mm[1])
|
|
assert abs(radius - instance.derived.planet_orbit_radius_mm) <= 1e-9
|
|
expected_angle = 360.0 * index / instance.parameters.planet_count
|
|
actual_angle = (math.degrees(math.atan2(pose.center_xyz_mm[1], pose.center_xyz_mm[0])) + 360.0) % 360.0
|
|
assert abs(((actual_angle - expected_angle + 180.0) % 360.0) - 180.0) <= 1e-9
|
|
|
|
|
|
def test_industrial_placement_solver_places_pin_bearing_spacer_coaxially() -> None:
|
|
instance = solve_parameters(requirement=load_requirement(INDUSTRIAL_REQ), template=load_template(SIMPLE_TEMPLATE))[0]
|
|
plan = solve_reducer_placements(instance)
|
|
for index in range(1, instance.parameters.planet_count + 1):
|
|
centers = [
|
|
plan.require_pose(f"planet_{index}").center_xyz_mm,
|
|
plan.require_pose(f"planet_pin_{index}").center_xyz_mm,
|
|
plan.require_pose(f"planet_bearing_{index}").center_xyz_mm,
|
|
plan.require_pose(f"planet_spacer_{index}").center_xyz_mm,
|
|
]
|
|
assert len({(round(center[0], 9), round(center[1], 9)) for center in centers}) == 1
|
|
|
|
|
|
def test_bolt_circle_positions_are_uniform() -> None:
|
|
instance = solve_parameters(requirement=load_requirement(INDUSTRIAL_REQ), template=load_template(SIMPLE_TEMPLATE))[0]
|
|
plan = solve_reducer_placements(instance)
|
|
params = instance.industrial_parameters
|
|
assert params is not None
|
|
radii = []
|
|
for index in range(1, params.bolt_count + 1):
|
|
pose = plan.require_pose(f"housing_bolt_{index}")
|
|
radii.append(math.hypot(pose.center_xyz_mm[0], pose.center_xyz_mm[1]))
|
|
assert max(radii) - min(radii) <= 1e-9
|
|
assert abs(radii[0] - params.bolt_circle_diameter_mm / 2.0) <= 1e-9
|
|
|
|
|
|
def test_cascade_placement_solver_stage_offsets() -> None:
|
|
instance = solve_parameters(requirement=load_requirement(CASCADE_REQ), template=load_template(CASCADE_TEMPLATE))[0]
|
|
plan = solve_reducer_placements(instance)
|
|
assert plan.frames["s1_frame"].translation_mm[2] == 0.0
|
|
assert plan.frames["s2_frame"].translation_mm[2] == instance.derived.axial_stack["stage_offsets_mm"]["s2"]
|
|
assert plan.require_pose("s2_sun").center_xyz_mm[2] > plan.require_pose("s1_sun").center_xyz_mm[2]
|
|
|
|
|
|
def test_ferguson_wolfrom_placement_solver_planet_sets_are_on_expected_orbits() -> None:
|
|
instance = solve_parameters(
|
|
requirement=load_requirement(FERGUSON_WOLFROM_REQ),
|
|
template=load_template(FERGUSON_WOLFROM_TEMPLATE),
|
|
)[0]
|
|
plan = solve_reducer_placements(instance)
|
|
planet1 = plan.require_pose("planet1_1")
|
|
planet2 = plan.require_pose("planet2_1")
|
|
teeth = instance.parameters.gear_tooth_counts
|
|
expected_orbit_1 = instance.parameters.module_mm * (teeth["sun1"] + teeth["planet1"]) / 2.0
|
|
expected_orbit_2 = instance.parameters.module_mm * (teeth["sun2"] + teeth["planet2"]) / 2.0
|
|
|
|
assert abs(math.hypot(planet1.center_xyz_mm[0], planet1.center_xyz_mm[1]) - expected_orbit_1) <= 1e-9
|
|
assert abs(math.hypot(planet2.center_xyz_mm[0], planet2.center_xyz_mm[1]) - expected_orbit_2) <= 1e-9
|
|
assert planet2.center_xyz_mm[2] > planet1.center_xyz_mm[2]
|
|
|
|
|
|
def test_ferguson_wolfrom_places_input_shaft_at_left_planet_pin() -> None:
|
|
instance = solve_parameters(
|
|
requirement=load_requirement(FERGUSON_WOLFROM_REQ),
|
|
template=load_template(FERGUSON_WOLFROM_TEMPLATE),
|
|
)[0]
|
|
plan = solve_reducer_placements(instance)
|
|
planet = plan.require_pose("planet1_1")
|
|
shaft = plan.require_pose("planet1_input_shaft")
|
|
static_carrier = plan.require_pose("static_carrier")
|
|
|
|
assert shaft.center_xyz_mm[:2] == planet.center_xyz_mm[:2]
|
|
assert shaft.center_xyz_mm[2] < planet.center_xyz_mm[2]
|
|
assert shaft.center_xyz_mm[2] < static_carrier.center_xyz_mm[2] + instance.parameters.face_width_mm
|
|
assert shaft.details["supported_by"] == "static_planet_pin_1"
|
|
|
|
|
|
def test_ferguson_wolfrom_output_carrier_is_phase_offset_60_degrees() -> None:
|
|
instance = solve_parameters(
|
|
requirement=load_requirement(FERGUSON_WOLFROM_REQ),
|
|
template=load_template(FERGUSON_WOLFROM_TEMPLATE),
|
|
)[0]
|
|
plan = solve_reducer_placements(instance)
|
|
planet1 = plan.require_pose("planet1_1")
|
|
planet2 = plan.require_pose("planet2_1")
|
|
output_carrier = plan.require_pose("output_carrier")
|
|
angle1 = math.degrees(math.atan2(planet1.center_xyz_mm[1], planet1.center_xyz_mm[0]))
|
|
angle2 = math.degrees(math.atan2(planet2.center_xyz_mm[1], planet2.center_xyz_mm[0]))
|
|
|
|
assert abs(angle1) <= 1e-9
|
|
assert abs(angle2 - 60.0) <= 1e-9
|
|
assert output_carrier.rotation_axis_angle_deg[3] == 60.0
|