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.

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