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.
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
from piping_3d import 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