User Guide: Design Rules Reference#

This page is the complete parameter reference for every design rule class in routing.core.design_rules.

Where to Attach Rules#

Design rules can be attached in three places:

  1. Per-volume rules — attach to a RuledVolume via its design_rules list. The rule applies relative to that CAD surface’s distance field.

    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"
    )
    

    The CadMap evaluates the weighting rules of its volumes once, when it is built. A volume rule changed afterwards takes effect once the weight fields are recomputed:

    for region in cadmap.regions:
        region.weights = region.compute_weights(cadmap.weighting_volumes)
    

    The CadMap then composes the weights of its volumes under weights_operator; a volume given an overwrite_distance overrides that composition around itself (see User Guide: Core Concepts).

  2. Global rules — pass to CadMap.from_project_inputs(design_rules=[...]) . These rules apply everywhere in the routing space.

    cadmap = CadMap.from_project_inputs(
        design_rules=[TurnPenalizer(cost=3.0), GravityRule(min_value=0.02)],
        ruled_volumes=[...],
        # bounding_box=..., voxel_size=...
    )
    
  3. Harness rules — pass to HarnessSettings(harness_rules=[...]). These are added on top of the cadmap’s own rules (which are always carried) and only make sense once the cables are bundled — see Design Rules on the Harness.

    from routing.core.design_rules import BranchPortDistanceRule
    from routing.core.harness import HarnessSettings
    
    settings = HarnessSettings(
        harness_rules=[BranchPortDistanceRule(branches_min_distance=0.05)]
    )
    

The table below shows where each rule can be used:

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 each conduit, with min_value = length_before_turn_min + 2 × bend radius whenever that sum is not zero

ClampingConstraint

✓

—

Requires DistanceAttributeToggler("clampable", ...) on the same volume. Can be set for each PipeType with UserProperty(attribute="clampable", ...) in PipeType.user_properties and the ClampingConstraint in PipeType.user_constraints.

WeightedZone, ForbiddenZone, ForcedZone

—

✓

Zones defined by an arbitrary 3D solid; global rules at CadMap level, see Zones: WeightedZone, ForbiddenZone, ForcedZone

PoolingRule

—

✓

Governs pipe bundling; see User Guide: Pipe Pooling (Bundling)

BranchPortDistanceRule

—

✓

Spacing between branch junctions and between branches and ports; also a harness rule — it constrains the harness optimizer, see BranchPortDistanceRule

BranchClampDistanceRule

—

✓

Clamp spacing at shared-zone boundaries; pair with DistanceAttributeToggler("clampable", ...); see BranchClampDistanceRule

WeightingRule#

Assigns a cost multiplier to voxels based on their distance from a CAD surface.

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,
)

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

TurnPenalizer#

Adds a fixed cost every time the path changes direction.

from routing.core.design_rules import TurnPenalizer

rule = TurnPenalizer(cost=3.0)

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=[...]).

CostRule (base class)#

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: 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.

The search calls these methods at every move, through the rule’s own code: there is nothing to declare and nothing to compile. During the search, node and neighbor stand for two states of the compiled search (SearchStateNode): the cell, the direction the state arrived with, its straight_length and its attribute lengths, and a parent read off the search on demand (see User Guide: The routing.pathfinding Module). Each grid prices the rule to_voxelized_rule(self, voxel_size) returns: the rule itself, with its parameters, by default. Redefine it only when a parameter is a length, to convert it to voxels on a copy.

Example — a turn penalty of your own (the built-in TurnPenalizer does the same, compiled):

from routing.core.design_rules import CostRule

class MyTurnPenalizer(CostRule):

    def __init__(self, cost: float = 50.0, name: str = ""):
        super().__init__(cost=cost, name=name)

    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=[...]).

Note

A cost that depends on the heading the path arrives with — a turn — is priced exactly by TurnPenalizer: the search then keeps one state per cell and arrival heading. With a turn rule of your own, it keeps one state per cell, the cheapest arrival setting the heading: a costlier arrival already aligned with the next move can be discarded, and the path found can cost more than with TurnPenalizer.

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.

from routing.core.design_rules import DistanceAttributeToggler

rule = DistanceAttributeToggler(
    attribute="clampable",
    min_distance=0.0,
    max_distance=0.08,
)

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).

GravityRule#

Enforces a minimum drainage slope for gravity-drained pipes (drain lines, coolant returns). Paths that slope insufficiently receive a cost penalty.

from routing.core.design_rules import GravityRule

rule = GravityRule(
    min_value=0.02,
    max_value=0.5,
)

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.

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.

from routing.core.design_rules import StraightLengthRule

rule = StraightLengthRule(
    min_value=0.030,
    distance_multiplier=2.0,
)

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.

