puda-drivers 0.0.7__py3-none-any.whl → 0.0.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,141 @@
1
+ {
2
+ "ordering": [
3
+ [
4
+ "A1",
5
+ "B1"
6
+ ],
7
+ [
8
+ "A2",
9
+ "B2"
10
+ ],
11
+ [
12
+ "A3",
13
+ "B3"
14
+ ],
15
+ [
16
+ "A4",
17
+ "B4"
18
+ ]
19
+ ],
20
+ "brand": {
21
+ "brand": "Polyelectric",
22
+ "brandId": []
23
+ },
24
+ "metadata": {
25
+ "displayName": "Polyelectric 8 Well Plate 30000 µL",
26
+ "displayCategory": "wellPlate",
27
+ "displayVolumeUnits": "µL",
28
+ "tags": []
29
+ },
30
+ "dimensions": {
31
+ "xDimension": 128,
32
+ "yDimension": 85.5,
33
+ "zDimension": 74
34
+ },
35
+ "wells": {
36
+ "A1": {
37
+ "depth": 70.5,
38
+ "totalLiquidVolume": 30000,
39
+ "shape": "circular",
40
+ "diameter": 16.9,
41
+ "x": 25,
42
+ "y": 59,
43
+ "z": 3.5
44
+ },
45
+ "B1": {
46
+ "depth": 70.5,
47
+ "totalLiquidVolume": 30000,
48
+ "shape": "circular",
49
+ "diameter": 16.9,
50
+ "x": 25,
51
+ "y": 29,
52
+ "z": 3.5
53
+ },
54
+ "A2": {
55
+ "depth": 70.5,
56
+ "totalLiquidVolume": 30000,
57
+ "shape": "circular",
58
+ "diameter": 16.9,
59
+ "x": 54,
60
+ "y": 59,
61
+ "z": 3.5
62
+ },
63
+ "B2": {
64
+ "depth": 70.5,
65
+ "totalLiquidVolume": 30000,
66
+ "shape": "circular",
67
+ "diameter": 16.9,
68
+ "x": 54,
69
+ "y": 29,
70
+ "z": 3.5
71
+ },
72
+ "A3": {
73
+ "depth": 70.5,
74
+ "totalLiquidVolume": 30000,
75
+ "shape": "circular",
76
+ "diameter": 16.9,
77
+ "x": 83,
78
+ "y": 59,
79
+ "z": 3.5
80
+ },
81
+ "B3": {
82
+ "depth": 70.5,
83
+ "totalLiquidVolume": 30000,
84
+ "shape": "circular",
85
+ "diameter": 16.9,
86
+ "x": 83,
87
+ "y": 29,
88
+ "z": 3.5
89
+ },
90
+ "A4": {
91
+ "depth": 70.5,
92
+ "totalLiquidVolume": 30000,
93
+ "shape": "circular",
94
+ "diameter": 16.9,
95
+ "x": 112,
96
+ "y": 59,
97
+ "z": 3.5
98
+ },
99
+ "B4": {
100
+ "depth": 70.5,
101
+ "totalLiquidVolume": 30000,
102
+ "shape": "circular",
103
+ "diameter": 16.9,
104
+ "x": 112,
105
+ "y": 29,
106
+ "z": 3.5
107
+ }
108
+ },
109
+ "groups": [
110
+ {
111
+ "metadata": {
112
+ "wellBottomShape": "flat"
113
+ },
114
+ "wells": [
115
+ "A1",
116
+ "B1",
117
+ "A2",
118
+ "B2",
119
+ "A3",
120
+ "B3",
121
+ "A4",
122
+ "B4"
123
+ ]
124
+ }
125
+ ],
126
+ "parameters": {
127
+ "format": "irregular",
128
+ "quirks": [],
129
+ "isTiprack": false,
130
+ "isMagneticModuleCompatible": false,
131
+ "loadName": "polyelectric_8_wellplate_30000ul"
132
+ },
133
+ "namespace": "custom_beta",
134
+ "version": 1,
135
+ "schemaVersion": 2,
136
+ "cornerOffsetFromSlot": {
137
+ "x": 0,
138
+ "y": 0,
139
+ "z": 0
140
+ }
141
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "ordering": [
3
+ ["A1"]
4
+ ],
5
+ "brand": {
6
+ "brand": "custom"
7
+ },
8
+ "metadata": {
9
+ "displayName": "Trash Bin"
10
+ },
11
+ "dimensions": {
12
+ "xDimension": 127.76,
13
+ "yDimension": 85.48,
14
+ "zDimension": 20
15
+ },
16
+ "wells": {
17
+ "A1": {
18
+ "depth": 0.0,
19
+ "shape": "rectangular",
20
+ "width": 10.0,
21
+ "height": 10.0,
22
+ "x": 60.0,
23
+ "y": 42.0
24
+ }
25
+ },
26
+ "groups": [
27
+ {
28
+ "metadata": {},
29
+ "wells": [
30
+ "A1"
31
+ ]
32
+ }
33
+ ],
34
+ "version": 1,
35
+ "schemaVersion": 2,
36
+ "cornerOffsetFromSlot": {
37
+ "x": 0,
38
+ "y": 0,
39
+ "z": 0
40
+ }
41
+ }
@@ -0,0 +1,3 @@
1
+ from .first import First
2
+
3
+ __all__ = ["First"]
@@ -0,0 +1,255 @@
1
+ """
2
+ First machine class containing Deck, GCodeController, and SartoriusController.
3
+
4
+ This class demonstrates the integration of:
5
+ - GCodeController: Handles motion control (hardware-specific)
6
+ - Deck: Manages labware layout (configuration-agnostic)
7
+ - SartoriusController: Handles liquid handling operations
8
+ """
9
+
10
+ import time
11
+ from typing import Optional, Dict, Tuple, Type, Union
12
+ from puda_drivers.move import GCodeController, Deck
13
+ from puda_drivers.core import Position
14
+ from puda_drivers.transfer.liquid.sartorius import SartoriusController
15
+ from puda_drivers.labware import StandardLabware
16
+ from puda_drivers.cv import CameraController
17
+
18
+
19
+ class First:
20
+ """
21
+ First machine class integrating motion control, deck management, liquid handling, and camera.
22
+
23
+ The deck has 16 slots arranged in a 4x4 grid (A1-D4).
24
+ Each slot's origin location is stored for absolute movement calculation.
25
+ """
26
+
27
+ # Default configuration values
28
+ DEFAULT_QUBOT_PORT = "/dev/ttyACM0"
29
+ DEFAULT_QUBOT_BAUDRATE = 9600
30
+ DEFAULT_QUBOT_FEEDRATE = 3000
31
+
32
+ DEFAULT_SARTORIUS_PORT = "/dev/ttyUSB0"
33
+ DEFAULT_SARTORIUS_BAUDRATE = 9600
34
+
35
+ DEFAULT_CAMERA_INDEX = 0
36
+
37
+ # origin position of Z and A axes
38
+ Z_ORIGIN = Position(x=0, y=0, z=0)
39
+ A_ORIGIN = Position(x=60, y=0, a=0)
40
+
41
+ # Default axis limits - customize based on your hardware
42
+ DEFAULT_AXIS_LIMITS = {
43
+ "X": (0, 330),
44
+ "Y": (-440, 0),
45
+ "Z": (-140, 0),
46
+ "A": (-175, 0),
47
+ }
48
+
49
+ # Height from z and a origin to the deck
50
+ CEILING_HEIGHT = 192.2
51
+
52
+ # Tip length
53
+ TIP_LENGTH = 59
54
+
55
+ # Slot origins (the bottom left corner of the slot relative to the deck origin)
56
+ SLOT_ORIGINS = {
57
+ "A1": Position(x=-2, y=-424),
58
+ "A2": Position(x=98, y=-424),
59
+ "A3": Position(x=198, y=-424),
60
+ "A4": Position(x=298, y=-424),
61
+ "B1": Position(x=-2, y=-274),
62
+ "B2": Position(x=98, y=-274),
63
+ "B3": Position(x=198, y=-274),
64
+ "B4": Position(x=298, y=-274),
65
+ "C1": Position(x=-2, y=-124),
66
+ "C2": Position(x=98, y=-124),
67
+ "C3": Position(x=198, y=-124),
68
+ "C4": Position(x=298, y=-124),
69
+ }
70
+
71
+ def __init__(
72
+ self,
73
+ qubot_port: Optional[str] = None,
74
+ sartorius_port: Optional[str] = None,
75
+ camera_index: Optional[Union[int, str]] = None,
76
+ axis_limits: Optional[Dict[str, Tuple[float, float]]] = None,
77
+ ):
78
+ """
79
+ Initialize the First machine.
80
+
81
+ Args:
82
+ qubot_port: Serial port for GCodeController (e.g., '/dev/ttyACM0')
83
+ sartorius_port: Serial port for SartoriusController (e.g., '/dev/ttyUSB0')
84
+ camera_index: Camera device index (0 for default) or device path/identifier.
85
+ Defaults to 0.
86
+ axis_limits: Dictionary mapping axis names to (min, max) limits.
87
+ Defaults to DEFAULT_AXIS_LIMITS.
88
+ """
89
+ # Initialize deck
90
+ self.deck = Deck(rows=4, cols=4)
91
+
92
+ # Initialize controllers
93
+ self.qubot = GCodeController(
94
+ port_name=qubot_port or self.DEFAULT_QUBOT_PORT,
95
+ )
96
+ # Set axis limits
97
+ limits = axis_limits or self.DEFAULT_AXIS_LIMITS
98
+ for axis, (min_val, max_val) in limits.items():
99
+ self.qubot.set_axis_limits(axis, min_val, max_val)
100
+
101
+ # Initialize pipette
102
+ self.pipette = SartoriusController(
103
+ port_name=sartorius_port or self.DEFAULT_SARTORIUS_PORT,
104
+ )
105
+
106
+ # Initialize camera
107
+ self.camera = CameraController(
108
+ camera_index=camera_index if camera_index is not None else self.DEFAULT_CAMERA_INDEX,
109
+ )
110
+
111
+ def connect(self):
112
+ """Connect all controllers."""
113
+ self.qubot.connect()
114
+ self.pipette.connect()
115
+ self.camera.connect()
116
+
117
+ def disconnect(self):
118
+ """Disconnect all controllers."""
119
+ self.qubot.disconnect()
120
+ self.pipette.disconnect()
121
+ self.camera.disconnect()
122
+
123
+ def load_labware(self, slot: str, labware_name: str):
124
+ """Load a labware object into a slot."""
125
+ self.deck.load_labware(slot=slot, labware_name=labware_name)
126
+
127
+ def load_deck(self, deck_layout: Dict[str, Type[StandardLabware]]):
128
+ """
129
+ Load multiple labware into the deck at once.
130
+
131
+ Args:
132
+ deck_layout: Dictionary mapping slot names (e.g., "A1") to labware classes.
133
+ Each class will be instantiated automatically.
134
+
135
+ Example:
136
+ machine.load_deck({
137
+ "A1": Opentrons96TipRack300,
138
+ "B1": Opentrons96TipRack300,
139
+ "C1": Rubbish,
140
+ })
141
+ """
142
+ for slot, labware_name in deck_layout.items():
143
+ self.load_labware(slot=slot, labware_name=labware_name)
144
+
145
+ def attach_tip(self, slot: str, well: Optional[str] = None):
146
+ """Attach a tip from a slot."""
147
+ if self.pipette.is_tip_attached():
148
+ raise ValueError("Tip already attached")
149
+
150
+ pos = self.get_absolute_z_position(slot, well)
151
+ # return the offset from the origin
152
+ self.qubot.move_absolute(position=pos)
153
+
154
+ # attach tip (move slowly down)
155
+ self.qubot.move_relative(
156
+ position=Position(z=-self.deck[slot].get_insert_depth()),
157
+ feed=500
158
+ )
159
+ self.pipette.set_tip_attached(attached=True)
160
+ # must home Z axis after, as pressing in tip might cause it to lose steps
161
+ self.qubot.home(axis="Z")
162
+
163
+ def drop_tip(self, slot: str, well: str):
164
+ """Drop a tip into a slot."""
165
+ if not self.pipette.is_tip_attached():
166
+ raise ValueError("Tip not attached")
167
+
168
+ pos = self.get_absolute_z_position(slot, well)
169
+ # move up by the tip length
170
+ pos += Position(z=self.TIP_LENGTH)
171
+ print("moving Z to", pos)
172
+ self.qubot.move_absolute(position=pos)
173
+
174
+ self.pipette.eject_tip()
175
+ time.sleep(5)
176
+ self.pipette.set_tip_attached(attached=False)
177
+
178
+ def aspirate_from(self, slot:str, well:str, amount:int):
179
+ """Aspirate a volume of liquid from a slot."""
180
+ if not self.pipette.is_tip_attached():
181
+ raise ValueError("Tip not attached")
182
+
183
+ pos = self.get_absolute_z_position(slot, well)
184
+ print("moving Z to", pos)
185
+ self.qubot.move_absolute(position=pos)
186
+ self.pipette.aspirate(amount=amount)
187
+ time.sleep(5)
188
+
189
+ def dispense_to(self, slot:str, well:str, amount:int):
190
+ """Dispense a volume of liquid to a slot."""
191
+ if not self.pipette.is_tip_attached():
192
+ raise ValueError("Tip not attached")
193
+
194
+ pos = self.get_absolute_z_position(slot, well)
195
+ print("moving Z to", pos)
196
+ self.qubot.move_absolute(position=pos)
197
+ self.pipette.dispense(amount=amount)
198
+ time.sleep(5)
199
+
200
+ def get_slot_origin(self, slot: str) -> Position:
201
+ """
202
+ Get the origin coordinates of a slot.
203
+
204
+ Args:
205
+ slot: Slot name (e.g., 'A1', 'B2')
206
+
207
+ Returns:
208
+ Position for the slot origin
209
+
210
+ Raises:
211
+ KeyError: If slot name is invalid
212
+ """
213
+ slot = slot.upper()
214
+ if slot not in self.SLOT_ORIGINS:
215
+ raise KeyError(f"Invalid slot name: {slot}. Must be one of {list(self.SLOT_ORIGINS.keys())}")
216
+ return self.SLOT_ORIGINS[slot]
217
+
218
+ def get_absolute_z_position(self, slot: str, well: Optional[str] = None) -> Position:
219
+ """
220
+ Get the absolute position for a slot (and optionally a well within that slot) based on the origin
221
+
222
+ Args:
223
+ slot: Slot name (e.g., 'A1', 'B2')
224
+ well: Optional well name within the slot (e.g., 'A1' for a well in a tiprack)
225
+
226
+ Returns:
227
+ Position with absolute coordinates
228
+ """
229
+ # Get slot origin
230
+ pos = self.get_slot_origin(slot)
231
+
232
+ # relative well position from slot origin
233
+ if well:
234
+ well_pos = self.deck[slot].get_well_position(well).get_xy()
235
+ # the deck is rotated 90 degrees clockwise for this machine
236
+ pos += well_pos.swap_xy()
237
+ # get z
238
+ z = Position(z=self.deck[slot].get_height() - self.CEILING_HEIGHT)
239
+ pos += z
240
+ return pos
241
+
242
+ def get_absolute_a_position(self, slot: str, well: Optional[str] = None) -> Position:
243
+ """
244
+ Get the absolute position for a slot (and optionally a well within that slot) based on the origin
245
+ """
246
+ pos = self.get_slot_origin(slot)
247
+
248
+ if well:
249
+ well_pos = self.deck[slot].get_well_position(well).get_xy()
250
+ pos += well_pos.swap_xy()
251
+
252
+ # get a
253
+ a = Position(a=self.deck[slot].get_height() - self.CEILING_HEIGHT)
254
+ pos += a
255
+ return pos
@@ -1 +1,4 @@
1
1
  from .gcode import GCodeController
