Tutorial: Shaped Routing Zones =============================== :class:`~routing.core.design_rules.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. .. contents:: On this page :local: :depth: 1 Overview -------- .. code-block:: python from routing.core.design_rules import ShapedWeightingRule All four zone types are created via class-method constructors: .. list-table:: :header-rows: 1 :widths: 25 20 55 * - 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 :meth:`~routing.core.cadmap.CadMap.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: .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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()``: .. code-block:: python 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 ---------------- .. code-block:: python 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 ---------- - :doc:`../user_guide/design_rules_reference` — complete reference for all rule types - :doc:`design_rules` — distance-based rules (WeightingRule, GravityRule, etc.) - :doc:`../user_guide/pooling` — bundle compatible pipes into packs