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:
Per-volume rules — attach to a
RuledVolumevia itsdesign_ruleslist. 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
CadMapevaluates 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
CadMapthen composes the weights of its volumes underweights_operator; a volume given anoverwrite_distanceoverrides that composition around itself (see User Guide: Core Concepts).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=... )
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 |
|---|---|---|---|
|
✓ |
— |
Requires a distance field; attach to a volume |
|
— |
✓ |
Applies at every direction change |
|
✓ |
— |
Tags voxels; usually paired with |
|
— |
✓ |
Automatically instantiated if |
|
— |
✓ |
Automatically instantiated for each conduit, with |
|
✓ |
— |
Requires |
|
— |
✓ |
Zones defined by an arbitrary 3D solid; global rules at |
|
— |
✓ |
Governs pipe bundling; see User Guide: Pipe Pooling (Bundling) |
|
— |
✓ |
Spacing between branch junctions and between branches and ports; also a harness rule — it constrains the harness optimizer, see BranchPortDistanceRule |
|
— |
✓ |
Clamp spacing at shared-zone boundaries; pair with
|
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 |
|---|---|---|
|
|
Shape of the cost curve. One of: |
|
|
Distance (m) at which the rule starts applying. Typically |
|
|
Distance (m) at which the rule stops applying. Beyond this, the rule has no effect. |
|
|
Cost multiplier at |
|
|
Cost multiplier at |
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 penalty per turn, in units of a single straight-voxel step. Default: |
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) → floatCalled for every candidate edge during pathfinding. Return the additional cost of moving from node to neighbor. Return
0.0for no penalty.node/neighbor:SmartGridNodevector: 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 |
|---|---|---|
|
|
Name of the boolean attribute to set. Standard names: |
|
|
Near edge of the distance band (m). |
|
|
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 |
|---|---|---|
|
|
Minimum required slope in m/m. Example: |
|
|
Maximum acceptable slope in m/m. Set to |
|
|
Scales the penalty for slope violations. Default: |
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 |
|---|---|---|
|
|
Minimum straight segment (m) required before each turn. |
|
|
Scales the penalty cost for violations. Default: |
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 |
|---|---|---|
|
|
Distance from the surface (m) at which a clamp sits. Should match the physical clamp bracket depth. |
|
|
Minimum spacing between successive clamps along the pipe (m). |
|
|
Scales the clamping cost contribution. Default: |
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 |
|---|---|---|
|
volmdlr |
The 3D shape defining the zone. Must support |
|
|
|
|
|
|
|
|
Optional identifier. |
Zone behaviour summary:
Rule |
Effect |
|---|---|
|
Weight × coefficient inside — the path prefers to enter |
|
Weight × coefficient inside — the path avoids the zone if possible |
|
Weight = 0 — the voxels are non-traversable |
|
|
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 |
|---|---|---|
|
list of |
Specifies which pipe type pairs are pooled or segregated, and how strongly. |
|
|
Default minimum separation (m) between pipes with no explicit spec. |
|
|
Cost of a move that rejoins a harness the pipe has left, when |
|
|
|
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 |
|---|---|---|
|
|
Minimum distance (m) a branch may approach a port. |
|
|
Minimum distance (m) between two branch junctions. |
|
|
Maximum number of pipes allowed to share a single branch junction. |
|
|
Penalty added to a path step that violates one of the distances above.
Default: |
|
|
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: |
|
|
How the rule enters the harness optimization problem. |
|
|
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 |
|---|---|---|
|
|
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. |
|
|
Minimum distance (m) between 2 clamps. Clampable stretches shorter than this are penalized. |
|
|
Maximum distance (m) between two clamps. Used to verify whether the spacing is valid when entering or leaving a branch under non-clampable conditions. |
|
|
Penalty added to a path step that violates one of the distances above.
Default: |
|
|
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),
)
|
Default |
Description |
|---|---|---|
|
|
Pose the rule as a hard constraint (the solver must satisfy it). |
|
|
Soft form only: multiplier of the aggregated violation in the objective. |
|
|
Soft form only: exponent applied to each violation before summing. |
|
|
Hard form only: offset subtracted from each violation, i.e. the slack granted. |
|
|
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.0if the move from node to neighbor is allowed; returnself.distance_multiplier(or another penalty) to discourage it.node/neighbor:SmartGridNodevector: 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)andnode.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_ruleis 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 inGridPath.indicators.fast_check(self, voxels, voxel_size, bool_matrix)Called on a complete candidate path during post-routing optimization.
voxelsholds the path’s voxel indices, an(N, 3)int array or a list of(x, y, z)tuples;bool_matrixis a numpy bool array marking where the rule’s attribute is active. Return0.0if 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.