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

ShapedWeightingRule.attractive_zone(shape, strength)

Cost × strength (< 1, lower = stronger)

Support corridors, preferred routeways

ShapedWeightingRule.repulsive_zone(shape, strength)

Cost × strength (> 1, higher = stronger)

Hot zones, vibration areas (avoidable)

ShapedWeightingRule.forbidden_zone(shape)

Cost = 0 (non-traversable)

Strict constraints: no routing allowed

ShapedWeightingRule.forced_zone(shape)

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.1 makes routing through the zone 10× cheaper. 1e-6 makes 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=5 makes routing through the zone 5× more expensive. strength=1000 makes 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 a ValueError if 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#