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 :class:`~routing.core.design_rules.PoolingRule` that you pass as a global design rule when building the :class:`~routing.core.cadmap.CadMap`. .. contents:: On this page :local: :depth: 1 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 :class:`~routing.core.design_rules.PoolingMode`. Three objects work together: 1. :class:`~routing.core.design_rules.PoolingMode` — defines the strength and direction (pool vs segregate) 2. :class:`~routing.core.design_rules.PoolingSpecification` — pairs a list of pipe types with a pooling mode 3. :class:`~routing.core.design_rules.PoolingRule` — holds all specifications and is passed to ``CadMap`` PoolingMode ----------- :class:`~routing.core.design_rules.PoolingMode` encodes the strength of the pooling or segregation constraint through a coefficient: .. list-table:: :header-rows: 1 :widths: 30 15 55 * - 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 .. code-block:: python 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 -------------------- :class:`~routing.core.design_rules.PoolingSpecification` specifies which **pipe types** should be pooled (or segregated) together, and with what mode and minimum distance. .. code-block:: python 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:** .. list-table:: :header-rows: 1 :widths: 25 15 60 * - 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 ----------- :class:`~routing.core.design_rules.PoolingRule` assembles multiple ``PoolingSpecification`` objects and is passed to ``CadMap.from_project_inputs``. .. code-block:: python 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:** .. list-table:: :header-rows: 1 :widths: 25 15 60 * - Parameter - Type - Description * - ``pooling_specs`` - list - The list of :class:`~routing.core.design_rules.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: .. code-block:: python 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: .. code-block:: python # 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 :meth:`~routing.core.route_planner.RoutingResults.collect_packs` to retrieve the grouped pipe bundles: .. code-block:: python 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 ---------------- .. code-block:: python 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 ---------- - :doc:`optimization` — refine path geometry with the optimizer - :doc:`design_rules_reference` — complete reference for all rule types - :doc:`../tutorials/design_rules` — tutorial on constraint rules including ClampingConstraint