Skip to content

Cables and tubes (bt.routing)

Routes between the ports of the connection plan: the line each is made of (a spec), the clamps and ducts it passes, and what is derived from them — centerline, lengths, cable list, BOM rows and the checks. See Cables and tubes.

import botrail as bt

rt = bt.routing
rt.spec(scene, "tu0604", kind="tube", media=["pneumatic"], od_mm=6,
        bend_r_fixed_mm=15, bend_r_moving_mm=20, order_step_m=20,
        manufacturer="SMC", model="TU0604BU-20")
rt.clamp(scene, "clip", at=("arm", "forearm_link", (-0.2, 0.0, 0.05)), length_m=0.03)
rt.route(scene, "air_hand", carries="air_hand", spec="tu0604", via=["clip"], slack_m=0.15)

rt.geometry(scene, "air_hand")["cut_length_m"]
rt.cable_list(scene).save("cables.csv")
assert rt.report(scene).ok

routing

Cable and tube routes: bt.routing.

A route is the physical run of a connection — which ports it joins (the connection plan, bt.connections), which line it is made of (a spec: outer diameter, bend radii, sold lengths), which way points, ducts and clamps it passes. Everything else is derived every time it is read, from the residents' current poses: the centerline with its bends, the cut and purchase length, the BOM rows, the cable list and the checks. Move the cabinet and the cable follows, and so does its length.

Numbers are declared, never guessed: a spec that states no bend radius leaves the bend checks unknown (route_spec_unknown), it does not get a default. A span between a clamp on a robot link and the next anchor is a moving span: drawn at its time-zero shape here, verified by the rope replay.

FILL_LIMIT module-attribute

FILL_LIMIT = 0.5

Convert a string or number to a floating-point number, if possible.

HOLDER_KINDS module-attribute

HOLDER_KINDS = (
    "fixed",
    "rotating",
    "sliding",
    "strap",
    "retracting",
)

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

CableList

One row per route — the cable list a wiring design, purchasing or an ECAD import starts from. Renders as CSV, Markdown or JSON.

RoutingReport

The checks: findings (severity / code / route / message and, when located, position, s_m and — on a cycle — t_s) over the routes of the cable list, and, when a timeline was given, the spans that move: what the kinematics say of each (the holders' distance and twist over the cycle, hence the free length and the stroke) and what the rope replay says (bend, stretch, sag, nearest body). ok is "no error"; warnings and unknowns stay listed.

adopt

adopt(scene, route: str, candidate) -> None

Give route the way of candidate — a row of :func:suggest, or its index in that list.

along

along(
    on,
    *,
    pitch_m: Optional[float] = None,
    face: Optional[str] = None,
    offset_m: Optional[float] = None,
    inset_m=None,
) -> dict

A way point meaning "run along the body on": an obstacle's name, or (robot, link) for a robot link.

The route is laid on a face of the body's collision bounds — face ("+x" … "-z", in the body's frame) when given, else the side face, one along the body's long axis, nearest the way in and the way out — the line's radius plus the clamp's standoff off the surface, and held by auto clamps every pitch_m (the route's clamp_pitch_m, 0.3 m, when unset) — <route>/c0, c1, … in the route's anchors, counted in the BOM under the route's clamp article. On a robot link the run rides the link: its spans are dressed, replayed with the arm but not sized for free length, and its ends are drawn in until they clear the robot's other links (the joint housings at either end of an arm link). A lateral offset between the way in and the way out smaller than a pitch is taken up by the way in, so the run itself is parallel to the body's edge; the entry and the exit are the projections of the neighbouring way points, so the run covers what lies between them. offset_m moves the run sideways on its face, along the body's axis across the run (toward that axis's positive end) — two lines tied along one face side by side, each with its own clips, are offset_m=0.007 and offset_m=-0.007. inset_m stops the run short at its ends, past the clearance of the neighbouring links: one figure for both ends or (way_in, way_out) — inset_m=(0.12, 0) leaves the first 12 cm of an arm link free for the loop that climbs to it.

animate

animate(
    scene,
    timeline,
    *,
    routes=None,
    settle_fixed: bool = False,
    ground_z=0.0,
    step_s: float = 0.0020833333333333333,
    spacing_m=None,
    axial_hz: float = 500.0,
    bending_hz: float = 120.0,
    friction: float = 0.5,
    margin_m: float = 0.5,
    arm_links: bool = True,
    settle_s: float = 1.0,
    rest: str = "straight",
    rod_hz: float = 120.0,
    solver: str = "physics",
    rigidity_nm2=None,
)

Replay the routes' moving spans as ropes against a baked cycle.

