Tutorial: Routing in Large Spaces with Obstacles#

This tutorial demonstrates routing multiple pipes through a large room environment (use_case_labyrinth.stp) that contains many fixed and mobile obstacles. For assemblies of this scale, the CadMap is decomposed into a coarse grid completed by a finer one where the coarse grid is blind: the search crosses open space in coarse cells and still reaches the narrow passages.

What you will learn:

  • Splitting a CAD model into fixed and mobile parts

  • Choosing voxel sizes for large assemblies

  • Using CadMap.decomposed with multi_scale=True

  • Routing multiple pipes pooled as trays

Source script: scripts/simple_test_cases/large_room_with_obstacles.py

Assembly Structure#

The test assembly contains a large room with several obstacles identified by name. Some obstacles are considered “mobile” (can be repositioned) and some are “fixed”. We partition the CAD primitives into two groups to apply weighting rules to each independently:

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

weighting_rule = WeightingRule("linear", 0.0, None, min_value=1, max_value=10)

# Names of mobile obstacles in this assembly
mobile_names = ["EnvSimpleDessia20", "EnvSimpleDessia21", ...]

mobile_primitives = [p for p in volume_model.primitives if p.name in mobile_names]
fixed_primitives  = [p for p in volume_model.primitives if p.name not in mobile_names]

mobile_volume = RuledVolume.from_volume_model(
    VolumeModel(mobile_primitives), [weighting_rule], name="mobile"
)
fixed_volume = RuledVolume.from_volume_model(
    VolumeModel(fixed_primitives), [weighting_rule], name="fixed"
)

Choosing Voxel Size for Large Assemblies#

The assembly measures about 10.4 × 9.0 × 6.9 metres, and its five conduits are 100 mm in diameter. A coarse voxel size is appropriate:

voxel_size = 0.21   # 210 mm — about 2× the pipe diameter

For the optimizer, use a finer grid:

settings = Settings(voxel_size=0.07, ...)   # 70 mm — 3× finer than routing grid

Building and Decomposing the CadMap#

The five conduits of the script are of pipe type "Type2"; ports maps each name to its PortCouple, pipe_types to its PipeType. A small volume around each port enters the bounding box, so that the routing space holds the ports:

from routing.core.core import Specification

specifications = [
    Specification(ports[name].start, ports[name].end, pipe_types[name], name) for name in ports
]

port_shapes = []
for couple in ports.values():
    port_shapes += [couple.start.volmdlr_primitives(), couple.end.volmdlr_primitives()]
ports_volume = RuledVolume.from_volume_model(VolumeModel(port_shapes), [], name="ports")

Build a regular CadMap first, then decompose it into a coarse and a fine region:

from routing.core.cadmap import CadMap
from routing.core.design_rules import PoolingMode, PoolingRule, PoolingSpecification, TurnPenalizer

pooling_rule = PoolingRule(
    pooling_specs=[
        PoolingSpecification(
            types=["Type2"],
            pooling_mode=PoolingMode.strong_pooling(),
            min_distance=0.0,
            parallel=True,
        )
    ],
    min_distance=0.0,
    cost=500,
)

cadmap = CadMap.from_project_inputs(
    design_rules=[TurnPenalizer(cost=1), pooling_rule],
    ruled_volumes=[fixed_volume, mobile_volume],
    bounding_box=CadMap.build_bounding_volume(
        [fixed_volume, mobile_volume, ports_volume], [], voxel_size
    ),
    voxel_size=voxel_size,
    exact_distances=True,
    weights_operator="min_distance",
)

# Multi-scale: coarse grid (210 mm) + fine grid (70 mm)
decomposed_map = cadmap.decomposed(
    multi_scale=True,
    size_factor=3,
    bounding_box=cadmap.bounding_box.bounding_box,
)

parallel=True pools the conduits as trays: side by side on the face they run along, in layers — two at most by default, max_layers=2. A tray rests on the volumes flagged RuledVolume(..., support=True); when none is flagged, as here, every volume is a support. See Parallel Packs: Trays.

Routing and Optimization#

Pass the decomposed map to RoutePlanner exactly like a single-region CadMap:

from routing.core.finders import SmartFinder
from routing.core.optimizer import Costs, Settings
from routing.core.route_planner import RoutePlanner

finder = SmartFinder("AStar", "manhattan", "never")
route_planner = RoutePlanner(decomposed_map, finder)

result = route_planner.generate(
    specifications=specifications,
    n_iterations=1,
    steady_mode=False,
)

# Visualize generated paths
packs = result.collect_packs(only_optimized=False)
result.specific_packs_display_3d(packs)

# Optimize
costs = Costs(
    length=1000, sideways=100, short_line=5000,
    gravity=5000, bend=250, constraints=5000, interferences=1e8,
)
settings = Settings(
    voxel_size=0.07,
    max_shift=0.5,
    stabilization_threshold=1.0,
    n_iterations=5,
    max_iter=1000,
)
result = route_planner.optimize(
    costs=costs, settings=settings, picked_path="best", max_refinements=0
)

packs = result.collect_packs(only_optimized=True)
result.specific_packs_display_3d(packs)

Memory and Performance Tips#

For assemblies larger than about 10 × 10 × 10 m with 200 mm voxels:

  • Check memory first: print(cadmap.nbytes / 1e9, "GB")

  • Use weights_operator="min_distance" — more accurate than the default "min"

  • Use multi-scale (cadmap.decomposed(multi_scale=True)) to keep the routing grid coarse, a finer grid completing it where it is blind

  • Reduce exact_distances=True to False if voxelization is too slow (slightly less accurate weighting, but much faster)

  • Increase voxel_size if memory is still an issue — halving the voxel count in each dimension reduces memory by 8×

See User Guide: Multi-Scale and Multi-Grid Routing for a complete guide to multi-scale routing.

Next Steps#