User Guide: Pipe Pooling (Bundling)#
Pooling — also called bundling or packs — lets the router group compatible pipes together so that they run side-by-side as a harness. You can also force pipes of incompatible types to stay apart (segregation).
Pooling is controlled by a PoolingRule that you pass
as a global design rule when building the CadMap.
How Pooling Works#
When a PoolingRule is active, every routed pipe joins a pack: the pipes running side
by side in one conduit. Before a new pipe is routed, the packs paint their influence on the
voxel grid for that pipe’s type: the cells the pipe cannot run through, the first row of cells
in contact with the pack, priced down so the new pipe hugs it, and the pack’s own lanes where
a pipe still fits. The strength of this attraction (or repulsion for segregation) is controlled
by the coefficient of the PoolingMode.
The packs also leave a corridor on the grid: one box around each pack and the row of cells
around it. A pipe may join a harness through its corridor, travel along it, leave it and bridge
to another harness it never touched. With allow_loops=False, a move that rejoins a harness
the pipe has left pays the rule’s cost: it would close a loop between the pipe and the
harness. See Loops: Leaving and Rejoining a Harness below. The corridors are also the shared zone, the
is_shared attribute, that BranchClampDistanceRule and BranchPortDistanceRule read.
Three objects work together:
PoolingMode— defines the strength and direction (pool vs segregate)PoolingSpecification— pairs a list of pipe types with a pooling modePoolingRule— holds all specifications and is passed toCadMap
PoolingMode#
PoolingMode encodes the strength of the pooling or
segregation constraint through a coefficient:
Factory method |
Default coeff |
Behaviour |
|---|---|---|
|
1e-14 |
Near-zero cost alongside existing pipes — very strong attraction |
|
0.5 |
Moderate preference to bundle with compatible pipes |
|
0.75 |
Weak preference to bundle |
|
0.99 |
Almost neutral; slight attraction |
|
0 |
Strict: pipes must stay apart; no co-routing |
|
10 |
Soft segregation: can pass through but not bundle |
|
0 (set manually) |
No restrictions; set |
|
user-defined |
Fully custom mode |
from routing.core.design_rules import PoolingMode
mode = PoolingMode.strong_pooling()
# Optionally override the coefficient (outside valid_range it is kept, with a warning)
mode.coeff = 0.01 # slightly weaker than the default 1e-14
The coefficient is a weight multiplier applied to voxels near an existing compatible pipe:
0 < coeff < 1: pooling — cost is reduced → pipes attracted togethercoeff == 0: strict segregation — voxels occupied by an incompatible pipe become non-traversablecoeff > 1: permissive segregation — cost is increased → pipes repelled
PoolingSpecification#
PoolingSpecification specifies which pipe types
should be pooled (or segregated) together, and with what mode and minimum distance.
from routing.core.design_rules import PoolingMode, PoolingSpecification
# Pool "TypeA" pipes with "TypeB" pipes — strong bundling
spec_ab = PoolingSpecification(
types=["TypeA", "TypeB"],
pooling_mode=PoolingMode.strong_pooling(),
min_distance=0.0, # allow pipes to be directly adjacent
parallel=False, # generic packs: the pipes gather in a bundle
)
# Keep "TypeC" pipes 50 mm apart from one another
spec_c = PoolingSpecification(
types=["TypeC"], # one type: the rule holds between two TypeC pipes
pooling_mode=PoolingMode.segregation(),
min_distance=0.05, # keep 50 mm apart
)
Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
list of str |
Pipe type labels ( |
|
|
Strength and direction of the constraint. |
|
float |
Minimum separation between pipes (metres). |
|
bool |
|
|
int or None |
Trays only: the layers a tray may stack. Default |
|
float or None |
Trays only: the width over height beyond which the first layer of a tray stops
widening. Default |
PoolingRule#
PoolingRule assembles multiple
PoolingSpecification objects and is passed to CadMap.from_project_inputs.
from routing.core.design_rules import PoolingRule
pooling_rule = PoolingRule(
pooling_specs=[spec_ab, spec_c],
min_distance=0.0, # default separation when no spec matches
cost=500, # cost of a move closing a loop
allow_loops=False, # price the moves that rejoin a harness the pipe has left
name="pooling_rule",
)
Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
list |
The list of |
|
float |
Default minimum separation (m) between pipes whose types have no explicit spec.
Defaults to |
|
float |
Cost of a move that rejoins a harness the pipe has left, charged when |
|
bool |
|
Pass the rule as a global design rule:
cadmap = CadMap.from_project_inputs(
design_rules=[TurnPenalizer(cost=3.0), pooling_rule],
ruled_volumes=[...],
# bounding_box=..., voxel_size=...
)
Parallel Packs: Trays#
A specification with parallel=True lays its pipes as in a cable tray: side by side on the
face of the volume they run along, then in layers on top of each other. Use it where pipes
must lie flat against a floor, a wall or a ramp; generic packs gather compatible pipes in a
round bundle instead. A tray only takes the pipes of types pooled as trays, and a generic pack
never takes them.
The support. A tray rests on the face its pipes run along. Flag the volumes trays may rest
on with support=True; when no volume is flagged, every volume is a support:
floor = RuledVolume.from_volume_model(
VolumeModel([floor_slab]), [weighting_rule], support=True, name="floor"
)
The map reads the support along the pipes of the tray, from each point of their axes to the nearest point of a support face. The pipes rest on that face when at least half of their length lies within three cells of it, cells of the coarsest grid of the map, and when three readings out of four point the same way, to 30°. A tray whose pipes run away from every support, or whose readings disagree, pools as a generic pack, and reads its support again when it takes a new pipe. Once found, the support is kept. A face met only at the end of the tray, such as the floor under a riser, is not a support.
Layers. On each cut across a tray, the pipes are grouped by the height of their axis over the support, to half a cell of the grid. The lowest group is the first layer, the one of the tray’s first pipe. A higher group is a layer when one of its pipes lies over a pipe of the layer below. A pipe higher than the layers with nothing under it is left out: it counts for nothing and invites no pipe, but its clearance still holds.
Where the next pipe is invited. A cell attracts the next pipe of a tray type when it is a seat of the tray, on the first row of cells in contact with its pipes:
Seat |
Invited |
|---|---|
Beside the pipes of the first layer |
Always: only the first layer widens |
Beside a pipe of a higher layer, with a pipe of the layer below or above it |
Always |
Over the last layer, on its pipes |
While the tray has fewer than |
Over the last layer, once the tray has |
No: the cells there weigh ten times more, up to the new pipe’s contact plus two cells. A pipe still passes there if it must |
Under the first layer, overhanging a layer, between two layers |
No: the cell keeps its weight |
With aspect set, the first layer stops widening once the tray is wider than aspect times
its height, and the next pipes stack instead.
trays = PoolingSpecification(
types=["Cable"],
pooling_mode=PoolingMode.strong_pooling(),
min_distance=0.0,
parallel=True,
max_layers=2, # at most two layers; None for no limit
aspect=None, # no limit on the width of the first layer
)
Route the largest pipes first. The first layer lies at the height of the first pipe’s axis. A larger pipe routed after smaller ones has its axis higher: it finds no seat on their layer and founds a tray of its own beside them.
Choose the voxel size from the diameters. The router lays each pipe’s axis at a cell centre, so two pipes much thinner than a cell may share one, axis on axis. A voxel size close to the diameters of the tray pipes, or a finer grid where the trays run (see User Guide: Multi-Scale and Multi-Grid Routing), lays them side by side.
The scripts of scripts/parallel_packs/ route trays in three cases: ramp_room_test_case.py,
a room with a floor, two walls, a ramp up to a mezzanine and obstacles, which compares the trays
with generic packs through model; window_parallel_packs.py, the window case; and
large_room_with_obstacles.py, the labyrinth of Tutorial: Routing in Large Spaces with Obstacles.
Loops: Leaving and Rejoining a Harness#
Packs that share a pipe form one component of the harness: a trunk with its branches.
Each voxel of the grid carries the set of components whose corridors cover it, 0 outside
every corridor. At each cell a route enters, the rule looks at the components the move
takes on, those of the entered cell that the left cell did not carry:
Move |
Cost |
Reading |
|---|---|---|
Stays in a corridor, or takes on no new component |
0 |
The pipe travels along the harness |
Leaves every corridor |
0 |
The pipe goes its own way |
Takes on a component it never held |
0 |
A bridge: this is how a harness branches |
Takes on again a component it has held and left |
|
A loop between the pipe and the strand it left |
Loops are allowed by default (allow_loops=True): the table applies once the rule is built
with allow_loops=False. The memory is the route’s own path; a Theta* segment is read cell by
cell and charged once. The loop is priced, never forbidden: a pipe with no other way through is
still routed. Two strands of one component are one component, so a pipe sliding from one to
the other inside their corridors is not charged.
Pipe Ordering Matters#
Pipes are routed in the order they appear in the specifications list — a tee excepted,
routed after the conduit it taps (see Tutorial: Tees — Tapping a Routed Conduit). Earlier pipes
establish the corridor; later pipes with compatible types follow it. Plan your
specification order accordingly:
# Route "TypeA" pipes first, then "TypeB" pipes follow their corridors
specifications = type_a_specs + type_b_specs
result = route_planner.generate(
specifications=specifications,
n_iterations=1,
)
Accessing Pack Results#
After routing, call collect_packs() to
retrieve the grouped pipe bundles:
packs = routing_result.collect_packs(only_optimized=False)
# Visualize the bundles in 3D
routing_result.specific_packs_display_3d(packs)
# Find bifurcation points (where bundles split)
bifurcation_pts = routing_result.cadmap.get_bifurcation_points(
packs=packs,
tolerance=0.04, # grouping tolerance in metres
with_ports=False,
)
collect_packs returns a list of pack objects. Each pack contains the set of pipes
that share a common sub-path. get_bifurcation_points returns the 3D coordinates
where packs split or merge.
Complete Example#
import routing.core.legacy_piping as piping
from volmdlr.model import VolumeModel
from volmdlr.step import Step
from routing.core.cadmap import CadMap
from routing.core.core import Port, PipeType, RuledVolume, Specification
from routing.core.design_rules import (
PoolingMode, PoolingRule, PoolingSpecification, TurnPenalizer, WeightingRule
)
from routing.core.finders import SmartFinder
from routing.core.route_planner import RoutePlanner
# --- Load CAD ---
model = Step.from_file("data/step/Cas_test_windows_V5.step")
volume_model = model.to_volume_model()
weighting_rule = WeightingRule("linear", 0.0, 0.2, min_value=1, max_value=10)
outer_volume = RuledVolume.from_volume_model(
VolumeModel([volume_model.primitives[1]]), [weighting_rule], name="outer"
)
border_volume = RuledVolume.from_volume_model(
VolumeModel([volume_model.primitives[0]]), [weighting_rule], name="border"
)
# --- Pooling: group TypeA and TypeB together ---
pooling_specs = [
PoolingSpecification(
types=["TypeA", "TypeB"],
pooling_mode=PoolingMode.strong_pooling(),
min_distance=0.0,
parallel=False,
)
]
pooling_rule = PoolingRule(
pooling_specs=pooling_specs,
min_distance=0.0,
cost=500,
name="pooling_rule",
)
# --- Pipe types ---
section_a = piping.Section(radius_equivalent=0.015)
section_b = piping.Section(radius_equivalent=0.008)
pipe_a = PipeType(section_a, "PipeA", radius_of_curvature_ratio_min=1.5,
color=(0, 150, 200), type_="TypeA")
pipe_b = PipeType(section_b, "PipeB", radius_of_curvature_ratio_min=1.5,
color=(200, 50, 0), type_="TypeB")
# --- Specifications ---
specs = [
Specification(
Port((0.05, -0.4, 0.1), (0, 1, 0), length=0.1),
Port((0.05, 0.8, 0.1), (0, -1, 0), length=0.1),
pipe_a, "route_A",
),
Specification(
Port((0.37, -0.4, 0.3), (0, 1, 0), length=0.08),
Port((0.45, 0.8, 0.18), (0, -1, 0), length=0.04),
pipe_b, "route_B",
),
]
# --- CadMap with pooling ---
voxel_size = 0.021
bounding_box = CadMap.build_bounding_volume(
[outer_volume, border_volume], [], voxel_size, voxel_margin=1
)
cadmap = CadMap.from_project_inputs(
design_rules=[TurnPenalizer(cost=3.0), pooling_rule],
ruled_volumes=[outer_volume, border_volume],
bounding_box=bounding_box,
voxel_size=voxel_size,
exact_distances=True,
)
# --- Route and inspect packs ---
route_planner = RoutePlanner(cadmap, SmartFinder("AStar", "manhattan", "never"))
result = route_planner.generate(specifications=specs, n_iterations=1)
packs = result.collect_packs(only_optimized=False)
result.specific_packs_display_3d(packs)
bifurcation_pts = result.cadmap.get_bifurcation_points(packs, tolerance=0.04)
print("Bifurcation points:", bifurcation_pts)
Next Steps#
User Guide: Path Geometry Optimization — refine path geometry with the optimizer
User Guide: Design Rules Reference — complete reference for all rule types
Tutorial: Engineering Design Rules — tutorial on constraint rules including ClampingConstraint