Skip to content

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 ValueRounded
100100
105100
109100
110120
111120
115120

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:

python
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_rounded

Examples

Exact Match (Passes)

python
# 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 exactly

Small Offset Within Grid Cell (Passes)

python
# 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 point

Large Offset (Fails)

python
# 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 difference

Grid 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

python
# 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 straddling

Example: 19 Pixels Apart, 20 Grid Difference

python
# 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 straddling

Running Validation

python
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:

python
node_pres = node.presentations[0]
clamped = node_pres.clamp_point(imprecise_coordinate)

Align to Grid

When creating coordinates programmatically, round to the grid:

python
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:

python
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

python
"""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)