585 lines
25 KiB
Python
585 lines
25 KiB
Python
from __future__ import annotations
|
|
|
|
import math
|
|
from pathlib import Path
|
|
|
|
from src.models import (
|
|
AssemblyManifest,
|
|
AssemblySnapshot,
|
|
ComponentManifest,
|
|
ConstraintManifest,
|
|
PhysicalRelation,
|
|
ReducerInstance,
|
|
sha256_json,
|
|
)
|
|
from src.cad.gear_factory import gear_spec_for_component
|
|
from src.cad.simplecad_generator import _component_manifests
|
|
from src.cad.simplecad_parts import role_for_component
|
|
from src.parameter_solver import load_requirement, solve_parameters
|
|
from src.placement import solve_reducer_placements
|
|
from src.physical_validators import PHYSICAL_VALIDATOR_REGISTRY
|
|
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 _instance() -> ReducerInstance:
|
|
return solve_parameters(requirement=load_requirement(REQ), template=load_template(TEMPLATE))[0]
|
|
|
|
|
|
def _snapshot(
|
|
instance: ReducerInstance,
|
|
*,
|
|
missing_component: bool = False,
|
|
missing_industrial_component: bool = False,
|
|
missing_physical_component: str | None = None,
|
|
missing_external_mesh: bool = False,
|
|
bad_helical_external_hand: bool = False,
|
|
planet_phase_offset_deg: float = 0.0,
|
|
) -> AssemblySnapshot:
|
|
p = instance.parameters
|
|
d = instance.derived
|
|
z_mid = p.face_width_mm / 2.0
|
|
component_ids = ["housing", "ring", "sun", "carrier"] + [
|
|
f"planet_{index + 1}" for index in range(p.planet_count)
|
|
]
|
|
components = {}
|
|
for cid in component_ids:
|
|
is_planet = cid.startswith("planet_")
|
|
if cid == "sun":
|
|
gear_type = "external"
|
|
tooth_count = p.z_sun
|
|
pitch_radius = d.sun_pitch_radius_mm
|
|
spec = gear_spec_for_component(instance, "sun")
|
|
elif cid == "ring":
|
|
gear_type = "internal"
|
|
tooth_count = p.z_ring
|
|
pitch_radius = d.ring_pitch_radius_mm
|
|
spec = gear_spec_for_component(instance, "ring")
|
|
elif is_planet:
|
|
gear_type = "external"
|
|
tooth_count = p.z_planet
|
|
pitch_radius = d.planet_pitch_radius_mm
|
|
spec = gear_spec_for_component(instance, "planet")
|
|
else:
|
|
gear_type = None
|
|
tooth_count = None
|
|
pitch_radius = None
|
|
spec = None
|
|
helix_hand = spec.helix_hand if spec else None
|
|
if bad_helical_external_hand and is_planet:
|
|
helix_hand = gear_spec_for_component(instance, "sun").helix_hand
|
|
components[cid] = ComponentManifest(
|
|
component_id=cid,
|
|
formula_node="planet" if is_planet else cid,
|
|
role="planet" if is_planet else role_for_component(instance, cid),
|
|
kind="gear" if cid in {"sun", "ring"} or is_planet else cid,
|
|
gear_type=gear_type,
|
|
tooth_count=tooth_count,
|
|
tooth_form=p.tooth_form if gear_type else None,
|
|
helix_angle_deg=spec.helix_angle_deg if spec else None,
|
|
helix_hand=helix_hand,
|
|
module_mm=p.module_mm if gear_type else None,
|
|
pressure_angle_deg=p.pressure_angle_deg if gear_type else None,
|
|
face_width_mm=p.face_width_mm if gear_type else None,
|
|
pitch_radius_mm=pitch_radius,
|
|
center_xyz_mm=(0.0, 0.0, z_mid),
|
|
tags=[f"formula.run_id.{instance.run_id}"],
|
|
)
|
|
fixed_member = instance.boundary.fixed
|
|
constraints = [
|
|
ConstraintManifest(
|
|
constraint_id=f"housing_fixed_to_{fixed_member}",
|
|
relation_type="fixed",
|
|
component_a=fixed_member,
|
|
component_b="housing",
|
|
source="simplecadapi_constraint",
|
|
cad_constraint_kind="fixed",
|
|
)
|
|
]
|
|
for member in ["sun", "ring", "carrier"]:
|
|
if member == fixed_member:
|
|
continue
|
|
constraints.append(
|
|
ConstraintManifest(
|
|
constraint_id=f"{member}_{role_for_component(instance, member)}_revolute",
|
|
relation_type="revolute_joint",
|
|
component_a=fixed_member,
|
|
component_b=member,
|
|
source="simplecadapi_constraint",
|
|
cad_constraint_kind="revolute",
|
|
)
|
|
)
|
|
identity_axis = {
|
|
"x_axis": [1.0, 0.0, 0.0],
|
|
"y_axis": [0.0, 1.0, 0.0],
|
|
"z_axis": [0.0, 0.0, 1.0],
|
|
}
|
|
placements = {
|
|
"housing": {"origin": [0.0, 0.0, 0.0], **identity_axis},
|
|
"ring": {"origin": [0.0, 0.0, 0.0], **identity_axis},
|
|
"sun": {"origin": [0.0, 0.0, 0.0], **identity_axis},
|
|
"carrier": {"origin": [0.0, 0.0, 0.0], **identity_axis},
|
|
}
|
|
for index in range(p.planet_count):
|
|
cid = f"planet_{index + 1}"
|
|
angle = 2.0 * math.pi * index / p.planet_count
|
|
phase = math.radians(
|
|
180.0
|
|
- 180.0 / p.z_planet
|
|
+ math.degrees(angle) * (1.0 + p.z_sun / p.z_planet)
|
|
+ (planet_phase_offset_deg if index == 0 else 0.0)
|
|
)
|
|
placements[cid] = {
|
|
"origin": [
|
|
instance.derived.planet_orbit_radius_mm * math.cos(angle),
|
|
instance.derived.planet_orbit_radius_mm * math.sin(angle),
|
|
0.0,
|
|
],
|
|
"x_axis": [math.cos(phase), math.sin(phase), 0.0],
|
|
"y_axis": [-math.sin(phase), math.cos(phase), 0.0],
|
|
"z_axis": [0.0, 0.0, 1.0],
|
|
}
|
|
constraints.extend(
|
|
[
|
|
ConstraintManifest(
|
|
constraint_id=f"{cid}_pin_revolute",
|
|
relation_type="revolute_joint",
|
|
component_a="carrier",
|
|
component_b=cid,
|
|
source="simplecadapi_constraint",
|
|
cad_constraint_kind="revolute",
|
|
),
|
|
ConstraintManifest(
|
|
constraint_id=f"sun_to_{cid}_external_mesh",
|
|
relation_type="external_mesh",
|
|
component_a="sun",
|
|
component_b=cid,
|
|
source="simplecadapi_constraint",
|
|
cad_constraint_kind="gear",
|
|
),
|
|
ConstraintManifest(
|
|
constraint_id=f"ring_to_{cid}_internal_mesh",
|
|
relation_type="internal_mesh",
|
|
component_a="ring",
|
|
component_b=cid,
|
|
source="simplecadapi_constraint",
|
|
cad_constraint_kind="belt",
|
|
),
|
|
]
|
|
)
|
|
if instance.assembly_topology is not None:
|
|
for physical in instance.assembly_topology.components:
|
|
if physical.component_id in components:
|
|
continue
|
|
center = (0.0, 0.0, z_mid)
|
|
if physical.component_id.startswith(("planet_pin_", "planet_bearing_", "planet_spacer_")):
|
|
index = int(physical.component_id.rsplit("_", 1)[1]) - 1
|
|
angle = 2.0 * math.pi * index / p.planet_count
|
|
center = (
|
|
instance.derived.planet_orbit_radius_mm * math.cos(angle),
|
|
instance.derived.planet_orbit_radius_mm * math.sin(angle),
|
|
z_mid,
|
|
)
|
|
elif physical.component_id == "sun_input_key" and instance.industrial_parameters:
|
|
center = (
|
|
instance.industrial_parameters.input_shaft_diameter_mm / 2.0
|
|
+ instance.industrial_parameters.sun_key_height_mm / 2.0
|
|
+ instance.industrial_parameters.bearing_seat_clearance_mm,
|
|
0.0,
|
|
z_mid,
|
|
)
|
|
details = {}
|
|
if physical.kind == "bearing" and instance.industrial_parameters:
|
|
params = instance.industrial_parameters
|
|
if physical.component_id.startswith("planet_bearing_"):
|
|
details = {
|
|
"bearing_type": "radial_ball_bearing",
|
|
"ball_count": params.planet_bearing_ball_count,
|
|
"ball_diameter_mm": params.bearing_ball_diameter_mm,
|
|
}
|
|
elif physical.component_id.startswith("output_bearing_"):
|
|
details = {
|
|
"bearing_type": "radial_ball_bearing",
|
|
"ball_count": params.output_bearing_ball_count,
|
|
"ball_diameter_mm": params.bearing_ball_diameter_mm,
|
|
}
|
|
elif physical.component_id == "sun_input_key" and instance.industrial_parameters:
|
|
details = {
|
|
"coupling_type": "parallel_key",
|
|
"width_mm": instance.industrial_parameters.sun_key_width_mm,
|
|
"height_mm": instance.industrial_parameters.sun_key_height_mm,
|
|
"length_mm": instance.industrial_parameters.sun_key_length_mm,
|
|
}
|
|
components[physical.component_id] = ComponentManifest(
|
|
component_id=physical.component_id,
|
|
formula_node=physical.formula_ref,
|
|
role=physical.role,
|
|
kind=physical.kind,
|
|
center_xyz_mm=center,
|
|
tags=[f"formula.run_id.{instance.run_id}", f"formula.ref.{physical.formula_ref}"],
|
|
details=details,
|
|
)
|
|
placements[physical.component_id] = {"origin": list(center), **identity_axis}
|
|
for relation in instance.assembly_topology.relations:
|
|
constraints.append(
|
|
ConstraintManifest(
|
|
constraint_id=relation.relation_id,
|
|
relation_type=relation.relation_type,
|
|
component_a=relation.source_component_id,
|
|
component_b=relation.target_component_id,
|
|
source="formula_semantic",
|
|
relation_id=relation.relation_id,
|
|
)
|
|
)
|
|
if missing_external_mesh:
|
|
constraints = [
|
|
constraint
|
|
for constraint in constraints
|
|
if constraint.constraint_id != "sun_to_planet_1_external_mesh"
|
|
]
|
|
all_component_ids = list(components)
|
|
found_components = component_ids[1:] if missing_component else all_component_ids
|
|
if missing_industrial_component:
|
|
found_components = [cid for cid in found_components if cid != "planet_bearing_1"]
|
|
if missing_physical_component is not None:
|
|
found_components = [cid for cid in found_components if cid != missing_physical_component]
|
|
manifest = AssemblyManifest(
|
|
run_id=instance.run_id,
|
|
formula_instance_hash=sha256_json(instance.model_dump(mode="json")),
|
|
generated_at="test",
|
|
generator="test",
|
|
components=components,
|
|
constraints=constraints,
|
|
exported_files={},
|
|
geometry_summary={
|
|
"preview_volume": 1.0,
|
|
"preview_solids": 1,
|
|
"model_graph_nodes": 1,
|
|
"internal_ring_phase_correction_deg": 180.0 / p.z_ring if p.z_planet % 2 else 0.0,
|
|
"planet_phase_formula": "180 - 180 / z_planet + planet_center_angle_deg * (1 + z_sun / z_planet)",
|
|
},
|
|
)
|
|
return AssemblySnapshot(
|
|
run_id=instance.run_id,
|
|
formula_instance_hash=manifest.formula_instance_hash,
|
|
model_path="/tmp/test.model.json",
|
|
manifest_path="/tmp/manifest.json",
|
|
build_meta_path=None,
|
|
components_from_model=found_components,
|
|
constraints_from_model=[],
|
|
component_placements=placements,
|
|
grounded_components=[fixed_member] if fixed_member else [],
|
|
gear_constraints=[
|
|
f"sun_to_planet_{index + 1}_external_mesh" for index in range(p.planet_count)
|
|
if not (missing_external_mesh and index == 0)
|
|
],
|
|
internal_mesh_constraints=[
|
|
f"ring_to_planet_{index + 1}_internal_mesh" for index in range(p.planet_count)
|
|
],
|
|
revolute_constraints=[
|
|
constraint.constraint_id
|
|
for constraint in constraints
|
|
if constraint.relation_type == "revolute_joint"
|
|
],
|
|
fixed_constraints=[f"housing_fixed_to_{fixed_member}"],
|
|
manifest=manifest,
|
|
build_meta={"run_id": instance.run_id, "preview_volume": 1.0},
|
|
model_graph_node_count=1,
|
|
exported_files={},
|
|
read_at="test",
|
|
)
|
|
|
|
|
|
def test_validation_report_fails_on_missing_component() -> None:
|
|
instance = _instance()
|
|
report = build_validation_report(instance, _snapshot(instance, missing_component=True))
|
|
assert not report.overall_passed
|
|
assert any(check.code == "all_formula_components_exist" and not check.passed for check in report.checks)
|
|
|
|
|
|
def test_relation_validators_generate_expected_checks() -> None:
|
|
instance = _instance()
|
|
report = build_validation_report(instance, _snapshot(instance))
|
|
relation_checks = [
|
|
check
|
|
for check in report.checks
|
|
if check.code.startswith(("external_mesh.", "internal_mesh.", "revolute_joint.", "fixed.", "coaxial."))
|
|
]
|
|
assert relation_checks
|
|
assert all(check.passed for check in relation_checks)
|
|
|
|
|
|
def test_validation_fails_when_topology_relation_missing_in_snapshot() -> None:
|
|
instance = _instance()
|
|
report = build_validation_report(instance, _snapshot(instance, missing_external_mesh=True))
|
|
assert not report.overall_passed
|
|
assert any(
|
|
check.code.startswith("external_mesh.")
|
|
and check.code.endswith(".cad_constraint")
|
|
and not check.passed
|
|
for check in report.checks
|
|
)
|
|
|
|
|
|
def test_validation_report_fails_on_wrong_ratio() -> None:
|
|
instance = _instance()
|
|
bad = instance.model_copy(
|
|
update={"derived": instance.derived.model_copy(update={"ratio": instance.derived.ratio + 1.0})}
|
|
)
|
|
report = build_validation_report(bad, _snapshot(instance))
|
|
assert not report.overall_passed
|
|
assert any(check.code == "ratio_matches_instance" and not check.passed for check in report.checks)
|
|
|
|
|
|
def test_manifest_roles_follow_boundary() -> None:
|
|
requirement = load_requirement(ROOT / "input" / "requirements" / "reducers" / "simple_2k_h" / "simple_2kh_boundary_a_spur.json")
|
|
instance = solve_parameters(requirement=requirement, template=load_template(TEMPLATE))[0]
|
|
components = _component_manifests(instance, placement_plan=solve_reducer_placements(instance))
|
|
assert components["ring"].role == "input"
|
|
assert components["sun"].role == "fixed"
|
|
assert components["carrier"].role == "output"
|
|
assert "formula.role.input" in components["ring"].tags
|
|
assert "formula.role.fixed" in components["sun"].tags
|
|
assert "formula.role.output" in components["carrier"].tags
|
|
|
|
|
|
def test_industrial_validation_requires_complete_carrier_assembly() -> None:
|
|
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]
|
|
report = build_validation_report(instance, _snapshot(instance))
|
|
codes = {check.code: check for check in report.checks}
|
|
|
|
assert codes["industrial_parameters_present"].passed
|
|
assert codes["assembly_topology_present"].passed
|
|
assert codes["industrial_required_components_declared"].passed
|
|
assert codes["industrial_planet_bearing_relation_count"].passed
|
|
assert codes["industrial_required_relations_declared"].passed
|
|
assert any(code.startswith("industrial.bearing_revolute.") for code in codes)
|
|
assert any(code.startswith("industrial.keyed.") for code in codes)
|
|
assert any(code.startswith("industrial.press_fit.") for code in codes)
|
|
assert any(code.startswith("industrial.bolted.") for code in codes)
|
|
|
|
|
|
def test_physical_validator_registry_contains_required_types() -> None:
|
|
assert {
|
|
"bearing_revolute",
|
|
"bearing_support",
|
|
"bolted",
|
|
"press_fit",
|
|
"slip_fit",
|
|
"axial_retention",
|
|
"clearance",
|
|
"rigid",
|
|
"coaxial",
|
|
"keyed",
|
|
} <= set(PHYSICAL_VALIDATOR_REGISTRY)
|
|
|
|
|
|
def test_industrial_relations_generate_per_relation_checks() -> None:
|
|
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]
|
|
report = build_validation_report(instance, _snapshot(instance))
|
|
relation_ids = {relation.relation_id for relation in instance.assembly_topology.relations}
|
|
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 relation_ids
|
|
|
|
|
|
def test_industrial_validation_fails_when_planet_bearing_missing() -> None:
|
|
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]
|
|
report = build_validation_report(instance, _snapshot(instance, missing_industrial_component=True))
|
|
|
|
assert any(
|
|
check.code == "industrial_all_physical_components_exist"
|
|
and not check.passed
|
|
and "planet_bearing_1" in check.actual["missing_from_model"]
|
|
for check in report.checks
|
|
)
|
|
|
|
|
|
def test_missing_planet_pin_fails_bearing_revolute() -> None:
|
|
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]
|
|
report = build_validation_report(instance, _snapshot(instance, missing_physical_component="planet_pin_1"))
|
|
|
|
assert any(
|
|
check.code == "industrial.bearing_revolute.planet_1_bearing_revolute_on_planet_pin_1.component_exists"
|
|
and not check.passed
|
|
and "planet_pin_1" in check.actual
|
|
for check in report.checks
|
|
)
|
|
|
|
|
|
def test_missing_output_shaft_fails_rigid_output_relation() -> None:
|
|
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]
|
|
report = build_validation_report(instance, _snapshot(instance, missing_physical_component="output_shaft"))
|
|
|
|
assert any(
|
|
check.code == "industrial.rigid.carrier_body_rigid_to_output_shaft.component_exists"
|
|
and not check.passed
|
|
and "output_shaft" in check.actual
|
|
for check in report.checks
|
|
)
|
|
|
|
|
|
def test_missing_sun_input_key_fails_keyed_relation() -> None:
|
|
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]
|
|
report = build_validation_report(instance, _snapshot(instance, missing_physical_component="sun_input_key"))
|
|
|
|
assert any(
|
|
check.code == "industrial.keyed.sun_input_shaft_keyed_to_sun.component_exists"
|
|
and not check.passed
|
|
and "sun_input_key" in check.actual
|
|
for check in report.checks
|
|
)
|
|
|
|
|
|
def test_missing_ring_mount_relation_fails_static() -> None:
|
|
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]
|
|
topology = instance.assembly_topology.model_copy(
|
|
update={
|
|
"relations": [
|
|
relation
|
|
for relation in instance.assembly_topology.relations
|
|
if relation.relation_id != "ring_bolted_to_housing"
|
|
]
|
|
}
|
|
)
|
|
bad = instance.model_copy(update={"assembly_topology": topology})
|
|
report = build_validation_report(bad, _snapshot(bad))
|
|
|
|
assert any(
|
|
check.code == "industrial_required_relations_declared"
|
|
and not check.passed
|
|
and "ring_bolted_to_housing" in check.expected
|
|
for check in report.checks
|
|
)
|
|
|
|
|
|
def test_bad_bearing_diameter_order_fails_slip_fit() -> None:
|
|
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]
|
|
bad_params = instance.industrial_parameters.model_copy(
|
|
update={"planet_bearing_outer_diameter_mm": 999.0}
|
|
)
|
|
bad = instance.model_copy(update={"industrial_parameters": bad_params})
|
|
report = build_validation_report(bad, _snapshot(bad))
|
|
|
|
assert any(
|
|
check.code == "industrial.slip_fit.planet_bearing_1_slip_fit_in_planet_1.geometry_clearance_or_fit"
|
|
and not check.passed
|
|
for check in report.checks
|
|
)
|
|
|
|
|
|
def test_bad_pin_diameter_fails_press_fit_or_bearing_revolute() -> None:
|
|
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]
|
|
bad_params = instance.industrial_parameters.model_copy(
|
|
update={"planet_pin_diameter_mm": 999.0}
|
|
)
|
|
bad = instance.model_copy(update={"industrial_parameters": bad_params})
|
|
report = build_validation_report(bad, _snapshot(bad))
|
|
|
|
assert any(
|
|
check.code.endswith(".geometry_clearance_or_fit")
|
|
and check.code.startswith(
|
|
(
|
|
"industrial.press_fit.planet_pin_1",
|
|
"industrial.bearing_revolute.planet_1",
|
|
)
|
|
)
|
|
and not check.passed
|
|
for check in report.checks
|
|
)
|
|
|
|
|
|
def test_unknown_physical_relation_type_fails_cleanly() -> None:
|
|
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]
|
|
topology = instance.assembly_topology.model_copy(
|
|
update={
|
|
"relations": instance.assembly_topology.relations
|
|
+ [
|
|
PhysicalRelation(
|
|
relation_id="mystery_relation",
|
|
relation_type="mystery_relation",
|
|
source_component_id="sun",
|
|
target_component_id="housing",
|
|
formula_ref="test.unknown",
|
|
)
|
|
]
|
|
}
|
|
)
|
|
bad = instance.model_copy(update={"assembly_topology": topology})
|
|
report = build_validation_report(bad, _snapshot(bad))
|
|
|
|
assert any(
|
|
check.code == "unsupported_physical_relation_type.mystery_relation"
|
|
and not check.passed
|
|
for check in report.checks
|
|
)
|
|
|
|
|
|
def test_helical_mesh_hand_rules() -> None:
|
|
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]
|
|
report = build_validation_report(instance, _snapshot(instance))
|
|
mesh_checks = [
|
|
check
|
|
for check in report.checks
|
|
if check.code.startswith(("external_mesh.", "internal_mesh."))
|
|
and check.code.endswith(".gear_compatibility")
|
|
]
|
|
assert mesh_checks
|
|
assert all(check.passed for check in mesh_checks)
|
|
|
|
bad_report = build_validation_report(instance, _snapshot(instance, bad_helical_external_hand=True))
|
|
assert any(
|
|
check.code.startswith("external_mesh.")
|
|
and check.code.endswith(".gear_compatibility")
|
|
and not check.passed
|
|
for check in bad_report.checks
|
|
)
|
|
|
|
|
|
def test_gear_mesh_strengthened_checks_are_generated() -> None:
|
|
instance = _instance()
|
|
report = build_validation_report(instance, _snapshot(instance))
|
|
gear_mesh_checks = [
|
|
check
|
|
for check in report.checks
|
|
if check.code.startswith("gear_mesh.")
|
|
]
|
|
|
|
assert gear_mesh_checks
|
|
assert any(check.code.endswith(".tip_root_clearance") for check in gear_mesh_checks)
|
|
assert any(check.code.endswith(".backlash_range") for check in gear_mesh_checks)
|
|
assert any(check.code.endswith(".placement_phase_policy") for check in gear_mesh_checks)
|
|
assert any(check.code.endswith(".internal_ring_half_tooth_phase") for check in gear_mesh_checks)
|
|
assert any(check.code.endswith(".motion_sample_residual") for check in gear_mesh_checks)
|
|
assert all(check.passed for check in gear_mesh_checks)
|
|
|
|
|
|
def test_gear_mesh_phase_error_fails_validation() -> None:
|
|
instance = _instance()
|
|
report = build_validation_report(instance, _snapshot(instance, planet_phase_offset_deg=2.0))
|
|
|
|
assert any(
|
|
check.code.startswith("gear_mesh.sun_planet_external_mesh_sun_to_planet_1.")
|
|
and check.code.endswith(".placement_phase_policy")
|
|
and not check.passed
|
|
for check in report.checks
|
|
)
|