Generated banner for Cdf Simulation
Generated logo for Cdf Simulation

Cdf Simulation

NVIDIAOfficialCreated by NVIDIA CDF

  • Skill
  • Tool integration
  • Software development
  • Apache-2.0
  • Medium risk

From the author

Component-owned guidance for CDF Simulation requests submitted directly or through Airflow-triggered DIG Flow Catalog prompts — golden THT board, THT pin-geometry variation, good/clean SMT images, structural pose defects, and solder texture defects, from a board USD. Turns one matched prompt into one validated `cdf.CDFJob` build and its compiled JobOrder payload; it never executes a render itself. Requires a caller-supplied board USD and output root, asks for either when missing from an interactive request, and resolves optional details with fixed, documented defaults.
— the description by NVIDIA CDF

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The cdf-simulation skill provides a structured, component-owned interface for creating synthetic data generation jobs for PCBA inspection. It translates user prompts or Airflow-triggered requests into validated CDFJob builds and compiled JobOrder payloads. The skill supports five primary catalog rows—including golden THT boards, pin-geometry variations, and various SMT structural and solder defects—alongside specific reference flows. It does not execute renders itself, instead producing a payload for external execution by an Airflow DAG or similar executor. The skill enforces strict input validation, requiring a board USD and output root, and resolves all optional parameters through documented, fixed defaults.

What it does

  • Matches user requests to specific catalog rows or reference flows.
  • Validates and compiles CDFJob builds and JobOrder payloads.
  • Resolves missing optional parameters using fixed, documented defaults.
  • Supports structural and solder defect simulation for SMT and THT boards.
  • Handles panel instances and component-level crop generation.
  • Enforces strict input requirements for board USD and output root paths.

When to use it

  • When you need to generate synthetic data for PCBA inspection using CDF.
  • When you have a board USD and need to create a simulation job for defects or golden board images.
  • When integrating PCBA simulation into an Airflow-based automated pipeline.
  • When you require a standardized, validated JobOrder payload for a rendering executor.

Advantages

  • Ensures consistency by using fixed defaults for all optional parameters.
  • Prevents invalid configurations through strict input validation and schema checks.
  • Decouples job definition from execution, allowing flexible rendering workflows.
  • Provides clear, deterministic mapping between prompt types and simulation jobs.

Usage examples

“Create a golden board dataset for my board.usd in /output/path”
The agent validates the board.usd, applies the golden board THT template, and returns a compiled CDFJob payload.
“Generate structural defects for C33 on board.usd”
The agent identifies the structural defect flow, validates the component name, and returns a JobOrder payload configured for C33.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/nvidia/cdf-simulation/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip cdf-simulation.zip -d .agents/skills

Documentation by NVIDIA CDF

cdf-simulation

Turns DIG Flow Catalog prompt templates and equivalent direct requests into cdf jobs through the declarative Python API. This skill is the component-owned guidance for cdf-simulation: a caller supplies a board USD and output root, this skill builds and compiles the job, and the caller can pass the payload to its own executor. Airflow may inject those required inputs, or the caller may provide them directly. If either is missing and interaction is possible, ask once. All optional ambiguity is resolved with fixed, documented defaults. If a request truly cannot be served (an unsupported defect type, a non-USD input), say so rather than guessing at a fake capability or silently doing nothing.

This skill does not execute. It produces a validated CDFJob build and its compiled JobOrder payload, then stops. An Airflow DAG or another executor chosen by the caller performs rendering.

Catalog coverage

This skill implements five DIG Flow Catalog rows, all taking a board USD as input:

#RowStatus
1Golden board (THT)Implemented
2Variation type (THT pin geometry)Implemented
3Good images (SMT)Implemented
4Structural defects (SMT)Implemented
5Texture defects (SMT solder)Implemented, except bridge (unsupported — see below)
Reference flows

Beside the catalog rows, the skill serves the five reference flows in references/reference-flows.md: complete inspection jobs, each phrased as one prompt whose compiled plan must equal the equivalent YAML config. A prompt carrying a flow marker -- a scan grid and "stitched", a defect on a named refdes ("shift on C33", "insufficient solder on J17", "missing-component defect on LS1"), "component-focus" or "pin-focus" crops -- is a flow, not a catalog row, and takes its fixed values from that file (they differ from the catalog defaults: shift is translate_range=0.5, rotate_z_range=5.0 with no variant_index, crops are ROI captures rather than postprocess.crop).

FlowPrompt marker
G golden scan"golden board dataset ... NxM scan ... stitched" (single board or panel)
C component defects"shift/tombstone/sideflip/reverse polarity on <refdes>" + "component-focus crops"
S SMT solder defects"insufficient/excess/interrupted/missing solder on <refdes>" + "component-focus crops"
M missing component"missing-component defect on <refdes>"
T THT solder defectssix THT solder defects on refdes + "pin-focus crops from the bottom" (single board or panel)

Fixed defaults

Every unspecified optional detail resolves to a documented default rather than a question. The full table -- board identity and paths, the rule that a named component must resolve on the attachment, THT board requirements, pin-variation magnitude ranges, the required structural-defect magnitudes (translate_range, rotate_z_range, angle_range -- the API has no defaults for these and a catalog prompt never states them), the coverage ratio, how multiple defect types combine, and the solder texture-defect mapping -- is in references/fixed-defaults.md.

Read it before building any script. The values are not obvious and several of them are load-bearing: the coverage ratio applies to structural and solder defects, and the crop class_filter taxonomy is not the same as component_types.

Payload contract

What this skill returns to its caller, what it accepts, and how each value is validated is in references/payload-contract.md. The short version: the output is a CDFJob build plus the compiled JobOrderPlan from job.compile_orders(...), serialized with JobOrderCodec.dumps(plan) (a bare plan.model_dump() is not JSON-serializable), together with a resolved-inputs summary naming every default that applied. The caller supplies the board USD and output root -- both required, with no fallback. This skill supplies everything else from the catalog row and fixed defaults, and marks each variant for an independent process.

Instructions