2
+ from .deck import Deck
3
+
4
+ __all__ = ["GCodeController", "Deck"]
@@ -0,0 +1,47 @@
1
+ # src/puda_drivers/move/deck.py
2
+
3
+ from puda_drivers.labware import StandardLabware
4
+
5
+
6
+ class Deck:
7
+ """
8
+ Deck class for managing labware layout.
9
+ """
10
+ def __init__(self, rows: int, cols: int):
11
+ """
12
+ Initialize the deck.
13
+
14
+ Args:
15
+ rows: The number of rows in the deck.
16
+ cols: The number of columns in the deck.
17
+ """
18
+ # A dictionary mapping Slot Names (A1, B4) to Labware Objects
19
+ self.slots = {}
20
+ for row in range(rows):
21
+ for col in range(cols):
22
+ slot = f"{chr(65 + row)}{col + 1}"
23
+ self.slots[slot] = None
24
+
25
+ def load_labware(self, slot: str, labware_name: str):
26
+ """
27
+ Load labware into a slot.
28
+ """
29
+ if slot.upper() not in self.slots:
30
+ raise KeyError(f"Slot {slot} not found in deck")
31
+ self.slots[slot.upper()] = StandardLabware(labware_name=labware_name)
32
+
33
+ def __str__(self):
34
+ """
35
+ Return a string representation of the deck layout.
36
+ """
37
+ lines = []
38
+ for slot, labware in self.slots.items():
39
+ if labware is None:
40
+ continue
41
+ else:
42
+ lines.append(f"{slot}: {labware.name}")
43
+ return "\n".join(lines)
44
+
45
+ def __getitem__(self, key):
46
+ """Allows syntax for: my_deck['B4']"""
47
+ return self.slots[key.upper()]