The layout sheet and the cell report¶
Two more documents a cell hands over, both derived from the scene the way the I/O list and the BOM are: the layout sheet — the cell seen from above, as a proposal drawing or the plant's 2D CAD wants it — and the cell report — one page that gathers the numbers everything else measured, with the digest of every file written from the same source.
scene.export_layout("layout.svg", scale=200) # for the review
scene.export_layout("layout.dxf") # for the 2D CAD (mm)
report = scene.cell_report({"cycle": tl}, scenarios=runs,
deliverables=["layout.svg", "layout.dxf", "bom.csv", "io.csv"])
report.save("cell_report.md")
report.save("cell_report.json")
The layout sheet¶
Scene.layout projects the scene onto the floor and
renders it; Scene.export_layout writes it,
picking the format from the extension:
| format | what it is |
|---|---|
svg |
a self-contained drawing (scale pixels per metre, default 100) — opens in a browser, drops into a slide |
dxf |
a minimal R12 file — LINE / POLYLINE / CIRCLE / TEXT on named layers, millimetres (units="m" for metres) — opens in any 2D CAD, no dependency on either side |
json |
the drawn items in world metres, for a front end that wants to draw them itself |
What is on it, layer by layer (the DXF layer names in capitals):
- EQUIPMENT — every visible obstacle as its footprint: the convex hull of a primitive, the bounding rectangle of a mesh.
- GROUND — floor slabs and painted markings, i.e. anything whose top is at
or below
ground_z(2 cm by default): drawn faint, and left out of the extents so a 20 m floor does not shrink the cell to a stamp. - ROBOT / REACH — each robot's base as a mark, and, when the robot came
from the catalog with a
reach_mm, its reach as a dashed circle. A dual-arm robot gets a circle per arm, centred on the arm's own base — the arm'sreach_mmwhen it was mounted from the catalog. - DEVICE — conveyor and sink zones (dashed, with the direction of travel), a linear axis's travel, a vehicle's route with its stations.
- SENSOR — zone boxes and beams.
- ROUTE — the cables and tubes, each route's centerline on the floor, dashed where a span moves with a robot, named at its middle.
- FRAME — named frames as crosses, labelled by their last segment.
- LABEL — names on the things worth naming: a part
pinned to an obstacle or a group labels it (
Pedestal (PD-500)); the rest label by their name-derived unit, so a USD subtree reads once asConveyorand twelve fence panels once asfence. A ring-shaped group (a fence, a guard) and anything with other labelled things inside it (a base with the cell on its plate) is labelled above its top edge, a compact one at its centre. Labels do not print over each other: the robots', devices' and sensors' names first, then the biggest things', each stepping a line down or up off one already there; a thing's name with no room within three lines of it is left off (a frame's small name gives way to all of them). - DIM / GRID — the overall width and depth, and a metre grid.
frames=False, labels=False, reach=False, grid=None switch the extras
off; title= names the sheet.
The extent the sheet measures is also a query of its own:
Scene.footprint returns min, max, width,
depth, area and height — the plan-view bounding rectangle of the
equipment, ground excluded — so assert scene.footprint()["area"] <= 20
is a one-liner.
What the sheet is not: a drawing with tolerances, a title-block standard, or a projection of every prim's true outline. It is the plan a proposal needs and a layout engineer can trace over — one kind of drawing, honestly derived.
The cell report¶
Scene.cell_report gathers, in one
CellReport:
| section | from | what |
|---|---|---|
robots |
the scene | name, DOF, base position, catalog identity and reach when known, and the cabinet driving the arm (controller: its node or derived line, model, and where it stands — place, null until placed) |
cycles |
the timelines you pass | duration, step spans, robot busy time and utilization (a row per arm under a dual-arm robot's own), the branches taken, and the tightest clearance re-scanned against the scene each timeline was baked from (clearance_dt, None to skip) |
io |
the I/O map | point counts by kind and status, node usage, lint findings |
scenarios |
a ScenarioRuns |
the matrix — which scenario completed at what cycle, which stalled and why |
machines |
the parts, devices, sensors and nodes | every machine tool (machine_tool.* part): its door — an axis the machine drives or a loose leaf — with drive, stroke and end-of-travel lanes, the panel's buttons, and the controller hosting its program |
bom |
the BOM | line count, unidentified count, quantity per category, numeric totals |
footprint |
the layout | the plan-view extent |
deliverables |
the paths you pass | size and SHA-256 of every file — the evidence that the drawing, the list and the program are one cell |
Pass the cycles as a {name: timeline} dict, a list, or a single
SequenceTimeline; a ScenarioRuns passed as scenarios= fills the matrix
and, when no timelines are given, supplies the cycles too. The matrix is
the FAT sheet's verdict column: a fault authored as a scenario (a stuck
switch, an open wire, the E-stop in) either lets the cycle through or
stalls it, and the row says which and at what step. Every section is
a plain dict or list — report.cycles[0]["duration"],
report.footprint["area"], report.io["unbound"] — and the report renders
the same data as Markdown (to_markdown() / save("…md")) or JSON
(to_json() / save("…json")), with every optional value present as
null so the JSON keeps one shape whether or not a bake was supplied.
The report reads; it does not judge. The numbers it shows are the ones a regression test asserts on:
report = build_cell().cell_report({"cycle": tl})
assert report.cycle_time("cycle") <= 8.0
assert report.min_clearance() > 0.05
assert report.io["unbound"] == 0
assert report.footprint["area"] <= 20.0
assert report.bom["unidentified"] == 0
The document set as one thing¶
Because every deliverable is derived from the same scene, a set of them is
a unit: write them, hash them into the report, and a later run can say
which ones an edit touched — by name, not by guess. Moving a photo-eye
changes the layout sheet and the generated script and leaves the BOM and
the I/O list byte-identical; adding a fence panel changes the BOM too. The
repository's own tests pin exactly that
(python/tests/test_deliverables.py),
and the Hand over the cell tutorial writes the
whole set from one script. The set has a control-design half too — the
I/O list, the handshake spec, the interlock table
and the PLCopen file — derived from the same
sequences the bake ran.
bt.export_cell writes the set in one call, from one
snapshot of the scene and one bake, so a set never mixes an old script with
a new I/O list:
manifest = bt.export_cell(scene, "deliverables/rev1", name="cell",
sequences=["pick"], scenarios=True)
exports= picks the formats (all of them by default); sequences= scopes
the bake and every program-dependent document — I/O list, topology,
interlocks, PLCopen, robot scripts and the report's I/O summary — with one
script per program when several are selected; scenarios=True bakes the
scenario matrix too. The saved project and generated Python keep the whole
authored cell. The files are generated in a temporary directory and moved into out only once every
exporter succeeded: out is created when missing, same-named files are
overwritten and anything else in the directory is left alone, so the export
can be re-run in place.
The call returns the path of <name>_manifest.json, which records:
| field | what |
|---|---|
files |
every file written — path, kind, sha256, bytes |
conditions |
what the bake ran under — sequences, scenarios, dt, max_duration, plan_resolution, clearance_dt, fps, layout_scale, title |
botrail_version |
the version that wrote the set |
issues |
what could not be produced in full — export_warning, plcopen_stubs, script_not_exported, scenario_execution_failed, no_programs |
The Python API writes the manifest by default (manifest=False returns the
directory instead); the CLI keeps it only with --manifest. The report JSON
carries the same deliverables (path, kind, SHA-256 and size of every file
written before it) and issues, and the Markdown report ends with a
Deliverables table and, when there are any, an "Export issues" list. A
program that cannot compile to the robot dialect, a PLCopen block left as a
stub or a scenario that did not complete is an issue in the manifest and
the report, not an export failure.
export_cell
¶
export_cell(
scene,
out: str | Path,
*,
name: str = "cell",
exports=None,
sequences=None,
scenarios: bool = False,
dt: float = 0.01,
max_duration: float = 120.0,
plan_resolution: float = 0.05,
clearance_dt: float | None = 0.01,
title: str | None = None,
fps: float = 30.0,
scale: float = 100.0,
manifest: bool = True,
) -> Path
Write the selected documents (and a manifest) from a fresh snapshot and bake.
out is created when missing; a document set already there is
overwritten file by file and anything else in the directory is left
alone, so the set can be re-run in place. exports=None means all
formats. Every program-dependent exporter uses sequences (all by
default). Bakes are kinematic. A program that cannot compile to the
robot dialect, a PLCopen block left as a stub, or a scenario that did
not complete is recorded under issues in the manifest and the
report rather than failing the export.
Returns the manifest path — or, with manifest=False, the directory.