Four steps, in order. Step 1 decides everything downstream, so do not start writing a script before the row is settled.

  1. Match the prompt to exactly one catalog row or one reference flow. Check the flow markers first (references/reference-flows.md, "Routing"): a named refdes target, a scan grid with "stitched", "component-focus" / "pin-focus" crops. With a marker present the prompt is that flow; without one it is a catalog row. A prompt matching no row and no flow explains the supported request shapes and asks the caller to choose one; it never assembles a best-effort substitute out of the pieces that do exist.

  2. Resolve every input from the precedence order in Inputs. Anything still unset takes its documented default from references/fixed-defaults.md.

  3. Build one cdf.CDFJob script, following the row's shape in Workflow. One job per variant: the executing side must launch each variant as its own process, because sequential job.capture.render(...) calls in one process crash the second Kit subprocess -- so the payload says one variant, never a loop.

  4. Validate, then return -- compile the job with job.compile_orders(...), read the result, and return the build plus the compiled plan to the caller. Do not call render() here. Report what was assumed alongside the payload.

Inputs

Each value is resolved from the first source that supplies it:

state file -> explicit prompt arguments -> agent context -> user prompt text

InputRequiredWhere it comes from if unset
Catalog rowyesMatched from the prompt shape; no default. No match asks the caller to choose a supported shape
Board USD (attachment path)yes, caller-providedNo fallback. Ask for a missing path when interaction is possible; for non-interactive automation, stop with refusal case 3. Never substitute a sample board
Output rootyes, caller-providedNo fallback. Ask for a missing path when interaction is possible; for non-interactive automation, stop with refusal case 3. Never synthesize a directory
Component sidecaroptional legacy inputPrefer embedded USD metadata with source="usd". Attach a matching <stem>.components.json only when a legacy standalone board relies on it or the caller explicitly selects that source; file presence alone does not select it. Instanced panels require embedded metadata
Board root_pathnoRead from the supplied stage, or from the selected legacy sidecar for a sidecar-backed board. For instanced panels follow references/reference-flows.md, "Panels". Opening the attachment is required; an unreadable or unresolved board stops with refusal case 3. Never use another board's root as a fallback
Named componentfrom the promptPassed through as named when present in the selected metadata source (embedded USD by default, or an explicitly selected legacy sidecar -- list types, do not guess); a missing name is refusal case 4, never substituted. State the check in the run summary
Component typesnoThe row's documented set
Seed, scan grid, board side, resolution, pin numbers (flows)noExplicit prompt arguments in the forms listed in references/reference-flows.md ("Seed 7", "a 5x4 scan", "of the bottom side", "at 2000x2000", "(pin SH)"); otherwise that file's per-flow defaults
Panel instancesfrom the attachmentWhen the attachment composes one board as several USD instances (<root>/StepRepeats/SR_*), register all of them with job.scene.instances(...); image budgets multiply by the instance count (references/reference-flows.md, "Panels")
Coverage rationoThe single documented ratio, applied to structural and solder defects; the executing side selects max(1, int(pool * ratio)), so a non-empty selection always yields at least one defect
Defect magnitudesnoThe per-type ranges in references/fixed-defaults.md
class_filternoNone. Never a component-type string -- different taxonomy

State what was resolved and from where in the run summary so the caller can see which defaults applied.

Workflow

1. Match the prompt to a catalog row

Use the sample-prompt phrasing in the table under "Catalog coverage" and in "Examples" below to classify the request into exactly one of rows 1-5.

If a prompt does not clearly match any of the rows or a reference flow, explain the supported request shapes and ask the caller to choose one.

2. Resolve inputs deterministically

Extract from the prompt: attached USD path, output root, named component(s), variation/defect type, "full board coverage" flag. Then open the attachment and read two things from it: the board root and its component types, using embedded metadata by default or the selected legacy sidecar for a sidecar-backed standalone board. A named component that appears in those types is used as named; a name that is absent is refusal case 4 -- never a substitution. Apply the fixed defaults above for optional details the prompt does not specify, and record both reads (root source, component check) in the run summary.

The attachment path and the output root are the two inputs with no default. If either is absent and interaction is possible, ask for it once. For non-interactive automation, stop and report it (refusal case 3). Never substitute a sample board or invent an output directory. Prefer embedded USD metadata. Attach a component sidecar only when a legacy standalone board relies on it or the caller explicitly selects it; do not switch sources merely because a .components.json file is nearby. Keep all metadata-dependent calls on that source. Instanced panels cannot use legacy component sidecars.

3. Build the CDFJob script

import pcb_aoi before constructing the job. The "pcb-aoi" domain registers on import; without that import CDFJob(domain="pcb-aoi", ...) raises unknown domain 'pcb-aoi' (registered: none) on the very first line. Every template below assumes import pcb_aoi and from cdf import CDFJob at the top of the script.

Every row requires job.capture.renderer(mode="pathtracing", resolution=..., spp=..., total_spp=..., max_bounces=...) before capture.camera/scan_grid/outputs -- it is a required field, compile_orders() rejects a plan missing it ("capture render pass 'default' is missing: renderer"). Every template below assumes this call is present even where not shown explicitly.

capture.camera(rotation_range=...) is required, not optional -- only side= has a default. A top-side capture with no rotation asked for still needs rotation_range={"z": 0}; omitting the kwarg is a TypeError before anything is compiled.

Several operations.add_* calls have required magnitude kwargs with no defaults (add_shift needs translate_range and rotate_z_range; add_tombstone and add_sideflip each need angle_range). A catalog prompt never states them, so they come from the fixed table in references/fixed-defaults.md -- do not invent a number and do not ask.

Declaration order matters and is enforced. job.scene.select_components(...) must be declared before job.annotations.label_components(...) -- compile_orders() rejects the reverse order with annotations.components.apply requires component selection to be declared first. Every template below follows select → (operations) → annotate.

