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).
This page covers the per-path optimizer: each pipe is refined on its own, and the result is still one path per cable. To consolidate the routed cables into a single optimizable harness — branches, junctions and port leaders shaped together — see User Guide: Harness Generation.
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, and a pass stops early when its cost varies less than this value over
|
|
5 |
Maximum number of successive optimization passes per pipe, each starting from the
result of the previous one (one |
|
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 recent accepted annealing iterations whose cost spread must fall below
|
|
|
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 path the solution stands by — the one you retained withsolution.pick(index), else 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.solutions (list[Solution], optional): The solutions to optimize; the others are kept as they stand. A solution
explore()returned, not laid yet, is laid at the end. Default: all of them.
The method returns a new RoutingResults over the same Solution objects, which now
carry their refinements.
Order and tees. The solutions are optimized in the order generate() routed them, and
the packs are rebuilt from each optimized geometry: a pipe is optimized against the pipes
finalized before it. A tee is routed again on the optimized geometry of the conduit it taps —
at its picked site if you picked one — and then optimized; its junction with that conduit is
not counted as a clash. See Tutorial: Tees — Tapping a Routed Conduit.
Forced zones. The optimizer does not hold a conduit inside its forced zones yet: an optimized geometry may leave a zone the generated path crossed. A fix is planned for 3.1.0.
Rejected refinements. The optimizer checks each refined geometry: one whose cost exceeds
1e6 (a penalty term — an interference, a fold back), or that breaks a constraint, is named
..._failed and kept for diagnosis, but never laid. solution.best_pipe then stays the
generated path, and solution.is_well_optimized is False. Annealing never stops early on
such a geometry.
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_display_3d(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_display_3d(packs)
Next Steps#
Tutorial: Routing Pipes Through a CAD Assembly — full tutorial including an optimization step
User Guide: Harness Generation — consolidate the routed cables into one optimizable harness
User Guide: Pipe Pooling (Bundling) — bundle pipes and read bifurcation points
User Guide: Accessing Routing Results — access per-pipe path data and export results
Tutorial: Tees — Tapping a Routed Conduit — tees, rerouted on their optimized conduit