273 lines
10 KiB
Python
273 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Literal
|
|
|
|
from ..models import ComponentManifest, ConcreteRelation, ValidationCheck
|
|
from .base import (
|
|
BaseRelationValidator,
|
|
RelationValidationContext,
|
|
add_check,
|
|
constraint_id_in_model,
|
|
find_manifest_constraint,
|
|
relation_code,
|
|
xy_distance,
|
|
)
|
|
|
|
|
|
def _component_pair(
|
|
context: RelationValidationContext,
|
|
relation: ConcreteRelation,
|
|
) -> tuple[ComponentManifest | None, ComponentManifest | None]:
|
|
return (
|
|
context.component(relation.source_component_id),
|
|
context.component(relation.target_component_id),
|
|
)
|
|
|
|
|
|
def _gear_compatibility(
|
|
source: ComponentManifest | None,
|
|
target: ComponentManifest | None,
|
|
*,
|
|
relation_type: Literal["external_mesh", "internal_mesh"],
|
|
) -> dict[str, object]:
|
|
if source is None or target is None:
|
|
return {"passed": False, "reason": "missing_component"}
|
|
source_helix_angle = source.helix_angle_deg
|
|
target_helix_angle = target.helix_angle_deg
|
|
source_hand = source.helix_hand
|
|
target_hand = target.helix_hand
|
|
source_tooth_form = source.tooth_form
|
|
target_tooth_form = target.tooth_form
|
|
|
|
base_passed = (
|
|
source.kind == "gear"
|
|
and target.kind == "gear"
|
|
and source.module_mm is not None
|
|
and target.module_mm is not None
|
|
and abs(source.module_mm - target.module_mm) <= 1e-12
|
|
and source.pressure_angle_deg is not None
|
|
and target.pressure_angle_deg is not None
|
|
and abs(source.pressure_angle_deg - target.pressure_angle_deg) <= 1e-12
|
|
and source_tooth_form in {"spur", "helical"}
|
|
and target_tooth_form == source_tooth_form
|
|
)
|
|
|
|
if source_tooth_form == "spur" and target_tooth_form == "spur":
|
|
tooth_form_passed = (
|
|
abs(float(source_helix_angle or 0.0)) <= 1e-12
|
|
and abs(float(target_helix_angle or 0.0)) <= 1e-12
|
|
)
|
|
hand_rule = "not_applicable_for_spur"
|
|
elif source_tooth_form == "helical" and target_tooth_form == "helical":
|
|
angles_present = source_helix_angle is not None and target_helix_angle is not None
|
|
equal_angle = (
|
|
angles_present
|
|
and abs(float(source_helix_angle) - float(target_helix_angle)) <= 1e-12
|
|
)
|
|
if relation_type == "external_mesh":
|
|
hand_rule = "opposite"
|
|
hands_passed = source_hand is not None and target_hand is not None and source_hand != target_hand
|
|
else:
|
|
hand_rule = "same"
|
|
hands_passed = source_hand is not None and target_hand is not None and source_hand == target_hand
|
|
tooth_form_passed = bool(angles_present and equal_angle and hands_passed)
|
|
else:
|
|
tooth_form_passed = False
|
|
hand_rule = "unsupported_or_missing_tooth_form"
|
|
|
|
fields = {
|
|
"source_kind": source.kind,
|
|
"target_kind": target.kind,
|
|
"source_module": source.module_mm,
|
|
"target_module": target.module_mm,
|
|
"source_pressure_angle": source.pressure_angle_deg,
|
|
"target_pressure_angle": target.pressure_angle_deg,
|
|
"source_tooth_form": source_tooth_form,
|
|
"target_tooth_form": target_tooth_form,
|
|
"source_helix_angle_deg": source_helix_angle,
|
|
"target_helix_angle_deg": target_helix_angle,
|
|
"source_helix_hand": source_hand,
|
|
"target_helix_hand": target_hand,
|
|
"helical_hand_rule": hand_rule,
|
|
}
|
|
passed = base_passed and tooth_form_passed
|
|
return {"passed": passed, **fields}
|
|
|
|
|
|
class ExternalMeshValidator(BaseRelationValidator):
|
|
relation_type = "external_mesh"
|
|
|
|
def validate_static(
|
|
self,
|
|
context: RelationValidationContext,
|
|
relation: ConcreteRelation,
|
|
checks: list[ValidationCheck],
|
|
) -> None:
|
|
source_node = context.node(relation.source_component_id)
|
|
target_node = context.node(relation.target_component_id)
|
|
source, target = _component_pair(context, relation)
|
|
compatibility = _gear_compatibility(source, target, relation_type=self.relation_type)
|
|
add_check(
|
|
checks,
|
|
section="static",
|
|
code=relation_code(relation, "gear_compatibility"),
|
|
passed=bool(compatibility["passed"])
|
|
and source_node is not None
|
|
and target_node is not None
|
|
and source_node.gear_type == "external"
|
|
and target_node.gear_type == "external",
|
|
message="external mesh connects compatible external gears with valid tooth-form pairing",
|
|
actual={
|
|
**compatibility,
|
|
"source_gear_type": source_node.gear_type if source_node else None,
|
|
"target_gear_type": target_node.gear_type if target_node else None,
|
|
},
|
|
expected="external gear pair with equal module, pressure angle, tooth form, and valid helix hand rule",
|
|
)
|
|
|
|
def validate_assembly(
|
|
self,
|
|
context: RelationValidationContext,
|
|
relation: ConcreteRelation,
|
|
checks: list[ValidationCheck],
|
|
) -> None:
|
|
constraint_id = find_manifest_constraint(
|
|
context.snapshot,
|
|
relation_type=relation.relation_type,
|
|
component_a=relation.source_component_id,
|
|
component_b=relation.target_component_id,
|
|
)
|
|
add_check(
|
|
checks,
|
|
section="assembly",
|
|
code=relation_code(relation, "cad_constraint"),
|
|
passed=constraint_id_in_model(
|
|
context.snapshot,
|
|
constraint_id=constraint_id,
|
|
relation_type=relation.relation_type,
|
|
),
|
|
message="external mesh relation exists in manifest and model constraints",
|
|
actual=constraint_id,
|
|
expected=f"{relation.source_component_id}<->{relation.target_component_id}",
|
|
)
|
|
|
|
def validate_geometry(
|
|
self,
|
|
context: RelationValidationContext,
|
|
relation: ConcreteRelation,
|
|
checks: list[ValidationCheck],
|
|
) -> None:
|
|
source, target = _component_pair(context, relation)
|
|
origin_a = context.placement_origin(relation.source_component_id)
|
|
origin_b = context.placement_origin(relation.target_component_id)
|
|
if not source or not target or not origin_a or not origin_b:
|
|
actual = "missing_component_or_placement"
|
|
passed = False
|
|
expected = None
|
|
else:
|
|
expected = (
|
|
None
|
|
if source.pitch_radius_mm is None or target.pitch_radius_mm is None
|
|
else source.pitch_radius_mm + target.pitch_radius_mm
|
|
)
|
|
actual = xy_distance(origin_a, origin_b)
|
|
passed = expected is not None and abs(actual - expected) <= 1e-6
|
|
add_check(
|
|
checks,
|
|
section="geometry",
|
|
code=relation_code(relation, "center_distance"),
|
|
passed=passed,
|
|
message="external mesh pitch center distance equals the sum of pitch radii",
|
|
actual=actual,
|
|
expected=expected,
|
|
)
|
|
|
|
|
|
class InternalMeshValidator(BaseRelationValidator):
|
|
relation_type = "internal_mesh"
|
|
|
|
def validate_static(
|
|
self,
|
|
context: RelationValidationContext,
|
|
relation: ConcreteRelation,
|
|
checks: list[ValidationCheck],
|
|
) -> None:
|
|
source_node = context.node(relation.source_component_id)
|
|
target_node = context.node(relation.target_component_id)
|
|
source, target = _component_pair(context, relation)
|
|
compatibility = _gear_compatibility(source, target, relation_type=self.relation_type)
|
|
add_check(
|
|
checks,
|
|
section="static",
|
|
code=relation_code(relation, "gear_compatibility"),
|
|
passed=bool(compatibility["passed"])
|
|
and source_node is not None
|
|
and target_node is not None
|
|
and source_node.gear_type == "internal"
|
|
and target_node.gear_type == "external",
|
|
message="internal mesh connects an internal gear to a compatible external gear with valid tooth-form pairing",
|
|
actual={
|
|
**compatibility,
|
|
"source_gear_type": source_node.gear_type if source_node else None,
|
|
"target_gear_type": target_node.gear_type if target_node else None,
|
|
},
|
|
expected="internal-to-external gear pair with equal module, pressure angle, tooth form, and valid helix hand rule",
|
|
)
|
|
|
|
def validate_assembly(
|
|
self,
|
|
context: RelationValidationContext,
|
|
relation: ConcreteRelation,
|
|
checks: list[ValidationCheck],
|
|
) -> None:
|
|
constraint_id = find_manifest_constraint(
|
|
context.snapshot,
|
|
relation_type=relation.relation_type,
|
|
component_a=relation.source_component_id,
|
|
component_b=relation.target_component_id,
|
|
)
|
|
add_check(
|
|
checks,
|
|
section="assembly",
|
|
code=relation_code(relation, "cad_constraint"),
|
|
passed=constraint_id_in_model(
|
|
context.snapshot,
|
|
constraint_id=constraint_id,
|
|
relation_type=relation.relation_type,
|
|
),
|
|
message="internal mesh relation exists in manifest and model constraints",
|
|
actual=constraint_id,
|
|
expected=f"{relation.source_component_id}<->{relation.target_component_id}",
|
|
)
|
|
|
|
def validate_geometry(
|
|
self,
|
|
context: RelationValidationContext,
|
|
relation: ConcreteRelation,
|
|
checks: list[ValidationCheck],
|
|
) -> None:
|
|
source, target = _component_pair(context, relation)
|
|
origin_a = context.placement_origin(relation.source_component_id)
|
|
origin_b = context.placement_origin(relation.target_component_id)
|
|
if not source or not target or not origin_a or not origin_b:
|
|
actual = "missing_component_or_placement"
|
|
passed = False
|
|
expected = None
|
|
else:
|
|
expected = (
|
|
None
|
|
if source.pitch_radius_mm is None or target.pitch_radius_mm is None
|
|
else source.pitch_radius_mm - target.pitch_radius_mm
|
|
)
|
|
actual = xy_distance(origin_a, origin_b)
|
|
passed = expected is not None and expected >= 0.0 and abs(actual - expected) <= 1e-6
|
|
add_check(
|
|
checks,
|
|
section="geometry",
|
|
code=relation_code(relation, "center_distance"),
|
|
passed=passed,
|
|
message="internal mesh pitch center distance equals the pitch-radius difference",
|
|
actual=actual,
|
|
expected=expected,
|
|
)
|