scene.select_components(source=..., types=None) without types= needs either USD-embedded component metadata or source="components" pointing at an attached sidecar -- bare source="usd" with no types and no sidecar fails at runtime with component selection needs explicit types or board metadata. Prefer confirmed embedded USD metadata. For a legacy standalone board that requires a sidecar, attach its matching file with job.scene.attach_sidecar(name="components", kind="pcb.components", path=...) and use source="components" consistently. Do not mix that source with embedded metadata or attach a sidecar solely because the file exists.

Generate a standalone script with imports, build_job() -> CDFJob, an argparse CLI accepting --usd-path and --output-dir, and main(), using the per-row template below -- or, for a reference flow, the flow's template and shared conventions in references/reference-flows.md (explicit source="usd", output_start(0), limits(...), realtime_subframes=1, the three-key camera rotation), which are the spellings the YAML interpreter produces. Place generated PCB scripts under pcb_aoi/examples/ only if the run is meant to be durable/reusable; for a one-off request, build and compile the job in-memory, then emit the payload (references/payload-contract.md) without necessarily writing a file to disk. Execution belongs to the caller's chosen executor, never this skill.

Row 1 — Golden board (THT): the template uses embedded USD metadata. For a legacy sidecar-backed standalone board, attach its matching component sidecar and use its job-local source name consistently instead of "usd".

