Branch Corner Validation
This guide explains how PyPtP validates branch corner coordinates using a grid-based tolerance system. Understanding this helps you create networks that pass validation and avoid subtle coordinate issues.
Full Example
View the complete code: 14_branch_corner_validation.py
The Grid System
Gaia and Vision use a 20-pixel grid for coordinate alignment. The validator compares coordinates after rounding to this grid:
Rounded coordinate = round(coordinate / 20) × 20| Raw Value | Rounded |
|---|---|
| 100 | 100 |
| 105 | 100 |
| 109 | 100 |
| 110 | 120 |
| 111 | 120 |
| 115 | 120 |
Validation Logic
The validator checks if the first point of first_corners aligns with node1's position, and the first point of second_corners aligns with node2's position.
Alignment is checked after grid rounding:
def coordinates_match(corner_coord, node_coord):
"""Check if coordinates match after grid rounding."""
corner_rounded = round(corner_coord / 20) * 20
node_rounded = round(node_coord / 20) * 20
return corner_rounded == node_roundedExamples
Exact Match (Passes)
# Node at (100, 100), corner at (100, 100)
node = NodeLV(
NodeLV.General(name="A"),
presentations=[NodePresentation(sheet=sheet_guid, x=100, y=100)],
)
link = LinkLV(
LinkLV.General(node1=node.general.guid, node2=other.general.guid),
presentations=[
BranchPresentation(
sheet=sheet_guid,
first_corners=[(100, 100)], # Exact match
second_corners=[(300, 100)],
)
],
)
# ✅ Passes - coordinates match exactlySmall Offset Within Grid Cell (Passes)
# Node at (100, 200), corner at (108, 195)
# Both round to (100, 200)
node = NodeLV(
NodeLV.General(name="C"),
presentations=[NodePresentation(sheet=sheet_guid, x=100, y=200)],
)
link = LinkLV(
LinkLV.General(node1=node.general.guid, node2=other.general.guid),
presentations=[
BranchPresentation(
sheet=sheet_guid,
first_corners=[(108, 195)], # Rounds to (100, 200)
second_corners=[(292, 205)], # Rounds to (300, 200)
)
],
)
# ✅ Passes - both round to same grid pointLarge Offset (Fails)
# Node at (100, 300), corner at (130, 300)
# Node rounds to (100, 300), corner rounds to (140, 300)
node = NodeLV(
NodeLV.General(name="E"),
presentations=[NodePresentation(sheet=sheet_guid, x=100, y=300)],
)
link = LinkLV(
LinkLV.General(node1=node.general.guid, node2=other.general.guid),
presentations=[
BranchPresentation(
sheet=sheet_guid,
first_corners=[(130, 300)], # Rounds to (140, 300) ≠ (100, 300)
second_corners=[(300, 300)],
)
],
)
# ❌ Fails - 40 pixel grid differenceGrid Boundary Edge Cases
Boundary Straddling
A small raw difference can cause a large grid difference when values straddle a grid boundary.
Example: 2 Pixels Apart, 20 Grid Difference
# Node at (109, 400), corner at (111, 400)
# Raw difference: only 2 pixels!
# But: 109 rounds to 100, 111 rounds to 120
# Grid difference: 20 pixels
node = NodeLV(
NodeLV.General(name="G"),
presentations=[NodePresentation(sheet=sheet_guid, x=109, y=400)],
)
link = LinkLV(
LinkLV.General(node1=node.general.guid, node2=other.general.guid),
presentations=[
BranchPresentation(
sheet=sheet_guid,
first_corners=[(111, 400)], # 109→100, 111→120
second_corners=[(300, 400)],
)
],
)
# ❌ Fails - grid boundary straddlingExample: 19 Pixels Apart, 20 Grid Difference
# Node at (100, 509), corner at (100, 490)
# Raw difference: 19 pixels
# But: 509 rounds to 500, 490 rounds to 480
# Grid difference: 20 pixels
node = NodeLV(
NodeLV.General(name="I"),
presentations=[NodePresentation(sheet=sheet_guid, x=100, y=509)],
)
link = LinkLV(
LinkLV.General(node1=node.general.guid, node2=other.general.guid),
presentations=[
BranchPresentation(
sheet=sheet_guid,
first_corners=[(100, 490)], # 509→500, 490→480
second_corners=[(300, 500)],
)
],
)
# ❌ Fails - grid boundary straddlingRunning Validation
from pyptp.validator import CheckRunner, Severity
runner = CheckRunner(network)
report = runner.run()
# Check for corner mismatch warnings
for issue in report.issues:
if issue.severity == Severity.WARNING:
logger.warning("{}: {}", issue.code, issue.message)Best Practices
Use clamp_point()
Always use clamp_point() when coordinates may be imprecise:
node_pres = node.presentations[0]
clamped = node_pres.clamp_point(imprecise_coordinate)Align to Grid
When creating coordinates programmatically, round to the grid:
def snap_to_grid(value, grid_size=20):
"""Snap value to nearest grid point."""
return round(value / grid_size) * grid_size
x = snap_to_grid(calculated_x)
y = snap_to_grid(calculated_y)Validate Before Saving
Always run validation before saving:
report = CheckRunner(network).run()
warnings = sum(1 for i in report.issues if i.severity == Severity.WARNING)
if warnings > 0:
logger.warning("Network has {} validation warnings", warnings)
for issue in report.issues:
logger.warning(" {}", issue.message)Complete Example
"""Validate branch corner coordinates with grid-aware tolerance.
The validator compares coordinates after rounding to the 20-pixel grid.
Small differences that round to the same grid point pass validation.
Only mismatches that persist after rounding are flagged.
IMPORTANT: A small raw difference can still cause a mismatch if values
straddle a grid boundary. Example: 109 rounds to 100, 111 rounds to 120.
Raw difference is only 2 pixels, but grid difference is 20 pixels.
"""
from pyptp import NetworkLV, configure_logging
from pyptp.elements.lv.link import LinkLV
from pyptp.elements.lv.node import NodeLV
from pyptp.elements.lv.presentations import BranchPresentation, NodePresentation
from pyptp.elements.lv.sheet import SheetLV
from pyptp.ptp_log import logger
from pyptp.validator import CheckRunner, Severity
configure_logging(level="INFO")
network = NetworkLV()
sheet = SheetLV(SheetLV.General(name="Validation Demo"))
sheet.register(network)
sheet_guid = sheet.general.guid
# Example 1: Exact match (passes)
node1 = NodeLV(
NodeLV.General(name="A"),
presentations=[NodePresentation(sheet=sheet_guid, x=100, y=100)],
)
node1.register(network)
node2 = NodeLV(
NodeLV.General(name="B"),
presentations=[NodePresentation(sheet=sheet_guid, x=300, y=100)],
)
node2.register(network)
link1 = LinkLV(
LinkLV.General(node1=node1.general.guid, node2=node2.general.guid),
presentations=[
BranchPresentation(
sheet=sheet_guid,
first_corners=[(100, 100)],
second_corners=[(300, 100)],
)
],
)
link1.register(network)
# Example 2: Small offset within same grid cell (passes)
node3 = NodeLV(
NodeLV.General(name="C"),
presentations=[NodePresentation(sheet=sheet_guid, x=100, y=200)],
)
node3.register(network)
node4 = NodeLV(
NodeLV.General(name="D"),
presentations=[NodePresentation(sheet=sheet_guid, x=300, y=200)],
)
node4.register(network)
link2 = LinkLV(
LinkLV.General(node1=node3.general.guid, node2=node4.general.guid),
presentations=[
BranchPresentation(
sheet=sheet_guid,
first_corners=[(108, 195)], # Rounds to (100, 200)
second_corners=[(292, 205)], # Rounds to (300, 200)
)
],
)
link2.register(network)
# Example 3: Large offset (fails)
node5 = NodeLV(
NodeLV.General(name="E"),
presentations=[NodePresentation(sheet=sheet_guid, x=100, y=300)],
)
node5.register(network)
node6 = NodeLV(
NodeLV.General(name="F"),
presentations=[NodePresentation(sheet=sheet_guid, x=300, y=300)],
)
node6.register(network)
link3 = LinkLV(
LinkLV.General(node1=node5.general.guid, node2=node6.general.guid),
presentations=[
BranchPresentation(
sheet=sheet_guid,
first_corners=[(130, 300)], # Rounds to (140, 300) ≠ (100, 300)
second_corners=[(300, 300)],
)
],
)
link3.register(network)
# Run validation
runner = CheckRunner(network)
report = runner.run()
logger.info("Validation complete: {}", report.summary())
for issue in report.issues:
if issue.severity == Severity.WARNING:
logger.warning("{}: {}", issue.code, issue.message)
# Expected: warnings for link3 (and potentially others with boundary issues)