Attach to: CadMap.from_project_inputs(design_rules=[...]).

From the pipe type: each conduit already gets its own StraightLengthRule, with min_value = PipeType.length_before_turn_min + 2 × bend radius, whenever that sum is not zero.

ClampingConstraint#

Specifies how pipe clamps are placed along the route. Works with a DistanceAttributeToggler("clampable", ...) on the same RuledVolume.

from routing.core.design_rules import ClampingConstraint

rule = ClampingConstraint(
    clamp_length=0.04,
    clamp_step=0.25,
)

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", ...).

Zones: WeightedZone, ForbiddenZone, ForcedZone#

A zone is a rule over the voxels an arbitrary 3D shape covers (box, cone, sphere, imported solid). Three kinds of zone exist, one class each.

from routing.core.design_rules import ForbiddenZone, ForcedZone, WeightedZone

# Attractive (coefficient < 1) or repulsive (coefficient > 1)
rule = WeightedZone(my_solid, coefficient=0.1)
rule = WeightedZone(my_solid, coefficient=10.0)

# Forbidden
rule = ForbiddenZone(my_solid, cut_ports=False)

# Forced: every conduit is routed through it
rule = ForcedZone(my_solid)

Constructor parameters:

Parameter

Type

Description

shape

volmdlr Solid, Shell or VolumeModel

The 3D shape defining the zone. Must support triangulation() or to_mesh(). A weighted or forced zone covers the voxels whose centre lies in the shape, a forbidden zone every voxel the shape touches, its interior included — see the supported shapes note.

coefficient

float

WeightedZone only: weight multiplier inside the zone. Default: 0.5.

cut_ports

bool

ForbiddenZone only: if True, truncate a port extension that enters the zone on the last voxel before it rather than raising an error. Default: False.

name

str

Optional identifier.

Zone behaviour summary:

Rule

Effect

WeightedZone, 0 < coefficient < 1

Weight × coefficient inside — the path prefers to enter

WeightedZone, coefficient > 1

Weight × coefficient inside — the path avoids the zone if possible

ForbiddenZone

Weight = 0 — the voxels are non-traversable

ForcedZone

RoutePlanner.generate routes every conduit through the zone, stage by stage toward its doors, zones passed in the order the search reaches them; a conduit that cannot reach it is left unrouted (Solution.is_routed). Through forced zones only the best path is searched, whatever n_iterations. The optimizer does not hold a conduit inside its forced zones yet (fix planned for 3.1.0)

Attach to: CadMap.from_project_inputs(design_rules=[...]). The map exposes them as cadmap.zones, cadmap.weighted_zones and cadmap.forced_zones; the forced zones laid on the routing grids are cadmap.grid_forced_zones().

ShapedWeightingRule and its attractive_zone / repulsive_zone / forbidden_zone / forced_zone constructors are deprecated: they build the zone above and emit a DeprecationWarning.

See Tutorial: Routing Zones for a full tutorial.

PoolingRule#

Controls pipe bundling (grouping pipes of compatible types together). See the dedicated User Guide: Pipe Pooling (Bundling) guide for full details.

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,
    cost=500,
)

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.

cost

float

Cost of a move that rejoins a harness the pipe has left, when allow_loops is False. Default: 500.0.

allow_loops

bool

True (default) lets a pipe rejoin a harness it has left; False charges such moves.

Attach to: CadMap.from_project_inputs(design_rules=[...]).

BranchPortDistanceRule#

Enforces a minimum spacing between branch junctions and between branches and ports. Reach for it on multi-port harnesses where bifurcations must not crowd one another or sit too close to a connector.

from routing.core.design_rules import BranchPortDistanceRule

rule = BranchPortDistanceRule(
    branches_min_distance=0.1,
    port_branch_min_distance=0.06,
)

Parameter

Type

Description

port_branch_min_distance

float

Minimum distance (m) a branch may approach a port. 0.0 disables the check. Default: 0.0.

branches_min_distance

float

Minimum distance (m) between two branch junctions. 0.0 disables the check. Default: 0.0.

max_pipes_per_branch

int

Maximum number of pipes allowed to share a single branch junction. 0 means no limit. Default: 0.

cost

float

Penalty added to a path step that violates one of the distances above. Default: 50.0.

neutral_fiber_condition

bool

Advanced. Use this option only when every pipe within each pack shares the same neutral fiber. This enables a simplification in the computation of forbidden voxels. Default: False

enforcement

RuleEnforcement

How the rule enters the harness optimization problem. None (default) = hard constraint on the shaping stage; see Design Rules on the Harness.

name

str

Optional identifier.

Distances are given in metres; the rule converts them to the routing grid automatically. You only instantiate it and add it to the design rules — the router refreshes the rule’s internal state between routing iterations, so no manual setup is required.

