Quickstart#

routing is a Python package for automatically routing pipes, tubes, and wires through 3D CAD environments. Given a 3D model of your assembly, port connection points, and a set of design rules, routing finds and optimizes feasible paths that respect your engineering constraints.

Installation#

Install with pip:

pip install routing

Requirements: Python ≥ 3.12.

All core dependencies (volmdlr, numpy, scipy, etc.) are installed automatically.

Verify the installation:

import routing
print(routing.__version__)

Your First Routing Problem#

This example routes a pipe through a window frame assembly — a 3D CAD model with two volumes (a border frame and an outer shell). We define where the pipe starts and ends, what type of pipe it is, and let routing find the path automatically.

_images/getting_started.svg

Step 1 — Load a CAD Model#

routing uses volmdlr to load STEP/STP files:

from volmdlr.step import Step
from volmdlr.model import VolumeModel

model = Step.from_file("path/to/your_assembly.step")
volume_model = model.to_volume_model()

Step 2 — Wrap Volumes with Routing Rules#

A RuledVolume wraps a CAD volume and attaches design rules to it — for example, a WeightingRule that tells the router to prefer paths near the surface (lower cost = preferred path). The window assembly has two volumes: the outer one (primitives[1], a box) and the border (primitives[0], a frame with a window). Its STEP file is data/step/Cas_test_windows_V5.step in the routing repository; it is not installed with the package.

from routing.core.core import RuledVolume
from routing.core.design_rules import WeightingRule

# Penalize routing far from the surface (paths stay closer to the assembly)
weighting_rule = WeightingRule(
    function="linear",
    min_length=0.0,    # start applying at distance 0
    max_length=0.2,    # stop applying at distance 0.2 m
    min_value=1,       # cost multiplier at min_length
    max_value=10,      # cost multiplier at max_length
)

# Wrap each CAD volume
outer_volume = RuledVolume.from_volume_model(
    VolumeModel([volume_model.primitives[1]]),
    design_rules=[weighting_rule],
    name="outer",
)
border_volume = RuledVolume.from_volume_model(
    VolumeModel([volume_model.primitives[0]]),
    design_rules=[weighting_rule],
    name="border",
)

Step 3 — Define Pipe Types#

A PipeType describes the physical properties of a pipe: its outer radius and minimum bend radius:

import routing.core.legacy_piping as piping
from routing.core.core import PipeType

# A 30 mm diameter pipe (radius = 0.015 m)
section = piping.Section(radius_equivalent=0.015)

pipe_type_A = PipeType(
    section=section,
    name="PipeA",
    radius_of_curvature_ratio_min=1.5,   # min bend radius = 1.5 × pipe radius
    length_before_turn_min=0.0,           # no minimum straight segment required
    color=(0, 100, 200),                  # RGB display colour, 0–255 or 0–1
    type_="TypeA",                        # group label for pooling
)

Step 4 — Define Ports#

A Port is a connection point — a position in 3D space with an orientation (the direction the pipe must leave/enter the port) and an optional minimum straight length before the first turn:

from routing.core.core import Port

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

port_end = Port(
    coordinates=(0.05, 0.8, 0.1),
    direction=(0, -1, 0),             # points along the pipe, into the room (−Y)
    length=0.1,
    name="end_port",
)

Step 5 — Create Specifications#

A Specification pairs a start port, end port, and pipe type into a routing requirement. Create one per pipe to route:

from routing.core.core import Specification

spec_A = Specification(
    start=port_start,
    end=port_end,
    pipe_type=pipe_type_A,
    name="PipeA_route",
)

Step 6 — Build the CadMap#

The CadMap voxelizes the routing space. The voxel_size parameter controls resolution — smaller values are more accurate but slower. It is a project decision with no universal formula: pick it from the narrowest passages your pipes must thread, then refine on the routed result (the window example runs at 21 mm):

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

voxel_size = 0.021  # 21 mm voxels

# Global design rules (apply to all pipe types)
design_rules = [TurnPenalizer(cost=3.0)]

# Build a bounding box around the routing space
bounding_box = CadMap.build_bounding_volume(
    [outer_volume, border_volume],
    border_rules=[],
    voxel_size=voxel_size,
    voxel_margin=1,
)

cadmap = CadMap.from_project_inputs(
    design_rules=design_rules,
    ruled_volumes=[outer_volume, border_volume],
    bounding_box=bounding_box,
    voxel_size=voxel_size,
    exact_distances=True,
)

Step 7 — Generate Routing Paths#

Create a RoutePlanner and call generate() with your list of specifications:

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

# A* pathfinding with Manhattan heuristic, no diagonal movement
finder = SmartFinder("AStar", "manhattan", "never")

route_planner = RoutePlanner(cadmap, finder)

routing_result = route_planner.generate(
    specifications=[spec_A],
    n_iterations=1,     # paths searched per conduit: 0 or 1 = the best only; N = up to N
    steady_mode=False,  # False: an error met while routing a conduit is raised
)

routing_result is a RoutingResults object containing the found paths. n_iterations given here becomes the planner’s (RoutePlanner(cadmap, finder, n_iterations=...), 0 by default). A conduit no path joins is kept as not routed, with a warning: check it before reading its geometry.

print(routing_result.summary())             # one line per conduit: routed, paths, cost
solution = routing_result.solution("PipeA_route")
if solution.is_routed:
    print(solution.best_path.cost)
routed = routing_result.routed_solutions    # the conduits with a geometry to read

Step 8 — Optimize the Paths (optional)#

The paths from generate() follow the voxel grid and may look “staircase-like”. The optimize() step refines them into smooth geometry respecting port tangency:

from routing.core.optimizer import Costs, Settings

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,
    stabilization_threshold=5.0,
    n_iterations=3,
    max_iter=1000,
)

routing_result = route_planner.optimize(
    costs=costs,
    settings=settings,
    picked_path="best",
)

Step 9 — Visualize Results#

Display the result in a 3D viewer, then compare the paths on their indicators:

# 3D view: the conduits and the CAD
routing_result.display_3d()

# Parallel plots of the paths' indicators, generated then optimized
routing_result.plot_data_generated().plot()
routing_result.plot_data_optimized().plot()

Pipes gather in packs only under a PoolingRule: see User Guide: Pipe Pooling (Bundling).

Controlling Verbosity#

Use routing.core.verbose to control the amount of output:

from routing.core.verbose import VerboseLevel, verbose

verbose.set_level(VerboseLevel.INFO)   # options: SILENT, ERROR, WARNING, INFO, DEBUG, TRACE

Set VerboseLevel.TRACE to see detailed pathfinding progress, or VerboseLevel.SILENT to suppress all output.

Next Steps#

Now that you have the basics working, explore more features: