Add a component¶
Goal: add an encoder, a stage, a head bundle, a post-processor, a transform, a hook or an evaluator without forking: write a class, register it, and point a config at it.
You need: GraspPanda installed and one Python file. The example project does exactly this and is the best starting point.
1. Write and register it¶
# my_components.py
from grasppanda.models import Backbone
from grasppanda.registry import BACKBONES
@BACKBONES.register()
class MyBackbone(Backbone):
"""One sentence that says what it is: the Registered classes page shows it."""
output = "dense" # the contract: features live on the input points
def __init__(self, in_channels: int = 3, out_channels: int = 256, depth: int = 4) -> None:
... # the whole class: see the fold below
Subclass the base class of the slot (Backbone, Grouper, HeadBundle, …): it decides
the registry and the config slot. Annotate every constructor parameter, since the
annotations are what config values are validated against.
The whole class, with what the Registered classes page prints
# my_components.py
from torch import nn
from grasppanda.models import Backbone
from grasppanda.registry import BACKBONES
@BACKBONES.register()
class MyBackbone(Backbone):
"""One sentence that says what it is: the Registered classes page shows it."""
output = "dense" # the contract: features live on the input points
status = "tested" # what you proved, not what you hope
source = "My method (Me et al., 2026)" # the paper, method or library it implements
extras = ("spconv",) # optional dependencies, if any
def __init__(self, in_channels: int = 3, out_channels: int = 256, depth: int = 4) -> None:
super().__init__()
self.out_channels = out_channels
...
status, source, extras and the first sentence of the docstring are what
grasppanda components and the Registered classes page print.
Components inside the grasppanda package must declare source on the class itself (a
subclass does not inherit a citation), add one tiny configuration to
tests/unit/test_swap_matrix.py if they are stages of the seed-based skeleton, and
regenerate the pages with python -m tools.make_component_docs; the unit suite fails
until all three are done.
2. Point a config at it¶
custom_imports: [../my_components.py] # or an importable module name
model:
backbone: {type: MyBackbone, depth: 6}
Put these lines in a config that inherits the method you build on (Configuration). Which slots each method has is on the Components page.
3. Check it, then probe it¶
grasppanda print-config my_config.yaml --check
grasppanda probe my_config.yaml
Expected result: ✓ my_config.yaml is valid with a hash, then a probe verdict. A probe
trains the config for 20 iterations on real frames and predicts
(Compose and probe).
Troubleshooting: what registration and the check refuse
**kwargs. Registration refuses it: a component that swallows arbitrary keys would turn typos into silent no-ops.- Annotations under
TYPE_CHECKING. Import annotation names at runtime; the validator reads them. - A key the class does not have.
MyBackbone has no parameter 'depths'. Did you mean 'depth'? Parameters: …, before anything is built. - A contract a neighbour cannot meet. The message names both sides; see Contracts.
Slots that hold other components
Annotate the parameter with Cfg[Base] and build it yourself, so you can hand the
child what you already know:
from grasppanda.models import Backbone, BaseGraspDetector, HeadBundle
from grasppanda.registry import BACKBONES, DETECTORS, HEADS, Cfg
@DETECTORS.register()
class MyDetector(BaseGraspDetector):
def __init__(self, backbone: Cfg[Backbone], head: Cfg[HeadBundle]) -> None:
super().__init__()
self.backbone = BACKBONES.build(backbone)
self.head = HEADS.build(head, in_channels=self.backbone.out_channels)
Cfg[Backbone] tells the validator which registry owns the slot, so nested configs are
checked without constructing anything.
Injected arguments
A child should not make the user repeat what the parent knows. Declare such a parameter
with the INJECTED default:
from grasppanda.registry import HEADS, INJECTED
@HEADS.register()
class MyHead(HeadBundle):
in_axes = ("seed", "channel")
def __init__(self, num_angle: int = 12, in_channels: int = INJECTED) -> None: ...
- The config may omit
in_channels; the parent supplies it throughHEADS.build(...). - If the config sets it and disagrees with the parent, building fails. Nothing is silently overridden.
Contracts
Declare what your component consumes and produces as class attributes; the base classes document which ones exist, and the Registered classes page explains each next to the components that declare it:
| Slot | Contract attributes |
|---|---|
| backbone (scene encoder) | output, output_channels(options) |
| proposal | requires, seed_scores, needs_supervision |
| view stage | seed_scores, label_formats, needs_matched, applies_seed_weights |
| assigner | label_format, optional_outputs, needs_supervision |
| grouper | support, out_axes, required_seed_channels(options) |
| head bundle | in_axes, label_format, score_kind, applies_seed_weights, needs_supervision, required_seed_scores(options) |
| image encoder | in_channels, input_layout, out_channels, out_strides (grasppanda.models.image_encoders.base.ImageEncoder) |
The first six are the stages of the seed-based skeleton. It compares them for every
detector built on it, on the config and again on the built stages
(stage_contract_problems in grasppanda.models.detectors.seed_based). A stage
therefore needs no checking code of its own: declare the attributes truthfully and an
illegal combination is refused with a message that names both sides. Say only what the
code does: a head that sets applies_seed_weights = True without reading
MatchedLabels.weight turns a config error into a silently wrong model. No config check
compares the widths of an image encoder: the decoder that follows is built for whatever
widths arrive.
Two of them reach beyond the pair of stages next to each other:
output_channels(options)is the width a backbone returns, told from its options alone; return the same number the built module has asout_channels(tests/unit/test_swap_matrix.pycompares them for your tiny config and for your defaults). The default reads an integerout_channelsoption, so an encoder that has one needs nothing more. ReturnNoneonly where the width is known once the encoder is built (a bridge whose width is that of a library model): the config check then builds the encoder once, without released weights, and the test asks for the reason (WIDTH_KNOWN_ONCE_BUILT). A grouper that fixes the width of the seed features states it withrequired_seed_channels(options)and names inprojected_variantthe grouper that takes any width, whichgrasppanda composeputs in its place.needs_supervisionlists what a stage reads from the trainingSupervision, by where it sits (objects,objects.tolerance,point_labels.<name>,extras.<name>), each with the option that makes the stage read it, orNonewhen it always does. A dataset reader says what a config makes it serve withserved_supervision(node), and the config check refuses a training dataset that does not serve what the model reads.tests/unit/test_supervision_contract.pytrains every legal stage set on a batch that carries exactly the declared fields and without each of them in turn, so a stage that reads more than it declares fails there. A detector that is not built on the skeleton declaresneeds_supervisionon itself.
A component that owns slots of another kind can compare its children's contracts before
anything is built by implementing check_config:
@classmethod
def check_config(cls, node):
grouper = GROUPERS.get(node["grouper"]["type"])
head = HEADS.get(node["head"]["type"])
if grouper.out_axes != head.in_axes:
yield f"{grouper.__name__} emits {grouper.out_axes}, but {head.__name__} expects {head.in_axes}."
Each yielded message becomes a config error at that node.
Head bundles
A head bundle is one class that owns the prediction module, the target encoder, the loss and the decoder. They share one grasp parameterization (angle and depth grids, width scale, score meaning), so they are swapped together or not at all. There is no way to configure a head with another bundle's decoder.
Optional dependencies
Import heavy dependencies inside the component, through require, so that
import grasppanda keeps working with the core install:
from grasppanda.utils.imports import require
class SparseThing(Backbone):
def __init__(self, ...) -> None:
spconv = require("spconv.pytorch", extra="spconv-cu118", purpose="SparseThing")
A user without the extra gets SparseThing needs 'spconv.pytorch' … pip install -e ".[spconv-cu118]".
Share it¶
Put it under projects/<your_project>/ with a README that states a support level; see
projects/README.md.
A pip-installable plugin can expose a grasppanda.plugins entry point to be imported
automatically.
Next¶
- Compose and probe: measure your option under the methods that have its slot.
- Data contracts: the structures stages exchange.