Parts and the bill of materials¶
A cell is more than geometry and behavior: every conveyor, sensor, controller and fence panel in it is a product somebody buys. botrail keeps that identity on the residents themselves — a part is what a thing is commercially — and derives the bill of materials from the scene, the way the I/O list is derived from the sequences. You author what is in the cell and where; the parts list falls out, and it can never disagree with the scene it was read from.
scene.set_part("belt", manufacturer="MISUMI", model="GVL-1200", mass_kg=45)
scene.set_part("eye", catalog="keyence/pz-g61n", model="PZ-G61N")
scene.set_part("fence", category="structure.fence", model="FP-2000", qty=12)
bom = scene.bom()
print(bom.to_markdown())
scene.export_bom("bom.csv")
What a part is¶
Scene.set_part pins identity to a resident by
name — a robot, a device, a sensor, an I/O node, an obstacle, or an obstacle
group (everything under name/: an imported USD subtree, a generated
assembly). Every field is optional:
| field | meaning |
|---|---|
catalog |
a catalog reference — "id", "id@revision", or (id, revision) |
manufacturer, model |
maker and model / part number |
category |
the BOM category (conveyor, sensor.photoelectric, structure.fence, ...); overrides the one the resident's kind implies |
description |
free text |
qty |
how many the target stands for — one by default; a fence group generated as twelve panels says 12 |
any other keyword, or attributes={...} |
free attributes: numbers (mass_kg=45, power_w=200, a price) are summed by bom().total(key), text (finish="RAL 7035") is carried along |
A part carries no geometry and no behavior. The shape stays on the obstacle, the motion on the device, the wiring on the I/O map; the part is only the name and the count. That is deliberate — botrail does not model shapes, and it does not size anything: it counts and checks.
Names live in separate name spaces (a conveyor device and its belt slab may
both be belt), so when a name resolves to several things set_part asks for
kind= ("robot", "device", "sensor", "io_node", "obstacle",
"group"). Re-pinning a target replaces its part; removing the resident
removes the pin.
Generated structures come identified¶
The standard parts — bt.parts.fence, table,
pedestal, conveyor, pallet, light_curtain, photoelectric — pin their parts as they
build: a fence is one line for the panels (its quantity the panel count),
one for the posts, one for the door; a conveyor's identity sits on the
device, its body being geometry. Give them model= / manufacturer= /
attributes and the BOM line is complete the moment the thing exists.
Catalog robots need no authoring¶
A robot loaded with Robot.from_catalog already
knows what it is: the package's manifest names the maker, the product, the
category and the headline specs, and botrail keeps them on the model's
provenance record — so a catalog arm is an identified BOM line with its
id@revision, payload_kg, reach_mm and mass_kg the moment it enters
the scene, and a tool welded on with attach_tool is its own line. A part
pinned to the robot's instance name overlays that (a description, a price,
a different category); it does not replace it.
Tools in the stack¶
A tool welded onto the robot with attach_tool is a line of its own,
named by its place in the stack — arm/tool for the first, arm/tool2
for the next, arm/tool/tool3 for one riding a tool that is itself a
stack. A catalog tool brings its identity; one made from a URDF string
(a bt.tools.multi_tool bracket, say) does not, and gets it pinned by
that row name:
scene.set_part("arm/tool", kind="tool", category="tool.multi",
catalog="botrail/hand/mph3/r1", manufacturer="botrail", model="MPH-3", mass_kg=0.3)
The pin follows the robot through a rename and rides the project; on a catalog tool's row it is the last word.
The bill of materials¶
Scene.bom lists every piece of equipment the scene
holds — robots and their tools, conveyors, axes and vehicles, sensors,
controller boxes — whether or not it has been identified, plus every obstacle
or group a part was pinned to. Bare obstacles are geometry (scenery, stock, a
workpiece pool) and are not parts unless you say so; a Source/Sink pair
models an endless line and is not equipment either.
Identical products merge into one row with the quantity summed and the
resident names kept for traceability; a row that names nothing yet — no
catalog reference, maker or model — stays under its own resident name, so
bom.unidentified() is the purchasing to-do list.
| # | category | manufacturer | model | catalog | qty | names | mass_kg |
|---|----------------------|--------------|-----------|----------------------|-----|------------------|---------|
| 1 | manipulator | FANUC | M-20iD/25 | fanuc/m/m-20id-25@… | 1 | arm | 250 |
| 2 | conveyor | MISUMI | GVL-1200 | | 1 | belt | 45 |
| 3 | sensor.photoelectric | KEYENCE | PZ-G61N | keyence/pz-g61n | 1 | eye | |
| 4 | plc | | R04CPU | | 1 | PLC1 | |
| 5 | part | | HFS8-1200 | | 2 | table_a; table_b | 30 |
| 6 | structure.fence | | FP-2000 | | 12 | fence | 8 |
Totals: mass_kg = 451
The Bom renders as CSV, Markdown or JSON (to_csv(),
to_markdown(), to_json(), save(path); scene.export_bom(path) picks the
format from the extension), and its numbers are pytest material like
everything else botrail bakes:
bom = build_cell().bom()
assert bom.unidentified() == [] # everything has a number
assert bom.total("mass_kg") <= 1500 # None, not 0, when nobody says
assert bom.total("power_w") <= 20_000
The generated Python and the .botrail project carry the pins, so a
reloaded cell — or the script generate_python() writes — produces the same
table.
What it is not¶
The BOM is a derived table, not a purchasing system: botrail holds the
slot for a price and sums it if you fill it in, but it does not know prices,
lead times or stock, and it does not validate attributes. Nothing here sizes
a frame, a supply or a valve — a part's mass_kg is a number you wrote down.
What the cell asks of each line¶
A BOM line also carries requirements — the payload, reach, stroke, span,
load the cell implies for it — and the check compares them with the part's
attributes. See Selecting parts: scene.requirements(),
scene.check() and bt.catalog.search.
Physical interface ports reference these same equipment targets. Supply capacity is compared with connected loads, using exact Part attributes where a single endpoint's rating can be resolved. Ports and cable references do not create additional BOM lines.