Core Concepts#

This page explains the main objects you interact with in every routing workflow. Understanding these classes and how they fit together is the foundation for using routing effectively.

../_static/index-images/user_guide.svg

Overview#

Every routing workflow follows the same structure:

  1. Define inputs: Port, PipeType, Specification

  2. Define the routing space: RuledVolume, CadMap

  3. Find paths: RoutePlanner

  4. Read results: RoutingResults

Here is how they relate:

Port (start) ──┐
               ├──► Specification ──┐
Port (end)   ──┘                    │
PipeType ──────────────────────────►│
                                    │
VolumeModel ──► RuledVolume ──────►CadMap ──► RoutePlanner.generate(specs) ──► RoutingResults
DesignRules ──►────────────────────►│
                                    │
(optional) ──────── RoutePlanner.optimize() ──► refined RoutingResults

Port#

A Port is a fixed connection point in 3D space where a pipe starts or ends. It has a position, an orientation, and an optional minimum straight length.

from routing.core.core import Port

port = Port(
    coordinates=(0.05, -0.4, 0.1),    # (x, y, z) in metres
    direction=(0, 1, 0),               # pipe exits in the +Y direction
    length=0.1,                        # 100 mm straight before the first turn
    name="inlet_port",
)

Parameters:

  • coordinates (tuple of 3 floats): The (x, y, z) position of the port in the global coordinate frame (metres).

  • direction (tuple of 3 floats): The direction the pipe must leave this port (for a start port) or arrive (for an end port). Does not need to be a unit vector — it is normalized automatically.

  • length (float, default 0.0): Minimum straight segment length at the port before the first direction change. Required for pipes with minimum bend radius constraints.

  • len_adaptable_with_environment (bool, default False): When True, the router will automatically extend the port length if the immediate environment would cause collisions. This attribute is not used in generic mode, it is an attribute for custom algorithms.

  • permitted_departure_angle (list of float, optional): If set, restricts the departure direction to a list of allowed angles (in radians). Leave as None to permit all directions. This attribute is not used in generic mode, it is an attribute for custom algorithms.

Importing ports from Excel:

For large assemblies, define ports in an Excel spreadsheet and import them:

# Excel columns: name | coordinates_x | coordinates_y | coordinates_z
#                    | direction_x | direction_y | direction_z | length
with open("ports.xlsx", "rb") as f:
    ports = Port.from_xlsx_stream(f)

PipeType#

A PipeType defines the physical properties of a class of pipe. You reuse the same PipeType for all pipes of that specification.

from piping_3d import piping
from routing.core.core import PipeType

section = piping.Section(radius_equivalent=0.015)  # 30 mm outer diameter

pipe_type = PipeType(
    section=section,
    name="HydraulicLine",
    radius_of_curvature_ratio_min=1.5,    # bend radius ≥ 1.5 × diameter
    length_before_turn_min=0.030,         # 30 mm straight before any turn
    color=(0, 100, 200),                  # visualization color (RGB 0–255)
    type_="Hydraulic",                    # group label for pooling
)

Key parameters:

  • section: A piping.Section object specifying the outer radius of the pipe (radius_equivalent).

  • radius_of_curvature_ratio_min (float): Minimum bend radius as a multiple of the pipe diameter. Common values: 1.5 for flexible hoses, 3–5 for rigid pipes.

  • length_before_turn_min (float): Minimum straight length required before any direction change. Set to 0 for fully flexible tubing.

  • type_ (str): A group label. Pipes with the same type_ can be pooled together (see User Guide: Pipe Pooling (Bundling)).

  • min_slope / max_slope (float, optional): For gravity-drained pipes, set min_slope to the minimum required slope (e.g. 0.02 = 2% grade). See Design Rules Reference for GravityRule.

  • priority_index (int, optional): Routing priority. Lower index = higher priority (routed first when ordering matters). This attribute is not used in generic mode, it is an attribute for custom algorithms.

  • color: RGB tuple in range (0–255). Used by the visualization layer.

Compact creation with Specification.from_complete_definition:

If you only have a few pipes and don’t need to reuse PipeType objects, you can skip creating them explicitly:

from routing.core.core import Specification

spec = Specification.from_complete_definition(
    start=port_start,
    end=port_end,
    radius=0.015,                        # pipe outer radius
    name="HydraulicLine_1",
    radius_of_curvature_ratio=1.5,
    min_straight_length=0.030,
    pooling_type="Hydraulic",
    color=(0, 100, 200),
)

Specification#

A Specification is a routing requirement: it pairs a start port, end port, and pipe type into one object that RoutePlanner.generate() processes.

from routing.core.core import Specification

# Standard construction
spec = Specification(
    start=port_start,
    end=port_end,
    pipe_type=pipe_type,
    name="HydraulicLine_route_1",
)

# Shorthand: use pipe_type.name automatically
spec = Specification.from_ports(port_start, port_end, pipe_type)

Create one Specification per pipe to route. Pass a list of them to generate().

RuledVolume#

A RuledVolume wraps a 3D CAD volume (a VolumeModel from volmdlr) and attaches a list of design rules to it. The design rules control how the routing algorithm weights the space near that volume.

from routing.core.core import RuledVolume
from routing.core.design_rules import WeightingRule
from volmdlr.model import VolumeModel

