ResourceR - declaring what backends need from the resource tree

Apologies if something similar has come up before, I’m still catching up on the forum history.

While implementing new device backends and studying the existing codebase, I found it hard to navigate the resource model. To understand what a backend actually needs from the resource tree, you have to read every line of every method, or ask an AI to do it for you. Either way, there’s no real overview or declaration. For example in biotek_backend.py, resource access is scattered:

# biotek_backend.py:532
rows = plate.num_items_y
# biotek_backend.py:533
columns = plate.num_items_x
# biotek_backend.py:542
top_left_well_center = top_left_well.location + top_left_well.get_anchor(x="c", y="c")
# biotek_backend.py:544-546
plate_size_y = plate.get_size_y()
plate_size_x = plate.get_size_x()
plate_size_z = plate.get_size_z()
# biotek_backend.py:547-548
if plate.lid is not None:
    plate_size_z += plate.lid.get_size_z() - plate.lid.nesting_z_height
# biotek_backend.py:1180-1181
first_well = plate.get_item(0)
well_size_x, well_size_y = (first_well.get_size_x(), first_well.get_size_y())

Same idea as SignalR (which declares what a backend produces), but for what it consumes:

class BiotekAbsorbanceBackend(AbsorbanceBackend):
  # What I produce
  absorbance = SignalR(unit="AU")

  # What I need from the resource labware tree
  num_rows     = ResourceR(source="plate", attr="num_items_y", dtype=int)
  num_columns  = ResourceR(source="plate", attr="num_items_x", dtype=int)
  plate_size   = ResourceR(source="plate", attr="get_size_x,get_size_y,get_size_z", unit="mm")
  well_center  = ResourceR(source="well", attr="get_anchor", unit="mm")
  well_size    = ResourceR(source="well", attr="get_size_x,get_size_y", unit="mm")
  lid_size_z   = ResourceR(source="plate.lid", attr="get_size_z", unit="mm")

  # What I need from my own device tree (if not monochromator)
  my_filter = ResourceR(source="self.filter_wheel", attr="wavelength", unit="nm") 

Open the file, read the header, full input/output contract. Makes it easier to implement new backends, validate before execution, and know exactly what to flatten for serialization across boundaries if needed.

why would there need to be?

is this about this post? Plr protobuf - #4 by rickwierenga. If so, backends can read arbitrary information from a resource it gets, its parents, etc. If we define what specific attributes are needed today, you might get serialization working with those specific attributes, but sooner or later this declaration turns into the whole resource tree, so we might as well design for that from the start.

For onboarding and documentation. When a new contributor wants to implement a backend for their device, they look at the ABC and see the method signatures, but not which resource properties their implementation actually needs to access. Today the only way to learn that is reading existing backends line by line. A declaration of resource needs per backend would make that immediately visible.

Not specifically about serialization, but related yes. More about making backends self-documenting. For complex backends the declaration might approach the full tree, but for many simpler devices it would be a short list. Your own remark in this post, “the smallest set of atomic commands which are easy to implement to make it easy to add new machines” already does this for plate readers, read_positions only gives the backend coordinates, not the full plate. Declaring resource needs per backend is a similar principle applied more broadly: what is the smallest set of resource properties this backend actually needs? We already declare capabilities per device (a Byonoy only needs absorbance, a Synergy needs absorbance, fluorescence, and luminescence). This is the same line of thinking applied to inputs rather than outputs

1 Like