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_filltrades 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_factortimes finer where the coarse grid is blind — narrow passages — optionally limited to abounding_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 mapcadmap.voxel_sizes— set of the voxel sizes in use (length > 1 → multi-scale)cadmap.nbytes— total memory across all regionscadmap.is_multi_scale—Trueif 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 |
|---|---|---|
|
|
If |
|
|
Resolution multiplier for the fine grid ( |
|
|
Optional bounding box to restrict fine-grid creation. Pass
|
|
|
Mono-scale only: the smallest share of free voxels in every slab a box grows by. |
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 |
|
One world over the free boxes only, no grid over empty space |
Assembly with a few critical tight spots |
|
Detail in tight areas, coarse and light elsewhere |
Assembly with distinct disconnected zones |
|
Build separate CadMaps per zone; combine manually |
Single small assembly |
Plain |
Simpler; one region, no stitching between grids |
Next Steps#
Tutorial: Routing Pipes Through a CAD Assembly — full routing tutorial with a single CadMap
User Guide: Path Geometry Optimization — tune the geometry optimizer
User Guide: Pipe Pooling (Bundling) — pipe bundling across multi-grid setups
BranchPortDistanceRule — space branch junctions and ports apart; works across multi-grid setups (the rule tracks
(x, y, z, grid_id)nodes)