Every moving span (between a clamp on a robot link, or a port on one, and its neighbour) becomes a bt.rope track named route:<route>#<span>: held at both ends by what holds it in the plan (a port's straight_m, half a clamp's length_m; a fixed or rotating holder holds the line's direction too — one sample spacing at least, led along its direction, the bisector of its legs, or the run of an along clip — so the line leaves the holder the way it points it; a strap holds a point; a port on a moving holder is a swivel fitting and holds a point). The spans the line slides through a holder between are one rope, through a ring on the holder's body at each sliding holder, named after the first of them, with their slack together. Each starts from its time-zero polyline with its share of the route's slack_m hung in as a dip, at the line's radius, colliding with the enabled obstacles within margin_m of it (not the ducts it runs in, not the residents its holders sit on, not a body its starting polyline already passes through — the replay would blow apart on it; the static report names that clash) and with the floor at ground_z (None for none). A span dressed along a link (both ends on it) is replayed the same way; the stretch between two holders of a carrier is replayed once, as the hose — carrier:<name>#<i>, the hose's diameter, its mass plus the lines inside, the largest slack any of them brings — and every line in it reads that track. A span held on a robot collides with that robot's links — it rests on the arm instead of falling through it — except a holding link whose shape holds the held point (a clip seated in the casting, which would push its own held particle away). The slack's dip is hung on the side with the most room (a stiff line keeps the loop it is laid in), and a link the starting shape still runs through is left out with a warning (the report then names the contact). arm_links=False leaves the arm out altogether. settle_fixed=True replays the fixed spans too, so they hang under gravity instead of standing on their polyline. Every rope first settles for settle_s with its holders parked at time zero — it was laid along a design polyline, not a hanging shape — before the cycle runs. The ropes are computed side by side (they do not interact).

The scene's current poses are time zero, as for bt.rope.animate. Returns the new timeline; :func:report with it judges the spans. A line rests straight (rest="straight": a tube or a cable off the reel, bent only by what holds it and by its weight — not the dip it is laid in) and is a rod, not a string (rod_hz=120: windows of a few particles each drawn toward their straight rest shape, so a bend anywhere is resisted — a tube holds a round loop between its holders instead of hanging in a V; bending_hz=120 is the fold-resisting spring beside it). The fixed spans, settled only to hang, stay ropes. The stiffness is the solver's native setting, not the line's, and it is bounded by the step — past about twice this the replay tears: shape, clearance and slack are judged, tension is not.

solver="static" draws the spans instead of simulating them (see :func:botrail.rope.animate): at every frame each span is the rod at rest between its holders — of least bending and gravitational energy, its length what the plan gives it, held by every holder as the replay holds it (a fixed port's stub and a run's clip in direction too; a moving port, a swivel fitting, and a declared clamp at their points), clear of every link of the arm and of the obstacles around — a harness configurator's picture, with no dynamics, settling or spring settings. rigidity_nm2 is the flexural rigidity that balances the bend against the weight (None: a polyurethane tube's for each line's diameter). step_s, settle_s, rest, rod_hz and the spring frequencies do not apply to it; the report judges its frames as it does the replay's.

attach

attach(scene, at) -> dict

Where something sits, as the plan stores it.

at is (obstacle_or_frame, (x, y, z)) — the name of an obstacle (it follows set_obstacle_pose) or, failing that, of a frame — or (robot, link, (x, y, z)) for a point on a robot link (it follows the joints), the offset in the resident's own frame in metres. A dict with kind (obstacle / link / frame) is taken as is.

cable_list

cable_list(scene) -> CableList

The cable list: every route with its line, ends, lengths and status.

carrier

carrier(
    scene,
    name: str,
    *,
    holders=None,
    inner_mm=None,
    od_mm=None,
    bend_r_moving_mm=None,
    torsion_deg_m=None,
    mass_kg_m: Optional[float] = None,
    stroke_m: Optional[float] = None,
    manufacturer: Optional[str] = None,
    model: Optional[str] = None,
    catalog: Optional[str] = None,
    robot: Optional[str] = None,
) -> str

A protective hose or 3D energy chain dressed along an arm: the dress pack the lines to the hand run in together.

