Tutorial: Engineering Design Rules ==================================== Design rules control **how** the router weights the space around your CAD geometry. They let you encode engineering constraints — minimum bend radii, gravity drainage slopes, clamping requirements, clearance zones — directly into the routing cost function. .. contents:: On this page :local: :depth: 1 Overview -------- Design rules are attached either to a :class:`~routing.core.core.RuledVolume` (so they apply relative to a specific CAD surface) or passed globally to :meth:`~routing.core.cadmap.CadMap.from_project_inputs` (so they apply everywhere). .. list-table:: :header-rows: 1 :widths: 30 70 * - Rule class - Purpose * - :class:`~routing.core.design_rules.WeightingRule` - Penalize or prefer paths at certain distances from a surface * - :class:`~routing.core.design_rules.TurnPenalizer` - Add a fixed cost to every direction change * - :class:`~routing.core.design_rules.DistanceAttributeToggler` - Tag voxels in a distance band with a boolean attribute * - :class:`~routing.core.design_rules.GravityRule` - Enforce minimum drainage slope (gravity-drained pipes) * - :class:`~routing.core.design_rules.StraightLengthRule` - Require a minimum straight run before each turn * - :class:`~routing.core.design_rules.ClampingConstraint` - Mark where pipe clamps can be placed and their spacing * - :class:`~routing.core.design_rules.ConstraintRule` - Base class for custom user-defined constraint rules All rule classes live in :mod:`routing.core.design_rules`. .. code-block:: python from routing.core.design_rules import ( WeightingRule, TurnPenalizer, DistanceAttributeToggler, GravityRule, StraightLengthRule, ClampingConstraint, ) WeightingRule ------------- :class:`~routing.core.design_rules.WeightingRule` assigns a cost multiplier to voxels based on their distance from a CAD surface. Higher cost = less preferred path. Use it to make pipes hug the structure rather than floating in free space. .. code-block:: python from routing.core.design_rules import WeightingRule # Weight rises linearly from 1 (at the surface) to 10 (at 200 mm away) weighting_rule = WeightingRule( function="linear", min_length=0.0, # apply from distance 0 m max_length=0.2, # stop applying at 0.2 m min_value=1, # cost at min_length (near surface) max_value=10, # cost at max_length (far from surface) ) **Parameters:** - **function** *(str)*: Shape of the cost curve between ``min_length`` and ``max_length``. Choices: ``"linear"``, ``"sqrt"`` (square root), ``"constant"``. - **min_length** / **max_length** *(float)*: Distance range (metres) over which the rule applies. Outside this range the rule has no effect. - **min_value** / **max_value** *(float)*: Cost multiplier at the near and far ends of the range. A value of 1.0 is neutral; higher values increase path cost. **Typical patterns:** .. code-block:: python # Attract — low cost near surface, higher cost away from it attract_rule = WeightingRule("linear", 0.0, 0.2, min_value=1, max_value=10) # Repel — high cost close to a hot or vibrating surface repel_rule = WeightingRule("linear", 0.0, 0.05, min_value=100, max_value=1) # Constant clearance zone — uniform high cost within 30 mm of an obstacle clearance_rule = WeightingRule("constant", 0.0, 0.03, min_value=50, max_value=50) Attach the rule to a :class:`~routing.core.core.RuledVolume`: .. code-block:: python from routing.core.core import RuledVolume from volmdlr.model import VolumeModel hot_surface = RuledVolume.from_volume_model( VolumeModel([engine_primitive]), design_rules=[repel_rule], name="engine_block", ) TurnPenalizer ------------- :class:`~routing.core.design_rules.TurnPenalizer` adds a fixed cost to every direction change in the path. It encourages straight runs and discourages unnecessary bends. Pass it as a **global** rule (applies to all pipes): .. code-block:: python from routing.core.design_rules import TurnPenalizer turn_penalty = TurnPenalizer(cost=3.0) cadmap = CadMap.from_project_inputs( design_rules=[turn_penalty], ruled_volumes=[...], ... ) **Parameter:** - **cost** *(float, default 50.0)*: Cost added per turn, in units of a single straight-voxel traversal. ``cost=3.0`` means a turn costs as much as traversing 3 more voxels in a straight line. The default of ``50.0`` strongly discourages turns; lower values allow more bends. DistanceAttributeToggler ------------------------ :class:`~routing.core.design_rules.DistanceAttributeToggler` tags all voxels within a distance band with a boolean attribute. The tagged attribute is then used by constraint rules (e.g. ``ClampingConstraint``) to identify valid locations. .. code-block:: python from routing.core.design_rules import DistanceAttributeToggler # Tag voxels 0–80 mm from the surface as "clampable" clamping_toggler = DistanceAttributeToggler( attribute="clampable", min_distance=0.0, max_distance=0.08, ) # Tag voxels 50–200 mm from the surface as "user_defined" zone_toggler = DistanceAttributeToggler( attribute="user_defined", min_distance=0.05, max_distance=0.20, ) **Parameters:** - **attribute** *(str)*: Name of the boolean attribute to set on matching voxels. Standard names: ``"clampable"`` (used by ``ClampingConstraint``), ``"user_defined"`` (available for custom constraint rules). - **min_distance** / **max_distance** *(float)*: Distance range (metres) from the surface. Only voxels within this band receive the attribute. Attach to a ``RuledVolume`` like any other rule: .. code-block:: python structural_beam = RuledVolume.from_volume_model( VolumeModel([beam_primitive]), design_rules=[clamping_toggler], name="main_beam", ) GravityRule ----------- :class:`~routing.core.design_rules.GravityRule` enforces a minimum drainage slope for gravity-drained pipes (e.g. drain lines, coolant returns). Paths that slope insufficiently receive a high cost. To add a ``GravityRule`` common to all pipes, create an instance and add it to the ``design_rules`` of the problem's ``CadMap``. .. code-block:: python from routing.core.design_rules import GravityRule gravity_rule = GravityRule( min_value=0.02, # minimum 2% slope (0.02 m/m) max_value=0.5, # maximum 50% slope (optional cap) distance_multiplier=1.0, # weight multiplier for violations ) To get a ``GravityRule`` specific to a ``PipeType``, set the corresponding ``PipeType``'s ``min_slope`` and/or ``max_slope`` attributes so the router adapts routing constraint for concerned pipes: .. code-block:: python from routing.core.core import PipeType from piping_3d import piping drain_pipe = PipeType( section=piping.Section(radius_equivalent=0.025), name="DrainLine", radius_of_curvature_ratio_min=3.0, min_slope=0.02, # minimum 2% grade — must match the GravityRule color=(0, 180, 100), type_="Drain", ) **Parameters:** - **min_value** *(float)*: Minimum acceptable slope in m/m (e.g. ``0.02`` = 2%). - **max_value** *(float, optional)*: Maximum acceptable slope in m/m. Set to ``None`` to allow any upward slope. .. note:: ``GravityRule`` is an **automatic** rule — it is automatically instantitated by the CadMap object when interpreting PipeType. So to use a ``GravityRule`` for a specific ``Specification`` just specify min and max slopes in PipeType. In order to use the same ``GravityRule`` for all ``Specification``, one can build a ``GravityRule`` as a **global** rule — pass it in ``design_rules`` to ``CadMap.from_project_inputs``, not to individual ``RuledVolume`` objects. StraightLengthRule ------------------ :class:`~routing.core.design_rules.StraightLengthRule` penalizes turns that occur before a minimum straight segment. Use this when your pipe or fitting catalogue requires a certain run-in distance before each bend. .. code-block:: python from routing.core.design_rules import StraightLengthRule straight_rule = StraightLengthRule( min_value=0.030, # 30 mm straight required before each turn distance_multiplier=2.0, # violation cost multiplier ) Also set ``length_before_turn_min`` on the ``PipeType``: .. code-block:: python rigid_pipe = PipeType( section=piping.Section(radius_equivalent=0.010), name="RigidTube", radius_of_curvature_ratio_min=5.0, length_before_turn_min=0.030, # same value as StraightLengthRule color=(200, 100, 0), type_="Rigid", ) **Parameters:** - **min_value** *(float)*: Minimum straight run (metres) required before each direction change. - **distance_multiplier** *(float, default 1.0)*: Scales the cost for violating the straight-length requirement. .. note:: ``StraightLengthRule`` is also an **automatic** rule — it is automatically instantitated by the CadMap object when interpreting PipeType. So to use a ``StraightLengthRule`` for a specific ``Specification`` just specify the ``min_straight_length`` attribute of PipeType. In order to use the same ``StraightLengthRule`` for all ``Specification``, one can build a ``StraightLengthRule`` as a **global** rule — pass it in ``design_rules`` to ``CadMap.from_project_inputs``, not to individual ``RuledVolume`` objects. ClampingConstraint ------------------ :class:`~routing.core.design_rules.ClampingConstraint` specifies how clamps (pipe supports) are to be placed along the routed pipe. It works in combination with a ``DistanceAttributeToggler`` that marks which voxels are close enough to a surface to accept a clamp. .. code-block:: python from routing.core.design_rules import ClampingConstraint clamping_rule = ClampingConstraint( clamp_length=0.04, # clamp point is 40 mm from the surface clamp_step=0.25, # clamps must be spaced ≥ 250 mm apart distance_multiplier=1.0, # weight multiplier for clamping cost ) This rule is attached to the same ``RuledVolume`` as the ``DistanceAttributeToggler`` that sets the ``"clampable"`` attribute: .. code-block:: python support_frame = RuledVolume.from_volume_model( VolumeModel([frame_primitive]), design_rules=[ DistanceAttributeToggler("clampable", 0.0, 0.08), ClampingConstraint(clamp_length=0.04, clamp_step=0.25), ], name="support_frame", ) **Parameters:** - **clamp_length** *(float)*: Distance from the surface at which a clamp sits (metres). Should match the physical clamp geometry. - **clamp_step** *(float)*: Minimum spacing between successive clamps along the pipe (metres). - **distance_multiplier** *(float, default 1.0)*: Scaling factor for the clamping cost contribution. Custom Constraint Rules ----------------------- You can subclass :class:`~routing.core.design_rules.ConstraintRule` to define your own constraint logic. There are two distinct methods to override, each serving a different purpose: - **``compute_heuristic``** is called at *every A\* step* during pathfinding. It steers the pathfinder in real time — returning a penalty causes the algorithm to avoid that move. Without this method the pathfinder will happily explore violating paths and only detect the problem afterward. - **``fast_check``** is called on a *complete candidate path* during post-routing optimization. It validates a batch of voxels once the route has been found. A robust custom constraint implements **both**: .. code-block:: python from routing.core.design_rules import ConstraintRule class UserDefinedConstraint(ConstraintRule): """Only allow routing through voxels tagged 'user_defined'.""" def __init__(self, name: str = ""): super().__init__(attribute="user_defined", name=name) def compute_heuristic(self, node, neighbor, vector): """Real-time enforcement: penalize moves toward un-tagged voxels.""" if not neighbor.get_custom_attr(self.attribute): return self.distance_multiplier return 0.0 def fast_check(self, voxels, voxel_size, bool_matrix): """Post-routing validation: penalize any segment missing the attribute.""" # bool_matrix is True where the 'user_defined' attribute is set if any(bool_matrix[v] for v in voxels): return self.distance_multiplier return 0.0 Use your constraint exactly like the built-in ones — attach it to a ``RuledVolume``. The ``attribute`` name passed to ``super().__init__()`` must match the ``DistanceAttributeToggler`` on the same volume: .. code-block:: python structure = RuledVolume.from_volume_model( VolumeModel([structure_primitive]), design_rules=[ DistanceAttributeToggler("user_defined", 0.05, 0.20), UserDefinedConstraint(), ], name="structure", ) Putting It All Together ----------------------- Here is a complete example that combines several design rules for a multi-pipe scenario with gravity drainage and clamping requirements: .. code-block:: python from volmdlr.step import Step from volmdlr.model import VolumeModel from piping_3d import piping from routing.core.cadmap import CadMap from routing.core.core import Port, PipeType, RuledVolume, Specification from routing.core.design_rules import ( ClampingConstraint, DistanceAttributeToggler, GravityRule, StraightLengthRule, TurnPenalizer, WeightingRule, ) from routing.core.finders import SmartFinder from routing.core.route_planner import RoutePlanner # --- Load CAD --- model = Step.from_file("data/step/my_assembly.step") volume_model = model.to_volume_model() # --- Per-volume rules --- structure_volume = RuledVolume.from_volume_model( VolumeModel([volume_model.primitives[0]]), design_rules=[ # Prefer routing near the surface WeightingRule("linear", 0.0, 0.2, min_value=1, max_value=8), # Tag near-surface voxels as clampable DistanceAttributeToggler("clampable", 0.0, 0.08), # Clamp spacing: 250 mm apart, 40 mm from surface ClampingConstraint(clamp_length=0.04, clamp_step=0.25), ], name="main_structure", ) hot_component = RuledVolume.from_volume_model( VolumeModel([volume_model.primitives[1]]), design_rules=[ # Keep pipes at least 50 mm away from this hot component WeightingRule("constant", 0.0, 0.05, min_value=500, max_value=500), ], name="hot_component", ) # --- Global rules --- global_rules = [ TurnPenalizer(cost=3.0), GravityRule(min_value=0.02), StraightLengthRule(min_value=0.03), ] # --- Drain pipe type (requires slope) --- drain_pipe = PipeType( section=piping.Section(radius_equivalent=0.012), name="DrainLine", radius_of_curvature_ratio_min=3.0, length_before_turn_min=0.03, min_slope=0.02, color=(0, 180, 100), type_="Drain", ) # --- Ports and specification --- spec = Specification( start=Port((0.05, -0.3, 0.2), (0, 1, 0), length=0.05, name="drain_in"), end=Port((0.3, 0.8, 0.05), (0, -1, 0), length=0.05, name="drain_out"), pipe_type=drain_pipe, name="drain_route", ) # --- Build CadMap --- voxel_size = 0.018 bounding_box = CadMap.build_bounding_volume( [structure_volume, hot_component], [], voxel_size ) cadmap = CadMap.from_project_inputs( design_rules=global_rules, ruled_volumes=[structure_volume, hot_component], bounding_box=bounding_box, voxel_size=voxel_size, exact_distances=True, ) # --- Route --- route_planner = RoutePlanner(cadmap, SmartFinder("AStar", "manhattan", "never")) result = route_planner.generate(specifications=[spec], n_iterations=1) result.plot_data_generated().plot() Next Steps ---------- - :doc:`shaped_zones` — define spatial zones that routing must avoid, prefer, or pass through - :doc:`../user_guide/design_rules_reference` — complete parameter reference for all rule types - :doc:`../user_guide/pooling` — bundle compatible pipes into organized packs - :doc:`../user_guide/optimization` — tune the geometry optimizer