Skip to content

Clamping Branch Corners

This guide covers using NodePresentation.clamp_point() to snap imprecise coordinates to valid node connection points. This is essential when working with imported data or programmatically generated layouts.

Full Example

View the complete code: 12_clamp_branch_corners.py

Why Clamping?

Branch corners must precisely match node positions for correct rendering. In practice, coordinates often have slight errors from:

  • External data imports: GIS systems, CAD exports, spreadsheets
  • Coordinate transformations: Scaling, rotation, projection changes
  • Manual entry: Typos, rounding differences
  • Algorithmic generation: Layout algorithms with floating-point precision

The clamp_point() method snaps coordinates to the nearest valid connection point on a node.

Basic Usage

python
from pyptp import NetworkLV, configure_logging
from pyptp.elements.lv.node import NodeLV
from pyptp.elements.lv.link import LinkLV
from pyptp.elements.lv.presentations import NodePresentation, BranchPresentation
from pyptp.elements.lv.sheet import SheetLV

configure_logging(level="INFO")

network = NetworkLV()

sheet = SheetLV(SheetLV.General(name="Clamp Example"))
sheet.register(network)
sheet_guid = sheet.general.guid

# Create nodes at precise positions
substation = NodeLV(
    NodeLV.General(name="Substation"),
    presentations=[NodePresentation(sheet=sheet_guid, x=100, y=100)],
)
substation.register(network)

load = NodeLV(
    NodeLV.General(name="Load"),
    presentations=[NodePresentation(sheet=sheet_guid, x=300, y=100)],
)
load.register(network)

The Problem: Imprecise Coordinates

Imagine coordinates from an external source with slight errors:

python
# These should connect at (100, 100) and (300, 100)
# but are a few pixels off
imported_first_corners = [(105, 98), (200, 100)]
imported_second_corners = [(295, 102)]

Without clamping, these misalignments cause validation warnings and visual glitches.

The Solution: clamp_point()

python
# Get the node presentations
substation_pres = substation.presentations[0]
load_pres = load.presentations[0]

# Clamp the first point of each corner list
first_corners = [
    substation_pres.clamp_point(imported_first_corners[0]),  # Snaps (105, 98) → (100, 100)
    *imported_first_corners[1:],  # Keep remaining points as-is
]

second_corners = [
    load_pres.clamp_point(imported_second_corners[0]),  # Snaps (295, 102) → (300, 100)
]

Creating the Branch

python
feeder = LinkLV(
    LinkLV.General(
        name="Feeder",
        node1=substation.general.guid,
        node2=load.general.guid,
    ),
    presentations=[
        BranchPresentation(
            sheet=sheet_guid,
            first_corners=first_corners,
            second_corners=second_corners,
        )
    ],
)
feeder.register(network)

How clamp_point() Works

For standard point symbols, clamp_point() simply returns the node's center position:

python
# Node at (100, 100) with default point symbol
node_pres.clamp_point((105, 98))   # Returns (100, 100)
node_pres.clamp_point((95, 103))   # Returns (100, 100)
node_pres.clamp_point((100, 100))  # Returns (100, 100)

For line symbols (busbars), it finds the nearest point on the line segment. See Line Symbol Clamping for details.

Batch Processing

When importing many branches, create a helper function:

python
def create_clamped_branch(network, node1, node2, raw_first, raw_second, sheet_guid):
    """Create branch with clamped corner coordinates."""
    # Find presentations on the target sheet
    pres1 = next(p for p in node1.presentations if p.sheet == sheet_guid)
    pres2 = next(p for p in node2.presentations if p.sheet == sheet_guid)

    # Clamp first point of each list
    first_corners = [pres1.clamp_point(raw_first[0]), *raw_first[1:]]
    second_corners = [pres2.clamp_point(raw_second[0]), *raw_second[1:]]

    branch = LinkLV(
        LinkLV.General(node1=node1.general.guid, node2=node2.general.guid),
        presentations=[
            BranchPresentation(
                sheet=sheet_guid,
                first_corners=first_corners,
                second_corners=second_corners,
            )
        ],
    )
    branch.register(network)
    return branch


# Usage
for row in imported_data:
    create_clamped_branch(
        network,
        nodes[row.from_id],
        nodes[row.to_id],
        row.path_from_source,
        row.path_from_target,
        sheet_guid,
    )

When to Clamp

ScenarioClamp?Reason
Manual creationNoYou control the coordinates
External importsYesSource may have precision issues
Layout algorithmsMaybeDepends on algorithm precision
Copy/paste operationsNoCoordinates should match
Coordinate transformsYesFloating-point accumulation

Complete Example

python
"""Clamp branch corners to node connection points.

Use NodePresentation.clamp_point() to snap imprecise coordinates to valid
connection points. This is useful when importing data with slightly off
coordinates or programmatically generating layouts.
"""

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

configure_logging(level="INFO")

network = NetworkLV()

sheet = SheetLV(SheetLV.General(name="Clamp Example"))
sheet.register(network)
sheet_guid = sheet.general.guid

# Two nodes we want to connect
substation = NodeLV(
    NodeLV.General(name="Substation"),
    presentations=[NodePresentation(sheet=sheet_guid, x=100, y=100)],
)
substation.register(network)

load = NodeLV(
    NodeLV.General(name="Load"),
    presentations=[NodePresentation(sheet=sheet_guid, x=300, y=100)],
)
load.register(network)

# Coordinates from external source with slight errors
imported_first_corners = [(105, 98), (200, 100)]
imported_second_corners = [(295, 102)]

# Clamp the first point of each corner list
substation_pres = substation.presentations[0]
load_pres = load.presentations[0]

first_corners = [
    substation_pres.clamp_point(imported_first_corners[0]),
    *imported_first_corners[1:],
]
second_corners = [
    load_pres.clamp_point(imported_second_corners[0]),
]

feeder = LinkLV(
    LinkLV.General(name="Feeder", node1=substation.general.guid, node2=load.general.guid),
    presentations=[
        BranchPresentation(
            sheet=sheet_guid,
            first_corners=first_corners,
            second_corners=second_corners,
        )
    ],
)
feeder.register(network)

network.save("clamp_corners_example.gnf")
logger.info("Saved clamp_corners_example.gnf")