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:

  1. PoolingMode — defines the strength and direction (pool vs segregate)

  2. PoolingSpecification — pairs a list of pipe types with a pooling mode

  3. PoolingRule — holds all specifications and is passed to CadMap

PoolingMode#

PoolingMode encodes the strength of the pooling or segregation constraint through a coefficient:

Factory method

Default coeff

Behaviour

PoolingMode.strong_pooling()

1e-14

Near-zero cost alongside existing pipes — very strong attraction

PoolingMode.moderate_pooling()

0.5

Moderate preference to bundle with compatible pipes

PoolingMode.intermediate_pooling()

0.75

Weak preference to bundle

PoolingMode.weak_pooling()

0.99

Almost neutral; slight attraction

PoolingMode.segregation()

0

Strict: pipes must stay apart; no co-routing

PoolingMode.permissive_segregation()

10

Soft segregation: can pass through but not bundle

PoolingMode.expert_mode()

0 (set manually)

No restrictions; set .coeff to any value

PoolingMode.custom(name, description, default_coeff, valid_range)

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 together

  • coeff == 0: strict segregation — voxels occupied by an incompatible pipe become non-traversable

  • coeff > 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

types

list of str

Pipe type labels (PipeType.type_) to which this specification applies. Use ["TypeA", "TypeB"] to pool A, A ; B, B and A, B together. Use ["TypeA"] for a self-rule (A with A).

pooling_mode

PoolingMode

Strength and direction of the constraint.

min_distance

float

Minimum separation between pipes (metres). 0.0 = pipes can touch.

parallel

bool

False (default): agglomerative — new pipe hugs the existing corridor. True: parallel — new pipe runs alongside the existing pipes’ neural fibers at min_distance.

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

pooling_specs

list

The list of PoolingSpecification objects.

min_distance

float

Default minimum separation (m) between pipes whose types have no explicit spec. Defaults to 0.0.

distance_multiplier

float

Scales the pooling cost contribution globally. Higher values produce tighter bundles. Default: 500.0.

volume_indices

list of int

Indices of ruled_volumes near which pooling is considered. Pass [] to apply everywhere.

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#