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.
Overview#
Design rules are attached either to a RuledVolume (so they
apply relative to a specific CAD surface) or passed globally to
from_project_inputs() (so they apply everywhere).
Rule class |
Purpose |
|---|---|
Penalize or prefer paths at certain distances from a surface |
|
Add a fixed cost to every direction change |
|
Tag voxels in a distance band with a boolean attribute |
|
Enforce minimum drainage slope (gravity-drained pipes) |
|
Require a minimum straight run before each turn |
|
Mark where pipe clamps can be placed and their spacing |
|
Base class for custom user-defined constraint rules |
All rule classes live in routing.core.design_rules.
from routing.core.design_rules import (
WeightingRule, TurnPenalizer, DistanceAttributeToggler,
GravityRule, StraightLengthRule, ClampingConstraint,
)
WeightingRule#
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.
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_lengthandmax_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:
# 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 RuledVolume:
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#
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):
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.0means a turn costs as much as traversing 3 more voxels in a straight line. The default of50.0strongly discourages turns; lower values allow more bends.
DistanceAttributeToggler#
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.
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 byClampingConstraint),"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:
structural_beam = RuledVolume.from_volume_model(
VolumeModel([beam_primitive]),
design_rules=[clamping_toggler],
name="main_beam",
)
GravityRule#
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.
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:
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
Noneto 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#
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.
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:
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#
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.
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:
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 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:
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:
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:
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#
Tutorial: Shaped Routing Zones — define spatial zones that routing must avoid, prefer, or pass through
Design Rules Reference — complete parameter reference for all rule types
User Guide: Pipe Pooling (Bundling) — bundle compatible pipes into organized packs
User Guide: Path Geometry Optimization — tune the geometry optimizer