This rule is also the reference harness rule: the same two distances are measured along the bundle’s branches and constrain the harness optimizer, and branches_min_distance is the distance under which close branch endpoints fuse into one junction during skeleton consolidation.

Attach to: CadMap.from_project_inputs(design_rules=[...]) or HarnessSettings(harness_rules=[...]).

See Tutorial: Engineering Design Rules for a worked multi-scale example.

BranchClampDistanceRule#

Enforces clamp spacing at the boundaries of shared (clampable) zones — when a branch enters or leaves an is_shared zone (the packs’ corridors, see Loops: Leaving and Rejoining a Harness), and across clampable transitions. Use it to guarantee that a branch is properly clamped where it joins or leaves a bundle.

This rule reads the clampable and is_shared voxel attributes, so it must be paired with a DistanceAttributeToggler setting the "clampable" attribute (the same pairing used by ClampingConstraint).

from routing.core.design_rules import BranchClampDistanceRule

rule = BranchClampDistanceRule(
    branch_clamp_min_distance=0.05,
    clamps_min_distance=0.1,
    clamps_max_distance=0.3,
)

Parameter

Type

Description

branch_clamp_min_distance

float

Minimum distance (m) between a branch junction and a cable clamp. A branch must stay clamped for at least this distance when entering or exiting a shared zone with previously generated cables.

clamps_min_distance

float

Minimum distance (m) between 2 clamps. Clampable stretches shorter than this are penalized.

clamps_max_distance

float

Maximum distance (m) between two clamps. Used to verify whether the spacing is valid when entering or leaving a branch under non-clampable conditions.

cost

float

Penalty added to a path step that violates one of the distances above. Default: 50.0.

name

str

Optional identifier.

Distances are given in metres; the rule converts them to the routing grid automatically.

Attach to: CadMap.from_project_inputs(design_rules=[...]). Requires a DistanceAttributeToggler("clampable", ...) on the relevant RuledVolume.

Design Rules on the Harness#

A rule has two projections. On the routing grid it steers the pathfinder (weights, step costs, heuristics). On the harness it constrains the optimizer that shapes the bundled geometry: a rule that measures something along the bundle’s branches implements evaluate_on_bundle(measures), returning one violation per atomic constraint under the value <= 0 = satisfied convention. Today BranchPortDistanceRule is the rule that does so.

Nothing has to be wired: every rule carried by the harness that contributes at least one entry becomes a term of the optimization problem automatically (OptimizationTerm.from_bundle). enforcement is how the rule declares the form that term takes — so a rule is declared once, with both of its projections.

from routing.core.design_rules import BranchPortDistanceRule, RuleEnforcement

# Default: hard constraint on the shaping stage
strict = BranchPortDistanceRule(branches_min_distance=0.05)

# Soft: a weighted cost the optimizer trades against length
preferred = BranchPortDistanceRule(
    branches_min_distance=0.05,
    enforcement=RuleEnforcement(hard=False, weight=50.0, power=2),
)

RuleEnforcement

Default

Description

hard

True

Pose the rule as a hard constraint (the solver must satisfy it). False poses it as a soft cost shaped by weight and power.

weight

1.0

Soft form only: multiplier of the aggregated violation in the objective.

power

1

Soft form only: exponent applied to each violation before summing.

tolerance

0.0

Hard form only: offset subtracted from each violation, i.e. the slack granted.

stages

["shape"]

Optimization stages whose problem the rule joins. Design rules default to the shaping stage only, being antagonistic to consolidation.

Because a hard constraint can make a cramped harness infeasible, the usual first move on a harness that will not converge is to read the violations (HarnessOptimizer.diagnose(bundle, stage="shape")) rather than to soften the rule. See User Guide: Harness Generation for the pipeline and its settings.

ConstraintRule (base class)#

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: 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. As for a cost rule, the search calls it through your own code, with nothing to declare or compile.

node.get_length(attribute) and node.get_length(f"no_{attribute}") count the cells the path has run holding, or lacking, the attribute, in cells of the grid hosting the move; to_voxelized_rule is where a bound in metres becomes cells. A move the heuristic weighs on is charged the violation penalty of 500 in the cost of the found path, and listed under the rule’s class name in GridPath.indicators.

fast_check(self, voxels, voxel_size, bool_matrix)

Called on a complete candidate path during post-routing optimization. voxels holds the path’s voxel indices, an (N, 3) int array or a list of (x, y, z) 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.

import numpy as np

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 a path running through voxels without the attribute."""
        voxels = np.asarray(voxels, dtype=int).reshape(-1, 3)
        if len(voxels) and not bool_matrix[tuple(voxels.T)].all():
            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 Tutorial: Engineering Design Rules for a working example.