holders are where it is fixed, in order (each as :func:attach, typically (robot, link, offset) on the upper arm, the forearm, the wrist — or dict(at=..., kind=..., direction=..., axis=..., stroke_m=..., force_n=...) with the holder's kind, direction and rail as for :func:clamp: a rotating holder lets the hose roll, a sliding one lets it slide through, a retracting one is the kit's retraction unit); inner_mm its bore. A route passes it as one way point, via=[name], and takes the holders as its anchors; every route through it is one bundle, whose cross-section is checked against the bore (duct_fill) and whose media are kept apart (route_separation for power with signal). The hose is a line of the BOM (cable.carrier) as long as the longest route dressed in it. Over a cycle, :func:animate replays the stretch between two holders as one hose — od_mm thick, mass_kg_m of its own plus the lines inside — and the lines read that replay (carrier:<name>#<i>). stroke_m is what the kit's retraction takes up, when it has one. With catalog= (a cable.carrier spec pack: a conduit, or a robot's dress-pack kit) the bore, the diameter, the bend radius, the torsion, the mass and the stroke the pack states are the defaults and its maker and part number the identity; a bore nobody states leaves the bundle's fill unchecked and the requirement row saying so. A kit that states where its holders go (holders_at: a link and an offset in mm per holder) places them on robot when you pass no holders.

The carrier is a line of scene.requirements(): the cell asks it for a bore (the bundle at 50 % fill) and, over a baked cycle, the hose length between the holders, the stroke, the twist per metre and the bend radius the replay showed — compared with what is declared here (or, through catalog=, what a kit states), and the figures bt.catalog.search_for looks for.

clamp

clamp(
    scene,
    name: str,
    *,
    at,
    length_m: Optional[float] = None,
    kind: Optional[str] = None,
    direction=None,
    axis=None,
    stroke_m: Optional[float] = None,
    force_n: Optional[float] = None,
    manufacturer: Optional[str] = None,
    model: Optional[str] = None,
    catalog: Optional[str] = None,
) -> str

A clamp: a holder the line is held in (at as :func:attach).

On a robot link it is where a moving span starts or ends; length_m is the material the holder keeps straight. kind is what the holder leaves the line free to do — "fixed" (a rigid clip or bracket: the place, the direction, no roll, no slide; the default), "rotating" (a rotating holder: the line rolls about its own axis), "sliding" (a guide the line slides through, and rolls in), "strap" (a strap: the point, and the line turns and rolls) or "retracting" (a carriage on a rail, holding like a fixed clamp, pulled toward the rail's start: a retraction unit, with axis the rail's direction in the body's frame, stroke_m how far it runs and force_n the pull) — and direction the way the line runs through it, in the body's frame (the bisector of its two legs when unset). Every clamp is a line of the BOM (cable.clamp); identical articles merge. With catalog= (a cable.clamp spec pack) the article's maker, part number, width (as the holding length) and kind come from the pack.

duct

duct(
    scene,
    name: str,
    *,
    path,
    inner_m,
    manufacturer: Optional[str] = None,
    model: Optional[str] = None,
    catalog: Optional[str] = None,
) -> str

A duct or tray as a support only: the centerline a route follows end to end (path, world metres, two or more points) and its inner cross-section inner_m = (width, height). bt.parts.duct builds the boxes on the floor and declares this in one call; call this directly for a duct whose body is already in the scene (imported geometry) or needs none.

geometry

geometry(scene, name: str) -> dict

One route's derived shape at the current poses: centerline, s (arc length at each point), anchors (each with its position, s_m, moving and the tangent the line runs through it along: a port's stub, an auto clamp's run, a declared clamp's bisector), spans, bends, length_m, cut_length_m, purchase_length_m.

remove_spec

remove_spec(scene, name: str) -> None

Remove a spec; routes made of it report route_endpoint.

remove_support

remove_support(scene, name: str) -> None

Remove a clamp or duct; routes that pass it report route_endpoint.

report

report(
    scene,
    timeline=None,
    *,
    dt: float = 0.05,
    sag_limit_m: float = 0.15,
) -> RoutingReport

Resolve every route at the current poses and check it: bends against the spec's fixed radius, the exit out of each connector, the cut length against the sold lengths and the maximum run, fixed spans against the enabled obstacles and the robots at their current pose, routes against each other, and the connections placed but not yet routed.

With a baked timeline the moving spans are judged over the cycle too, every dt seconds: the distance between a span's holders gives the free length it needs (route_tension when the line between them is shorter) and the stroke a retraction takes up; their whole relative rotation the twist (route_twist against the spec's torsion_deg_m — bend and torsion together, the worst case); and, where :func:animate replayed the span as a rope, its tightest bend (route_bend_moving), stretch (route_stretch), sag over sag_limit_m (route_sag) and the nearest body (route_contact / route_clearance) — each finding with its time.

restore

restore(scene, plan: dict) -> None

Restore a serialized routing plan (specs, supports, routes).

route

route(
    scene,
    name: str,
    *,
    carries,
    spec: str,
    via=(),
    orthogonal: bool = False,
    slack_m=0.0,
    collide: bool = True,
    color=None,
    clamp_pitch_m: Optional[float] = None,
    clamp=None,
) -> str

