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 generate() on the specifications — one grid path per cable.

n_candidate_paths, steady_mode

bundle

Voxelizes the routed cables, thins the volume to its medial axis, fits the solutions back onto it and wires the result into a Bundle (branches, junctions, port leaders).

bundling

unlock

Drives the harness to feasibility. On a violation plateau the cramped zones become repulsive locks on the CadMap and the cables they carry are rerouted around them.

unlocking

consolidate

Reruns coarse passes until the topology freezes (experimental — disabled by default).

consolidation

shape

Refines the frozen topology with the design-rule constraints active.

shaping

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

clearance

0.0

CAD safety margin in metres, padding every pipe radius. One source read by both the port-base preparation and the CAD-interference constraint.

harness_rules

[]

Design rules added on top of the cadmap’s own (which are always carried). See Design Rules on the Harness.

n_candidate_paths

1

Grid paths searched per specification, kept as its alternatives; the solution stands by the cheapest.

steady_mode

True

Keep routing the remaining specifications when one raises. A conduit no path joins is kept unrouted either way, and left out of the harness (routed_solutions).

bundling

SkeletonSettings()

Skeletonization recipe — see Skeletonization.

optimization

OptimizationSettings()

Search space, lattice and seed repair shared by every stage.

unlocking

UnlockSettings()

Unlock stage (enabled).

consolidation

ConsolidationSettings()

Consolidate stage (disabled — experimental, opt in explicitly).

shaping

ShapingSettings()

Shape stage (enabled).

name

""

Object name; an unnamed settings takes "<run name>_settings" at completion.

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 None

Resolves to

bundling.voxel_size

Half the routing voxel size

optimization.voxel_size

Half the routing voxel size (the optimizer-window lattice step)

optimization.max_shift

5 routing voxels

unlocking.ga

UnlockSettings.production_ga — jittered seed, stop_when_feasible, violation-plateau stall

consolidation.ga / shaping.ga

GAConfig(jitter_sigma=max_shift) — the campaign’s converged recipe

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

max_shift

None

Per-point movement budget in metres (None = 5 routing voxels). Also the natural jitter unit of the GA seeding.

voxel_size

None

Optimizer-window lattice step (None = half the routing voxel).

exclude_tree_hops

2

Kinship depth exempting collision pairs: 1 = adjacent lines only, 2 also exempts free-line pairs one intermediate line apart (branches forking from a shared junction). Kinship never exempts a port leader — a route folding back onto its own entry stays constrained.

relaxation

RelaxationConfig()

Seed repair before each pass (enabled, max_search=2, max_passes=8). Moves interfering points to their best voxel-aligned neighbour so the GA starts from a legal seed.

out_of_grid_penalty

1000.0

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

enabled

True

Run the stage.

max_iterations

3

Lock + regenerate rounds before giving up (a warning is emitted, the run continues as is).

lock_dilation_voxels

5.0

Dilation of each lock cylinder, in routing voxels.

lock_strength

100.0

Weight multiplier inside a lock.

ga

None

Stage GA recipe (None = production_ga).

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

enabled

False

Opt in explicitly.

max_iterations

5

Rerun cap before the topology is declared unfrozen (warning, last bundle kept).

ga

None

Stage GA recipe.

Shape#

The final refinement, with the design-rule terms active on the frozen topology.

Parameter

Default

Description

enabled

True

Run the stage.

max_iterations

1

Chain that many passes on the improved seed (relaxation and GA rerun each time).

repair

False

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 history.final().bundle (what generate_harness returns), and the states recorded before it stay replayable.

ga

None

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

voxel_size

None

Skeletonization lattice step (None = half the routing voxel under complete; the routing voxel size in a standalone to_skeleton()).

dilation_voxels

2

Dilation applied when voxelizing the cables, before thinning.

opening_radius

2

Morphological opening applied before thinning — erases small protrusions.

closing_radius / erosion_radius

0

Further morphology before thinning: closing fills small gaps, erosion shrinks fat regions. A radius of 0 skips its stage.

smoothing_window

2

Smoothing window applied to each branch before it is split into straight segments.

tangent_window

3

Window used to detect the direction changes a branch is split at.

min_turn_angle

pi / 12

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

GAConfig

Default

Description

pop_size

2

Population size. At 2, always set jitter_sigma — two full-domain random individuals are a dead start.

seed

1

Random seed.

x0 / jitter_sigma / jitter_distribution

None / None / "normal"

Seeding: x0 defaults to the bundle’s current state; jitter_sigma None means full-domain random sampling instead of a jitter around the seed.

termination

TerminationCriteria()

Stopping policy — every active criterion races, first to trip stops the run.

verbose

False

pymoo’s own per-generation output table.

mutation / crossover / survival

PolynomialMutationConfig() / None / None

Operator levers; None = pymoo defaults (SBX crossover, Deb survival).

term_record_period

None

Record the per-term objective/violation breakdown every N generations (one extra evaluation each — noticeable at tiny populations).

TerminationCriteria

Default

Description

n_gen

12500

Generation cap; always active.

max_seconds

None

Wall-clock cap racing against n_gen.

feasible_tol

1e-6

Best constraint violation at or below this counts as feasible.

feasible_stall

StallCriterion(ftol=0.001)

Stagnation window on the objective while feasible. StallCriterion trips when the tracked best improves by less than ftol over evals (default 1000) consecutive evaluations.

violation_stall

None

Stagnation window on the violation while infeasible. None keeps the escape on its full budget.

stop_when_feasible

False

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

bundle.display_3d()

3D view of the harness.

bundle.to_cylinders()

The body branches as plain cylinders at packed radius — imposed entry geometry excluded (it is re-derived from the ports at every rebirth).

bundle.distinct_solutions()

The routed cables the harness carries.

bundle.to_skeleton()

Re-derive a voxel skeleton from the current geometry (round-trip).

bundle.repaired()

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 (unlocking.max_iterations), stronger locks (lock_dilation_voxels, lock_strength). Read optimizer.diagnose(bundle, stage="unlock") first.

Passes are slow

Coarsen optimization.voxel_size (the cadmap’s own step avoids recomputing the distance fields), lower the GA budget (termination.n_gen) or arm a stall.

Cables pinned too close to CAD

Raise clearance (metres).

Junctions crowd each other or sit on a connector

Add a BranchPortDistanceRule to harness_rules; arm shaping.repair.

Branch topology looks noisy

Coarsen bundling.voxel_size / raise bundling.opening_radius; enable the consolidation stage.

Solver barely moves the harness

Raise optimization.max_shift (also raise the GA jitter_sigma with it).

Next Steps#