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.
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
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 stateupdate_with_solutionswrote; 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 CostRule (base class) 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:
# 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()
# 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 User Guide: Multi-Scale and Multi-Grid Routing.
One voxelization type: RoutingVoxelization#
VoxelVolume and RoomVoxelization merge into
RoutingVoxelization. Both remain as deprecated shims; mind
the argument order when you port a RoomVoxelization:
# Before (v2.12)
from routing.core.voxels import RoomVoxelization
room = RoomVoxelization(primitives, voxel_matrix, voxel_size, voxel_origin)
# 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:
Before (v2.12) |
After |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Removed, with the |
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
SmartGrid or a SmartWorld. Code
that uses the routing pipeline does not see the change; code that runs the finders
directly does:
# 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)
# 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)
Before (v2.12) |
After |
|---|---|
|
|
|
|
|
|
|
Removed: the search keeps its own open list and path |
|
|
|
|
|
It prices weights and rules, as Dijkstra does; |
|
Removed, with the |
|
Removed; |
|
Removed: |
|
Removed: every grid prices its design rules, Theta* included |
|
Removed: the search prices its moves itself. |
|
It returns |
|
|
|
|
See User Guide: The routing.pathfinding Module for the engine as it is now.
Pipe lines live in world coordinates#
A pipe line no longer belongs to a grid:
Before (v2.12) |
After |
|---|---|
|
|
|
Removed |
|
|
|
|
Pack construction changed#
Reading a pack is unchanged; building one is not:
Before (v2.12) |
After |
|---|---|
|
|
|
|
|
|
|
Removed: a pack lives in world coordinates and paints every region its window reaches |
|
|
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 Parallel Packs: Trays). The volumes they rest on are flagged on the volumes,
no longer listed in the rule:
Before (v2.12) |
After |
|---|---|
|
|
Trays stacked two layers at most |
|
|
|
|
Removed: |
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 Loops: Leaving and Rejoining a Harness). Loops are allowed by
default, so a rule built as in v2.12 prices no move, as before.
Before (v2.12) |
After |
|---|---|
|
|
|
Removed |
|
|
|
|
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.
Before (v2.12) |
After |
|---|---|
|
|
|
|
|
|
|
|
|
The class; |
|
|
|
|
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.generatenow routes every conduit through eachForcedZoneof 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, son_iterationsabove 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 Tutorial: Routing 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
ForbiddenZonekeeps 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:
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 Solution keeps every route found for its conduit as an
Alternative — a generated path, the two ports it joins, and its
refinements — and each pass of the optimizer as a Refinement.
The old names keep working with a DeprecationWarning, stored objects included:
Before (v2.12) |
After |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
They return |
|
|
|
|
Solution joins terminals#
A solution no longer holds two ports: it holds its terminals, what its routes join — a
PortCouple for a conduit between two ports, a
TeeTerminals for a tee. Its ports are read on the route it stands
by.
Before (v2.12) |
After |
|---|---|
|
|
|
Unchanged; its first argument, renamed |
|
Read-only: the ports of the route the solution stands by. Change
|
|
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:
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.
Before (v2.12) |
After |
|---|---|
|
|
|
|
|
|
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_voxelsrouting.core.cadmap:bounding_volume_from_box,get_faces_in_bounding_box,CadMap.update_packs_with_last_pipe(optimizerebuilds the packs), and the tray internalsParallelPack.add_pipe_line,initialize_parallel_propertiesandupdate_parallel_propertiesrouting.core.grid:SmartGrid.get_node_neighbors_index,build_lengths_dictrouting.core.voxels:make_boxandmake_boxes—RoutingVoxelization.voxel_boxesdraws a voxelization —,VoxelVolume.apply_closing_per_componentrouting.core.geometry:fiber_tube,Cylinder.voxelize_oldrouting.core.functions:build_frame,build_voxel_primitive,dot,extract_point_carrier_voxels,filter_voxels_by_axis_rangeand itsx,y,zvariants,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
SmartFinderpasses:SmartFinder("ThetaStar")now searches withdiagonal="never"and the Manhattan heuristic by default, where v2.12 forced"always"and the octile heuristic — passdiagonal="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_distanceand 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.
BranchClampDistanceRuleandBranchPortDistanceRulereadis_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
CadMapis 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_solutionsdefaults to the cadmap voxel size instead of half of it.generateplans the list: a tee is routed after the conduit it taps, the order you give is kept otherwise — unchanged for a list without tees.optimizerebuilds 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 belowCosts.interferences × 0.1(1e7by 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_<attribute>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_worldempties 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 raiseValueErrorwhen 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_worldandCadMap.world_frame;VoxelRegioncarries the CAD fields of one region,VoxelFrameplaces a grid in space.Goal sets —
Finder.set_goals/clear_goalsend a search on the first goal reached (see User Guide: The routing.pathfinding Module).Pack diagnostics —
CadMap.diagnose_pack_fieldandCadMap.pack_field_primitivesprint and draw the field one pack paints for a candidate pipe;Pack.section_deviationmeasures 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 User Guide: Pipe Pooling (Bundling).Forced passages —
ForcedZonerouted through in stages byRoutePlanner.generate, laid on the grids asGridForcedZone(CadMap.grid_forced_zones) whose doors the search targets; see Tutorial: Routing 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_sitesandexplored_stepsay where it may tap;RoutePlanner.exploreroutes a tee once per site without laying it; see Tutorial: Tees — Tapping a Routed Conduit.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()andRoutingResults.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 User Guide: Core Concepts.
See Also#
User Guide: The routing.pathfinding Module — the compiled engine
User Guide: Multi-Scale and Multi-Grid Routing — regions, decomposition and multi-scale routing
User Guide: Pipe Pooling (Bundling) — packs, their corridors and the loop tax
User Guide: Accessing Routing Results — alternatives, refinements and the pick
Migrating to v2.12 — the previous upgrade note