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 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 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

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 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

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

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 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

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:

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 Parallel Packs: Trays). The volumes they rest on are flagged on the volumes, no longer listed in the rule:

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 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

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.

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 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 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:

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

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 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

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:

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

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_<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_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 User Guide: The routing.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 User Guide: Pipe Pooling (Bundling).

  • 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 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_sites and explored_step say where it may tap; RoutePlanner.explore routes 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() 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 User Guide: Core Concepts.

See Also#