Rick’s Backend kwargs proposal makes it explicit which operation parameters are backend-specific. On another level, there is also per-unit configuration that could be made explicit, which optional modules and optics are installed on THIS specific instrument in the lab. Some earlier thoughts on this in Updating PLR API:
Some devices have optional capabilities that vary per unit. An iSWAP on a STAR is optional, gas control on a Cytation is rare, and objectives/filter cubes differ between units. PLR already handles this ad-hoc: the STAR reads DriveConfiguration from firmware, the Cytation takes a CytationImagingConfig dataclass with objectives and filters from the user. Same problem, no shared pattern.
On the capability branch, devices create capabilities in __init__() before setup() connects to hardware, so optional capabilities can’t be discovered from firmware at construction time. Making a separate class per configuration doesn’t scale (iSWAP × 96-head × tube gripper × channels = too many classes).
What if we had a config object, a “device card”, that tells __init__ what this specific unit has? A model base (what every unit always has) plus instance overrides (what THIS unit has):
# STAR: optional modules
STAR_BASE = DeviceCard(capabilities={"liquid_handling": {"channels": 8}})
my_star = STAR_BASE.merge(DeviceCard.instance(capabilities={
"liquid_handling": {"channels": 16},
"iswap": {"rotation": True},
}))
my_star.has("iswap") # True
and for a cytation:
# Cytation 5: always has plate reading + microscopy, but optics vary per unit
CYTATION5_BASE = DeviceCard(capabilities={
"absorbance": {}, "fluorescence": {}, "luminescence": {},
"microscopy": {}, # always present, but objectives/filters vary
})
# Lab A: 4x + 20x, DAPI + GFP
lab_a = CYTATION5_BASE.merge(DeviceCard.instance(capabilities={
"microscopy": {"objectives": [O_4X_PL_FL, O_20X_PL_FL],
"filters": [DAPI, GFP]},
}))
# Lab B: 4x + 40x, GFP + Texas Red + Cy5, plus gas control
lab_b = CYTATION5_BASE.merge(DeviceCard.instance(capabilities={
"microscopy": {"objectives": [O_4X_PL_FL, O_40X_PL_APO],
"filters": [GFP, TEXAS_RED, CY5]},
"gas_control": {"co2": True},
}))
lab_a.has("gas_control") # False
lab_b.has("gas_control") # True
lab_a.get("microscopy", "objectives") # [O_4X_PL_FL, O_20X_PL_FL]
lab_b.get("microscopy", "objectives") # [O_4X_PL_FL, O_40X_PL_APO]
The card can come from firmware (STAR already reads DriveConfiguration, Cytation already queries its turret and filter slots) or a config file (service engineer or user programs the instrument when hardware changes). This is what manufacturers already require, PLR just doesn’t capture it as a shared concept yet.
How this all connects to backend_params: the card could validate that STARMoveParams(grip_force=80) is only sent to a STAR that actually has an iSWAP.
Any thoughts?