Split project into frontend and backend
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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
|
||||
Reference in New Issue
Block a user