Declare (or replace) a route.

carries names the connections (bt.connections) this line runs — one, or several for a multi-core cable; the first decides the two end ports, which must have geometry (at= / exit= on the port). spec names the line. via is the ordered way: points (x, y, z), clamp names, duct names (:func:through), bodies to run along (:func:along). orthogonal inserts axis-aligned elbows between way points (z, then x, then y), the way a line runs along a wall and a floor. slack_m is material beyond the measured run — the service loop a moving span needs: one figure shared by the route's moving spans, or a list with one figure per moving span in route order. collide makes the fixed spans colliders for the planner, the tick verification and the clearance readout (named route:<name>#<span>); the static clash check runs either way. clamp_pitch_m is the clamp pitch of the runs along a body, clamp the article those clamps are (with kind as for :func:clamp: straps along a cobot's arm are clamp=dict(kind="strap", ...)) — the article those clamps are — a dict with manufacturer, model, catalog, length_m (material held straight in each) and standoff_m (the saddle's height, 5 mm when unset).

spec

spec(
    scene,
    name: str,
    *,
    kind: Optional[str] = None,
    media=None,
    od_mm=None,
    bend_r_fixed_mm=None,
    bend_r_moving_mm=None,
    mass_kg_m=None,
    max_length_m=None,
    torsion_deg_m=None,
    end_allowance_m: float = 0.0,
    sold_lengths_m=None,
    order_step_m=None,
    manufacturer: Optional[str] = None,
    model: Optional[str] = None,
    catalog: Optional[str] = None,
    color=None,
) -> str

Declare (or replace) a line — the rule every route made of it follows.

kind is cable, tube or hose; media the media it may carry (power / pneumatic / signal / network; unstated = unchecked). Dimensions are in millimetres: od_mm the outer diameter, bend_r_fixed_mm the minimum bend radius laid fixed (the fillet at every corner), bend_r_moving_mm the one in continuous motion. sold_lengths_m (6 and 12 m robot cables) or order_step_m (20 m reels) decide the purchase length; end_allowance_m is added at each end for stripping and the connector; max_length_m is a product or standard limit on one run; torsion_deg_m the twist the line takes per metre of free length (a moving span's holders turning about each other). Identity (manufacturer / model / catalog) goes onto the BOM row of every route made of this line. A value you do not know you leave out: the checks say unknown.

With catalog= — the id of a line's spec pack (cable.* / pneumatic.tube), or a package directory — the figures the pack states (od_mm, the bend radii, mass_kg_m, media, max_length_m, sold_lengths_m / order_step_m) are the defaults, its maker and part number the identity, and the reference is recorded; what you pass explicitly wins, and what the pack does not state stays unknown.

suggest

suggest(scene, route: str) -> list[dict]

The ways route could take through the ducts the cell has, best first. The way points you fixed — points, clamps, carriers, runs along a body — stay where they are, in order; every ordered choice of up to three ducts is tried between them, each candidate resolved and checked at the current poses. A candidate carries its via (what :func:adopt or :func:route takes), the ducts it passes, length_m, cut_length_m, bends, supports, the static errors / warnings with their codes (the ducts' bundle findings included), its open_m (the run outside any duct), a score (length + the open run again + 0.3 per bend + 0.2 per support: a metre in a duct beats a metre on the floor) and current for the way the route takes now. The order is by errors, then score, then warnings — the same every time for the same cell. Free space is not searched: a candidate is a choice of ducts, not a path around an obstacle.

sweep_holder

sweep_holder(
    scene,
    timeline,
    carrier: str,
    holder: int,
    *,
    x=None,
    y=None,
    z=None,
    dt: float = 0.05,
)

Try places for one holder of a carrier and table what the hose then goes through over the baked cycle.

holder is the holder's index in the carrier's list; x / y / z are the candidate coordinates in the holder's own frame (the link's), each a list — the grid is their product, an axis not given keeps the holder's current value. Nothing is re-baked: the cycle is the same, only the plan's holder moves, and the kinematic report is read again at every point. Each row carries the holder's coordinates and stroke_m (the largest over the spans the holder ends), free_length_m (what the hose needs between its holders, in total), twist_deg (the largest over those spans) and errors (the route findings, static and over the cycle, that are errors on the routes dressed in the carrier). The replay is not run: contact is not among the figures. Returns a :class:botrail.study.Sweep — best("stroke_m") is the place a retraction has least to do — and leaves the plan as it was.

through

through(name: str) -> dict

A way point meaning "run through the support name" — the same as passing the support's name as a string in via.