Skip to content

Assembly and fastening (bt.assembly)

Bolted joints as a drawing states them, the screwdriver's program, and what a bake says about every screw. See Assembly and fastening.

screw = bt.assembly.iso4762(5, 20)
joint = bt.assembly.joint(scene, "cover_joint", a="housing", b="cover", pattern_a=threads,
                          pattern_b=clearances, fastener=screw, seat=seat, thickness=0.010,
                          torque_nm=(4.0, 5.0), min_engagement_mm=10)
driver = bt.assembly.driver(scene, "driver", robot="arm", bit="drv_bit", shank="drv_shank",
                            stroke=0.055, fastener=screw, torque_nm=4.5, cycles=7)
fastening = bt.assembly.fasten(sq, joint, driver, feeder, motions={...})

assembly

Assembly: fit and fasten (design/design-assembly.md).

A mechanical assembly cell does two things with a part: it fits it — carries it onto its mate, over pins, into a bore — and fastens it, one screw at a time. botrail says both in the vocabulary it already has. A fit is an attach, a move and a detach, plus a report of where the part landed against where the joint says it goes. A fastening is that same carry for the screw, then the screwdriver's own stroke running it down at the thread's feed, while a driver program — one more PLC program scanned beside the robot's, the way a machine tool's is — times the rundown from the screw's pitch and answers OK or NOK. Threads, torque curves and cam-out are not simulated: the numbers are data with a source, the motion is geometry, the result is a signal.

fastener = bt.assembly.iso4762(5, 20)                   # M5×20, 8.8
holes = [bt.assembly.Hole(f"h{i}", xy, "threaded", depth_mm=13) ...]
joint = bt.assembly.joint(scene, "cover", a="housing", b="cover",
                          pattern_a=bt.assembly.BoltPattern(holes), pattern_b=...,
                          fastener=fastener, seat=scene.frame("housing/top"),
                          thickness=0.010, torque_nm=(4.0, 5.0), min_engagement_mm=10)
driver = bt.assembly.driver(scene, "driver", robot="arm", bit="drv_bit",
                            shank="drv_shank", stroke=0.055, fastener=fastener,
                            torque_nm=4.5, cycles=len(joint.order) + 1)
sq = scene.sequence("assemble")
fastening = bt.assembly.fasten(sq, joint, driver, feeder, motions={...})
tl = scene.simulate_sequences(["assemble", driver.program])
rows = bt.assembly.fastening_report(tl, fastening)

A fitted part meets what it is fitted into, and the collision check has to know that is intended: fasten and place declare it (Scene.allow_object_obstacle_contact) — the cover may meet its housing and dowels anywhere, a screw may be inside the cover and the housing only within its hole's window (the hole's axis, the hole's tolerance), so a screw taught off its hole is still a collision the bake refuses.

What a joint knows is what a drawing states: the holes of each part (threaded or clearance, depth, grip), the locators (pins and their holes), the screw, the torque the joint is designed for and the engagement it needs. From that come the hole frames a teach aims at, the order the screws go in (a star pattern, opposite pairs from the centre out), the engagement (length − grip − washer − thread start), the tip's depth against the hole's bottom, and the checks a driver has to pass — its torque range, its bit, its stroke. Fastener figures for ISO 4762 socket head cap screws are tabled (iso4762), with the ISO 898-1 reference torque at μ = 0.14 as a ceiling, never as the joint's own value.

ISO_4762 module-attribute

ISO_4762 = {
    3: (0.5, 5.5, 3.0, 2.5),
    4: (0.7, 7.0, 4.0, 3.0),
    5: (0.8, 8.5, 5.0, 4.0),
    6: (1.0, 10.0, 6.0, 5.0),
    8: (1.25, 13.0, 8.0, 6.0),
    10: (1.5, 16.0, 10.0, 8.0),
    12: (1.75, 18.0, 12.0, 10.0),
}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

TORQUE_REF_NM module-attribute

TORQUE_REF_NM = {
    "8.8": {
        3: 1.37,
        4: 3.1,
        5: 6.15,
        6: 10.5,
        8: 26.0,
        10: 51.0,
        12: 89.0,
    },
    "10.9": {
        3: 1.92,
        4: 4.4,
        5: 8.65,
        6: 15.0,
        8: 36.0,
        10: 72.0,
        12: 125.0,
    },
}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

BoltPattern

The holes and locators one part shows on a mating plane, in the plane's frame (millimetres, +Z out of the part).

Driver

What driver authored: the screwdriver's program (program, on the I/O node node), the links it drives with (bit, the shank joint and its stroke_m), the screw and torque it is set up for, its phase timing, and its signals by role — signal("start") is the output the robot's program raises, signal("ok") / signal("nok") the inputs it waits on. tool is what the tool's datasheet says (torque_nm range, screw_length_mm, bit_mm) for check.

