Files
reducers/backend/tests/test_project_structure_and_placement.py
T

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