Tutorial: Shaped Routing Zones#
ShapedWeightingRule lets you define arbitrary 3D
zones that influence where pipes are routed — independent of the CAD geometry of the
assembly itself. You can make the router:
attract paths through a support corridor
repel paths away from a hot or vibrating region
forbid paths from entering a zone entirely
force all paths to pass through a specific area
Zones are defined by any volmdlr solid or VolumeModel — a box, a cone, a sphere,
or an imported CAD body.
Overview#
from routing.core.design_rules import ShapedWeightingRule
All four zone types are created via class-method constructors:
Constructor |
Behaviour inside shape |
When to use |
|---|---|---|
|
Cost × |
Support corridors, preferred routeways |
|
Cost × |
Hot zones, vibration areas (avoidable) |
|
Cost = 0 (non-traversable) |
Strict constraints: no routing allowed |
|
Near-zero inside, ×100 outside |
Impose a mandatory passage point |
Shaped rules are global — pass them in design_rules to
from_project_inputs(), not to individual
RuledVolume objects.
Creating Shapes#
Zones can be defined using any volmdlr solid. The most common approach is to build a simple primitive from a bounding box:
from volmdlr.core import BoundingBox
from volmdlr.shapes import Solid
# A rectangular box zone defined by its extent
bbox = BoundingBox(xmin=0.3, xmax=0.5, ymin=0.5, ymax=0.8, zmin=0.1, zmax=0.3)
box_shape = Solid.make_box(
length=bbox.x_length,
width=bbox.y_length,
height=bbox.z_length,
frame=bbox.to_frame(),
frame_centered=True,
name="my_zone",
)
# A cone-shaped zone
cone_shape = Solid.make_cone(
radius1=0.05,
radius2=0.15,
height=0.3,
frame=bbox.to_frame(),
name="cone_zone",
)
You can also use a solid imported from a STEP file for complex zone shapes.
Note
Whether a voxel or a port extension lies inside a zone is decided from the
triangulated mesh of the shape, so the shape must be closed and consistently
oriented. volmdlr.shapes.Solid, volmdlr.shapes.Shell and VolumeModel
qualify. The deprecated volmdlr.primitives3d.Block and
volmdlr.shells.ClosedShell3D do not: their triangulation carries inconsistent
normals, the inside of the shape is never detected, and the zone silently has no
effect. Build boxes with Solid.make_box as shown above.
Attractive Zone#
An attractive zone reduces the routing cost inside the shape — the router will prefer to pass through this area.
from routing.core.design_rules import ShapedWeightingRule
from volmdlr.core import BoundingBox
from volmdlr.shapes import Solid
# Define a support corridor
bbox = BoundingBox(xmin=0.7, xmax=0.8, ymin=0.5, ymax=0.6, zmin=0.0, zmax=0.3)
support_shape = Solid.make_cone(
radius1=0.05, radius2=0.15, height=0.3,
frame=bbox.to_frame(), name="support_zone",
)
attractive_rule = ShapedWeightingRule.attractive_zone(
shape=support_shape,
strength=1e-6, # near-zero multiplier: very strongly attractive
name="support_corridor",
)
Parameter:
strength (float, 0 < strength < 1): Weight multiplier inside the zone. A value of
0.1makes routing through the zone 10× cheaper.1e-6makes it almost free (path will almost always pass through if reachable).
Tip
Use strength=0.1 for a mild preference. Use strength=1e-6 when you want to
guarantee the path goes through (but without hard-blocking other routes).
Repulsive Zone#
A repulsive zone increases the routing cost inside the shape — the router avoids it but can still route through if there is no other option.
bbox = BoundingBox(xmin=0.3, xmax=0.5, ymin=0.5, ymax=0.8, zmin=0.1, zmax=0.3)
hot_shape = Solid.make_box(
length=bbox.x_length, width=bbox.y_length, height=bbox.z_length,
frame=bbox.to_frame(), frame_centered=True, name="hot_zone",
)
repulsive_rule = ShapedWeightingRule.repulsive_zone(
shape=hot_shape,
strength=1000, # weight × 1000 inside: very strongly avoided
name="hot_zone",
)
Parameter:
strength (float, > 1): Weight multiplier inside the zone.
strength=5makes routing through the zone 5× more expensive.strength=1000makes it very unlikely but not impossible.
Repulsive zones are suitable for areas that are undesirable but not strictly forbidden (e.g., thermal hot-spots, vibration sources).
Forbidden Zone#
A forbidden zone blocks all routing inside it — voxels inside the shape are marked as non-traversable.
bbox = BoundingBox(xmin=0.35, xmax=0.65, ymin=-0.3, ymax=-0.15, zmin=0.2, zmax=0.45)
forbidden_shape = Solid.make_box(
length=bbox.x_length, width=bbox.y_length, height=bbox.z_length,
frame=bbox.to_frame(), frame_centered=True, name="vibration_zone",
)
forbidden_rule = ShapedWeightingRule.forbidden_zone(
shape=forbidden_shape,
cut_ports=False, # raise an error if a port falls inside the zone
name="vibration_constraint",
)
Parameter:
cut_ports (bool, default False): Controls what happens when a port’s straight extension passes through the forbidden zone.
False: raise aValueErrorif the port extension enters the zone.True: silently truncate the port extension at the zone boundary.
Warning
If a port itself (not just its extension) is inside the forbidden zone, a
ValueError is always raised regardless of cut_ports. Move the port or the
zone.
Forced Zone#
A forced zone requires the path to pass through the shape. It applies a near-zero
cost inside (×1e-16) and a high cost outside (×100), making the router strongly
prefer any path that passes through.
bbox = BoundingBox(xmin=0.7, xmax=0.9, ymin=-0.4, ymax=-0.15, zmin=0.2, zmax=0.45)
forced_shape = Solid.make_box(
length=bbox.x_length, width=bbox.y_length, height=bbox.z_length,
frame=bbox.to_frame(), frame_centered=True, name="mandatory_zone",
)
forced_rule = ShapedWeightingRule.forced_zone(
shape=forced_shape,
name="mandatory_passage",
)
Forced zones are suitable for pre-defined passage points — for example, a grommet, a bulkhead penetration, or a routing tray that all pipes must pass through.
Note
forced_zone does not guarantee the path passes through the zone if the zone is
geometrically unreachable from both ports. In that case, the router will find the
lowest-cost path that goes as close as possible to the zone.
Combining Multiple Zones#
Pass all shaped rules together with other global rules to
CadMap.from_project_inputs():
from routing.core.cadmap import CadMap
from routing.core.design_rules import TurnPenalizer, ShapedWeightingRule
design_rules = [
TurnPenalizer(cost=1.0),
attractive_rule,
forced_rule,
forbidden_rule,
repulsive_rule,
]
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,
weights_operator="min_distance", # recommended when using shaped zones;
# default is "min"
)
Shaped rules are applied after the distance-based weighting rules, so they override the base weights inside the zone.
Complete Example#
from volmdlr.core import BoundingBox
from volmdlr.model import VolumeModel
from volmdlr.shapes import Solid
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 (
ShapedWeightingRule,
TurnPenalizer,
WeightingRule,
)
from routing.core.finders import SmartFinder
from routing.core.route_planner import RoutePlanner
import routing.core.legacy_piping as piping
# --- Load assembly ---
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"
)
# --- Define shaped zones ---
# 1. Attractive corridor (cone)
bbox1 = BoundingBox(xmin=0.7, xmax=0.8, ymin=0.5, ymax=0.6, zmin=0.0, zmax=0.3)
attractive_rule = ShapedWeightingRule.attractive_zone(
shape=Solid.make_cone(0.05, 0.15, 0.3, frame=bbox1.to_frame(), name="cone"),
strength=1e-6,
name="support_corridor",
)
# 2. Forbidden region (box)
bbox2 = BoundingBox(xmin=0.35, xmax=0.65, ymin=-0.3, ymax=-0.15, zmin=0.2, zmax=0.45)
forbidden_rule = ShapedWeightingRule.forbidden_zone(
shape=Solid.make_box(
bbox2.x_length, bbox2.y_length, bbox2.z_length,
frame=bbox2.to_frame(), frame_centered=True, name="forbidden_box",
),
cut_ports=True,
name="vibration_zone",
)
# --- Pipe and specification ---
pipe_type = PipeType(
section=piping.Section(radius_equivalent=0.015),
name="PipeA",
radius_of_curvature_ratio_min=1.5,
color=(0, 100, 200),
type_="TypeA",
)
spec = Specification(
start=Port((0.05, -0.4, 0.1), (0, 1, 0), length=0.1),
end=Port((0.05, 0.8, 0.1), (0, -1, 0), length=0.1),
pipe_type=pipe_type,
name="route_A",
)
# --- CadMap with zones as global rules ---
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=1.0), attractive_rule, forbidden_rule],
ruled_volumes=[outer_volume, border_volume],
bounding_box=bounding_box,
voxel_size=voxel_size,
exact_distances=True,
weights_operator="min_distance", # recommended for shaped zones; default is "min"
)
# --- Route ---
route_planner = RoutePlanner(cadmap, SmartFinder("AStar", "manhattan", "never"))
result = route_planner.generate(specifications=[spec], n_iterations=0) # single best path
result.plot_data_generated().plot()
See the full working example in scripts/simple_test_cases/shaped_zone_example.py.
Next Steps#
Design Rules Reference — complete reference for all rule types
Tutorial: Engineering Design Rules — distance-based rules (WeightingRule, GravityRule, etc.)
User Guide: Pipe Pooling (Bundling) — bundle compatible pipes into packs