Skip to content

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's reach_mm when 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 as Conveyor and twelve fence panels once as fence. 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.