# A weighting rule: routes prefer to stay close to this volume
weighting_rule = WeightingRule(
    function="linear",
    min_length=0.0, max_length=0.2,
    min_value=1,    max_value=10,
)

# Wrap the CAD volume
ruled_volume = RuledVolume.from_volume_model(
    volume_model=VolumeModel([my_cad_primitive]),
    design_rules=[weighting_rule],
    name="main_structure",
)

Factory methods:

  • RuledVolume.from_volume_model(volume_model, design_rules, name) — the standard way to create a RuledVolume from a volmdlr VolumeModel.

  • RuledVolume.from_ruled_volume(existing_rv, design_rules) — copy an existing RuledVolume and override its global rules.

in_border parameter: Set in_border=True to use this volume for computing the automatic bounding box of the routing space. In practice you need to set this manually when using CadMap.build_bounding_volume() in order to use the specified volume’s bounding box into the construction of the main bounding box.

See Design Rules Reference for all available design rule types.

CadMap#

CadMap is the central routing space object. It:

  1. Takes all your RuledVolume objects and voxelizes the free space between them

  2. Computes distance fields from the CAD surfaces to every voxel

  3. Applies the design rules to compute a weight matrix

  4. Stores the result as a compact grid ready for pathfinding

from routing.core.cadmap import CadMap
from routing.core.design_rules import TurnPenalizer

voxel_size = 0.021   # 21 mm — about 0.7× the largest pipe diameter

# Global rules applied to all pipes
global_rules = [TurnPenalizer(cost=3.0)]

# Auto-compute a bounding box from the volumes
bounding_box = CadMap.build_bounding_volume(
    ruled_volumes=[ruled_vol_1, ruled_vol_2],
    border_rules=[],
    voxel_size=voxel_size,
    voxel_margin=1,
)

# Build the CadMap (this does the voxelization + distance computation)
cadmap = CadMap.from_project_inputs(
    design_rules=global_rules,
    ruled_volumes=[ruled_vol_1, ruled_vol_2],
    bounding_box=bounding_box,
    voxel_size=voxel_size,
    exact_distances=True,
)

Key parameters of from_project_inputs:

Parameter

Default

Description

design_rules

[]

Global rules applied to every pipe type (e.g. TurnPenalizer)

ruled_volumes

required

List of RuledVolume objects to voxelize against

bounding_box

None

Bounding RuledVolume; auto-computed if None

voxel_size

0.1

Voxel edge length in metres. Use 1.5–3× the largest pipe radius.

exact_distances

False

Exact mesh distances (accurate, slower) vs voxel approximation (fast)

weights_operator

"min"

How to combine weights from multiple volumes. "min_distance" is recommended.

voxel_inside

False

Set True to fill enclosed cavities (route through solid bodies)

build_cad

False

Build visual box primitives per voxel (expensive; use only for debugging)

Choosing voxel_size:

  • Start with voxel_size = 1.5 × largest_pipe_radius

  • Halve it if paths look inaccurate or miss narrow passages

  • Double it if voxelization takes too long

Memory: cadmap.nbytes reports total memory usage. Large assemblies with fine voxels can require several GB.

RoutePlanner#

RoutePlanner coordinates the pathfinding and optimization. It takes a CadMap and a SmartFinder and exposes two main methods: generate() and optimize().

from routing.core.finders import SmartFinder
from routing.core.route_planner import RoutePlanner

finder = SmartFinder(
    algorithm="AStar",      # "AStar", "ThetaStar", "Dijkstra"
    heuristic="manhattan",  # "manhattan", "euclidean", "octile"
    diagonal="never",       # "never", "always", "only_when_no_obstacle"
)

route_planner = RoutePlanner(cadmap, finder)

generate() — find routing paths:

routing_result = route_planner.generate(
    specifications=[spec1, spec2, spec3],
    n_iterations=1,      # k-shortest paths; 1 = only the best path per pipe
    steady_mode=False,   # True = continue even if some pipes fail to route
)

Pipes are processed in the order they appear in specifications. Earlier pipes influence the grid weights for later pipes (through the pooling and history mechanisms).

optimize() — refine path geometry:

from routing.core.optimizer import Costs, Settings

routing_result = route_planner.optimize(
    costs=Costs(length=1000, sideways=100, short_line=5000,
                gravity=5000, bend=250, constraints=5000, interferences=1e8),
    settings=Settings(voxel_size=0.01, max_shift=0.1, n_iterations=3, max_iter=1000),
    picked_path="best",    # "best" (lowest cost path) or "last" (most recent)
    max_refinements=0,     # extra passes to fix constraint violations
)

See User Guide: Path Geometry Optimization for a full guide to the optimizer.

RoutingResults#

RoutingResults is the output of generate() and optimize(). It contains all routing solutions and exposes visualization methods:

# Plot all generated paths (before optimization)
routing_result.plot_data_generated().plot()

# Plot all optimized paths
routing_result.plot_data_optimized().plot()

# Collect pipe bundles (groups of pipes routed together)
packs = routing_result.collect_packs(only_optimized=False)
routing_result.specific_packs_cad_view(packs)

# Get bifurcation points (where bundles split)
bifurcation_pts = routing_result.cadmap.get_bifurcation_points(packs, tolerance=0.04)

See User Guide: Accessing Routing Results for a complete guide to reading and exporting results.

Next Steps#