cycles class-attribute

cycles = 1

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.

int('0b100', base=0) 4

engage_m class-attribute

engage_m = 0.003

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

final_turn class-attribute

final_turn = 0.3333333333333333

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

rpm_final class-attribute

rpm_final = 40.0

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

rpm_run class-attribute

rpm_run = 340.0

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

run_m property

run_m

The stroke one screw runs down: its length less the depth the engage move already put its tip into the hole.

t_cycle_s property

t_cycle_s

Find, run, pre-tighten, final: the driver's time per screw.

t_find_s class-attribute

t_find_s = 0.2

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

t_pre_s class-attribute

t_pre_s = 0.2

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

Fastener

One screw, by its figures (millimetres): the thread and its pitch, the length under the head, the head's diameter head_dk_mm and height head_k_mm, the drive's across-flats drive_s_mm (the bit it takes), the head standard and property class, and — where a table gives one — the reference torque torque_ref_nm it must not be tightened beyond. model is the part number it goes on the bill as.

head_standard class-attribute

head_standard = 'ISO 4762'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

model_name property

model_name

The part number: what was given, else <standard> M<d>x<L>-<class>.

property_class class-attribute

property_class = '8.8'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

from_catalog classmethod

from_catalog(
    ref,
    *,
    length_mm: Optional[float] = None,
    property_class: Optional[str] = None,
) -> Fastener

A screw from a fastener spec pack — the id, or a package directory: the thread, pitch, head and drive the pack's screw component states, the length_mm and property_class it sells (its defaults when omitted), the article number, the mass and the class's reference torque (torque_ref_<class>_nm in its specs).

place

place(
    scene,
    name: str,
    position: tuple,
    quaternion: Optional[tuple] = None,
    *,
    manufacturer: Optional[str] = None,
    **attributes,
) -> str

The screw as an obstacle (bt.parts.bolt): its tip at position, +Z up the shank, pinned as a fastener with these figures — and to its pack, for a screw read from_catalog.

Fastening

What fasten appended: which screw went to which hole, in order (pairs), the driver and the feeder it used, the joint, the halted lane, and the step names per screw (steps[k]["run"]…).

Finding

Finding(severity: 'str', code: 'str', message: 'str', target: 'Optional[str]' = None)

Hole

A hole of a bolt pattern at xy_mm on the mating plane: threaded (with a usable depth_mm from the plane and an unthreaded thread_start_mm) or clearance (through a part grip_mm thick under the head — the part's thickness when omitted). tolerance_mm is the position a placed screw is judged against.

kind class-attribute

kind = 'threaded'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

thread_start_mm class-attribute

thread_start_mm = 0.0

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

tolerance_mm class-attribute

tolerance_mm = 0.2

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

Joint

A bolted joint the cell makes: part a (the threaded side) and part b (the clearance side) with their patterns, the screw, the joint's torque range and required engagement, the pose of the mating plane (seat: its centre, +Z out of a, where b's underside lands), b's thickness, and what joint() derived — the tightening order and one frame per hole (frames[id] = <name>/hole/<id>, on b's top face, +Z out, where a screw's head seats).

seat_frame class-attribute

seat_frame = ''

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

washer_mm class-attribute

washer_mm = 0.0

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

check

check(driver: Optional[Driver] = None) -> list[Finding]

What the drawing and the tool say against each other — see check().

engagement_mm

engagement_mm(hole_id: str) -> float

Thread engaged in a: length − grip − washer − thread start.

grip_mm

grip_mm(hole_id: str) -> float

Under the head to the mating plane: the clearance hole's grip, else b's thickness.

tip_depth_mm

tip_depth_mm(hole_id: str) -> float

How deep the tip goes into a from the mating plane.

Locator

A locating feature at xy_mm on the mating plane: a pin or boss standing depth_mm proud of one part, a hole or recess that deep in the other.

kind class-attribute

kind = 'pin'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

Placement

What place appended: the part carried, its expected seat, the step names (steps["release"] is where it was let go) and the seated signal the release raises — what a fastening of the part waits on.

seated class-attribute

seated = ''

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

check

check(
    joint: Joint, driver: Optional[Driver] = None
) -> list[Finding]

The joint against its screw, and — given the driver — against the tool: hole_mismatch (a hole in one part not over its mate), hole_kinds (a clearance hole where a thread should be), engagement_short (thread engaged under what the joint needs), hole_bottom (the tip through the usable depth), torque_over_table (the joint's torque above the class's reference), torque_off_joint (the driver set outside the joint's range), torque_over_tool (the joint's torque outside what the tool delivers), bit_mismatch, screw_too_long and stroke_short. Errors are what makes a joint impossible as drawn; warnings are what a reviewer would flag.

driver

