User Guide: Path Geometry Optimization ======================================== The paths produced by :meth:`~routing.core.route_planner.RoutePlanner.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). .. contents:: On this page :local: :depth: 1 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 :class:`~routing.core.optimizer.Costs`, and the optimizer's resolution and stopping criteria through :class:`~routing.core.optimizer.Settings`. Costs ----- :class:`~routing.core.optimizer.Costs` sets the weight of each penalty term in the objective function. Higher weight = that term matters more. .. code-block:: python 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 ) .. list-table:: :header-rows: 1 :widths: 20 15 65 * - 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 -------- :class:`~routing.core.optimizer.Settings` controls the resolution of the optimization domain and the simulated annealing schedule. .. code-block:: python 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 ) .. list-table:: :header-rows: 1 :widths: 30 15 55 * - 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 :meth:`~routing.core.route_planner.RoutePlanner.optimize` after :meth:`~routing.core.route_planner.RoutePlanner.generate`: .. code-block:: python 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 ----------------------------- .. code-block:: python # 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 ---------------- .. code-block:: python 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 ---------- - :doc:`../tutorials/basic_routing` — full tutorial including an optimization step - :doc:`pooling` — bundle pipes and read bifurcation points - :doc:`results_access` — access per-pipe path data and export results