routing.core.skeleton#

Medial-axis topology of a voxelised bundle and its projection onto routing solutions.

Flow:

RoutingVoxelization
   │  Skeleton.from_volume(volume, opening_radius=..., closing_radius=...)
   ▼
Skeleton                 thinned voxels + 26-connectivity graph
   │  .fit_to(solutions)
   ▼
Skeleton (fitted)        port entries resolved first (:class:`PortEntry` — one shared
   │                     departure per entry corridor); graph pruned to walked voxels,
   │                     junctions fused by sol-set difference; solution indices stored as
   │                     graph node attributes ("solutions"), per-solution paths in
   │                     :attr:`paths_by_solution`.
   │  .extract_cylinders(**params)
   ▼
list[SkeletonCylinder]   body segments + :attr:`Skeleton.entries` — handoff to the bundle
class routing.core.skeleton.EntryTerminal(port: Port, solution: Solution, solution_index: int, is_input: bool, entry_length: float, seed: ndarray)[source]#

Bases: NamedTuple

One cable end at its specification port, with the routed data anchoring it to the skeleton.

property anchor: ndarray#

Wall position of the port.

property axis: ndarray#

Unit direction of the port, pointing into the room.

entry_length: float#

Alias for field number 4

is_input: bool#

Alias for field number 3

port: Port#

Alias for field number 0

seed: ndarray#

Alias for field number 5

solution: Solution#

Alias for field number 1

solution_index: int#

Alias for field number 2

class routing.core.skeleton.MeetingZone(voxels: list[tuple[int, int, int]], graph: Graph)[source]#

Bases: object

A cluster of skeleton voxels where solutions of differing sets meet.

Resolves to a genuine junction (the solutions merge into one run) or a parasitic bridge (independent runs that only touch — a parallel pinch or a perpendicular crossing).

classmethod find_all(graph: Graph) → list[MeetingZone][source]#

Every place where solutions meet: connected clusters of solution-set-boundary voxels.

fuse_into_junction() → dict[tuple[int, int, int], tuple[int, int, int]][source]#

Fuse the cluster into a single junction at its centroid; return absorbed -> representative.

Public: the skeleton’s port-fanout pass calls this directly (bypassing resolve(), whose parasitic-break branch is meaningless for the path terminals a fanout collapses).

graph: Graph#
resolve() → tuple[dict[tuple[int, int, int], tuple[int, int, int]], set[tuple[int, int, int]]][source]#

Fuse into a junction if the solutions merge, else break the parasitic bridge.

Mutates the graph; returns the fused absorbed -> representative map and the voxels removed while un-crossing — both consumed by the skeleton to re-thread its paths.

voxels: list[tuple[int, int, int]]#
class routing.core.skeleton.PortEntry(stations: list[list[EntryTerminal]], station_abscissas: list[float])[source]#

Bases: object

One physical entry corridor: the cable ends entering along one port axis, resolved before fitting.

Cable ends whose entry segments superpose — a shared or duplicated connector, or a coaxial connector row — form one entry: anchor stations along the axis (rear to front) and a single departure on the skeleton, shared by every member’s fit, so superposed leaders are never born. The bundle births the matching geometry from it (Bundle._lines_of_entry()): one rigid chain between stations, one leader, one bridge.

property departure: ndarray#

World seed shared by every member’s fit: the front terminal’s own seed.

property front: EntryTerminal#

Terminal owning the consolidated leader: first cable end of the front station.

property leader_min_length: float#

Minimum leader length from the front anchor honoring every member’s own entry requirement.

property ports: list[Port]#

Distinct member ports, rear first (a port shared by several solutions appears once).

classmethod resolve(solutions: list[Solution], extension_by_port: dict[int, ndarray]) → list[PortEntry][source]#

Group every solution’s cable ends into entries — connected components of superposition.

extension_by_port (port id to world position) replaces the raw routed seeds on a round-trip, so the resolution follows the optimized leader tips.

property terminals: list[EntryTerminal]#

Every member cable end, rear station first.

class routing.core.skeleton.Skeleton(voxel_volume: RoutingVoxelization, graph: Graph, solutions: list[Solution] = <factory>, paths_by_solution: dict[int, list[tuple[int, int, int]]]=<factory>, entries: list[PortEntry] = <factory>)[source]#

Bases: object

Medial axis of a voxelised volume, optionally fitted to routing solutions.

The graph is built from RoutingVoxelization 26-connectivity. Each node carries two attributes:

  • voxel_index (float): integer indices initially; possibly a fractional centroid in voxel-grid coordinates after junction clustering.

  • solutions (set[int]): indices of solutions traversing this voxel. Initialised to set() in from_volume(); filled by fit_to(); non-empty for every surviving node post-prune.

fit_to(solutions) mutates the skeleton in place: prunes voxels never walked, fuses sol-set-differing junctions, and fills paths_by_solution.

JUNCTION_COLOR: ClassVar[tuple[float, float, float]] = (1.0, 0.0, 0.0)#
TERMINAL_COLOR: ClassVar[tuple[float, float, float]] = (1.0, 0.6, 0.0)#
diagnose(label: str = '', *, detour_threshold_voxels: float = 2.0) → None[source]#

Print pre-trace structure stats + per-solution tracing quality.

Rebuilds the pre-trace graph via RoutingVoxelization.to_graph() for the before-state comparison (cost equivalent to a copy, paid only on demand).

