Design Rules Reference ======================= This page is the complete parameter reference for every design rule class in :mod:`routing.core.design_rules`. .. contents:: On this page :local: :depth: 1 Where to Attach Rules --------------------- Design rules can be attached in two places: 1. **Per-volume rules** — attach to a :class:`~routing.core.core.RuledVolume` via its ``design_rules`` list. The rule applies relative to that CAD surface's distance field. .. code-block:: python from routing.core.core import RuledVolume from routing.core.design_rules import WeightingRule volume = RuledVolume.from_volume_model( volume_model, design_rules=[WeightingRule(...)], name="v1" ) 2. **Global rules** — pass to ``CadMap.from_project_inputs(design_rules=[...])`` . These rules apply everywhere in the routing space. .. code-block:: python cadmap = CadMap.from_project_inputs( design_rules=[TurnPenalizer(cost=3.0), GravityRule(min_value=0.02)], ruled_volumes=[...], ... ) The table below shows where each rule can be used: .. list-table:: :header-rows: 1 :widths: 35 15 15 35 * - Rule - Per-volume - Global - Notes * - ``WeightingRule`` - ✓ - — - Requires a distance field; attach to a volume * - ``TurnPenalizer`` - — - ✓ - Applies at every direction change * - ``DistanceAttributeToggler`` - ✓ - — - Tags voxels; usually paired with ``ClampingConstraint`` * - ``GravityRule`` - — - ✓ - Automatically instantiated if ``PipeType.min_slope`` and/or ``PipeType.max_slope`` are set in PipeType ; Can be instantiated at CadMap level for being applied to all ``Specification`` * - ``StraightLengthRule`` - — - ✓ - Automatically instantiated for a specific ``Specification`` if ``PipeType.length_before_turn_min != 0`` * - ``ClampingConstraint`` - ✓ - — - Requires ``DistanceAttributeToggler("clampable", ...)`` on the same volume. Can be set for each ``PipeType`` with a ``UserProperty`` which name is set to "clampable". * - ``ShapedWeightingRule`` - — - ✓ - Zone defined by an arbitrary 3D solid ; instantiated as a global_rule at ``CadMap`` level. * - ``PoolingRule`` - — - ✓ - Governs pipe bundling; see :doc:`pooling` .. _ref-weighting-rule: WeightingRule ------------- Assigns a cost multiplier to voxels based on their **distance from a CAD surface**. .. code-block:: python from routing.core.design_rules import WeightingRule rule = WeightingRule( function="linear", min_length=0.0, max_length=0.2, min_value=1, max_value=10, ) .. list-table:: :header-rows: 1 :widths: 25 15 60 * - Parameter - Type - Description * - ``function`` - ``str`` - Shape of the cost curve. One of: ``"linear"``, ``"sqrt"``, ``"constant"``. * - ``min_length`` - ``float`` - Distance (m) at which the rule starts applying. Typically ``0.0`` (at the surface). * - ``max_length`` - ``float`` - Distance (m) at which the rule stops applying. Beyond this, the rule has no effect. * - ``min_value`` - ``float`` - Cost multiplier at ``min_length``. ``1.0`` is neutral; lower = cheaper. * - ``max_value`` - ``float`` - Cost multiplier at ``max_length``. **Cost function shapes:** - ``"linear"``: cost = min_value + (max_value - min_value) × (d / max_length) - ``"sqrt"``: cost increases with the square root of distance (gentler than linear) - ``"constant"``: cost = min_value everywhere in the range **Attach to**: ``RuledVolume.design_rules`` .. _ref-turn-penalizer: TurnPenalizer ------------- Adds a fixed cost every time the path changes direction. .. code-block:: python from routing.core.design_rules import TurnPenalizer rule = TurnPenalizer(cost=3.0) .. list-table:: :header-rows: 1 :widths: 25 15 60 * - Parameter - Type - Description * - ``cost`` - ``float`` - Cost penalty per turn, in units of a single straight-voxel step. Default: ``50.0``. The penalty is zero when the direction is unchanged and ``cost`` when the direction changes (any angle). Increase this to produce longer but straighter paths. **Attach to**: ``CadMap.from_project_inputs(design_rules=[...])``. .. _ref-cost-rule: CostRule (base class) --------------------- :class:`~routing.core.design_rules.CostRule` is the base class for building custom per-step cost rules (like ``TurnPenalizer``). Subclass it and override: ``compute_cost(self, node, neighbor, vector) → float`` Called for **every candidate edge** during pathfinding. Return the additional cost of moving from *node* to *neighbor*. Return ``0.0`` for no penalty. - ``node`` / ``neighbor``: :class:`~routing.pathfinding.core.node_compiled.SmartGridNode` - ``vector``: direction tuple ``(dx, dy, dz)`` of the candidate move ``CostRule.compute_heuristic`` always returns ``0.0`` by design — cost rules add traversal cost rather than heuristic penalties. Example — reproducing ``TurnPenalizer``'s logic: .. code-block:: python from routing.core.design_rules import CostRule class MyTurnPenalizer(CostRule): def __init__(self, cost: float = 50.0): super().__init__() self.cost = cost def compute_cost(self, node, neighbor, vector): if node.direction == vector: return 0.0 # no direction change, no penalty return self.cost # any turn costs self.cost Pass the rule as a global rule to ``CadMap.from_project_inputs(design_rules=[...])``. .. _ref-distance-attribute-toggler: DistanceAttributeToggler ------------------------ Tags voxels within a distance band with a boolean attribute. Other constraint rules (e.g. ``ClampingConstraint``) read these attributes to decide where they apply. .. code-block:: python from routing.core.design_rules import DistanceAttributeToggler rule = DistanceAttributeToggler( attribute="clampable", min_distance=0.0, max_distance=0.08, ) .. list-table:: :header-rows: 1 :widths: 25 15 60 * - Parameter - Type - Description * - ``attribute`` - ``str`` - Name of the boolean attribute to set. Standard names: ``"clampable"`` (for ``ClampingConstraint``), ``"user_defined"`` (for custom constraint rules). * - ``min_distance`` - ``float`` - Near edge of the distance band (m). * - ``max_distance`` - ``float`` - Far edge of the distance band (m). Voxels beyond this are not tagged. **Attach to**: ``RuledVolume.design_rules`` (same volume as ``ClampingConstraint``). .. _ref-gravity-rule: GravityRule ----------- Enforces a minimum drainage slope for gravity-drained pipes (drain lines, coolant returns). Paths that slope insufficiently receive a cost penalty. .. code-block:: python from routing.core.design_rules import GravityRule rule = GravityRule( min_value=0.02, max_value=0.5, ) .. list-table:: :header-rows: 1 :widths: 25 15 60 * - Parameter - Type - Description * - ``min_value`` - ``float`` - Minimum required slope in m/m. Example: ``0.02`` = 2% grade. * - ``max_value`` - ``float`` - Maximum acceptable slope in m/m. Set to ``None`` for no upper limit. * - ``distance_multiplier`` - ``float`` - Scales the penalty for slope violations. Default: ``500.0``. Also set ``PipeType.min_slope`` to the same value so the router knows which pipe types require slope enforcement. **Attach to**: ``CadMap.from_project_inputs(design_rules=[...])`` for a global requirement. **Only Specify in PipeType**: ``min_slope`` and/or ``max_slope`` for a constraint specific to a ``PipeType``. .. _ref-straight-length-rule: StraightLengthRule ------------------ Penalizes turns that occur before a minimum straight run. 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 rule = StraightLengthRule( min_value=0.030, distance_multiplier=2.0, ) .. list-table:: :header-rows: 1 :widths: 25 15 60 * - Parameter - Type - Description * - ``min_value`` - ``float`` - Minimum straight segment (m) required before each turn. * - ``distance_multiplier`` - ``float`` - Scales the penalty cost for violations. Default: ``500.0``. Also set ``PipeType.length_before_turn_min`` to the same value. **Attach to**: ``CadMap.from_project_inputs(design_rules=[...])``. **Only Specify in PipeType**: ``length_before_turn_min != 0`` for a constraint specific to a ``PipeType``. .. _ref-clamping-constraint: ClampingConstraint ------------------ Specifies how pipe clamps are placed along the route. Works with a ``DistanceAttributeToggler("clampable", ...)`` on the same ``RuledVolume``. .. code-block:: python from routing.core.design_rules import ClampingConstraint rule = ClampingConstraint( clamp_length=0.04, clamp_step=0.25, ) .. list-table:: :header-rows: 1 :widths: 25 15 60 * - Parameter - Type - Description * - ``clamp_length`` - ``float`` - Distance from the surface (m) at which a clamp sits. Should match the physical clamp bracket depth. * - ``clamp_step`` - ``float`` - Minimum spacing between successive clamps along the pipe (m). * - ``distance_multiplier`` - ``float`` - Scales the clamping cost contribution. Default: ``19.0``. **Attach to**: ``RuledVolume.design_rules``, on the same volume as the ``DistanceAttributeToggler("clampable", ...)``. .. _ref-shaped-weighting-rule: ShapedWeightingRule ------------------- Applies weight modifications based on whether a voxel is **inside an arbitrary 3D shape** (box, cone, sphere, imported solid). Four zone types are available. .. code-block:: python from routing.core.design_rules import ShapedWeightingRule # Attractive rule = ShapedWeightingRule.attractive_zone(shape=my_solid, strength=0.1) # Repulsive rule = ShapedWeightingRule.repulsive_zone(shape=my_solid, strength=10.0) # Forbidden rule = ShapedWeightingRule.forbidden_zone(shape=my_solid, cut_ports=False) # Forced rule = ShapedWeightingRule.forced_zone(shape=my_solid) **Constructor parameters (direct):** .. list-table:: :header-rows: 1 :widths: 25 15 60 * - Parameter - Type - Description * - ``shape`` - volmdlr ``Solid`` or ``VolumeModel`` - The 3D shape defining the zone extent. Must support ``triangulation()``. * - ``coefficient`` - ``float`` - Weight multiplier inside the zone. Meaning depends on ``zone_type``. * - ``zone_type`` - ``str`` - One of: ``"attractive"``, ``"repulsive"``, ``"forbidden"``, ``"forced"``. * - ``cut_ports`` - ``bool`` - For forbidden zones only: if ``True``, silently truncate port extensions that enter the zone rather than raising an error. Default: ``False``. * - ``name`` - ``str`` - Optional identifier. **Zone behaviour summary:** .. list-table:: :header-rows: 1 :widths: 20 20 60 * - ``zone_type`` - ``coefficient`` meaning - Effect inside the shape * - ``"attractive"`` - 0 < coeff < 1 (lower = stronger) - Weight × coeff — path prefers to enter * - ``"repulsive"`` - coeff > 1 (higher = stronger) - Weight × coeff — path avoids if possible * - ``"forbidden"`` - ignored - Weight = 0 — voxels are non-traversable * - ``"forced"`` - ignored - Weight × 1e-16 inside; × 100 outside — path must enter **Attach to**: ``CadMap.from_project_inputs(design_rules=[...])``. See :doc:`../tutorials/shaped_zones` for a full tutorial. .. _ref-pooling-rule: PoolingRule ----------- Controls pipe bundling (grouping pipes of compatible types together). See the dedicated :doc:`pooling` guide for full details. .. code-block:: python from routing.core.design_rules import ( PoolingMode, PoolingSpecification, PoolingRule ) pooling_specs = [ PoolingSpecification( types=["TypeA", "TypeB"], pooling_mode=PoolingMode.strong_pooling(), min_distance=0.0, parallel=False, ) ] rule = PoolingRule( pooling_specs=pooling_specs, min_distance=0.0, distance_multiplier=500, volume_indices=[0, 1], ) .. list-table:: :header-rows: 1 :widths: 25 15 60 * - Parameter - Type - Description * - ``pooling_specs`` - list of ``PoolingSpecification`` - Specifies which pipe type pairs are pooled or segregated, and how strongly. * - ``min_distance`` - ``float`` - Default minimum separation (m) between pipes with no explicit spec. * - ``distance_multiplier`` - ``float`` - Scales the pooling cost contribution. Default: ``500.0``. * - ``volume_indices`` - list of ``int`` - Indices of ``ruled_volumes`` that the pooling rule considers. ``[]`` = all. **Attach to**: ``CadMap.from_project_inputs(design_rules=[...])``. ConstraintRule (base class) --------------------------- :class:`~routing.core.design_rules.ConstraintRule` is the base class for building custom constraint rules. Subclass it to encode any voxel-level constraint not provided by the built-in rules. **Methods to override:** ``compute_heuristic(self, node, neighbor, vector)`` Called at **every step during A*** to steer the pathfinder in real time. Return ``0.0`` if the move from *node* to *neighbor* is allowed; return ``self.distance_multiplier`` (or another penalty) to discourage it. - ``node`` / ``neighbor``: :class:`~routing.pathfinding.core.node_compiled.SmartGridNode` - ``vector``: direction tuple ``(dx, dy, dz)`` of the candidate move Without this method, pathfinding ignores your constraint; it will find paths that violate it and only detect the violation after the fact. ``fast_check(self, voxels, voxel_size, bool_matrix)`` Called on a **complete candidate path** during post-routing optimization. ``voxels`` is a list of ``(x, y, z)`` index tuples; ``bool_matrix`` is a numpy bool array marking where the rule's attribute is active. Return ``0.0`` if satisfied, or a penalty cost otherwise. A full custom constraint typically implements **both**. .. code-block:: python from routing.core.design_rules import ConstraintRule class UserDefinedConstraint(ConstraintRule): """Allow routing only 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 check: penalize moves toward voxels not tagged 'user_defined'.""" 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 check: penalize any path segment missing the attribute.""" if any(bool_matrix[v] for v in voxels): return self.distance_multiplier return 0.0 .. note:: ``ConstraintRule.__init__`` requires ``attribute: str`` as a positional argument. Every subclass must call ``super().__init__(attribute="your_attribute_name")``. The ``attribute`` name must match the ``DistanceAttributeToggler`` used on the same ``RuledVolume``. Use a ``DistanceAttributeToggler`` on the relevant ``RuledVolume`` to set the attribute that your custom rule reads. See :doc:`../tutorials/design_rules` for a working example.