User Guide: Harness Generation#
Routing produces one path per cable. A harness is what those paths become once they
are consolidated into a single physical object: cables that share a route are merged into
one bundle of branches, branches meet at junctions, and each connector is fed by one
leader. generate_harness() runs that whole
chain — route, bundle, then optimize the geometry — from one settings object.
The Pipeline#
Stage |
What it does |
Settings group |
|---|---|---|
route |
Runs |
|
bundle |
Voxelizes the routed cables, thins the volume to its medial axis, fits the
solutions back onto it and wires the result into a
|
|
unlock |
Drives the harness to feasibility. On a violation plateau the cramped zones become
repulsive locks on the |
|
consolidate |
Reruns coarse passes until the topology freezes (experimental — disabled by default). |
|
shape |
Refines the frozen topology with the design-rule constraints active. |
|
The solver only moves points: the topology is fixed for the whole duration of a pass. It
changes between passes only — through Bundle.repaired() (the consolidation round-trip)
or through the junction repair armed by shaping.repair.
One Call#
from routing.core.design_rules import BranchPortDistanceRule
from routing.core.finders import SmartFinder
from routing.core.harness import HarnessSettings
from routing.core.route_planner import RoutePlanner
route_planner = RoutePlanner(cadmap, SmartFinder("AStar", "manhattan", "never"))
settings = HarnessSettings(
clearance=0.005, # 5 mm of CAD safety margin around every pipe radius
harness_rules=[BranchPortDistanceRule(branches_min_distance=0.05,
port_branch_min_distance=0.03)],
)
bundle, history = route_planner.generate_harness(specifications, settings, name="harness")
bundle.display_3d() # the optimized harness
history.volume_models().display_3d() # the whole chronology, state by state
settings may be omitted entirely — HarnessSettings
defaults are the validated production recipe.
HarnessSettings#
HarnessSettings is the single declaration surface: one
group per pipeline stage, plus the optimization workshop the stages share. It is fully
platform-serializable, so a run recipe can be stored and replayed.
Parameter |
Default |
Description |
|---|---|---|
|
|
CAD safety margin in metres, padding every pipe radius. One source read by both the port-base preparation and the CAD-interference constraint. |
|
|
Design rules added on top of the cadmap’s own (which are always carried). See Design Rules on the Harness. |
|
|
Grid paths searched per specification, kept as its alternatives; the solution stands by the cheapest. |
|
|
Keep routing the remaining specifications when one raises. A conduit no path joins is
kept unrouted either way, and left out of the harness ( |
|
|
Skeletonization recipe — see Skeletonization. |
|
|
Search space, lattice and seed repair shared by every stage. |
|
|
Unlock stage (enabled). |
|
|
Consolidate stage (disabled — experimental, opt in explicitly). |
|
|
Shape stage (enabled). |
|
|
Object name; an unnamed settings takes |
from routing.core.harness import (
ConsolidationSettings,
HarnessSettings,
OptimizationSettings,
ShapingSettings,
UnlockSettings,
)
settings = HarnessSettings(
clearance=0.005,
optimization=OptimizationSettings(max_shift=0.1),
unlocking=UnlockSettings(max_iterations=5, lock_dilation_voxels=5.0),
consolidation=ConsolidationSettings(enabled=True, max_iterations=3),
shaping=ShapingSettings(max_iterations=2, repair=True),
)
Derived Defaults#
Fields that depend on the computation cadmap stay None until
complete() resolves them against it —
generate_harness calls it for you, on a copy (your settings object is never mutated).
Explicit values pass through untouched. The routing voxel size is the step of the map’s first
region (cadmap.room_voxels), the coarse one on a multi-scale map.
Field left |
Resolves to |
|---|---|
|
Half the routing voxel size |
|
Half the routing voxel size (the optimizer-window lattice step) |
|
5 routing voxels |
|
|
|
|
A finer optimizer lattice than the routing one costs a one-shot recomputation of the
distance fields; setting optimization.voxel_size to the cadmap’s own step makes every
pass read the cadmap’s stored state instead.
The Optimization Workshop#
OptimizationSettings is shared by all three stages: it
defines what the solver is allowed to move and on which lattice.
Parameter |
Default |
Description |
|---|---|---|
|
|
Per-point movement budget in metres ( |
|
|
Optimizer-window lattice step ( |
|
|
Kinship depth exempting collision pairs: |
|
|
Seed repair before each pass ( |
|
|
Charged to weighted-length samples that leave the grid. |
The Stages#
Each stage runs when its enabled flag is set, in canonical
unlock → consolidate → shape order, and carries its own GA recipe (ga).
Unlock#
Feasibility is the whole objective here: no length polishing. When a pass plateaus while
still infeasible, every cramped free line becomes a repulsive lock on the cadmap — its
cylinder dilated by lock_dilation_voxels routing voxels, weights inside multiplied by
lock_strength — the cables it carries are withdrawn and rerouted around the locks, and
the harness is rebuilt. Each (re)generation is snapshot into the history with its locks.
Parameter |
Default |
Description |
|---|---|---|
|
|
Run the stage. |
|
|
Lock + regenerate rounds before giving up (a warning is emitted, the run continues as is). |
|
|
Dilation of each lock cylinder, in routing voxels. |
|
|
Weight multiplier inside a lock. |
|
|
Stage GA recipe ( |
Consolidate#
Experimental, disabled by default. Reruns a coarse pass followed by
Bundle.repaired() until two successive reruns share the same topology signature, then
hands the frozen bundle to the shaping stage. No design-rule term takes part: they are
antagonistic to consolidation.
Parameter |
Default |
Description |
|---|---|---|
|
|
Opt in explicitly. |
|
|
Rerun cap before the topology is declared unfrozen (warning, last bundle kept). |
|
|
Stage GA recipe. |
Shape#
The final refinement, with the design-rule terms active on the frozen topology.
Parameter |
Default |
Description |
|---|---|---|
|
|
Run the stage. |
|
|
Chain that many passes on the improved seed (relaxation and GA rerun each time). |
|
|
Arm the junction repair at the head of each pass — junctions are split, merged or
given slack before the problem is built, so the design-rule terms enter on the
repaired topology. Because that changes the topology in place, an armed repair runs
the stage on a structural twin: the shaped harness is |
|
|
Stage GA recipe. |
Skeletonization#
settings.bundling is a SkeletonSettings: the recipe
that turns routed cables into branches. The bundle carries it, so every rebuild during the
unlock loop — and every Bundle.repaired() — reuses the same recipe.
Parameter |
Default |
Description |
|---|---|---|
|
|
Skeletonization lattice step ( |
|
|
Dilation applied when voxelizing the cables, before thinning. |
|
|
Morphological opening applied before thinning — erases small protrusions. |
|
|
Further morphology before thinning: closing fills small gaps, erosion shrinks fat
regions. A radius of |
|
|
Smoothing window applied to each branch before it is split into straight segments. |
|
|
Window used to detect the direction changes a branch is split at. |
|
|
A direction change below this angle is not a turn — the branch is not split there. |
GA Recipes#
Each stage’s ga is a GAConfig. Its defaults
are the converged production recipe (small population, ~3 mutated genes per child), so
leaving ga=None is the normal case. Override it to change the budget, watch the solver,
or probe an operator lever.
from routing.core.bundle_optimization import GAConfig, StallCriterion, TerminationCriteria
from routing.core.harness import ShapingSettings
shaping = ShapingSettings(
ga=GAConfig(
jitter_sigma=0.1, # seed jitter; at pop_size=2 always set it
verbose=True, # pymoo's per-generation table
termination=TerminationCriteria(
n_gen=2000,
feasible_stall=StallCriterion(ftol=1e-3, evals=1000),
),
)
)
|
Default |
Description |
|---|---|---|
|
|
Population size. At |
|
|
Random seed. |
|
|
Seeding: |
|
|
Stopping policy — every active criterion races, first to trip stops the run. |
|
|
pymoo’s own per-generation output table. |
|
|
Operator levers; |
|
|
Record the per-term objective/violation breakdown every N generations (one extra evaluation each — noticeable at tiny populations). |
|
Default |
Description |
|---|---|---|
|
|
Generation cap; always active. |
|
|
Wall-clock cap racing against |
|
|
Best constraint violation at or below this counts as feasible. |
|
|
Stagnation window on the objective while feasible. |
|
|
Stagnation window on the violation while infeasible. |
|
|
End the run as soon as the best individual is feasible — the unlock stage’s criterion. |
Operator levers, all optional and documented in
routing.core.bundle_optimization: PolynomialMutationConfig (mutated genes per
child), CauchyMutationConfig (heavy-tailed jumps across violation ridges),
ChainTranslationConfig (translate a whole branch chain at once),
BlockCrossoverConfig (inherit a point or chain whole from one parent) and
AnnealedSurvivalConfig (tolerance to infeasibility, cooling over the budget).
Reading the Result#
generate_harness returns (bundle, history). The bundle is left at the last pass’s
best state.
# Every (re)generation, with the locks that led to it
for snapshot in history.snapshots:
print(snapshot.label, len(snapshot.bundle.lines), len(snapshot.locks))
# Every optimization pass, in order
for record in history.passes:
print(record.stage, record.result.f_best, record.result.max_violation,
record.result.n_eval, record.result.elapsed_seconds)
final = history.final() # last pass — the refined result
best = history.best() # lowest feasible objective across passes
final.materialize() # write that pass's state back onto the bundle
history.volume_models().display_3d() # chronology: snapshots + raw/relaxed/optimized per pass
history.plot_data().plot() # convergence dashboard
HarnessOptimizationHistory serializes with the
platform object: the passes share one bundle by reference and the per-line local cadmaps
stay runtime-only, so the curves and states are carried, not the gigabytes.
What you can do with the bundle itself:
Call |
Returns |
|---|---|
|
3D view of the harness. |
|
The body branches as plain cylinders at packed radius — imposed entry geometry excluded (it is re-derived from the ports at every rebirth). |
|
The routed cables the harness carries. |
|
Re-derive a voxel skeleton from the current geometry (round-trip). |
|
A re-consolidated copy: the only step that changes the topology. |
Driving the Stages by Hand#
HarnessOptimizer is the orchestrator generate_harness
builds internally. Use it directly to run one stage on an existing bundle — the shape of a
calibration bench.
from routing.core.harness import HarnessOptimizer, HarnessSettings
settings = HarnessSettings(...).complete(cadmap) # mandatory: no derived field may stay None
optimizer = HarnessOptimizer(settings)
optimizer.unlock(bundle) # one feasibility pass -> SolverResult
frozen = optimizer.consolidate(bundle) # reruns until the topology freezes -> Bundle
history = optimizer.shape(frozen) # shaping pass(es) -> HarnessOptimizationHistory
shaped = history.final().bundle # with shaping.repair armed, the stage worked on a twin
history = optimizer.optimize(bundle) # the enabled stages, in canonical order
optimizer.diagnose(bundle, stage="shape") # per-entity violation report at the current geometry
diagnose prints the objective, each cost contribution and, per constraint, every
violated entry with its residual (in metres and in voxels) and the entity carrying it —
a line, a collision pair, a turn. It returns those entries, so a script can act on them.
Pass attach=True to force a re-preparation; by default it reuses the local cadmaps the
last pass left, so the diagnosed geometry is exactly the stuck one.
Design Rules on the Harness#
A design rule has two projections: the routing grid (weights, costs, heuristics) and the
harness optimization problem. A rule that implements evaluate_on_bundle — today
BranchPortDistanceRule — automatically becomes a term of the shaping problem,
under the form its RuleEnforcement declares.
from routing.core.design_rules import BranchPortDistanceRule, RuleEnforcement
rule = BranchPortDistanceRule(
branches_min_distance=0.05,
port_branch_min_distance=0.03,
enforcement=RuleEnforcement(hard=False, weight=50.0, power=2), # soft cost instead of hard constraint
)
settings = HarnessSettings(harness_rules=[rule])
Rules carried by the cadmap are always part of the harness; harness_rules adds the ones
that only make sense once the cables are bundled. See
Design Rules on the Harness for the parameter reference.
Custom Optimization Terms#
The problem solved at each pass is built by
OptimizationTerm.from_bundle:
the intrinsic trio — weighted length as the objective, CAD interference and line
interpenetration as hard constraints — plus one term per bundle-active rule. To add a cost
of your own, subclass Phenomenon, wrap it in an
OptimizationTerm and hand the list to the
optimizer:
from routing.core.bundle_optimization import OptimizationTerm
terms = OptimizationTerm.from_bundle(bundle, alpha=clearance_in_voxels)
terms.append(OptimizationTerm(MyPhenomenon, hard=False, soft=True, weight=10.0, power=2))
history = HarnessOptimizer(settings, terms=terms).optimize(bundle)
Terms are a runtime override: phenomenon factories are callables, so they live on the
orchestrator, never on the serialized settings. Each term declares the stages it joins
through active_in.
Replaying a Run#
initial_routing starts the pipeline from an existing routing instead of routing from
scratch — the replay lever on a mutable cadmap. Rerunning the unlock stage with other lock
knobs, without paying for the initial generation again:
from routing.core.route_planner import RoutingResults
seed = history.snapshots[0] # "generated 1": full solutions, no lock
cadmap.restore_solutions(seed.solutions)
cadmap.withdraw_zones([lock for snapshot in history.snapshots for lock in snapshot.locks])
bundle, history = route_planner.generate_harness(
specifications,
settings,
initial_routing=RoutingResults(cadmap.solutions, cadmap),
)
Bundling Without Optimizing#
To inspect the skeleton or the bundle without running any GA, go through the routing result:
from routing.core.skeleton import SkeletonSettings
routing_result = route_planner.generate(specifications, n_iterations=1, steady_mode=True)
skeleton = routing_result.to_skeleton(SkeletonSettings(voxel_size=0.01))
skeleton.display()
bundle = routing_result.to_bundle(settings=SkeletonSettings(voxel_size=0.01), name="harness")
bundle.display_3d()
Both take the same recipe object. to_bundle carries the cadmap’s design rules over by
default; pass rules=[...] to override that set.
Tuning Guide#
Symptom |
Knob |
|---|---|
Harness never reaches feasibility |
More unlock rounds ( |
Passes are slow |
Coarsen |
Cables pinned too close to CAD |
Raise |
Junctions crowd each other or sit on a connector |
Add a BranchPortDistanceRule to |
Branch topology looks noisy |
Coarsen |
Solver barely moves the harness |
Raise |
Next Steps#
User Guide: Design Rules Reference — every rule’s parameters, including harness enforcement
User Guide: Path Geometry Optimization — the per-path geometry optimizer (a different, cable-by-cable step)
User Guide: Accessing Routing Results — read routed paths, packs and bifurcation points
User Guide: Pipe Pooling (Bundling) — control which cables are allowed to share a route in the first place