Migrating to v3.0.0 =================== Version 3.0.0 brings **one compiled pathfinding engine**, **one model of the routing space** — a ``CadMap`` carries its own regions, each sampled on a grid a ``VoxelFrame`` places in space — and **tees**, conduits that tap other conduits. Routing code built on ``CadMap``, ``RoutePlanner`` and ``SmartFinder`` keeps working, and so do design rules of your own. The breaking points are the requirements, the construction of decomposed maps (``MultiCadMap``), the direct use of ``routing.pathfinding``, the ``PoolingRule`` parameters, the zone rules, the voxel and pack internals, the construction of a ``Solution`` by hand, and a ``name`` passed by position to a few constructors. .. contents:: On this page :local: :depth: 1 Requirements ------------ routing 3.0.0 requires ``volmdlr>0.22.4,<0.23.0``, was ``volmdlr>=0.21.0``. The ``vis`` extra (plotly) is removed with the pathfinding3D grid viewer it served, ``Grid.visualize``. Design rules of your own need no change --------------------------------------- A rule defined outside routing — a subclass of ``DesignRule``, ``CostRule``, ``ConstraintRule`` or of a built-in rule — works unchanged: the compiled search calls its own ``compute_cost`` and ``compute_heuristic`` at every move, and the map hands it the routed solutions through ``update_with_solutions`` when it redefines that method. There is nothing to declare and nothing to compile. ``TurnPenalizer``, ``StraightLengthRule``, ``GravityRule``, ``BoundedAttributeRule``, ``BoundedCustomLengthRule``, ``ClampingConstraint`` and ``PoolingRule`` are priced by compiled code, written once: their ``compute_cost`` and ``compute_heuristic`` call it. A subclass that keeps them is priced the same way; a rule that writes its own is priced by them, and ``super()`` reaches the compiled pricing. Those methods read the move off real ``SmartGridNode`` objects: a stand-in object mimicking a node, as a unit test may build, no longer does. During the search, the node a rule's method receives is a :class:`~routing.pathfinding.finder.finder.SearchStateNode`: it stands for a state of the compiled search and carries the cell, the arrival direction, the straight length and the attribute lengths of that state. Its ``parent`` is read off the search on demand, so a method walking the path behind a node sees it, one node per read; ``super().compute_cost`` in a subclass of ``PoolingRule`` prices the loops as the built-in rule does. Two points of the rule contract are worth checking on a rule of your own: - ``to_voxelized_rule`` — each grid prices the rule this method returns. The base implementation now returns the rule itself, with its parameters and the state ``update_with_solutions`` wrote; in v2.12 it rebuilt the rule from its name alone, resetting its other parameters to their defaults, or failing on a required one. A redefinition written to work around that can go; keep it where a parameter is a length to convert to voxels. - **Turn-like costs** — a cost depending on the heading the path arrives with is priced exactly by ``TurnPenalizer``, whose search keeps one state per cell and arrival heading. A turn rule of your own is searched on one state per cell. See :ref:`ref-cost-rule` for an example. ``MultiCadMap`` gives way to the regions of ``CadMap`` ------------------------------------------------------ A ``CadMap`` now carries its routing space as a list of regions (``cadmap.regions``), one unless decomposed. Decomposing is a method of the map, and assembling maps a factory of ``CadMap``: .. code-block:: python # Before (v2.12) from routing.core.cadmap import MultiCadMap multi_cadmap = MultiCadMap.from_cad_map(cadmap) # several grids, one voxel size multi_cadmap = MultiCadMap.from_cad_map(cadmap, multi_scale=True, size_factor=3) multi_cadmap = MultiCadMap.from_cad_maps([cadmap_a, cadmap_b]) world = multi_cadmap.to_grid() .. code-block:: python # After decomposed_map = cadmap.decomposed() decomposed_map = cadmap.decomposed(multi_scale=True, size_factor=3) assembled_map = CadMap.from_cad_maps([cadmap_a, cadmap_b]) world = decomposed_map.to_world() ``MultiCadMap`` stays as a deprecated alias: its constructor and its factories (``from_cad_map``, ``from_cad_maps``, ``from_project_inputs``) still work and emit a ``DeprecationWarning``, and what they build is a ``CadMap`` — ``main_cadmap``, ``port_points`` and ``get_port_and_bifurcation_nodes`` are gone with the class. ``to_grid(grid_id)`` returns the grid of the single region ``grid_id`` on every map, a decomposed one included, where ``MultiCadMap.to_grid()`` returned the world; the routing space as a whole is ``cadmap.to_world()``, one grid per region stitched at their borders — ``RoutePlanner`` always routes on it, single region as well as decomposed. See :doc:`multi_scale`. One voxelization type: ``RoutingVoxelization`` ---------------------------------------------- ``VoxelVolume`` and ``RoomVoxelization`` merge into :class:`~routing.core.voxels.RoutingVoxelization`. Both remain as deprecated shims; mind the argument order when you port a ``RoomVoxelization``: .. code-block:: python # Before (v2.12) from routing.core.voxels import RoomVoxelization room = RoomVoxelization(primitives, voxel_matrix, voxel_size, voxel_origin) .. code-block:: python # After from routing.core.voxels import RoutingVoxelization room = RoutingVoxelization(voxel_matrix, voxel_origin, voxel_size, primitives=primitives) ``VoxelVolume`` keeps its argument order but for ``primitives``, which now comes after ``name``: pass it by keyword. The module ``routing.core.voxel_utils`` is removed. Its helpers served the decomposition of the map; their operations now live on the objects they act on: grid derivation and morphology on ``RoutingVoxelization`` (``sub_voxelization``, ``refined_box``, ``subtract_coarser``, ``nesting_in``, ``maximal_boxes``, ``covering_frame``), world ↔ index conversions on ``VoxelFrame``, index windows in ``routing.core.functions`` (``grid_window``, ``box_window``, ``clipped_grid_window``, ``index_sphere``, ``grid_index_bounds``, ``cluster_points``). Coordinate conversions move to ``VoxelFrame`` --------------------------------------------- ``routing.core.geometry.VoxelFrame`` — origin, voxel size and shape — is the one chart between world coordinates and grid indices. A grid, a world or a voxelization exposes it as ``frame``: .. list-table:: :header-rows: 1 :widths: 45 55 * - Before (v2.12) - After * - ``grid.coord_in_grid(point)``, ``grid.coord_in_grid_npy(points)`` - ``grid.frame.fractional_indices(points)`` * - ``grid.coord_to_index(point)`` - ``grid.frame.nearest_index(point)`` * - ``grid.coord_to_index_npy(points)`` - ``grid.frame.nearest_indices(points)`` * - ``grid.index_to_coord(index)`` - ``grid.frame.center(index)`` * - ``grid.index_to_coord_npy(indices)`` - ``grid.frame.centers(indices)`` * - ``room_voxels.coord_to_voxel_index(point)`` - ``room_voxels.world_to_index(point)`` * - ``room_voxels.voxel_index_to_coord(index)`` - ``room_voxels.indices_to_world(index)`` * - ``room_voxels.points_are_in_voxels(indices)``, ``cadmap.points_are_in_voxels(indices)`` - ``room_voxels.indices_are_active(indices)`` * - ``room_voxels.get_resampled_grid_properties(bounds, voxel_size, dilation_voxels)`` - ``RoutingVoxelization.covering_frame(bounds, voxel_size, dilation_voxels, origin=room_voxels.origin)``, a ``VoxelFrame`` * - ``functions.coord_to_voxel_index(point, voxel_origin, voxel_size)``, ``coord_to_voxel_index_continuous(...)``, ``voxel_index_to_coord(index, ...)`` - ``VoxelFrame(origin, voxel_size, shape)`` and its ``nearest_index(point)``, ``fractional_indices(points)``, ``center(index)`` * - ``SmartGridInterface(width, height, depth, origin, voxel_size)`` - ``SmartGridInterface(frame)`` — for subclasses; ``SmartGrid(weights, voxel_size, origin, ...)`` is unchanged * - ``shape.voxelize(grid_origin, grid_shape, voxel_size, dilation_radius, extension_voxels, capsule)`` - ``shape.voxelize(frame, dilation_radius)`` * - ``cylinder.fiber_indices(grid_origin, grid_shape, voxel_size)`` - ``cylinder.fiber_indices(frame)`` * - ``VoxelVolume.from_routing_shapes(shapes, grid_origin, grid_shape, voxel_size, dilation_voxels, extension_voxels)`` - ``RoutingVoxelization.from_routing_shapes(shapes, frame, dilation_voxels)`` * - ``VoxelVolume.enclosing(shapes, voxel_size, dilation_voxels, extension_voxels)`` - ``RoutingVoxelization.enclosing(shapes, voxel_size, dilation_voxels)`` * - ``SkeletonSettings(extension_voxels=...)`` - Removed, with the ``extension_voxels`` and ``capsule`` of the voxelization ``routing.pathfinding`` is a compiled engine -------------------------------------------- The module keeps pathfinding3D's layout but is no longer pathfinding3D: its grid, its nodes and its search are compiled, and its finders search a :class:`~routing.core.grid.SmartGrid` or a :class:`~routing.core.world.SmartWorld`. Code that uses the routing pipeline does not see the change; code that runs the finders directly does: .. code-block:: python # Before (v2.12) from routing.pathfinding.core.grid import Grid from routing.pathfinding.finder.a_star import AStarFinder grid = Grid(matrix=matrix) path, runs = AStarFinder().find_path(grid.node(0, 0, 0), grid.node(9, 9, 9), grid) .. code-block:: python # After from routing.core.grid import SmartGrid from routing.pathfinding.finder.a_star import AStarFinder grid = SmartGrid(matrix, voxel_size=1.0, origin=(0.0, 0.0, 0.0)) start = grid.node(0, 0, 0) start.direction = (1, 0, 0) # the search leaves its start with a heading path, runs = AStarFinder().find_path(start, grid.node(9, 9, 9), grid) .. list-table:: :header-rows: 1 :widths: 45 55 * - Before (v2.12) - After * - ``routing.pathfinding.core.node_compiled`` - ``routing.pathfinding.core.node`` * - ``routing.pathfinding.core.grid.Grid(width, height, depth, matrix)`` - ``Grid(weights, attribute_map, grid_id, voxel_size)``, the compiled base of ``SmartGrid``; build a ``SmartGrid`` to search * - ``routing.pathfinding.core.world.World`` - :class:`~routing.core.world.SmartWorld` * - ``routing.pathfinding.core.heap``, ``routing.pathfinding.core.util`` - Removed: the search keeps its own open list and path * - ``BestFirst`` - ``BestFirstFinder`` * - ``AStarFinder(weight=...)``, the same for ``ThetaStarFinder`` and ``BestFirst`` - ``heuristic_weight=...``; ``DijkstraFinder`` and ``BreadthFirstFinder`` take neither a heuristic nor its weight * - ``SmartFinder("BreadthFirst")`` searched in plain steps - It prices weights and rules, as Dijkstra does; ``BreadthFirstFinder`` used directly still searches in plain steps * - ``grid.visualize(...)`` - Removed, with the ``vis`` extra * - ``IDAStarFinder``, ``BiAStarFinder``, ``MinimumSpanningTree``, and their ``SmartFinder`` names (``"IDAStar"``, ``"BiAStar"``, ``"MSP"``…) - Removed; ``SmartFinder`` now raises ``ValueError`` on an unknown name. Use ``"AStar"`` * - ``SmartAStar``, ``SmartBestFirst``, ``SmartBreadthFirst``, ``SmartIDAStar``, ``SmartBiAStar``, ``SmartMinimumSpanningTree`` - Removed: ``SmartFinder.finder`` is the engine's finder (``AStarFinder``…) * - ``grid.set_smartness(...)``, ``grid.smart`` - Removed: every grid prices its design rules, Theta* included * - ``SmartGrid.calc_cost(node, neighbor, weighted)``, ``SmartWorld.calc_cost(...)``: the cost of one move - Removed: the search prices its moves itself. ``calc_path_cost(path)`` still gives the cost of a found path, and per rule the nodes it charged * - ``grid.build_pipe_ends(solution)`` returns ``(start, end)`` - It returns ``(start, goals)``: the departure and a list of goal ports — one for a conduit, one per reachable tap port for a tee * - ``grid.path_to_pipe_path(path, grid_start, solution, runs, runtime)`` - ``grid.path_to_pipe_path(path, grid_start, grid_end, solution, runs, runtime)``, ``grid_end`` being the goal the path reached * - ``SmartGridNode.update_straight_length()``, ``update_custom_attr_lengths()``, ``update_custom_attr_lengths_along(cells)``, ``update_attribute_length(attribute)``, ``update_no_attribute_length(attribute)`` - ``SmartGridNode.derive_path_state(crossed_cells=None)``: the state a path carries at a node, derived from its parent as the search derives it See :doc:`pathfinding_module` for the engine as it is now. Pipe lines live in world coordinates ------------------------------------ A pipe line no longer belongs to a grid: .. list-table:: :header-rows: 1 :widths: 45 55 * - Before (v2.12) - After * - ``pipe.to_pipe_lines(grid_id)``, ``pipe.to_pipe_lines_multi_grid(nodes)`` - ``pipe.to_pipe_lines()`` * - ``pipe_line.grid_id`` - Removed * - ``PipeLine.from_points(start, end, radius, input_voxel, output_voxel, type_, grid_id)`` - ``PipeLine.from_points(start, end, radius, input_voxel, output_voxel, type_)`` * - ``grid_path.to_pipe(input_port, output_port, pipe_type, voxel_size, voxel_origin)`` - ``grid_path.to_pipe(input_port, output_port, pipe_type)`` ``Pack`` construction changed ----------------------------- Reading a pack is unchanged; building one is not: .. list-table:: :header-rows: 1 :widths: 45 55 * - Before (v2.12) - After * - ``Pack.from_pipe_line(pipe_line, solution, parallel, room_voxels, support_distances)`` - ``Pack.from_pipe_line(pipe_line, solution)``, and the same for trays: ``ParallelPack.from_pipe_line(pipe_line, solution)`` * - ``Pack.from_pipe_lines(pipe_lines, solution, parallel, pooling_rule, room_voxels, support_distances)``, ``Pack.catch_pipe_lines(...)`` - ``Pack.from_pipe_lines(pipe_lines, solution, pooling_rule, clustering_step)``, same for ``catch_pipe_lines``; trays: ``ParallelPack.from_pipe_lines(...)`` * - ``pack.get_axis_voxels(room_voxels)`` - ``pack.get_axis_voxels(room_voxels, region_index)`` * - ``pack.grid_id`` - Removed: a pack lives in world coordinates and paints every region its window reaches * - ``cadmap.update_rules_with_solutions(toggled_attributes)`` - ``cadmap.update_rules_with_solutions(toggled_attributes_by_region)``, keyed by region index Trays rest on flagged volumes ----------------------------- Trays (``parallel=True``) are now generic packs that rest on the face their pipes run along and stack in layers (see :ref:`pooling-trays`). The volumes they rest on are flagged on the volumes, no longer listed in the rule: .. list-table:: :header-rows: 1 :widths: 45 55 * - Before (v2.12) - After * - ``PoolingRule(..., volume_indices=[0, 1])`` - ``RuledVolume.from_volume_model(..., support=True)`` on volumes 0 and 1; with no volume flagged, trays rest on every volume * - Trays stacked two layers at most - ``PoolingSpecification(..., parallel=True, max_layers=2, aspect=None)``: ``max_layers`` sets the cap, ``None`` lifts it * - ``ParallelPack(pipe_lines, vector, crossed_voxels=None, axis=None, name="")`` - ``ParallelPack(pipe_lines, vector, normal=None, axis=None, name="")``: the map gives a tray its ``normal`` once its pipes rest on a support * - ``PipeLine.get_support_surface_vectors(min_distances, voxel_size)`` - Removed: ``CadMap.support_normal(lines)`` reads the support on the volumes' meshes A ``PoolingRule`` saved by v2.12 loads, its ``volume_indices`` ignored: flag the support volumes again. A ``PoolingSpecification`` saved by v2.12 loads with ``max_layers=2`` and ``aspect=None``. ``PoolingRule`` prices loops, on corridor labels ------------------------------------------------ ``PoolingRule`` becomes a ``CostRule``: with ``allow_loops=False``, a move that rejoins a harness the pipe has left pays ``cost`` (see :ref:`pooling-loops`). Loops are allowed by default, so a rule built as in v2.12 prices no move, as before. .. list-table:: :header-rows: 1 :widths: 45 55 * - Before (v2.12) - After * - ``PoolingRule(..., distance_multiplier=500, ...)``, a ``ConstraintRule`` - ``PoolingRule(..., cost=500, ...)``, a ``CostRule`` * - ``pooling_rule.has_history``, ``has_matching_history``, ``exceeds_history_min_length`` - Removed * - ``pooling_rule.attribute``: ``"history"`` - ``"is_shared"`` (``PoolingRule.POOLED_ATTRIBUTE``) * - ``cadmap.shared_voxels`` and the ``is_shared`` node attribute: booleans over the cells the packs attract - ``int32`` corridor labels, ``0`` outside every corridor: test ``!= 0`` for "pooled" A ``PoolingRule`` saved by v2.12 loads with the default ``cost``: its ``distance_multiplier`` priced nothing there. Zones by kind replace ``ShapedWeightingRule`` --------------------------------------------- The four ``zone_type`` values of ``ShapedWeightingRule`` become three classes, one per behaviour. ``ShapedWeightingRule`` and its constructors stay as a deprecated shell: they build the zone below and emit a ``DeprecationWarning``. .. list-table:: :header-rows: 1 :widths: 45 55 * - Before (v2.12) - After * - ``ShapedWeightingRule.attractive_zone(shape, strength)``, ``ShapedWeightingRule(shape, coefficient, zone_type="attractive")`` - ``WeightedZone(shape, coefficient)`` with ``coefficient < 1``, the former ``strength`` * - ``ShapedWeightingRule.repulsive_zone(shape, strength)`` - ``WeightedZone(shape, coefficient)`` with ``coefficient > 1``, the former ``strength`` * - ``ShapedWeightingRule.forbidden_zone(shape, cut_ports)`` - ``ForbiddenZone(shape, cut_ports)`` * - ``ShapedWeightingRule.forced_zone(shape)`` - ``ForcedZone(shape)`` * - ``rule.zone_type``, ``rule.forbids_zones`` - The class; ``forbids_zones`` stays true on a ``ForbiddenZone`` only * - ``cadmap.shaped_weighting_rules`` - ``cadmap.zones``, ``cadmap.weighted_zones``, ``cadmap.forced_zones`` * - ``cadmap.apply_shaped_weighting_rules()`` - ``cadmap.apply_zone_weights()`` Two behaviours change with the classes: - **A forced zone is routed through, not weighted.** In v2.12 ``zone_type="forced"`` priced the cells inside the zone near zero and the cells outside ×100, so a route could still skip the zone. ``RoutePlanner.generate`` now routes every conduit through each ``ForcedZone`` of the map in stages — toward the *doors* of the zones left, then to the end port — with the zones passed in the order the search reaches them, whatever the order of the rules. Through forced zones only the best path is searched, so ``n_iterations`` above 1 yields one candidate. The optimizer, which v2.12 drew inside the zone through those weights, does not hold a conduit inside its forced zones yet: an optimized geometry may leave a zone (fix planned for 3.1.0). See :doc:`../tutorials/shaped_zones`. - **A weighted or forced zone covers the voxels whose centre lies in the shape**, instead of every voxel the shape crosses: a zone is now as wide as its CAD shape, to half a voxel. A ``ForbiddenZone`` keeps the conservative v2.12 containment — every voxel it touches — and the port cut it performs reads that same set. A conduit no route joins is kept, not raised -------------------------------------------- When the search finds no path for a conduit — its ports walled in by the packs of the conduits routed before, or a forced zone it cannot reach — ``generate`` used to fail on that specification (caught by ``steady_mode=True``) or, when several paths were asked, to fall back on a straight line between the two ports, through the CAD (``SmartGridInterface.build_two_ports_path``, removed). It now records the ``Solution`` as **not routed** and warns: .. code-block:: python result = route_planner.generate(specifications, n_iterations=1) for solution in result.solutions: if not solution.is_routed: print(solution.name, "is not routed") routed = result.routed_solutions # the solutions with a geometry to read An unrouted solution has ``best_pipe`` and ``best_path`` set to ``None``; pooling, optimization and harness generation read ``routed_solutions`` (``RoutingResults`` and ``CadMap``) and leave it out. In 3D it is drawn as its two port stubs, in the pipe type colour. Code reading ``solution.best_pipe`` on every solution of a result must skip the unrouted ones. ``OptimizedPipes`` and ``OptimizedPipe`` become ``Alternative`` and ``Refinement`` ---------------------------------------------------------------------------------- A :class:`~routing.core.core.Solution` keeps every route found for its conduit as an :class:`~routing.core.core.Alternative` — a generated path, the two ports it joins, and its refinements — and each pass of the optimizer as a :class:`~routing.core.core.Refinement`. The old names keep working with a ``DeprecationWarning``, stored objects included: .. list-table:: :header-rows: 1 :widths: 45 55 * - Before (v2.12) - After * - ``OptimizedPipes`` - ``Alternative`` * - ``OptimizedPipe`` - ``Refinement`` * - ``solution.optimized_pipes`` - ``solution.alternatives`` * - ``optimized_pipes.optimized_pipes`` (on one candidate) - ``alternative.refinements`` * - ``OptimizedPipes.best_optimized_pipe(pipes)`` - ``Alternative.best_refinement(pipes)`` * - ``solution.set_optimized_pipes(pipes, index)`` - ``solution.add_refinements(pipes, index)`` * - ``RoutePlanner.find_paths`` and ``SmartFinder.search_ports_connections`` return ``list[GridPath]`` - They return ``list[Alternative]``: each route with the ports it joins — for a tee, the tap port it reached. Add them with ``solution.add_alternatives(alternatives)`` * - ``optimizer.optimize_global_3d(solution, costs, settings, picked_path)``, with ``picked_path`` ``"best"`` or ``"last"`` - ``optimizer.optimize_global_3d(solution, costs, settings, alternative_index=None)``: ``None`` optimizes the alternative the solution stands by, ``-1`` the last one * - ``RoutePlanner.optimize_with_new_costs(..., picked_path)``, ``RoutePlanner.restart_avoiding_dead_ends(..., picked_path, ...)`` - ``alternative_index`` in place of ``picked_path``; ``RoutePlanner.optimize(..., picked_path)`` is unchanged ``Solution`` joins terminals ---------------------------- A solution no longer holds two ports: it holds its **terminals**, what its routes join — a :class:`~routing.core.core.PortCouple` for a conduit between two ports, a :class:`~routing.core.core.TeeTerminals` for a tee. Its ports are read on the route it stands by. .. list-table:: :header-rows: 1 :widths: 45 55 * - Before (v2.12) - After * - ``Solution(optimized_pipes, input_port, output_port, pipe_type, name)`` - ``Solution(alternatives, PortCouple(input_port, output_port), pipe_type, name)`` * - ``Solution.from_pipe_definition(ports, pipe_type)`` - Unchanged; its first argument, renamed ``terminals``, takes any terminals, and it takes a ``name`` * - ``solution.input_port = port``, ``solution.output_port = port`` - Read-only: the ports of the route the solution stands by. Change ``solution.terminals`` instead * - ``solution.port_couple`` - Unchanged: the ports of the route the solution stands by Solutions stored by earlier versions — ``input_port`` and ``output_port`` in their data — are read back as a ``PortCouple``. ``best_pipe`` is the geometry the checks accepted ------------------------------------------------- ``solution.best_pipe`` used to be the cheapest optimized pipe of every candidate, a rejected one included — its name ending with ``_failed`` — so a geometry the optimizer's checks refused could be laid in the packs and drawn. It is now the cheapest refinement **the checks accepted**, else the generated path of the route the solution stands by: a rejected refinement is kept for diagnosis and never laid. ``solution.is_well_optimized`` tells whether an accepted refinement exists. The route a solution stands by is the cheapest generated path, unless you pick another one: .. code-block:: python solution = result.solution("O7") solution.pick(1) # best_path, best_pipe, input_port and output_port follow it solution.picked # the Alternative it stands by solution.picked_index # 1; None when nothing is picked Solutions are named after their specification --------------------------------------------- ``generate`` used to build unnamed solutions, and its messages read the pipe type name. A solution now takes the name of its specification, else the name of its pipe type; a name several specifications share takes an index suffix in planning order (``hydraulic_1``, ``hydraulic_2``). The refinements are named after the solution (``O7_opt_1``). Look a solution up with ``result.solution(name)``. Pass ``name`` by keyword ------------------------ New parameters come before ``name`` in a few signatures: a name passed by position now lands in one of them. .. list-table:: :header-rows: 1 :widths: 45 55 * - Before (v2.12) - After * - ``RuledVolume(primitives, global_rules, in_border, voxel_inside, name)``, the same for ``RuledVolume.from_volume_model`` - ``RuledVolume(primitives, global_rules, in_border, voxel_inside, overwrite_distance=0.0, support=False, name="")`` * - ``PoolingSpecification(types, pooling_mode, min_distance, parallel, name)`` - ``PoolingSpecification(types, pooling_mode, min_distance, parallel, max_layers=2, aspect=None, name="")`` * - ``GridPath(path, pipe, cost, indicators, runs, runtime, name)`` - ``GridPath(path, pipe, cost, indicators, runs, runtime, states=None, name="")`` Removed helpers --------------- These had no caller left inside routing, most of them since ``voxel_utils`` went, and were deleted: - ``routing.core.core``: ``RuledVolume.from_intersecting_faces``, ``RuledVolume.get_faces_in_shell``, ``PipeLine.contains_point``, ``PipeLine.is_successive_to``, ``PipeLine.bbox_intersections_segment``, ``PipeLine.other_parametric_position``, ``OptimizedPipe.update_crossed_voxels`` - ``routing.core.cadmap``: ``bounding_volume_from_box``, ``get_faces_in_bounding_box``, ``CadMap.update_packs_with_last_pipe`` (``optimize`` rebuilds the packs), and the tray internals ``ParallelPack.add_pipe_line``, ``initialize_parallel_properties`` and ``update_parallel_properties`` - ``routing.core.grid``: ``SmartGrid.get_node_neighbors_index``, ``build_lengths_dict`` - ``routing.core.voxels``: ``make_box`` and ``make_boxes`` — ``RoutingVoxelization.voxel_boxes`` draws a voxelization —, ``VoxelVolume.apply_closing_per_component`` - ``routing.core.geometry``: ``fiber_tube``, ``Cylinder.voxelize_old`` - ``routing.core.functions``: ``build_frame``, ``build_voxel_primitive``, ``dot``, ``extract_point_carrier_voxels``, ``filter_voxels_by_axis_range`` and its ``x``, ``y``, ``z`` variants, ``get_all_neighbors``, ``get_nearest_active_voxel``, ``get_node_neighbors_coords``, ``get_voxels_in_bounding_box``, ``point_to_local_frame``, ``points_to_local_frames``, ``reduce_cylinder_radius_3d_fast``, ``tensor_to_non_zero_indices``, ``voxel_offsets_in_radius``, ``voxelize_box_domain`` Behaviour changes without an API change --------------------------------------- Code that runs unchanged but may produce different numbers: - **Turns are priced exactly.** With a ``TurnPenalizer``, A* keeps one state per cell and arrival heading and charges the true turns of the path: routes can change, at a cost equal or lower. - **Theta* searches under the design rules**, instead of ignoring them: a shortcut is one move, priced on the mean weight of the cells it crosses, and the path cost no longer charges a turn and a violation at every shortcut. It honours the heuristic and the diagonal policy ``SmartFinder`` passes: ``SmartFinder("ThetaStar")`` now searches with ``diagonal="never"`` and the Manhattan heuristic by default, where v2.12 forced ``"always"`` and the octile heuristic — pass ``diagonal="always"`` to search as before. - **Multi-scale searches** measure the heuristic in the unit of the traversal cost, and a step into another grid keeps its heading: crossing a change of scale is no longer priced as a turn. - **Pooling** follows the new generic packing policy: each member forbids the cells closer to its axis than ``R + r + min_distance`` and attracts the first cell row beyond; packs draw their lanes on the world frame, the same in every region they cross. - **The branch rules read the packs' corridors as the shared zone.** ``BranchClampDistanceRule`` and ``BranchPortDistanceRule`` read ``is_shared``, which now covers the box around each pack and its row of contact cells, one voxel longer at each end, instead of the cells the packs attract: 3.6 to 6.9 times as many cells on the window case. A branch enters and leaves that zone accordingly. - **Pipe geometry** follows the routed cell centres, and the short runs made only to reach a gateway between grids collapse into one junction point. - **Volume weighting rules are evaluated once**, when the ``CadMap`` is built, instead of at each conduit. A volume rule changed afterwards takes effect once the weight fields are recomputed (``region.weights = region.compute_weights(cadmap.weighting_volumes)`` for each region). - **Voxelized results**: ``RoutingResults.voxelize_solutions`` defaults to the cadmap voxel size instead of half of it. - ``generate`` **plans the list**: a tee is routed after the conduit it taps, the order you give is kept otherwise — unchanged for a list without tees. - ``optimize`` **rebuilds the packs in routing order** from each optimized geometry, instead of keeping the packs of the generation: a pipe is optimized against the pipes optimized before it, and the packs of the result carry the optimized geometry. - **A rejected refinement is never laid**, and the annealing stops early only on a cost below ``1e6``, the cost above which the checks reject a refinement, instead of below ``Costs.interferences × 0.1`` (``1e7`` by default). - **A route of two cells** is drawn port, extension, extension, port, like any other route, instead of one straight line between the ports. - **Fixes that move routes**: a start cell holding an attribute starts with no gap (``no_`` is 0 there, was 1); the optimizer keeps the extensions of a short pipe — three lines, or one interior point — on the port axes. The changelog lists every fix. - **Conduits that do not pool block each other.** Under a ``PoolingRule``, a pipe no longer crosses the cells of a pipe it does not pool with — the default between two types no specification relates — unless both fit in one cell; v2.12 only weighed a band around it, neutral at the default coefficient. A conduit can now be left unrouted where it used to cross. - **One map at a time in the engine.** ``CadMap.to_world`` empties the engine's grid registry, so a map routes as in a fresh process whatever was routed before; the nodes of an earlier map then raise ``ValueError`` when their position is read: read or pick its paths before building another world. Also new -------- - **Regions and frames** — ``CadMap.decomposed``, ``CadMap.from_cad_maps``, ``CadMap.to_world`` and ``CadMap.world_frame``; ``VoxelRegion`` carries the CAD fields of one region, ``VoxelFrame`` places a grid in space. - **Goal sets** — ``Finder.set_goals`` / ``clear_goals`` end a search on the first goal reached (see :doc:`pathfinding_module`). - **Pack diagnostics** — ``CadMap.diagnose_pack_field`` and ``CadMap.pack_field_primitives`` print and draw the field one pack paints for a candidate pipe; ``Pack.section_deviation`` measures how much wider a pack is than its real section. - **Anti-loop pooling** — ``PoolingRule(allow_loops=False)``, on the corridors the packs lay (``Pack.corridor``, ``OrientedBox``) and their labels (``CorridorLabels``); see :doc:`pooling`. - **Forced passages** — ``ForcedZone`` routed through in stages by ``RoutePlanner.generate``, laid on the grids as ``GridForcedZone`` (``CadMap.grid_forced_zones``) whose doors the search targets; see :doc:`../tutorials/shaped_zones`. - **Tees** — ``TeeSpecification(port, tapped, pipe_type)`` routes a conduit from its port to anywhere along another conduit, a specification or a conduit already routed; ``TapSite``, ``tap_sites`` and ``explored_step`` say where it may tap; ``RoutePlanner.explore`` routes a tee once per site without laying it; see :doc:`../tutorials/tee_routing`. - **Planning and picking** — ``RoutePlanning`` (the routing order, ``dependents``), ``Solution.pick`` / ``pick_nearest``, ``RoutePlanner(..., n_iterations)``, ``RoutePlanner.optimize(..., solutions=[...])`` to optimize some conduits and keep the others, ``RoutingResults.summary()`` and ``RoutingResults.solution(name)``. - **Weight override** — ``RuledVolume(overwrite_distance=...)``: up to that distance from its surface, a volume imposes its own weights over what the map composed; see :doc:`core_concepts`. See Also -------- - :doc:`pathfinding_module` — the compiled engine - :doc:`multi_scale` — regions, decomposition and multi-scale routing - :doc:`pooling` — packs, their corridors and the loop tax - :doc:`results_access` — alternatives, refinements and the pick - :doc:`../tutorials/tee_routing` — tees - :doc:`migration_v2_12` — the previous upgrade note