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:
NamedTupleOne 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
- seed: ndarray#
Alias for field number 5
- solution_index: int#
Alias for field number 2
- class routing.core.skeleton.MeetingZone(voxels: list[tuple[int, int, int]], graph: Graph)[source]#
Bases:
objectA 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 -> representativemap 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:
objectOne 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
departureon 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(portidto 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:
objectMedial axis of a voxelised volume, optionally fitted to routing solutions.
The graph is built from
RoutingVoxelization26-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 toset()infrom_volume(); filled byfit_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 fillspaths_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.
- entry_anchor_world(entry: PortEntry) ndarray | None[source]#
World position where
entry’s fitted paths attach to the skeleton;Nonewhen 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. Whenbranches_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()). Fillspaths_by_solutionand the"solutions"graph node attribute.extension_by_portmaps a port’sidto 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
volumeto 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 of0skips 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.
- nodes_world(nodes: list[tuple[int, int, int]]) ndarray[source]#
World coordinates of graph nodes via their
voxel_indexattribute.
- paths_by_solution: dict[int, list[tuple[int, int, int]]]#
- 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:
CylinderA cylinder fitted to a straight sub-segment of the skeleton.
Inherits
start,endplusvoxelize/bounding_boxfromCylinder.radiusis derived fromsolutionsviaPack.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 whichradiiandradiusare 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:
ModelVoxelization, 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_sizeleftNoneis derived downstream — half the routing voxel underHarnessSettings.complete(), the routed shapes’ own step in a standaloneRoutingResults.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#