job = CDFJob(domain="pcb-aoi", random_seed=0)
job.scene.configure(usd_path=..., root_path=...)  # root confirmed on the supplied stage
job.scene.select_components(types=[<the attachment's component family>], source="usd")
job.appearance.lighting(mode="white_light", exposure_range=(0.0, 0.0),
                         cone_angle_range=(120.0, 120.0), cone_softness_range=(1.0, 1.0),
                         dome={"intensity": (1000.0, 1000.0), "visible": True})
# The catalog's golden board carries PROCEDURAL solder: joints are generated,
# no defect is applied. `procedural` is valid here precisely because no
# add_* solder-defect call follows (those require surface_evolver).
job.operations.solder_joints(solver="procedural", component_types=[<the same family>], source="usd")  # component_types names the family the joints are generated for. With a sidecar attached, this reads source="components" too -- all three source= arguments in a row must name the same metadata source
job.annotations.label_components(source="usd")  # likewise source="components" when the sidecar is attached
job.capture.renderer(mode="pathtracing", resolution=(1920, 1080), spp=1, total_spp=32, max_bounces=4)
job.capture.camera(side="bottom", rotation_range={"z": -90})
job.capture.scan_grid(x_count=1, y_count=1)
job.capture.outputs(rgb=True, bounding_box_2d_tight=True,
                     semantic_segmentation=True, colorize_semantic_segmentation=True)
# No postprocess.crop() call -- this row has no crop.

camera_path/camera_xform_path/ring_light_root are optional (scene.configure's default is None) -- omit them rather than assuming the /World/camera_light/... Allegro rig exists verbatim on every board; the pipeline falls back to a synthesized camera when unset.

Row 2 — Variation type (THT pin geometry):

job = CDFJob(domain="pcb-aoi", random_seed=0)
job.scene.configure(usd_path=..., root_path=...)  # a board that authors pcba:mesh:role -- see below
job.appearance.lighting(mode="white_light", exposure_range=(0.0, 0.0),
                         cone_angle_range=(120.0, 120.0), cone_softness_range=(1.0, 1.0),
                         dome={"intensity": (1000.0, 1000.0), "visible": True})
job.operations.pin_variation(components=[<THT refdes>], offset_range=(0.1, 0.4))  # or tilt_range/cut_angle/height_variation
job.capture.renderer(mode="pathtracing", resolution=(1920, 1080), spp=1, total_spp=32, max_bounces=4)
job.capture.camera(side="bottom", rotation_range={"z": -90})
job.capture.scan_grid(x_count=1, y_count=1)
job.capture.outputs(rgb=True)

Row 2 needs a board whose through-hole leads are authored with the pcba:mesh:role schema (pkg_lead meshes); a THT board without it cannot run pin_variation at all (see "THT board requirements" in references/fixed-defaults.md). pin_variation(...) targets leads directly by components=/package_types= -- it does not need scene.select_components(...) or annotations.label_components(...) declared first (unlike every other row). Selector: the refdes the prompt names, confirmed on the attachment; with no part named, see the selector rule in references/fixed-defaults.md.

Row 3 — Good images (SMT): board-level ("20 good PCB images") uses capture.scan_grid(x_count=X, y_count=Y) where X * Y is the requested count, not N in each axis -- scan_grid(x_count=20, y_count=20) is 400 renders, not 20. Pick the most square factorisation at or just above the ask (20 -> 5x4; 30 -> 6x5; a prime like 17 -> 5x4 = 20 and say so), no crop, select_components(types=[<every component family the attachment carries>], source="usd") -- the families are read from the attachment (pcba:component:footprintId), so on a one-family board the list has one entry (e.g. types: [C_0805_2012Metric]); a bare select_components(source="usd") with no types= compiles to a different plan (types: null). Component-level ("good images of <component>, max 3") narrows first with select_components(types=[<component_type>], source="usd"), then after the same capture block adds job.postprocess.crop(method="bbox_tight", types=["rgb", "semantic_segmentation", "component_instance"], class_filter=None, offset=10, xform_depth=7, output_subdir="cropped") and caps capture.limits(max_images=3) (or the requested max). class_filter=None, never the component-type string -- see "Pitfall: class_filter is not component_types" below: class_filter=["PASSIVE_SMD_0805"] compiles and executes with "error": "" but silently produces zero crop files, because annotations.label_components assigns the semantic class "capacitor" to that component, not the literal type string. No solder or structural-defect operations for this row -- clean/golden only; output stops at the rendered/cropped RGB.

Board-level Row 3, complete (only the grid and the render size come from the prompt):

job = CDFJob(domain="pcb-aoi", random_seed=0)
job.scene.configure(usd_path=..., root_path="/Board")                       # the attachment's default prim
job.scene.select_components(types=[<all families of the attachment>], source="usd")
job.appearance.lighting(mode="white_light", exposure_range=(0.0, 0.0),
                         cone_angle_range=(120.0, 120.0), cone_softness_range=(1.0, 1.0),
                         dome={"intensity": (1000.0, 1000.0), "visible": True})
job.annotations.label_components(source="usd")                              # always: the good images ship their labels
job.capture.renderer(mode="pathtracing", resolution=(1920, 1080), spp=1, total_spp=32, max_bounces=4)
job.capture.camera(side="top", rotation_range={"z": 0})
job.capture.scan_grid(x_count=X, y_count=Y)                                 # X * Y = the requested count
job.capture.outputs(rgb=True, bounding_box_2d_tight=True,
                     semantic_segmentation=True, colorize_semantic_segmentation=True)
# no operations, no postprocess.crop

Row 4 — Structural defects (SMT): the same SMT scene as Row 3, with pose operations declared after select_components and before label_components, then operation labels and the per-component defect crop. Rows 4 and 5 always emit per-component defect crops -- this is the one output contract for both rows (references/class-filter-taxonomy.md): annotations.enable_operation_labels(namespace="defect") puts the defect class in the segmentation output, and the same postprocess.crop(...) block as Row 3's component-level case follows the capture. A record with no file under cropped/ is a failed record, not a board-level variant. The narrowed select_components(types=[...]) is the scope of every pose operation; whether add_tombstone also takes component_types= depends on the attachment's metadata source -- see "Scoping by metadata source" below.

This legacy standalone-board example uses a component sidecar (the CAD_BOARD board used by pcb_aoi/examples/allegro_defect_job.py); on an embedded-metadata board use source="usd" throughout and the tombstone scoping below.

job = CDFJob(domain="pcb-aoi", random_seed=0)
job.scene.configure(usd_path=..., root_path="/Board_CAD_BOARD")
job.scene.attach_sidecar(name="components", kind="pcb.components", path=...)  # sidecar board
job.scene.select_components(types=["PASSIVE_SMD_0805"], source="components")  # the scope of every pose op
job.appearance.lighting(...)                              # same block as Row 1
job.operations.add_tombstone(ratio=0.2, angle_range=(70.0, 90.0))
# job.operations.add_shift(ratio=0.2, translate_range=0.2, rotate_z_range=15.0)
# job.operations.add_sideflip(ratio=0.2, angle_range=(70.0, 90.0))
job.annotations.label_components(source="components")
job.annotations.enable_operation_labels(namespace="defect")
job.capture.renderer(mode="pathtracing", resolution=(1920, 1080), spp=1, total_spp=32, max_bounces=4)
job.capture.camera(side="top", rotation_range={"z": 0})
job.capture.scan_grid(x_count=1, y_count=1)
job.capture.outputs(rgb=True, bounding_box_2d_tight=True,
                    semantic_segmentation=True, colorize_semantic_segmentation=True)
job.postprocess.crop(method="bbox_tight", types=["rgb", "semantic_segmentation", "component_instance"],
                     class_filter=None, offset=10, xform_depth=7, output_subdir="cropped")

One defect type per script when the prompt wants one image set per type (see "Tell the executing side: one render() per process" below); all three in one script only when the prompt wants a board carrying all three at once.

Scoping by metadata source. add_shift and add_sideflip have no component_types= kwarg, so the narrowed select_components(types=[...]) is always their scope. add_tombstone does have one, and whether to use it is decided by where the attachment's component metadata lives:

  • Embedded USD metadata (pcba:* schema, no sidecar -- every pcb-builder export): narrow with select_components(types=[...], source="usd"), pass eligibility_source="usd", and omit component_types. Supplying it applies the operation to nothing: the list resolves to prim paths the stage does not carry and the run fails with tombstone selected from 1 components but applied none of them.

  • Sidecar board (<stem>.components.json attached): component_types=[...] on add_tombstone works there and may accompany the narrowed selection. It is never a substitute for narrowing the selection, and it is never carried over to an embedded-metadata board.

Row 5 — Texture defects (SMT solder): as Row 4 -- same operation labels, same outputs, same per-component defect crop -- but operations.solder_joints(...) is declared once before any add_* solder call, and the solver must actually be available in the executing environment. The solder add_* calls take ratio only (no count) and their component_types= works on both metadata sources.

The following is a legacy sidecar-backed standalone-board example. Prefer source="usd" for embedded-metadata boards and omit attach_sidecar(...).

job = CDFJob(domain="pcb-aoi", random_seed=0)
job.scene.configure(usd_path=..., root_path="/Board_CAD_BOARD")
job.scene.attach_sidecar(name="components", kind="pcb.components", path=...)  # sidecar board
job.scene.select_components(types=["PASSIVE_SMD_0805"], source="components")   # embedded-metadata board: types=[<family>], source="usd"
job.appearance.lighting(...)                              # same block as Row 1
job.operations.solder_joints(solver="surface_evolver", component_types=["PASSIVE_SMD_0805"], source="components")  # component_types = the selected family
job.operations.add_excess(ratio=0.2, component_types=["PASSIVE_SMD_0805"])
# chain further supported types here, e.g.
# job.operations.add_missing_solder(ratio=0.2, component_types=["PASSIVE_SMD_0805"])
job.annotations.label_components(source="components")
job.annotations.enable_operation_labels(namespace="defect")
job.capture.renderer(mode="pathtracing", resolution=(1920, 1080), spp=1, total_spp=32, max_bounces=4)
job.capture.camera(side="top", rotation_range={"z": 0})
job.capture.scan_grid(x_count=1, y_count=1)
job.capture.outputs(rgb=True, bounding_box_2d_tight=True,
                    semantic_segmentation=True, colorize_semantic_segmentation=True)
job.postprocess.crop(method="bbox_tight", types=["rgb", "semantic_segmentation", "component_instance"],
                     class_filter=None, offset=10, xform_depth=7, output_subdir="cropped")

solder_joints(solver="surface_evolver") resolves the solver inside that call: with manifold3d or the native evolver binary missing it raises there, before compile_orders() is reached. See references/fixed-defaults.md — a Row 5 script cannot even be dry-run validated in an environment that lacks them.

Pitfall: class_filter is not component_types

postprocess.crop(class_filter=...) takes a derived semantic class, not a component-type string. Given the latter it compiles, reports "error": "", and writes zero files. Default to class_filter=None and filter with types=. Worked example and the two taxonomies side by side: references/class-filter-taxonomy.md.

4. Validate, then return -- do not execute

Run job.compile_orders(output_dir=...) to catch a malformed script before anyone spends Isaac boot time. A clean compile is where this skill's job ends: return the CDFJob build, the compiled JobOrderPlan, and the resolved-inputs summary to the caller, in the shape references/payload-contract.md defines. Do not call job.capture.render(...) from here -- this guidance is read, not run, and execution belongs to the caller's chosen executor. Report what was returned; do not claim images were generated, because at this point none have been.

Tell the executing side: one render() per process. Running several variants (for example shift, tombstone and sideflip) sequentially in one Python script crashes the second render(): the Kit subprocess segfaults mid-run (carb.crashreporter-breakpad fatal trace), while each variant as its own process runs cleanly. This matches documented Isaac/replicator fragility around rebuilding the render graph mid-session. So when a prompt implies several output variants ("generate shift, tombstone, and sideflip defects"), emit one payload per variant and mark them as independent launches -- never a loop of render() calls inside one script.

Examples

PromptRowKey resolved values
"Generate a golden board image using the attached USD"1bottom camera, solder_joints(solver="procedural") with no defect calls, rgb+bbox+semantic, no crop
"Using the attached USD, generate images with tilt variation of J13"2tilt_range=(5.0, 15.0), components=["J13"]
"Using the attached USD, generate images with cut variation of J13"2cut_angle=(5.0, 15.0)
"Generate 20 good PCB images using the attached USD"3board-level, scan_grid(x_count=5, y_count=4) = 20 images, no crop
"Generate good images of PASSIVE_SMD_0805, max 3"3select_components(types=["PASSIVE_SMD_0805"]), component-level crop, max_images=3 (a component the attachment does not have is refused, case 4 -- never substituted)
"Using the attached USD, generate tombstone structural defects"4add_tombstone(ratio=0.2, angle_range=(70.0, 90.0)) (no coverage phrase → default ratio, which still yields at least one defect on a non-empty selection; angle_range is required and comes from the fixed table); enable_operation_labels + per-component crop as on every row 4/5 record
"Generate shift structural defects for PASSIVE_SMD_0805, with full board coverage"4select_components(types=["PASSIVE_SMD_0805"]) then add_shift(ratio=1.0, translate_range=0.2, rotate_z_range=15.0); labels + crop
"Using the attached USD, generate excess solder on the passives"5solder_joints(solver="surface_evolver") + add_excess(ratio=0.2, component_types=["PASSIVE_SMD_0805"]); labels + crop
"Generate excess solder and missing solder defects for PASSIVE_SMD_0805"5one solder_joints(...) declaration, then add_excess(ratio=0.2, ...) and add_missing_solder(ratio=0.2, ...) chained (see "Combining multiple supported defect types")
"Using the attached USD, generate bridge defects on the ICs"5Fail loudly — bridge unsupported

When to refuse

Four cases follow. Each states plainly what was not done and why, produces no substitute, and — where part of the request is servable — still delivers that part.

1. bridge mixed with supported solder defects. Partial fulfilment, not refusal: generate exactly the supported types named, drop the bridge clause, report it.

Requested: bridge and excess solder. Generated: excess solder only (add_excess(ratio=0.2, component_types=["PASSIVE_SMD_0805"]), 1 variant, output at <dir>). Not generated: bridge — solder shorting between adjacent pads/leads has no method in the current cdf API. Available texture defects: insufficient, excess, bright, interrupted, missing_solder, dry_contact. I did not add any of those to make up the count, and did not approximate a bridge from the ones that exist.

2. A board whose real metadata is being overridden by the defaults. The CAD_BOARD values belong to the CAD_BOARD example board only.

Not run. The attached USD carries its own metadata (a <name>.components.json sidecar, or embedded pcba:* metadata whose default prim is the board root) naming a different board root, so applying the CAD_BOARD values would silently render the wrong board identity — a mismatch that does not show up in the image. Re-run with that board's own root_path, or confirm the values are genuinely intended for it.

3. No board USD, or no output root. Both are required caller inputs. An attachment that is not a USD stage (for example a photograph) counts as no board USD. Ask the caller to provide the missing value when interaction is possible. In non-interactive automation, stop; do not substitute a sample board for the attachment or synthesize a directory for the output root.

Not built. This request carries no board USD (or: no output root). Both are required caller inputs and have no defaults; images are not accepted as the board input. Provide a USD attachment path and the output root.

4. A named component the attachment does not have. A validation failure, not a substitution. Check component types and refdes in the selected metadata source (embedded USD by default, or the selected legacy sidecar); if the named one is not there, build nothing.

Not built. The prompt names <name>, which the attached board does not have (checked: <sidecar types | embedded component types>: <list>). Rendering another component in its place would deliver a dataset for the wrong part. Re-run naming one of the listed components, or attach the board that carries <name>.

What none of these do: assemble a lookalike out of the capabilities that do exist, or report success for a run that did not happen. This skill returns a payload and never renders, so it never has grounds to say images were generated -- that report comes back from the executor, or not at all.

Guardrails

  • Ask only for a missing board USD or output root when interaction is possible. Apply fixed defaults for optional details and state what was assumed in the run summary.

  • Never invent a component name, prim path, or API method that was not confirmed in this repo's own code (pcb_aoi/sections.py, pcb_aoi/examples/*.py, cdf/API_SPEC.md). If something needed does not exist (for example bridge defects), say so plainly.

  • Never build on a board root that was not read from the attachment. The CAD_BOARD paths belong to the CAD_BOARD example board; an attachment that could not be opened is refusal case 3, not a reason to assume that board. Confirmed metadata includes the embedded USD default prim, not only a sidecar.

  • Never substitute a component for one the prompt names. Check the attachment's component types first: pcb-builder footprint names (C_0805_2012Metric, R_0603_1608Metric, SOT-723) look unfamiliar and are real, and a name the attachment does not have is refusal case 4.

  • Never substitute a sample board for a missing attachment, or a synthesized directory for a missing output root. Both are required caller inputs; ask when interaction is possible or stop non-interactive automation (refusal case 3).

  • Never hand off a defect record whose selection is empty, and never describe a zero-defect outcome as the default doing its job: on a non-empty selection the executing side applies at least one defect (max(1, int(pool * ratio))), and a clean board on a defect request is a failed record.

  • Never claim that a postprocessing step which does not exist in this repo was applied.

  • Never execute from this skill. Compile, then return the build and the JobOrderPlan to the caller. A render() call here is a bug, not a shortcut.

  • Never pass a component-type string to postprocess.crop(class_filter=...) — it is a different taxonomy than component_types/types= and will silently produce zero crop files with no error. Default to class_filter=None.

Limitations

  • bridge solder defects are unsupported; the other texture defects in row 5 are.

  • One variant per process, on the executing side. Sequential job.capture.render(...) calls in a single Python process segfault the second Kit subprocess, so each payload this skill emits is one variant and is marked as an independent launch.

  • Guidance only. This skill compiles and returns a payload; it does not render. The caller's executor owns execution, and this skill has no view of the run's outcome beyond what is reported back.

  • Requires a USD input. Images or photographs are not accepted as the board input.

  • Two required caller inputs. The board USD and output root may be supplied directly; non-interactive automation must inject both. Optional ambiguity resolves to the documented defaults.

  • The defaults are documented, not universal. A different board needs its own confirmed metadata, not the CAD_BOARD values.

Troubleshooting

ErrorCauseSolution
"error": "" but zero crop filesA component-type string was passed to postprocess.crop(class_filter=...), which is a different taxonomy from component_typesUse class_filter=None; filter by types= instead
Second render segfaults (reported back by the executor)Two job.capture.render(...) calls in one Python processEmit one payload per variant, each marked as an independent launch; never a render() loop
This skill was asked to renderA caller treated component guidance as an executorCompile and return the JobOrderPlan; execution belongs to the caller's chosen executor
select_components returns nothingThe sidecar and the board USD disagree, or the source is wrong for that boardConfirm the sidecar belongs to that board; check source= against the row
A defect request rendered a clean board with exit 0The selection was empty (types= matched nothing on that attachment); a non-empty selection always yields at least one defectCheck types= against the attachment's own metadata; do not hand off a plan whose selection is empty
Row 4/5 record with no file under cropped/enable_operation_labels(...) or postprocess.crop(...) was left off the record, or class_filter carried a component-type stringBoth calls are part of the Row 4/5 contract; class_filter=None
Compile error naming an unknown operationAn API method was invented rather than confirmedCheck pcb_aoi/sections.py and cdf/API_SPEC.md; if it is absent, the capability does not exist
Prompt matches nothing in the catalogA request shape this skill does not coverExplain the supported shapes; do not substitute
pin_variation found no through-hole leadsThe board does not author pcba:mesh:role lead meshesReport that the board's THT metadata is insufficient for pin-geometry variation; do not retry with invented parameters

Files

references/fixed-defaults.md

Fixed defaults

Reference for skills/cdf-simulation. Every optional value here is a documented, consistent choice rather than a guess made fresh each run. Read this before building any script. The board USD and output root are required caller inputs and have no defaults; every run takes the caller-supplied attachment, and a missing attachment is refusal case 3 in SKILL.md.

Board identity and paths

Resolve root_path from the selected metadata source. Inspecting the attachment is a required step, not an option: an attachment that was never opened has no confirmed root.

  1. Prefer embedded USD metadata: use the stage's confirmed board root. Read it from the file -- Usd.Stage.Open(path).GetDefaultPrim().GetPath() with pxr available, else the defaultPrim = "..." token in the .usda header (a .usd/.usdc attachment needs pxr). It is /Board on pcb-builder board exports. Exception -- a panel: when the stage composes several USD instances of one board under a StepRepeats scope, the default prim (/World) is only a container; the root is the carrier Xform that holds StepRepeats (reference-flows.md, "Panels");

  2. for a legacy standalone board that relies on a selected <stem>.components.json sidecar, use the board root it names and confirm that it resolves in the supplied USD. A nearby sidecar does not override embedded metadata automatically;

  3. if the attachment cannot be opened at all (path unreadable, sandbox forbids it, binary file and no pxr), stop and report it -- refusal case 3 with the reason "attachment root unresolvable" -- rather than guessing. A run that could not read its attachment has failed to resolve a required input; it must not hand off a plan built on another board's root.

payload-contract.md (Row 1) states the same order.

The CAD_BOARD example board used by every pcb_aoi/examples/allegro_*.py example has these paths. Use them only when the attachment resolves to that root; never as a stand-in for a board whose root was not read:

root_path         = "/Board_CAD_BOARD"
camera_path       = "/World/camera_light/Camera"
camera_xform_path = "/World/camera_light"
ring_light_root   = "/World/camera_light/aoi_ring_light"

These four values are NOT mechanically derivable from the USD filename (CAD_BOARD_layout.usd and CAD_BOARD_aoi.usd are different files but share the same root_path). If the attached USD is a different board (it ships its own <name>.components.json naming a different root, or its embedded metadata resolves to a different default prim such as /Board), use that board's own metadata instead and note the source in the run summary — do not force the CAD_BOARD paths onto a board that carries its own. If inspection does not resolve a unique board root, request an explicit root path rather than selecting the first root prim.

Component source: prefer source="usd" for embedded USD metadata. Attach a matching legacy component sidecar only when a standalone board relies on it or the caller explicitly selects that source. Use job.scene.attach_sidecar(name="components", kind="pcb.components", path=...) before referring to source="components". Keep component selection, solder, and annotations on that source; do not mix it with embedded metadata. File presence alone does not choose a source. Instanced panels require embedded USD metadata and cannot use legacy component sidecars.

Named components must resolve on the attachment

A component the prompt names -- a type or footprint (C_0805_2012Metric) or a refdes (C22) -- is used exactly as named, and only if the attached board has it. There is no substitution. A name the attachment cannot resolve fails validation (refusal case 4 in SKILL.md): say what was asked, list what the attachment has, build nothing. Rendering some other component in its place would hand back a successful dataset for the wrong part, and the mismatch does not show in the images.

PromptScope used
Names a component the attachment's own metadata confirms (sidecar entry, or embedded component metadata)That component, as named, however unfamiliar it looks (C_0805_2012Metric, R_0603_1608Metric, SOT-723 ... are pcb-builder footprint names)
Names a component the attachment does not haveNone: refusal case 4, nothing built
Names no componentThe components the attachment itself carries (SKILL.md, Inputs, "Component types"): every SMT family for rows 3-5, the through-hole parts by refdes for row 2. On a one-family board that is that one family. If the attachment carries none of the row's kind, refuse rather than hand off an empty selection

PASSIVE_SMD_0805 is the SMT passive family of the CAD_BOARD example board. It is a valid scope on that board; it is neither a default for a prompt that names no component nor a fallback for a name another board does not have.

Check the attachment, not an impression. Enumerate its component types before deciding either way:

  • embedded source: the metadata attribute pcba:component:footprintId on each component prim of the composed stage, e.g. with pxr {p.GetAttribute("pcba:component:footprintId").Get() for p in stage.Traverse() if p.HasAttribute("pcba:component:footprintId")}. Without pxr, text-search the attachment's whole folder, not only the root file: a pcb-builder export keeps its components in a sublayer (usd/components.usda) that the root .usda merely references.

  • selected legacy sidecar: the type (or footprint) field of each entry in that board's matching <stem>.components.json.

The run summary says which check was made ("C_0805_2012Metric found in the attachment's component types, used as named"); a refusal lists the types that were found.

THT board requirements

Rows 1 and 2 both take a THT board, but they need different things from it:

  • Row 1 (Golden board) works on any THT board whose components resolve as described above.

  • Row 2 (pin variation) additionally needs the through-hole leads authored with the pcba:mesh:role schema (pkg_lead meshes, plus pcba:board:holeRadiusMm). On a board without it, job.operations.pin_variation(...) fails at execution time with ValidationError: pin_variation found no through-hole leads: this board authors no 'pcba:mesh:role' = 'pkg_lead' meshes....

Do not assume an arbitrary attached USD authors the pcba:mesh:role schema. If Row 2's pin_variation call fails with the "found no through-hole leads" error above, report that the attached board's THT metadata is insufficient for pin-geometry variation rather than retrying with invented parameters.

Coverage on small selections

A requested defect that modifies zero components is not an acceptable outcome on a non-empty pool, and the executing side agrees: proportional targeting selects max(1, int(pool * ratio)), clamped to the eligible pool (pose operations in scripts/sdg/standalone/defects_utils/selection.py, solder defects in cdf/execution/isaac/job_order_runtime.py), and a pose operation that selects from a non-empty pool and applies to none of it raises instead of exiting clean. So ratio=0.2 (the no-coverage-phrase default) on a one-component selection yields one defect, exactly as ratio=1.0 does; the skill states the expected count (max(1, int(1 * 0.2)) = 1) in its summary. count=1 on add_shift/add_tombstone/add_sideflip is the explicit spelling of the same thing for a one-component selection (the solder add_* calls take ratio only). A clean render with exit 0 on a defect request therefore means the selection matched nothing, which is a failed case, never a default doing its job. On a board with tens of parts the same default selects 20% of them, as intended.

THT pin-variation magnitude ranges

job.operations.pin_variation(...) takes a (lo, hi) range for each axis, not a single value or boolean — a bare "generate images with tilt variation" names no magnitude, so use these fixed ranges (all inside PIN_VARIATION_BOUNDS, chosen as representative mid-range values, not derived from any specific board spec — note this in the run summary):

Prompt termAPI paramFixed default range
protrusionheight_variation (mm)(0.3, 1.0)
tilttilt_range (degrees)(5.0, 15.0)
offsetoffset_range (mm)(0.1, 0.4)
cutcut_angle (degrees)(5.0, 15.0)

Selector: components=[...] (explicit refdes list). Use the part the prompt names, confirmed on the attachment; when the prompt names no part, use the through-hole parts the attachment carries, read from it by refdes (on a one-part board, that one refdes). Use package_types=[...] only when the prompt names a package type. If the attachment has no through-hole part, report that no THT target exists rather than guessing a refdes.

Coverage ratio — applies to BOTH structural and solder defects

  • "full board coverage" → ratio=1.0 against whatever pool is currently selected (ratio is literally "fraction of the selected/eligible pool").

  • A bare count-free request with no "full board coverage" phrase → a fixed moderate default of ratio=0.2 (20% of the selected pool), matching the ratio scale used across the repo's own worked examples. This default applies identically to every add_* call in this skill — structural (add_shift/add_tombstone/add_sideflip) and solder (add_excess/add_missing_solder/etc.) alike; there is nothing solder-specific about it.

  • ratio never rounds a non-empty pool down to zero: the executing side selects max(1, int(pool * ratio)) for pose and solder defects alike. A request that resolves to a non-empty selection therefore always produces at least one defect -- state the expected count in the run summary. A selection that is empty (types= matched nothing on the attachment) is not served by any ratio; report it instead of handing off a plan that renders a clean board.

Structural-defect magnitudes — required, and never in the prompt

ratio is not enough to build a row-4 call. Each structural operation has required magnitude kwargs with no defaults in the API, and a catalog prompt ("generate tombstone structural defects") never supplies them. Omitting one is a TypeError at the call, before compilation. Use these fixed values, drawn from the repo's own Allegro-board defect example (pcb_aoi/examples/allegro_defect_job.py):

Prompt termAPI methodFixed default magnitudes
shift / offset (pose)add_shifttranslate_range=0.2 (mm), rotate_z_range=15 (degrees)
tombstoneadd_tombstoneangle_range=(70.0, 90.0) (degrees)
sideflipadd_sideflipangle_range=(70.0, 90.0) (degrees)

Other examples in the repo (allegro_v7_defects_with_solder.py, distributable_board_defects.py) use larger values — translate_range=1.0, rotate_z_range=25.0, sideflip (60.0, 80.0). Those are for other boards; this skill uses the set above for consistency, and the run summary should say which set was applied.

Only add_tombstone takes component_types directly, and only on a sidecar board. add_shift and add_sideflip do not — to scope either to a named component, call job.scene.select_components(types=[...], source=...) BEFORE the add_shift/add_sideflip call, narrowing the pool first. Do not attempt to pass a component_types= kwarg to add_shift/add_sideflip; it does not exist and will raise. On an embedded-metadata board omit component_types from add_tombstone as well and scope it through the narrowed selection with eligibility_source="usd": supplying the list there applies the operation to nothing. See payload-contract.md, Row 4, "scoping".

Combining multiple supported defect types in one request

A prompt can name more than one supported defect in the same request (e.g. "generate excess solder and missing defects", or in principle two structural defects together) — this is different from mixing a supported type with unsupported bridge (see below, which is about dropping the unsupported clause). For two or more supported types: declare solder_joints(...) once (solder defects only need it declared a single time, not once per defect call), then chain each mapped add_* call back to back against the same component_types scope, each with its own independent ratio resolved by the rule above. Order between the add_* calls does not matter for solder defects (each targets independent joints); for structural defects, pin_variation-style "last group wins" ordering does not apply either — add_shift/add_tombstone/add_sideflip are independent operations and can all be declared in one job.

But read that together with "Tell the executing side: one render() per process" in SKILL.md, because the two answer different questions. Declaring several structural add_* calls in one job puts all of those defects in the SAME images: one job, one render, one process. That is right when the prompt wants a board carrying several defects at once. It is NOT how you produce one image set per defect type — that is several variants, and each variant needs its own process, because a second render() in one process segfaults Kit. So: several defects in one picture is one job; several pictures each with one defect is several processes.

Solder texture-defect mapping

Prompt termAPI methodNotes
"excess solder"add_excess(ratio, component_types=...)Direct match
"missing" (solder/texture)add_missing_solder(ratio, component_types=...)Direct match, no shape params
"insufficient"add_insufficient(ratio, ...)Direct match
"bright"add_bright(ratio, ...)THT-only
"interrupted" / "dewetting"add_interrupted(ratio, ...)Both THT/SMT
"dry contact"add_dry_contact(ratio, ...)THT-only
"bridge"No API method exists.Fail loudly — see below

All solder defects require job.operations.solder_joints(solver="surface_evolver", ...) declared first — solver="procedural" plus any solder-defect call is a validation error by design (the API rejects procedural there deliberately). Use source="usd" for embedded metadata. Use the attached sidecar's source name for solder_joints(...) only when the job uses a selected legacy component sidecar, consistent with selection and annotations.

Row 1 is the one place procedural is the right solver. The catalog's golden board is "procedural solder, no defects", so the Row 1 template declares solder_joints(solver="procedural", ...) and no add_* call — which is exactly the combination the API accepts. A golden board rendered without that declaration shows bare pads, which is not what the catalog row describes. Do not pair procedural with any solder defect, and do not switch Row 1 to surface_evolver just because Row 5 needs it: the procedural solver has no environment prerequisites, which is also why Row 1 can be dry-run validated anywhere.

surface_evolver needs TWO things the environment may not have, and BOTH of them fail at the same, earliest possible stage: inside the job.operations.solder_joints(...) call itself. pcb_aoi/sections.py calls resolve_solver(solver) in the body of solder_joints, so neither missing dependency waits for compile_orders() or for render(). A Row 5 script cannot even be dry-run validated without them:

  1. The manifold3d Python package. Without it, job.operations.solder_joints(solver="surface_evolver", ...) raises ValidationError: unknown solder solver 'surface_evolver' (registered: ['none', 'procedural']) immediately at that call — not at render(), not even at compile_orders(), but the moment solder_joints(...) itself is declared. The repo's requirements.txt lists manifold3d, but a minimal validation environment (numpy/Pillow/pydantic/PyYAML only, as in the CI unit-tests job) does not install it — so a Row 5 script that compiles cleanly in a full cdf-simulation environment will fail to even declare solder_joints() in a bare validation container unless manifold3d is installed too.

  2. The native evolver binary. With manifold3d present but no binary, the same call raises ValidationError: solder solver 'surface_evolver' is unavailable: Surface Evolver backend unimportable: Surface Evolver binary not found — again at solder_joints(...), not at compile_orders() and not at render().

The shipped cdf-simulation container has both preinstalled. Outside it, install manifold3d (pip install manifold3d) AND either build the binary from pcb_aoi/solder_evolver/third_party/evolver/build.sh or point SOLDER_EVOLVER_BIN at an already-built evolver binary. With either one missing, even a compile-only dry-run validation of a Row 5 script is impossible, not merely a render-time risk. Before running or even validating any Row 5 job, confirm the executing environment has both — an Airflow worker image or CI validation container without them will reject every solder-defect script at declaration time, not just fail to render.

Bridge defects are unsupported. If a prompt asks for "bridge defects" (alone or mixed with other solder-defect requests), do not generate anything for the bridge portion and do not silently substitute add_interrupted or any other method — respond:

Bridge (solder short between adjacent pads/leads) is not supported by the current cdf API. Available solder defect types: insufficient, excess, bright, interrupted, missing_solder, dry_contact.

If the prompt mixed bridge with a supported type (e.g. "generate bridge and excess solder defects"), still generate the supported part and report the bridge portion as unsupported in the same response — do not fail the whole request over one unsupported clause.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Python

Security

What our scanner noticed· Medium risk

  • Large base64-encoded blobs
  • Instructions that use the network

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
NVIDIA CDF
Publisher
NVIDIA (Official)
Source
NVIDIA/skills · skills/cdf-simulation · commit fce3cca
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE-APACHE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by NVIDIA CDF or NVIDIA.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download cdf-simulation.zip