puda-drivers 0.0.6__py3-none-any.whl → 0.0.8__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,242 @@
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
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
+
17
+
18
+ class First:
19
+ """
20
+ First machine class integrating motion control, deck management, and liquid handling.
21
+
22
+ The deck has 16 slots arranged in a 4x4 grid (A1-D4).
23
+ Each slot's origin location is stored for absolute movement calculation.
24
+ """
25
+
26
+ # Default configuration values
27
+ DEFAULT_QUBOT_PORT = "/dev/ttyACM0"
28
+ DEFAULT_QUBOT_BAUDRATE = 9600
29
+ DEFAULT_QUBOT_FEEDRATE = 3000
30
+
31
+ DEFAULT_SARTORIUS_PORT = "/dev/ttyUSB0"
32
+ DEFAULT_SARTORIUS_BAUDRATE = 9600
33
+
34
+ # origin position of Z and A axes
35
+ Z_ORIGIN = Position(x=0, y=0, z=0)
36
+ A_ORIGIN = Position(x=60, y=0, a=0)
37
+
38
+ # Default axis limits - customize based on your hardware
39
+ DEFAULT_AXIS_LIMITS = {
40
+ "X": (0, 330),
41
+ "Y": (-440, 0),
42
+ "Z": (-140, 0),
43
+ "A": (-175, 0),
44
+ }
45
+
46
+ # Height from z and a origin to the deck
47
+ CEILING_HEIGHT = 192.2
48
+
49
+ # Tip length
50
+ TIP_LENGTH = 59
51
+
52
+ # Slot origins (the bottom left corner of the slot relative to the deck origin)
53
+ SLOT_ORIGINS = {
54
+ "A1": Position(x=-2, y=-424),
55
+ "A2": Position(x=98, y=-424),
56
+ "A3": Position(x=198, y=-424),
57
+ "A4": Position(x=298, y=-424),
58
+ "B1": Position(x=-2, y=-274),
59
+ "B2": Position(x=98, y=-274),
60
+ "B3": Position(x=198, y=-274),
61
+ "B4": Position(x=298, y=-274),
62
+ "C1": Position(x=-2, y=-124),
63
+ "C2": Position(x=98, y=-124),
64
+ "C3": Position(x=198, y=-124),
65
+ "C4": Position(x=298, y=-124),
66
+ }
67
+
68
+ def __init__(
69
+ self,
70
+ qubot_port: Optional[str] = None,
71
+ sartorius_port: Optional[str] = None,
72
+ axis_limits: Optional[Dict[str, Tuple[float, float]]] = None,
73
+ ):
74
+ """
75
+ Initialize the First machine.
76
+
77
+ Args:
78
+ qubot_port: Serial port for GCodeController (e.g., '/dev/ttyACM0')
79
+ sartorius_port: Serial port for SartoriusController (e.g., '/dev/ttyUSB0')
80
+ axis_limits: Dictionary mapping axis names to (min, max) limits.
81
+ Defaults to DEFAULT_AXIS_LIMITS.
82
+ """
83
+ # Initialize deck
84
+ self.deck = Deck(rows=4, cols=4)
85
+
86
+ # Initialize controllers
87
+ self.qubot = GCodeController(
88
+ port_name=qubot_port or self.DEFAULT_QUBOT_PORT,
89
+ )
90
+ # Set axis limits
91
+ limits = axis_limits or self.DEFAULT_AXIS_LIMITS
92
+ for axis, (min_val, max_val) in limits.items():
93
+ self.qubot.set_axis_limits(axis, min_val, max_val)
94
+
95
+ # Initialize pipette
96
+ self.pipette = SartoriusController(
97
+ port_name=sartorius_port or self.DEFAULT_SARTORIUS_PORT,
98
+ )
99
+
100
+ def connect(self):
101
+ """Connect all controllers."""
102
+ self.qubot.connect()
103
+ self.pipette.connect()
104
+
105
+ def disconnect(self):
106
+ """Disconnect all controllers."""
107
+ self.qubot.disconnect()
108
+ self.pipette.disconnect()
109
+
110
+ def load_labware(self, slot: str, labware_name: str):
111
+ """Load a labware object into a slot."""
112
+ self.deck.load_labware(slot=slot, labware_name=labware_name)
113
+
114
+ def load_deck(self, deck_layout: Dict[str, Type[StandardLabware]]):
115
+ """
116
+ Load multiple labware into the deck at once.
117
+
118
+ Args:
119
+ deck_layout: Dictionary mapping slot names (e.g., "A1") to labware classes.
120
+ Each class will be instantiated automatically.
121
+
122
+ Example:
123
+ machine.load_deck({
124
+ "A1": Opentrons96TipRack300,
125
+ "B1": Opentrons96TipRack300,
126
+ "C1": Rubbish,
127
+ })
128
+ """
129
+ for slot, labware_name in deck_layout.items():
130
+ self.load_labware(slot=slot, labware_name=labware_name)
131
+
132
+ def attach_tip(self, slot: str, well: Optional[str] = None):
133
+ """Attach a tip from a slot."""
134
+ if self.pipette.is_tip_attached():
135
+ raise ValueError("Tip already attached")
136
+
137
+ pos = self.get_absolute_z_position(slot, well)
138
+ # return the offset from the origin
139
+ self.qubot.move_absolute(position=pos)
140
+
141
+ # attach tip (move slowly down)
142
+ self.qubot.move_relative(
143
+ position=Position(z=-self.deck[slot].get_insert_depth()),
144
+ feed=500
145
+ )
146
+ self.pipette.set_tip_attached(attached=True)
147
+ # must home Z axis after, as pressing in tip might cause it to lose steps
148
+ self.qubot.home(axis="Z")
149
+
150
+ def drop_tip(self, slot: str, well: str):
151
+ """Drop a tip into a slot."""
152
+ if not self.pipette.is_tip_attached():
153
+ raise ValueError("Tip not attached")
154
+
155
+ pos = self.get_absolute_z_position(slot, well)
156
+ # move up by the tip length
157
+ pos += Position(z=self.TIP_LENGTH)
158
+ print("moving Z to", pos)
159
+ self.qubot.move_absolute(position=pos)
160
+
161
+ self.pipette.eject_tip()
162
+ time.sleep(5)
163
+ self.pipette.set_tip_attached(attached=False)
164
+
165
+ def aspirate_from(self, slot:str, well:str, amount:int):
166
+ """Aspirate a volume of liquid from a slot."""
167
+ if not self.pipette.is_tip_attached():
168
+ raise ValueError("Tip not attached")
169
+
170
+ pos = self.get_absolute_z_position(slot, well)
171
+ print("moving Z to", pos)
172
+ self.qubot.move_absolute(position=pos)
173
+ self.pipette.aspirate(amount=amount)
174
+ time.sleep(5)
175
+
176
+ def dispense_to(self, slot:str, well:str, amount:int):
177
+ """Dispense a volume of liquid to a slot."""
178
+ if not self.pipette.is_tip_attached():
179
+ raise ValueError("Tip not attached")
180
+
181
+ pos = self.get_absolute_z_position(slot, well)
182
+ print("moving Z to", pos)
183
+ self.qubot.move_absolute(position=pos)
184
+ self.pipette.dispense(amount=amount)
185
+ time.sleep(5)
186
+
187
+ def get_slot_origin(self, slot: str) -> Position:
188
+ """
189
+ Get the origin coordinates of a slot.
190
+
191
+ Args:
192
+ slot: Slot name (e.g., 'A1', 'B2')
193
+
194
+ Returns:
195
+ Position for the slot origin
196
+
197
+ Raises:
198
+ KeyError: If slot name is invalid
199
+ """
200
+ slot = slot.upper()
201
+ if slot not in self.SLOT_ORIGINS:
202
+ raise KeyError(f"Invalid slot name: {slot}. Must be one of {list(self.SLOT_ORIGINS.keys())}")
203
+ return self.SLOT_ORIGINS[slot]
204
+
205
+ def get_absolute_z_position(self, slot: str, well: Optional[str] = None) -> Position:
206
+ """
207
+ Get the absolute position for a slot (and optionally a well within that slot) based on the origin
208
+
209
+ Args:
210
+ slot: Slot name (e.g., 'A1', 'B2')
211
+ well: Optional well name within the slot (e.g., 'A1' for a well in a tiprack)
212
+
213
+ Returns:
214
+ Position with absolute coordinates
215
+ """
216
+ # Get slot origin
217
+ pos = self.get_slot_origin(slot)
218
+
219
+ # relative well position from slot origin
220
+ if well:
221
+ well_pos = self.deck[slot].get_well_position(well).get_xy()
222
+ # the deck is rotated 90 degrees clockwise for this machine
223
+ pos += well_pos.swap_xy()
224
+ # get z
225
+ z = Position(z=self.deck[slot].get_height() - self.CEILING_HEIGHT)
226
+ pos += z
227
+ return pos
228
+
229
+ def get_absolute_a_position(self, slot: str, well: Optional[str] = None) -> Position:
230
+ """
231
+ Get the absolute position for a slot (and optionally a well within that slot) based on the origin
232
+ """
233
+ pos = self.get_slot_origin(slot)
234
+
235
+ if well:
236
+ well_pos = self.deck[slot].get_well_position(well).get_xy()
237
+ pos += well_pos.swap_xy()
238
+
239
+ # get a
240
+ a = Position(a=self.deck[slot].get_height() - self.CEILING_HEIGHT)
241
+ pos += a
242
+ 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()]