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, the router inspects the path history of previously-routed
pipes. When routing a new pipe, it applies a lower cost to voxels that already have a
nearby compatible pipe — encouraging the new pipe to follow the same corridor.
The strength of this attraction (or repulsion for segregation) is controlled by the
coefficient of the PoolingMode.
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 (must be within valid_range)
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, # agglomerative mode (follow path corridor)
)
# Segregate "TypeC" from everything else
spec_c = PoolingSpecification(
types=["TypeC", "TypeC"], # repeat type to express same-type self-segregation
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 |
|
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
distance_multiplier=500, # strength of the pooling cost
volume_indices=[0, 1], # apply only near volumes 0 and 1
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 |
Scales the pooling cost contribution globally. Higher values produce tighter
bundles. Default: |
|
list of int |
Indices of |
Pass the rule as a global design rule:
cadmap = CadMap.from_project_inputs(
design_rules=[TurnPenalizer(cost=3.0), pooling_rule],
ruled_volumes=[...],
...
)
Pipe Ordering Matters#
Pipes are routed in the order they appear in the specifications list. 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_cad_view(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#
from piping_3d import 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,
distance_multiplier=500,
volume_indices=[0, 1],
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_cad_view(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
Design Rules Reference — complete reference for all rule types
Tutorial: Engineering Design Rules — tutorial on constraint rules including ClampingConstraint