display(color: tuple[float, float, float] = (0.0, 0.0, 1.0), decimation: int = 1, port_volume: RoutingVoxelization | None = None, title: str = '') → None[source]#

Display skeleton voxels with junctions (red) / terminals (orange) highlighted.

display_path_vs_polyline(solution_index: int, label: str = '', overlay_prims: list | None = None, decimation: int = 3) → None[source]#

Overlay one solution’s skeleton path on its polyline.

Greyed raw skeleton + red path voxels + blue polyline tubes + orange port markers.

entries: list[PortEntry]#
entry_anchor_world(entry: PortEntry) → ndarray | None[source]#

World position where entry’s fitted paths attach to the skeleton; None when no member fitted.

Every member departs from the entry’s shared seed, so the first fitted member gives the node; junction fusion may have re-posed it (fractional centroid), hence the read through the graph.

extract_cylinders(smoothing_window: int = 2, tangent_window: int = 3, min_turn_angle: float = 0.5235987755982988) → list[SkeletonCylinder][source]#

Body cylinders fitted to skeleton branches.

Branches (anchor-to-anchor chains) are smoothed and split at detected direction changes — one cylinder per straight sub-segment. Entry geometry (rigid chains, leaders, bridges) is not extracted from voxels: the bundle births it from entries (Bundle._lines_of_entry()).

find_parasitic_bridges() → list[tuple[tuple[int, int, int], tuple[int, int, int]]][source]#

Edges joining two non-empty, solution-disjoint voxels — a parasitic bridge.

Post-fit_to() this must be empty: any such edge is two runs still glued at a meeting point (a parallel pinch or a perpendicular crossing) that escaped resolution.

fit_to(solutions: list[Solution], branches_min_distance: float = 0.0, extension_by_port: dict[int, ndarray] | None = None) → None[source]#

Adapt the skeleton to solutions: resolve entries, fit each path, prune and cluster in place.

Entry resolution runs first (PortEntry.resolve()): cable ends sharing an entry corridor share one departure, so their fitted paths enter the skeleton at the same node and superposed leaders are never born. Per-solution Dijkstra then hugs each polyline; voxels never walked are dropped (parasitic bridges); voxels straddling a sol-set difference are fused into single representatives. When branches_min_distance > 0, the close-but-distinct path-endpoints a port fanout left behind are fused so cylinder extraction sees one junction (see _fuse_port_fanouts()). Fills paths_by_solution and the "solutions" graph node attribute.

extension_by_port maps a port’s id to a world position replacing that port’s raw routed seed: Bundle.to_skeleton() passes the optimized leader tips so the resolution follows where the consolidated body actually meets each entry.

classmethod from_volume(volume: RoutingVoxelization, opening_radius: int = 2, closing_radius: int = 0, erosion_radius: int = 0, display_operations: bool = False) → Skeleton[source]#

Thin volume to its medial axis via Lee-1994 topological thinning.

Optional morphological preprocessing runs in order before skimage.morphology.skeletonize: opening erases small protrusions, closing fills small gaps, erosion shrinks fat regions. Each radius of 0 skips its stage.

graph: Graph#
junction_markers(scale: float, color: tuple[float, float, float]) → list[source]#

Box primitives at the (possibly clustered) junction nodes for visualization.

junctions() → list[tuple[int, int, int]][source]#

Nodes of degree > 2 — where branches meet.

nodes_world(nodes: list[tuple[int, int, int]]) → ndarray[source]#

World coordinates of graph nodes via their voxel_index attribute.

paths_by_solution: dict[int, list[tuple[int, int, int]]]#
solutions: list[Solution]#
terminals() → list[tuple[int, int, int]][source]#

Nodes of degree 1 — free ends.

property voxel_size: float#

Voxel edge length.

voxel_volume: RoutingVoxelization#
class routing.core.skeleton.SkeletonCylinder(start: ndarray, end: ndarray, covered_voxels: list[tuple[int, int, int]], solutions: tuple[Solution, ...])[source]#

Bases: Cylinder

A cylinder fitted to a straight sub-segment of the skeleton.

Inherits start, end plus voxelize / bounding_box from Cylinder. radius is derived from solutions via Pack.pack_radii(). Adds:

  • covered_voxels : skeleton voxels spanned by this segment.

  • solutions : distinct solutions walking the segment’s body (sampled from an interior degree-2 voxel, not from endpoint anchors whose sol-set unions every incident branch). Source of truth from which radii and radius are derived.

property radii: tuple[float, ...]#

Pipe radii of the distinct solutions walking the body of this segment.

class routing.core.skeleton.SkeletonSettings(*, name: str = '', voxel_size: float | None = None, dilation_voxels: int = 2, opening_radius: int = 2, closing_radius: int = 0, erosion_radius: int = 0, smoothing_window: int = 2, tangent_window: int = 3, min_turn_angle: float = 0.2617993877991494)[source]#

Bases: Model

Voxelization, thinning and cylinder-extraction parameters for the bundle ⇄ skeleton round-trip.

Carried by a Bundle so a chain of repairs/optimizations reuses one recipe without re-passing it. voxel_size left None is derived downstream — half the routing voxel under HarnessSettings.complete(), the routed shapes’ own step in a standalone RoutingResults.to_skeleton().

closing_radius: int#
dilation_voxels: int#
erosion_radius: int#
min_turn_angle: float#
opening_radius: int#
smoothing_window: int#
tangent_window: int#
voxel_size: float | None#