User Guide: Multi-Scale and Multi-Grid Routing#

For large or complex assemblies you can split the routing space into multiple grids. This gives you two capabilities:

  • Multi-grid routing (mono-scale): model a very large space as one world built only where it matters. The routing space is cut into maximal boxes of free voxels at one voxel size, one grid per box, so the search never holds one huge tensor over the whole bounding box, empty space included. min_fill trades fewer boxes against some empty space held.

  • Multi-scale routing: add detail while staying light in memory. One coarse grid covers open space, completed by one grid size_factor times finer where the coarse grid is blind — narrow passages — optionally limited to a bounding_box.

cadmap.decomposed() starts from a map computed whole: the gain is in the grids the search holds, not in the map’s construction. To keep the construction light too, build one map per zone of interest and assemble them with CadMap.from_cad_maps.

In both scenarios a CadMap carries several regions, each sampled on its own grid, and goes to RoutePlanner exactly like a single-region map.

Regions of a CadMap#

A CadMap holds its routing space as a list of VoxelRegion — one region unless decomposed. Each region carries its voxels and the CAD fields sampled on its grid (distances, weights, weight tensor). The router treats the regions as one space, stitched at their shared borders: cadmap.to_world() builds one grid per region.

Key properties:

  • cadmap.regions — the regions; cadmap.regions[0] for a single-region map

  • cadmap.voxel_sizes — set of the voxel sizes in use (length > 1 → multi-scale)

  • cadmap.nbytes — total memory across all regions

  • cadmap.is_multi_scale — True if the regions have different voxel sizes

MultiCadMap remains as a deprecated alias whose factories build a CadMap with these regions (see Migrating to v3.0.0).

Decomposing a CadMap#

Three ways are available:

1. From an existing CadMap (most common):

from routing.core.cadmap import CadMap

# Build a regular CadMap first
cadmap = CadMap.from_project_inputs(
    design_rules=design_rules,
    ruled_volumes=[outer_volume, border_volume],
    bounding_box=bounding_box,
    voxel_size=0.021,
    exact_distances=True,
)

# Mono-scale: cut the room into sub-regions at the same voxel size
decomposed_map = cadmap.decomposed()

# Or: multi-scale (adds a finer grid at size_factor× resolution)
decomposed_map = cadmap.decomposed(
    multi_scale=True,
    size_factor=3,    # fine grid = voxel_size / 3 ≈ 7 mm
    bounding_box=cadmap.bounding_box.bounding_box,
)

Parameters of decomposed:

Parameter

Default

Description

multi_scale

False

If True, completes the coarse region with one size_factor times finer where the coarse grid is blind (multi-scale). If False, cuts the grid into maximal boxes at its voxel size (mono-scale).

size_factor

3

Resolution multiplier for the fine grid (voxel_size / size_factor). Used only when multi_scale=True.

bounding_box

None

Optional bounding box to restrict fine-grid creation. Pass cadmap.bounding_box.bounding_box to cover the full space.

min_fill

1.0

Mono-scale only: the smallest share of free voxels in every slab a box grows by. 1 cuts boxes of free voxels only; below, a box takes in some empty space and the space splits into fewer, larger boxes — fewer grids to stitch, more empty cells held.

2. Directly from project inputs (chains CadMap.from_project_inputs and decomposed):

decomposed_map = CadMap.from_project_inputs(
    design_rules=design_rules,
    ruled_volumes=[outer_volume, border_volume],
    bounding_box=bounding_box,
    voxel_size=0.021,
    exact_distances=True,
).decomposed(multi_scale=True, size_factor=3)

3. From a list of pre-built CadMaps (same voxel size required):

assembled_map = CadMap.from_cad_maps([cadmap_region_A, cadmap_region_B])

All CadMaps in the list must share the same voxel_size; the assembled map carries the regions of all of them.

Routing a Decomposed Map#

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

from routing.core.finders import SmartFinder
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)

Visualizing the Multi-Grid Layout#

You can inspect the grid decomposition before routing:

from volmdlr.model import VolumeModel

# Show all region boundaries and their connections
VolumeModel(decomposed_map.voxel_meshes(show_connections=True)).display_3d()

Complete Multi-Scale Example#

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 TurnPenalizer, WeightingRule
from routing.core.finders import SmartFinder
from routing.core.optimizer import Costs, Settings
from routing.core.route_planner import RoutePlanner
import routing.core.legacy_piping as piping

# --- 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"
)

# --- Build coarse CadMap ---
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)],
    ruled_volumes=[outer_volume, border_volume],
    bounding_box=bounding_box,
    voxel_size=voxel_size,
    exact_distances=True,
    weights_operator="min_distance",
)

# --- Decompose into a coarse and a fine region ---
decomposed_map = cadmap.decomposed(
    multi_scale=True,
    size_factor=3,       # adds a ~7 mm fine grid
    bounding_box=cadmap.bounding_box.bounding_box,
)

# --- Route ---
pipe_type = PipeType(
    section=piping.Section(radius_equivalent=0.015),
    name="PipeA",
    radius_of_curvature_ratio_min=1.5,
    color=(0, 150, 200),
    type_="TypeA",
)
spec = 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_type, "route_A",
)

route_planner = RoutePlanner(decomposed_map, SmartFinder("AStar", "manhattan", "never"))
result = route_planner.generate(specifications=[spec], n_iterations=1)

# --- Optimize ---
costs = Costs(length=1000, sideways=100, short_line=5000, gravity=5000,
              bend=250, constraints=5000, interferences=1e8)
settings = Settings(voxel_size=0.006, max_shift=0.1, stabilization_threshold=1.0,
                    n_iterations=3, max_iter=1000)
result = route_planner.optimize(costs=costs, settings=settings, picked_path="best")
result.specific_packs_display_3d(result.collect_packs(only_optimized=True))

Design Rules Across Grids#

Each grid prices the rule to_voxelized_rule(voxel_size) returns for its own voxel size — the rule itself, or a copy in cells for a rule whose bounds are lengths: a bound in metres becomes the right number of cells on the coarse grid and on the fine one. A step from one grid into another keeps the heading the path arrived with — a change of grid is no turn — and the straight length and the attribute lengths keep counting.

They count cells, whatever the cell. A run without a clampable cell that starts on the coarse grid and continues on the fine one adds coarse and fine cells to one count, and that count is compared with the bound of the grid hosting each move. Across a change of grid, the bound of a length rule is therefore not a physical length: a run of 3.0 m straddling a 0.21 m grid and a 0.07 m grid can go uncharged under a 2.5 m bound. On one grid, and on a single-scale map, the count and the bound are in the same cell. Where a length rule matters at the meeting of two scales, read the run lengths off the routes, as scripts/simple_test_cases/labyrinth_toggled_constraint.py does.

When to Use Multi-Scale vs Multi-Grid#

Scenario

Recommended approach

Why

Very large space, routing in zones of interest

cadmap.decomposed()

One world over the free boxes only, no grid over empty space

Assembly with a few critical tight spots

cadmap.decomposed(multi_scale=True, size_factor=3)

Detail in tight areas, coarse and light elsewhere

Assembly with distinct disconnected zones

CadMap.from_cad_maps([zone_A, zone_B])

Build separate CadMaps per zone; combine manually

Single small assembly

Plain CadMap

Simpler; one region, no stitching between grids

Next Steps#