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

length

1000

Penalizes total path length. Higher values produce shorter paths at the cost of more aggressive turns.

sideways

5000

Penalizes lateral movement (moving perpendicular to the dominant direction). Higher values produce straighter paths.

short_line

5000

Penalizes very short pipe segments. Prevents degenerate spiky geometry.

gravity

5000

Penalizes slope violations for gravity-drained pipes (requires PipeType.min_slope and GravityRule to be set).

bend

500

Penalizes number of bends and widely penalizes negative bends.

constraints

5000

Penalizes design-rule constraint violations (custom ConstraintRule).

interferences

1e8

Penalizes physical collisions with CAD bodies. Keep this very high — it should dominate all other terms.

Typical tuning guidance:

  • Increase length if paths take large detours.

  • Decrease sideways if you want more freedom to deviate from the dominant direction.

  • Increase bend if the optimizer is producing too many bends.

  • Never reduce interferences below 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

voxel_size

required

Grid resolution for the optimization domain (metres). Use 2–5× finer than the pathfinding voxel_size. Example: pathfinding at 0.021 m → optimize at 0.006–0.010 m.

max_shift

required

Maximum displacement allowed for any single control point per iteration (m). Larger values allow bigger shape changes but slow convergence.

len_tolerance

None

Minimum distance between points to consider them distinct. Defaults to voxel_size if not set.

stabilization_threshold

5.0

Optimization stops early when the cost improvement between passes drops below this value. Lower values allow more refinement but take longer.

n_iterations

5

Number of independent optimization passes per pipe. More passes can improve quality but increase runtime.

max_iter

500

Maximum number of scipy dual_annealing iterations per pass. Increase this for difficult problems.

initial_temp

50000

Initial temperature for simulated annealing. Higher = more exploration early on.

restart_temp_ratio

2e-4

Temperature restart ratio relative to the initial temperature.

convergence_window

5

Number of consecutive passes without improvement before early stopping.

plot_cost_history

False

If True, plots the cost history during optimization (requires matplotlib).

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 > 1 in generate(). "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:

  1. Run pathfinding at voxel_size = 0.021 (coarse grid)

  2. Optimize at settings.voxel_size = 0.010 (2× finer)

  3. 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#