driver(
    scene,
    name: str,
    *,
    robot: str,
    bit: str,
    fastener: Fastener,
    torque_nm: float,
    cycles: int,
    shank: Optional[str] = None,
    stroke: Optional[float] = None,
    engage: float = 0.003,
    rpm_run: float = 340.0,
    rpm_final: float = 40.0,
    t_find_s: float = 0.2,
    t_pre_s: float = 0.2,
    final_turn: float = 0.3333333333333333,
    program: Optional[str] = None,
    node: bool = True,
    estop: Optional[str] = None,
    finish: Optional[str] = None,
    tool: Optional[Mapping[str, object]] = None,
    channels: Optional[list] = None,
) -> Driver

The screwdriver's controller as a program of its own, scanned beside the robot's — the way a tightening controller runs: on <name>/start it finds the drive (t_find_s), runs the screw down at rpm_run for as long as the thread takes (the length less engage, the depth the engage move already put the tip into the hole, at pitch × rpm), pre-tightens (t_pre_s), final-tightens a final_turn at rpm_final, then answers <name>/ok or <name>/nok and holds <name>/busy until the start drops. <name>/run is on while the bit turns (what a spin effect binds to). The result is OK unless a scenario says otherwise: <name>/fault_first fails the next run, <name>/fault_retry the one after — the two faults a fastening's retry branch is tested with.

cycles is how many starts the program can serve (screws plus the retries allowed): the program is that many rundowns nested so it ends on <name>/finish (or finish, a signal the robot's program raises when it is done) from whichever cycle it is waiting in. With estop (an input lane) no run starts while it is pressed. shank and stroke name the tool's own feed joint and its travel, for the ramp fasten runs the screw down with; tool carries the datasheet figures check compares against (torque_nm=(min, max), screw_length_mm, bit_mm). node (default) hosts the program on the I/O node <name>/controller, so the handshake lands on the I/O list as wires between the two controllers; channels (bt.io.channels) are that controller's terminals, what Scene.auto_assign_io gives the wires addresses on.

export_sheet

export_sheet(
    path: Union[str, Path],
    fastening: Fastening,
    *,
    rows: Optional[list[dict]] = None,
    format: Optional[str] = None,
    title: Optional[str] = None,
) -> None

The tightening sheet: one line per screw in the order it is driven — hole, screw, thread, length, class, head, engagement, torque, the driver's rundown time and the step that runs it — as Markdown or CSV (by format or the file's suffix). With rows (a fastening_report) the baked result columns are added and the Markdown states the driver's timing per screw and how many screws were driven.

fasten

fasten(
    sq,
    joint: Joint,
    driver: Driver,
    feeder: ScrewFeeder,
    *,
    motions: Mapping[str, str],
    retry: int = 1,
    robot: Optional[str] = None,
    prefix: str = "",
    retract_s: float = 0.4,
    tilt_deg: float = 1.0,
    after: Sequence[str] = (),
) -> Fastening

Appends the fastening of joint to the sequence sq, one screw per hole in the joint's order, each from the feeder: request a screw (start the feeder, wait for present), the taught to_pick, pick (the bit into the head), attach the screw to the bit, lift, over the hole, engage (the tip into the hole by the driver's engage depth — motions named with {i} for the screw's index and {hole} for its hole id), then raise the driver's start; while the driver finds the drive and runs, the tool's shank ramps the screw down its length at the same thread-derived time, and the step waits for ok or nok. On OK the screw is released and the shank retracted; on NOK the shank backs out and the run is repeated up to retry times — a last NOK releases the screw where it stands, raises <program>/halted and stops the program there (a refused cycle, the FAT row). A driver without its own feed runs the screw down with the taught motion drive instead.

Each screw is allowed to meet the joint's two parts only within its hole's window (Scene.allow_object_obstacle_contact): its tip within the hole's tolerance_mm of the hole's axis, leaning at most tilt_deg — so the engage move, which is collision-checked, refuses a screw taught off its hole, and the clearance measure does not count a screw in its hole as contact.

