I’ve been thinking about the PLR resource model in relation to networked interfaces. Here are two problems it solves:
- Sharing between machines. If you have 1 incubator connected to two difference workcells, you want to be able to sync the resources in some way.
- Persistent connections. If you are over a network and your protocol disconnects, for whatever reason, you are screwed because now the server and client are desynced.
The logic doesn’t need to be reproduced, just the fundamental operations which sync to disk. Pretty much everything (well, not including the well tracking / item ordering) can be instantiated in a single SQL table:
CREATE TABLE resources (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
type TEXT NOT NULL, -- 'Resource', 'Plate', 'TipRack', 'Well', etc.
-- Tree structure
parent_id INTEGER REFERENCES resources(id),
-- Spatial: Local position relative to parent
loc_x REAL,
loc_y REAL,
loc_z REAL,
-- Spatial: Local rotation (Euler angles in degrees)
rot_x REAL DEFAULT 0,
rot_y REAL DEFAULT 0,
rot_z REAL DEFAULT 0,
-- Spatial: Size
size_x REAL NOT NULL,
size_y REAL NOT NULL,
size_z REAL NOT NULL,
-- Metadata
category TEXT,
model TEXT,
-- Timestamps for change tracking
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CHECK (parent_id IS NULL OR parent_id != id)
);
CREATE INDEX idx_resources_parent ON resources(parent_id);
CREATE INDEX idx_resources_name ON resources(name);
However, I think the easiest route here is producing an API which can be the backend instead of just in-memory. You only need 5 operations, since you’re operating really off a single table:
- create_resource
- get_resource
- update_resource
- delete_resource
- query_resources
Though you would likely want a couple more, just for convenience:
- get_tree (avoid N queries)
- batch_create (create plate with 96 wells)
- batch_update (move multiple resources)
- batch_delete (clear deck)
- get_state (volume/tip state)
- update_state
Here is what I imagine the code looking like:
from pylabrobot.liquid_handling import LiquidHandler
from pylabrobot.liquid_handling.backends import STARBackendAPI
from pylabrobot.resources import Deck
auth_key = "AUTH_KEY"
deck = Deck.from_api("machine1", base_url = "pylabrobot.org/api/v1/state", auth = auth_key)
backend = STARBackendAPI("machine1", base_url = "pylabrobot.org/api/v1/star", auth = auth_key)
lh = LiquidHandler(backend=STARBackendAPI(), deck=deck)
await lh.setup()
await lh.pick_up_tips(lh.deck.get_resource("tip_rack")["A1"])
await lh.aspirate(lh.deck.get_resource("plate")["A1"], vols=100)
await lh.dispense(lh.deck.get_resource("plate")["A2"], vols=100)
await lh.return_tips()
This would give:
- Method to integrate the states of many machines together at once (same base_url, just different machine). Backend handles the pass-off of material
- Fully atomic transactions: pickup where you left off without ever thinking about serializing
- Ability for full log tracking (if there is a log table). Track movement of every plate.
However, unlike the network backend, this would require modifying the code of the deck, because we want the business logic to be able to use an API instead of just in-memory. But seems very doable.