User Guide: Path Geometry Optimization#
The paths produced by generate() follow
the voxel grid and can look “staircase-like”. The optimization step refines them into
smooth, physically plausible geometry — shorter paths, fewer bends, better compliance
with engineering constraints (slope, bend radius, clamping).
How the Optimizer Works#
The optimizer uses simulated annealing (scipy dual_annealing) to move the
control points of each pipe within a high-resolution voxel grid. The objective function
penalizes:
Total pipe length
Sideways deviation from the main direction
Very short pipe segments
Slope violations (gravity-drained pipes)
Number of bends
Design-rule constraint violations
Physical interferences with CAD geometry
You control the relative importance of each penalty term through
Costs, and the optimizer’s resolution and stopping
criteria through Settings.
Costs#
Costs sets the weight of each penalty term in the
objective function. Higher weight = that term matters more.
from routing.core.optimizer import Costs
costs = Costs(
length=1000, # penalize total pipe length
sideways=100, # penalize lateral deviation
short_line=5000, # penalize very short pipe segments
gravity=5000, # penalize slope constraint violations
bend=250, # penalize number of bends
constraints=5000, # penalize general constraint violations
interferences=1e8, # strongly penalize collisions with CAD
)
Parameter |
Default |
Description |
|---|---|---|
|
1000 |
Penalizes total path length. Higher values produce shorter paths at the cost of more aggressive turns. |
|
5000 |
Penalizes lateral movement (moving perpendicular to the dominant direction). Higher values produce straighter paths. |
|
5000 |
Penalizes very short pipe segments. Prevents degenerate spiky geometry. |
|
5000 |
Penalizes slope violations for gravity-drained pipes (requires
|
|
500 |
Penalizes number of bends and widely penalizes negative bends. |
|
5000 |
Penalizes design-rule constraint violations (custom |
|
1e8 |
Penalizes physical collisions with CAD bodies. Keep this very high — it should dominate all other terms. |
Typical tuning guidance:
Increase
lengthif paths take large detours.Decrease
sidewaysif you want more freedom to deviate from the dominant direction.Increase
bendif the optimizer is producing too many bends.Never reduce
interferencesbelow 1e6.
Settings#
Settings controls the resolution of the optimization
domain and the simulated annealing schedule.
from routing.core.optimizer import Settings
settings = Settings(
voxel_size=0.01, # fine grid resolution for optimization
max_shift=0.1, # max displacement per control point (m)
stabilization_threshold=5.0, # stop when improvement < this
n_iterations=3, # number of optimization passes per pipe
max_iter=1000, # max scipy dual_annealing iterations
)
Parameter |
Default |
Description |
|---|---|---|
|
required |
Grid resolution for the optimization domain (metres). Use 2–5× finer than
the pathfinding |
|
required |
Maximum displacement allowed for any single control point per iteration (m). Larger values allow bigger shape changes but slow convergence. |
|
|
Minimum distance between points to consider them distinct. Defaults to
|
|
5.0 |
Optimization stops early when the cost improvement between passes drops below this value. Lower values allow more refinement but take longer. |
|
5 |
Number of independent optimization passes per pipe. More passes can improve quality but increase runtime. |
|
500 |
Maximum number of scipy |
|
50000 |
Initial temperature for simulated annealing. Higher = more exploration early on. |
|
2e-4 |
Temperature restart ratio relative to the initial temperature. |
|
5 |
Number of consecutive passes without improvement before early stopping. |
|
|
If |
Running the Optimizer#
Call optimize() after
generate():
routing_result = route_planner.optimize(
costs=costs,
settings=settings,
picked_path="best", # optimize the best path found by generate()
max_refinements=0, # extra re-optimization passes for problem pipes
)
Parameters:
costs (Costs): Cost weights (see above).
settings (Settings): Optimization settings (see above).
picked_path (str): Which path to optimize when
n_iterations > 1ingenerate()."best"selects the lowest-cost path;"last"selects the most recently found path.max_refinements (int): Number of additional optimization passes for pipes that still have constraint violations after the first pass.
0= no extra passes.
The method returns the same RoutingResults object, now with optimized paths.
Displaying Optimized Results#
# Show optimized paths
routing_result.plot_data_optimized().plot()
# Collect packs (only from optimized paths)
packs = routing_result.collect_packs(only_optimized=True)
routing_result.specific_packs_cad_view(packs)
Choosing voxel_size for Optimization#
The optimization voxel_size is independent of the pathfinding voxel_size. A
smaller value produces more precise geometry but increases computation time.
Typical workflow:
Run pathfinding at
voxel_size = 0.021(coarse grid)Optimize at
settings.voxel_size = 0.010(2× finer)If geometry is still imprecise, reduce to
0.006(3.5× finer)
Note
The optimization domain is always bounded by the CadMap. If max_shift
produces a bounding box that is partially outside the cadmap, is cropped by the cadmap itself in order to
only produce position that are in allowed voxels.
Complete Example#
from routing.core.optimizer import Costs, Settings
from routing.core.finders import SmartFinder
from routing.core.route_planner import RoutePlanner
# ... (build cadmap and specifications as in the quickstart) ...
route_planner = RoutePlanner(cadmap, SmartFinder("AStar", "manhattan", "never"))
# Step 1 — generate grid-aligned paths
result = route_planner.generate(specifications=[spec], n_iterations=1)
# Step 2 — optimize geometry
costs = Costs(
length=1000,
sideways=100,
short_line=5000,
gravity=5000,
bend=250,
constraints=5000,
interferences=1e8,
)
settings = Settings(
voxel_size=0.01,
max_shift=0.1,
stabilization_threshold=5.0,
n_iterations=3,
max_iter=1000,
)
result = route_planner.optimize(
costs=costs,
settings=settings,
picked_path="best",
max_refinements=0,
)
# Step 3 — visualize
result.plot_data_optimized().plot()
packs = result.collect_packs(only_optimized=True)
result.specific_packs_cad_view(packs)
Next Steps#
Tutorial: Routing Pipes Through a CAD Assembly — full tutorial including an optimization step
User Guide: Pipe Pooling (Bundling) — bundle pipes and read bifurcation points
User Guide: Accessing Routing Results — access per-pipe path data and export results