@@ -91,14 +91,19 @@ async def aspirate_in_place(
91
91
) -> float :
92
92
"""Set flow-rate and aspirate."""
93
93
# get mount and config data from state and hardware controller
94
+ adjusted_volume = _validate_aspirate_volume (
95
+ state_view = self ._state_view , pipette_id = pipette_id , aspirate_volume = volume
96
+ )
94
97
hw_pipette = self ._state_view .pipettes .get_hardware_pipette (
95
98
pipette_id = pipette_id ,
96
99
attached_pipettes = self ._hardware_api .attached_instruments ,
97
100
)
98
101
with self ._set_flow_rate (pipette = hw_pipette , aspirate_flow_rate = flow_rate ):
99
- await self ._hardware_api .aspirate (mount = hw_pipette .mount , volume = volume )
102
+ await self ._hardware_api .aspirate (
103
+ mount = hw_pipette .mount , volume = adjusted_volume
104
+ )
100
105
101
- return volume
106
+ return adjusted_volume
102
107
103
108
async def dispense_in_place (
104
109
self ,
@@ -108,6 +113,9 @@ async def dispense_in_place(
108
113
push_out : Optional [float ],
109
114
) -> float :
110
115
"""Dispense liquid without moving the pipette."""
116
+ adjusted_volume = _validate_dispense_volume (
117
+ state_view = self ._state_view , pipette_id = pipette_id , dispense_volume = volume
118
+ )
111
119
hw_pipette = self ._state_view .pipettes .get_hardware_pipette (
112
120
pipette_id = pipette_id ,
113
121
attached_pipettes = self ._hardware_api .attached_instruments ,
@@ -119,10 +127,10 @@ async def dispense_in_place(
119
127
)
120
128
with self ._set_flow_rate (pipette = hw_pipette , dispense_flow_rate = flow_rate ):
121
129
await self ._hardware_api .dispense (
122
- mount = hw_pipette .mount , volume = volume , push_out = push_out
130
+ mount = hw_pipette .mount , volume = adjusted_volume , push_out = push_out
123
131
)
124
132
125
- return volume
133
+ return adjusted_volume
126
134
127
135
async def blow_out_in_place (
128
136
self ,
@@ -194,8 +202,8 @@ async def aspirate_in_place(
194
202
) -> float :
195
203
"""Virtually aspirate (no-op)."""
196
204
self ._validate_tip_attached (pipette_id = pipette_id , command_name = "aspirate" )
197
- return self . _validate_aspirate_volume (
198
- pipette_id = pipette_id , aspirate_volume = volume
205
+ return _validate_aspirate_volume (
206
+ state_view = self . _state_view , pipette_id = pipette_id , aspirate_volume = volume
199
207
)
200
208
201
209
async def dispense_in_place (
@@ -212,8 +220,8 @@ async def dispense_in_place(
212
220
"push out value cannot have a negative value."
213
221
)
214
222
self ._validate_tip_attached (pipette_id = pipette_id , command_name = "dispense" )
215
- return self . _validate_dispense_volume (
216
- pipette_id = pipette_id , dispense_volume = volume
223
+ return _validate_dispense_volume (
224
+ state_view = self . _state_view , pipette_id = pipette_id , dispense_volume = volume
217
225
)
218
226
219
227
async def blow_out_in_place (
@@ -231,57 +239,6 @@ def _validate_tip_attached(self, pipette_id: str, command_name: str) -> None:
231
239
f"Cannot perform { command_name } without a tip attached"
232
240
)
233
241
234
- def _validate_aspirate_volume (
235
- self , pipette_id : str , aspirate_volume : float
236
- ) -> float :
237
- """Get whether the aspirated volume is valid to aspirate."""
238
- working_volume = self ._state_view .pipettes .get_working_volume (
239
- pipette_id = pipette_id
240
- )
241
-
242
- current_volume = (
243
- self ._state_view .pipettes .get_aspirated_volume (pipette_id = pipette_id ) or 0
244
- )
245
-
246
- available_volume = working_volume - current_volume
247
- available_volume_with_tolerance = (
248
- available_volume + _VOLUME_ROUNDING_ERROR_TOLERANCE
249
- )
250
-
251
- if aspirate_volume > available_volume_with_tolerance :
252
- raise InvalidAspirateVolumeError (
253
- attempted_aspirate_volume = aspirate_volume ,
254
- available_volume = available_volume ,
255
- max_pipette_volume = self ._state_view .pipettes .get_maximum_volume (
256
- pipette_id = pipette_id
257
- ),
258
- max_tip_volume = self ._get_max_tip_volume (pipette_id = pipette_id ),
259
- )
260
- else :
261
- return min (aspirate_volume , available_volume )
262
-
263
- def _validate_dispense_volume (
264
- self , pipette_id : str , dispense_volume : float
265
- ) -> float :
266
- """Validate dispense volume."""
267
- aspirated_volume = self ._state_view .pipettes .get_aspirated_volume (pipette_id )
268
- if aspirated_volume is None :
269
- raise InvalidDispenseVolumeError (
270
- "Cannot perform a dispense if there is no volume in attached tip."
271
- )
272
- else :
273
- remaining = aspirated_volume - dispense_volume
274
- if remaining < - _VOLUME_ROUNDING_ERROR_TOLERANCE :
275
- raise InvalidDispenseVolumeError (
276
- f"Cannot dispense { dispense_volume } µL when only { aspirated_volume } µL has been aspirated."
277
- )
278
- else :
279
- return min (dispense_volume , aspirated_volume )
280
-
281
- def _get_max_tip_volume (self , pipette_id : str ) -> Optional [float ]:
282
- attached_tip = self ._state_view .pipettes .get_attached_tip (pipette_id = pipette_id )
283
- return None if attached_tip is None else attached_tip .volume
284
-
285
242
286
243
def create_pipetting_handler (
287
244
state_view : StateView , hardware_api : HardwareControlAPI
@@ -292,3 +249,68 @@ def create_pipetting_handler(
292
249
if state_view .config .use_virtual_pipettes is False
293
250
else VirtualPipettingHandler (state_view = state_view )
294
251
)
252
+
253
+
254
+ def _validate_aspirate_volume (
255
+ state_view : StateView , pipette_id : str , aspirate_volume : float
256
+ ) -> float :
257
+ """Get whether the given volume is valid to aspirate right now.
258
+
259
+ Return the volume to aspirate, possibly clamped, or raise an
260
+ InvalidAspirateVolumeError.
261
+ """
262
+ working_volume = state_view .pipettes .get_working_volume (pipette_id = pipette_id )
263
+
264
+ current_volume = (
265
+ state_view .pipettes .get_aspirated_volume (pipette_id = pipette_id ) or 0
266
+ )
267
+
268
+ # TODO(mm, 2024-01-11): We should probably just use
269
+ # state_view.pipettes.get_available_volume()? Its whole `None` return vs. exception
270
+ # raising thing is confusing me.
271
+ available_volume = working_volume - current_volume
272
+ available_volume_with_tolerance = (
273
+ available_volume + _VOLUME_ROUNDING_ERROR_TOLERANCE
274
+ )
275
+
276
+ if aspirate_volume > available_volume_with_tolerance :
277
+ raise InvalidAspirateVolumeError (
278
+ attempted_aspirate_volume = aspirate_volume ,
279
+ available_volume = available_volume ,
280
+ max_pipette_volume = state_view .pipettes .get_maximum_volume (
281
+ pipette_id = pipette_id
282
+ ),
283
+ max_tip_volume = _get_max_tip_volume (
284
+ state_view = state_view , pipette_id = pipette_id
285
+ ),
286
+ )
287
+ else :
288
+ return min (aspirate_volume , available_volume )
289
+
290
+
291
+ def _validate_dispense_volume (
292
+ state_view : StateView , pipette_id : str , dispense_volume : float
293
+ ) -> float :
294
+ """Get whether the given volume is valid to dispense right now.
295
+
296
+ Return the volume to dispense, possibly clamped, or raise an
297
+ InvalidDispenseVolumeError.
298
+ """
299
+ aspirated_volume = state_view .pipettes .get_aspirated_volume (pipette_id )
300
+ if aspirated_volume is None :
301
+ raise InvalidDispenseVolumeError (
302
+ "Cannot perform a dispense if there is no volume in attached tip."
303
+ )
304
+ else :
305
+ remaining = aspirated_volume - dispense_volume
306
+ if remaining < - _VOLUME_ROUNDING_ERROR_TOLERANCE :
307
+ raise InvalidDispenseVolumeError (
308
+ f"Cannot dispense { dispense_volume } µL when only { aspirated_volume } µL has been aspirated."
309
+ )
310
+ else :
311
+ return min (dispense_volume , aspirated_volume )
312
+
313
+
314
+ def _get_max_tip_volume (state_view : StateView , pipette_id : str ) -> Optional [float ]:
315
+ attached_tip = state_view .pipettes .get_attached_tip (pipette_id = pipette_id )
316
+ return None if attached_tip is None else attached_tip .volume
0 commit comments