Two interlocks are written in, rows on the interlock table rather than conventions: the first screw is picked only once every signal in after is on (the part's seated from place) and a screw is present, and no start is raised while the driver's E-stop lane is pressed (the engage move completes only with it released).

The driver must have a cycle per possible start: len(order) × (1 + retry) at most.

fastening_report

fastening_report(
    tl, fastening: Fastening, *, tilt_deg: float = 1.0
) -> list[dict]

One row per screw of a baked fastening, facts then checks: hole, screw, order, t_engage / t_done, the screw's offset_mm from the hole's axis and tilt_deg when it engaged, seated_mm (how far it went down, against the run the driver was set for), rundown_s, attempts and result (ok, retry — OK after a NOK — or nok), the data-derived engagement_mm and tip_mm, the torque_nm, and checks — align, tilt, seated, engagement, depth, torque — each pass / fail / n/a (grasp_report's form).

fit_report

fit_report(
    tl,
    placement: Placement,
    *,
    tolerance_mm: float = 0.5,
    tilt_deg: float = 0.5,
) -> dict

Where a placed part landed against where it should (expected), at the moment it was released: offset_mm in the seat's plane, height_mm along its normal (positive = proud of the seat), tilt_deg, t_release, and checks — position within tolerance_mm, tilt within tilt_deg, seated (within the tolerance along the normal).

iso4762

iso4762(
    thread_mm: int,
    length_mm: float,
    property_class: str = "8.8",
    model: Optional[str] = None,
) -> Fastener

An ISO 4762 (DIN 912) hexagon socket head cap screw of the tabled sizes (M3–M12), length_mm under the head. The mass is the steel of its envelope; the reference torque is the class's at μ = 0.14 where the table has the class.

joint

joint(
    scene,
    name: str,
    *,
    a: str,
    b: str,
    seat: tuple,
    pattern_a: Optional[BoltPattern] = None,
    pattern_b: Optional[BoltPattern] = None,
    fastener: Optional[Fastener] = None,
    thickness: Optional[float] = None,
    torque_nm: Union[
        float, tuple[float, float], None
    ] = None,
    min_engagement_mm: Optional[float] = None,
    washer_mm: float = 0.0,
    order: Union[str, Sequence[str]] = "star",
    strict: bool = True,
    catalog=None,
) -> Joint

Declares the bolted joint between obstacles a (threaded holes) and b (clearance holes, thickness metres thick) and puts its frames in the scene: <name>/seat at the mating plane's centre (seat = its world pose, +Z out of a — where b's underside lands), <name>/hole/<id> on b's top face over each hole (+Z out, the head's seat — a screw is driven by aiming the tool tip's +Z along it) and <name>/locator/<id> on the plane. The patterns must agree hole for hole (same ids, same positions).

torque_nm is the joint's design value (a range, or one figure); min_engagement_mm the thread engagement it needs (a drawing's figure — 2×d in aluminium is the usual rule of thumb, but the joint states it, botrail does not guess). order is "star" (derived) or the hole ids in the order to tighten. With strict (the default) a joint the screw cannot make — too little engagement, a tip through the hole's bottom — is refused here rather than found on the line.

With catalog= — a workpiece spec pack's id or package directory (what bt.parts.workpiece stood the parts from) — the patterns, the screw, the torque range, the engagement and the thickness are read from the pack's mounting: its flange face is a's threaded holes and locators, its mount face b's clearance holes, its fasteners the screw and the torque; nothing is typed in. Arguments given alongside override what the pack states.

place

place(
    sq,
    part: str,
    *,
    motions: Mapping[str, str],
    close: Mapping[str, float],
    open: Mapping[str, float],
    expected: tuple,
    touch: Sequence[str] = (),
    grip_s: float = 0.4,
    robot: Optional[str] = None,
    prefix: str = "",
) -> Placement

Appends the fit of part to the sequence sq: the taught motions approach (over the part), down (onto it), up, carry (over its seat), seat (down onto the mate) and clear, with the gripper ramped to close before the grasp and open after the release. expected is the pose the part should stand at when seated — what fit_report measures the bake against. touch names what the carried part is meant to meet — its mate, the dowels — and declares that contact allowed (Scene.allow_object_obstacle_contact), so setting it down is not a collision. The release raises the internal signal <sequence>/<part>_seated (Placement.seated): what a fastening of the part waits on (fasten(after=[placement.seated])) — a row on the interlock table, not a convention.

report_section

report_section(
    fastening: Fastening,
    rows: Optional[list[dict]] = None,
    fit: Optional[dict] = None,
    *,
    title: str = "Assembly",
) -> dict

The assembly section of a cell report (scene.cell_report(..., sections=[...])): the joint — its parts, screw, torque, engagement and order — the driver's timing, the fit of the placed part (a fit_report) and the baked fastening rows (a fastening_report), as Markdown and as JSON. What a reviewer reads before the interlock table and the FAT rows.

rundown_s

rundown_s(
    length_m: float, pitch_mm: float, rpm: float
) -> float

Seconds to run length_m of thread down at rpm: the feed is the pitch per turn.

star_order

star_order(
    holes: Union[Sequence[Hole], Mapping[str, tuple]],
) -> list[str]

The tightening order a flange is brought down flat by: opposite pairs through the pattern's centroid, the innermost pair first (the middle of a rectangular cover), then the pairs spread around the circle rather than walked round it — 1, 5, 3, 7, 2, 6, 4, 8 on a ring of eight, the star pattern flange assembly guidance (ASME PCC-1) tightens in. Circular passes at rising torque are a matter of repeating the sequence.