Skip to content

Commit 1ccf581

Browse files
authored
clearly differentiate between local and absolute size (#235)
1 parent 4cee056 commit 1ccf581

21 files changed

+182
-172
lines changed

Diff for: CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
3232
- `material_z_thickness` of a `Container` is used in computing its bottom (https://github.com/PyLabRobot/pylabrobot/pull/205/)
3333
- Default `pickup_distance_from_top` in `LiquidHandler.{move_plate,move_lid}` were lowered by 3.33 (https://github.com/PyLabRobot/pylabrobot/pull/205/)
3434
- `PlateCarrierSite` can now take `ResourceStack` as a child, as long as the children are `Plate`s (https://github.com/PyLabRobot/pylabrobot/pull/226)
35+
- `Resource.get_size_{x,y,z}` now return the size of the resource in local space, not absolute space (https://github.com/PyLabRobot/pylabrobot/pull/235)
36+
- `Resource.center` now returns the center of the resource in local space, not absolute space (https://github.com/PyLabRobot/pylabrobot/pull/235)
3537

3638
### Added
3739

@@ -57,6 +59,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5759
- `size_z` and `nesting_z_height` for `Cor_96_wellplate_360ul_Fb_Lid` (https://github.com/PyLabRobot/pylabrobot/pull/226)
5860
- `NestedTipRack` (https://github.com/PyLabRobot/pylabrobot/pull/228)
5961
- `HTF_L_ULTRAWIDE`, `ultrawide_high_volume_tip_with_filter` (https://github.com/PyLabRobot/pylabrobot/pull/229/)
62+
- `get_absolute_size_x`, `get_absolute_size_y`, `get_absolute_size_z` for `Resource` (https://github.com/PyLabRobot/pylabrobot/pull/235)
6063

6164
### Deprecated
6265

Diff for: pylabrobot/liquid_handling/backends/hamilton/STAR.py

+35-24
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import re
1212
from typing import Callable, Dict, List, Literal, Optional, Sequence, Type, TypeVar, Union, cast
1313

14+
from pylabrobot import audio
1415
from pylabrobot.liquid_handling.backends.hamilton.base import HamiltonLiquidHandler
1516
from pylabrobot.liquid_handling.errors import ChannelizedError
1617
from pylabrobot.liquid_handling.liquid_classes.hamilton import (
@@ -39,7 +40,8 @@
3940
from pylabrobot.resources.hamilton.hamilton_decks import STAR_SIZE_X, STARLET_SIZE_X
4041
from pylabrobot.resources.liquid import Liquid
4142
from pylabrobot.resources.ml_star import HamiltonTip, TipDropMethod, TipPickupMethod, TipSize
42-
from pylabrobot import audio
43+
from pylabrobot.resources.utils import get_child_location
44+
from pylabrobot.utils.linalg import matrix_vector_multiply_3x3
4345

4446
T = TypeVar("T")
4547

@@ -1583,7 +1585,7 @@ async def aspirate(
15831585
for wb, op in zip(well_bottoms, ops)]
15841586
if lld_search_height is None:
15851587
lld_search_height = [
1586-
(wb + op.resource.get_size_z() + (2.7 if isinstance(op.resource, Well) else 5)) # ?
1588+
(wb + op.resource.get_absolute_size_z() + (2.7 if isinstance(op.resource, Well) else 5)) # ?
15871589
for wb, op in zip(well_bottoms, ops)
15881590
]
15891591
else:
@@ -1845,7 +1847,7 @@ async def dispense(
18451847
[ls + (op.liquid_height or 0) for ls, op in zip(well_bottoms, ops)]
18461848
if lld_search_height is None:
18471849
lld_search_height = [
1848-
(wb + op.resource.get_size_z() + (2.7 if isinstance(op.resource, Well) else 5)) #?
1850+
(wb + op.resource.get_absolute_size_z() + (2.7 if isinstance(op.resource, Well) else 5)) #?
18491851
for wb, op in zip(well_bottoms, ops)
18501852
]
18511853
else:
@@ -2378,11 +2380,11 @@ async def iswap_pick_up_resource(
23782380

23792381
# Get center of source plate. Also gripping height and plate width.
23802382
center = resource.get_absolute_location(x="c", y="c", z="b") + offset
2381-
grip_height = center.z + resource.get_size_z() - pickup_distance_from_top
2383+
grip_height = center.z + resource.get_absolute_size_z() - pickup_distance_from_top
23822384
if grip_direction in (GripDirection.FRONT, GripDirection.BACK):
2383-
plate_width = resource.get_size_x()
2385+
plate_width = resource.get_absolute_size_x()
23842386
elif grip_direction in (GripDirection.RIGHT, GripDirection.LEFT):
2385-
plate_width = resource.get_size_y()
2387+
plate_width = resource.get_absolute_size_y()
23862388
else:
23872389
raise ValueError("Invalid grip direction")
23882390

@@ -2435,7 +2437,7 @@ async def iswap_move_picked_up_resource(
24352437
x_direction=0,
24362438
y_position=round(center.y * 10),
24372439
y_direction=0,
2438-
z_position=round((location.z + resource.get_size_z() / 2) * 10),
2440+
z_position=round((location.z + resource.get_absolute_size_z() / 2) * 10),
24392441
z_direction=0,
24402442
grip_direction={
24412443
GripDirection.FRONT: 1,
@@ -2476,15 +2478,24 @@ async def iswap_release_picked_up_resource(
24762478

24772479
assert self.iswap_installed, "iswap must be installed"
24782480

2479-
# Get center of source plate. Also gripping height and plate width.
2480-
center = location + resource.rotated(z=rotation).center() + offset
2481-
grip_height = center.z + resource.get_size_z() - pickup_distance_from_top
2481+
# Get center of source plate in absolute space.
2482+
# The computation of the center has to be rotated so that the offset is in absolute space.
2483+
center_in_absolute_space = Coordinate(*matrix_vector_multiply_3x3(
2484+
resource.rotated(z=rotation).get_absolute_rotation().get_rotation_matrix(),
2485+
resource.center().vector()
2486+
))
2487+
# This is when the resource is rotated (around its origin), but we also need to translate
2488+
# so that the left front bottom corner of the plate is lfb in absolute space, not local.
2489+
center_in_absolute_space += get_child_location(resource.rotated(z=rotation))
2490+
2491+
center = location + center_in_absolute_space + offset
2492+
grip_height = center.z + resource.get_absolute_size_z() - pickup_distance_from_top
24822493
# grip_direction here is the put_direction. We use `rotation` to cancel it out and get the
24832494
# original grip direction. Hack.
24842495
if grip_direction in (GripDirection.FRONT, GripDirection.BACK):
2485-
plate_width = resource.rotated(z=rotation).get_size_x()
2496+
plate_width = resource.rotated(z=rotation).get_absolute_size_x()
24862497
elif grip_direction in (GripDirection.RIGHT, GripDirection.LEFT):
2487-
plate_width = resource.rotated(z=rotation).get_size_y()
2498+
plate_width = resource.rotated(z=rotation).get_absolute_size_y()
24882499
else:
24892500
raise ValueError("Invalid grip direction")
24902501

@@ -2540,8 +2551,8 @@ async def core_pick_up_resource(
25402551

25412552
# Get center of source plate. Also gripping height and plate width.
25422553
center = resource.get_absolute_location(x="c", y="c", z="b") + offset
2543-
grip_height = center.z + resource.get_size_z() - pickup_distance_from_top
2544-
grip_width = resource.get_size_y() #grip width is y size of resource
2554+
grip_height = center.z + resource.get_absolute_size_z() - pickup_distance_from_top
2555+
grip_width = resource.get_absolute_size_y() #grip width is y size of resource
25452556

25462557
if self.core_parked:
25472558
await self.get_core(p1=channel_1, p2=channel_2)
@@ -2625,8 +2636,8 @@ async def core_release_picked_up_resource(
26252636

26262637
# Get center of destination location. Also gripping height and plate width.
26272638
center = location + resource.center() + offset
2628-
grip_height = center.z + resource.get_size_z() - pickup_distance_from_top
2629-
grip_width = resource.get_size_y()
2639+
grip_height = center.z + resource.get_absolute_size_z() - pickup_distance_from_top
2640+
grip_width = resource.get_absolute_size_y()
26302641

26312642
await self.core_put_plate(
26322643
x_position=round(center.x * 10),
@@ -2691,7 +2702,7 @@ async def move_resource(
26912702

26922703
previous_location = move.resource.get_absolute_location() + move.resource_offset
26932704
minimum_traverse_height = 284.0
2694-
previous_location.z = minimum_traverse_height - move.resource.get_size_z() / 2
2705+
previous_location.z = minimum_traverse_height - move.resource.get_absolute_size_z() / 2
26952706

26962707
for location in move.intermediate_locations:
26972708
if use_arm == "iswap":
@@ -2793,9 +2804,9 @@ async def core_check_resource_exists_at_location_center(
27932804
"""
27942805

27952806
center = location + resource.centers()[0] + offset
2796-
y_width_to_gripper_bump = resource.get_size_y() - gripper_y_margin*2
2797-
assert 9 <= y_width_to_gripper_bump <= round(resource.get_size_y()), \
2798-
f"width between channels must be between 9 and {resource.get_size_y()} mm" \
2807+
y_width_to_gripper_bump = resource.get_absolute_size_y() - gripper_y_margin*2
2808+
assert 9 <= y_width_to_gripper_bump <= round(resource.get_absolute_size_y()), \
2809+
f"width between channels must be between 9 and {resource.get_absolute_size_y()} mm" \
27992810
" (i.e. the minimal distance between channels and the max y size of the resource"
28002811

28012812
# Check if CoRe gripper currently in use
@@ -4325,7 +4336,7 @@ async def get_core(self, p1: int, p2: int):
43254336
# This appears to be deck.get_size_x() - 562.5, but let's keep an explicit check so that we
43264337
# can catch unknown deck sizes. Can the grippers exist at another location? If so, define it as
43274338
# a resource on the robot deck and use deck.get_resource().get_absolute_location().
4328-
deck_size = self.deck.get_size_x()
4339+
deck_size = self.deck.get_absolute_size_x()
43294340
if deck_size == STARLET_SIZE_X:
43304341
xs = 7975 # 1360-797.5 = 562.5
43314342
elif deck_size == STAR_SIZE_X:
@@ -4354,7 +4365,7 @@ async def get_core(self, p1: int, p2: int):
43544365
async def put_core(self):
43554366
""" Put CoRe gripper tool at wasteblock mount. """
43564367
assert self.deck is not None, "must have deck defined to access CoRe grippers"
4357-
deck_size = self.deck.get_size_x()
4368+
deck_size = self.deck.get_absolute_size_x()
43584369
if deck_size == STARLET_SIZE_X:
43594370
xs = 7975
43604371
elif deck_size == STAR_SIZE_X:
@@ -5591,7 +5602,7 @@ async def unload_carrier(self, carrier: Carrier):
55915602
""" Use autoload to unload carrier. """
55925603
# Identify carrier end rail
55935604
track_width = 22.5
5594-
carrier_width = carrier.get_absolute_location().x - 100 + carrier.get_size_x()
5605+
carrier_width = carrier.get_absolute_location().x - 100 + carrier.get_absolute_size_x()
55955606
carrier_end_rail = int(carrier_width / track_width)
55965607
assert 1 <= carrier_end_rail <= 54, "carrier loading rail must be between 1 and 54"
55975608

@@ -5655,7 +5666,7 @@ async def load_carrier(
56555666
}
56565667
# Identify carrier end rail
56575668
track_width = 22.5
5658-
carrier_width = carrier.get_absolute_location().x - 100 + carrier.get_size_x()
5669+
carrier_width = carrier.get_absolute_location().x - 100 + carrier.get_absolute_size_x()
56595670
carrier_end_rail = int(carrier_width / track_width)
56605671
assert 1 <= carrier_end_rail <= 54, "carrier loading rail must be between 1 and 54"
56615672

Diff for: pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -637,8 +637,8 @@ async def test_iswap_plate_reader(self):
637637
"xs#####xd#yj####yd#zj####zd#th####te####go####gr#ga#")
638638

639639
assert self.plate.rotation.z == 270
640-
self.assertAlmostEqual(self.plate.get_size_x(), 85.48, places=2)
641-
self.assertAlmostEqual(self.plate.get_size_y(), 127.76, places=2)
640+
self.assertAlmostEqual(self.plate.get_absolute_size_x(), 85.48, places=2)
641+
self.assertAlmostEqual(self.plate.get_absolute_size_y(), 127.76, places=2)
642642

643643
await self.lh.move_plate(plate_reader.get_plate(), self.plt_car[0],
644644
pickup_distance_from_top=8.2-3.33, get_direction=GripDirection.LEFT,

Diff for: pylabrobot/liquid_handling/backends/hamilton/vantage.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ async def aspirate(
628628
liquid_surfaces_no_lld = liquid_surface_at_function_without_lld or [wb + (op.liquid_height or 0)
629629
for wb, op in zip(well_bottoms, ops)]
630630
# -1 compared to STAR?
631-
lld_search_heights = lld_search_height or [wb + op.resource.get_size_z() + \
631+
lld_search_heights = lld_search_height or [wb + op.resource.get_absolute_size_z() + \
632632
(2.7-1 if isinstance(op.resource, Well) else 5) #?
633633
for wb, op in zip(well_bottoms, ops)]
634634

@@ -793,7 +793,7 @@ async def dispense(
793793
liquid_surfaces_no_lld = [wb + (op.liquid_height or 0)
794794
for wb, op in zip(well_bottoms, ops)]
795795
# -1 compared to STAR?
796-
lld_search_heights = lld_search_height or [wb + op.resource.get_size_z() + \
796+
lld_search_heights = lld_search_height or [wb + op.resource.get_absolute_size_z() + \
797797
(2.7-1 if isinstance(op.resource, Well) else 5) #?
798798
for wb, op in zip(well_bottoms, ops)]
799799

@@ -964,12 +964,12 @@ async def aspirate96(
964964
aspiration.offset + Coordinate(z=top_left_well.material_z_thickness)
965965
# -1 compared to STAR?
966966
well_bottoms = position.z
967-
lld_search_height = well_bottoms + top_left_well.get_size_z() + 2.7-1
967+
lld_search_height = well_bottoms + top_left_well.get_absolute_size_z() + 2.7-1
968968
else:
969969
position = aspiration.container.get_absolute_location(y="b") + aspiration.offset + \
970970
Coordinate(z=aspiration.container.material_z_thickness)
971971
bottom = position.z
972-
lld_search_height = bottom + aspiration.container.get_size_z() + 2.7-1
972+
lld_search_height = bottom + aspiration.container.get_absolute_size_z() + 2.7-1
973973

974974
liquid_height = position.z + (aspiration.liquid_height or 0)
975975

@@ -1095,12 +1095,12 @@ async def dispense96(
10951095
dispense.offset + Coordinate(z=top_left_well.material_z_thickness)
10961096
# -1 compared to STAR?
10971097
well_bottoms = position.z
1098-
lld_search_height = well_bottoms + top_left_well.get_size_z() + 2.7-1
1098+
lld_search_height = well_bottoms + top_left_well.get_absolute_size_z() + 2.7-1
10991099
else:
11001100
position = dispense.container.get_absolute_location(y="b") + dispense.offset + \
11011101
Coordinate(z=dispense.container.material_z_thickness)
11021102
bottom = position.z
1103-
lld_search_height = bottom + dispense.container.get_size_z() + 2.7-1
1103+
lld_search_height = bottom + dispense.container.get_absolute_size_z() + 2.7-1
11041104

11051105
liquid_height = position.z + (dispense.liquid_height or 0) + 10
11061106

@@ -1202,8 +1202,8 @@ async def pick_up_resource(
12021202
allows you to pick up and move a resource with a single command. """
12031203

12041204
center = resource.get_absolute_location() + resource.center() + offset
1205-
grip_height = center.z + resource.get_size_z() - pickup_distance_from_top
1206-
plate_width = resource.get_size_x()
1205+
grip_height = center.z + resource.get_absolute_size_z() - pickup_distance_from_top
1206+
plate_width = resource.get_absolute_size_x()
12071207

12081208
await self.ipg_grip_plate(
12091209
x_position=round(center.x * 10),
@@ -1247,8 +1247,8 @@ async def release_picked_up_resource(
12471247
"""
12481248

12491249
center = destination + resource.center() + offset
1250-
grip_height = center.z + resource.get_size_z() - pickup_distance_from_top
1251-
plate_width = resource.get_size_x()
1250+
grip_height = center.z + resource.get_absolute_size_z() - pickup_distance_from_top
1251+
plate_width = resource.get_absolute_size_x()
12521252

12531253
await self.ipg_put_plate(
12541254
x_position=round(center.x * 10),

Diff for: pylabrobot/liquid_handling/backends/opentrons_backend.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -191,14 +191,14 @@ def _get_volume(well: Resource) -> float:
191191

192192
well_definitions = {
193193
child.name: {
194-
"depth": child.get_size_z(),
195-
"x": cast(Coordinate, child.location).x + child.get_size_x() / 2,
196-
"y": cast(Coordinate, child.location).y + child.get_size_y() / 2,
194+
"depth": child.get_absolute_size_z(),
195+
"x": cast(Coordinate, child.location).x + child.get_absolute_size_x() / 2,
196+
"y": cast(Coordinate, child.location).y + child.get_absolute_size_y() / 2,
197197
"z": cast(Coordinate, child.location).z,
198198
"shape": "circular",
199199

200200
# inscribed circle has diameter equal to the width of the well
201-
"diameter": child.get_size_x(),
201+
"diameter": child.get_absolute_size_x(),
202202

203203
# Opentrons requires `totalLiquidVolume`, even for tip racks!
204204
"totalLiquidVolume": _get_volume(child),
@@ -247,9 +247,9 @@ def _get_volume(well: Resource) -> float:
247247
"z": 0
248248
},
249249
"dimensions":{
250-
"xDimension": resource.get_size_x(),
251-
"yDimension": resource.get_size_y(),
252-
"zDimension": resource.get_size_z(),
250+
"xDimension": resource.get_absolute_size_x(),
251+
"yDimension": resource.get_absolute_size_y(),
252+
"zDimension": resource.get_absolute_size_z(),
253253
},
254254
"wells": well_definitions,
255255
"groups": [

Diff for: pylabrobot/liquid_handling/backends/tecan/EVO.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ async def aspirate(
320320
for op, tlc in zip(ops, tecan_liquid_classes):
321321
op.volume = tlc.compute_corrected_volume(op.volume) if tlc is not None else op.volume
322322

323-
ys = int(ops[0].resource.get_size_y() * 10)
323+
ys = int(ops[0].resource.get_absolute_size_y() * 10)
324324
zadd: List[Optional[int]] = [0] * self.num_channels
325325
for i, channel in enumerate(use_channels):
326326
par = ops[i].resource.parent
@@ -397,7 +397,7 @@ async def dispense(
397397
"""
398398

399399
x_positions, y_positions, z_positions = self._liha_positions(ops, use_channels)
400-
ys = int(ops[0].resource.get_size_y() * 10)
400+
ys = int(ops[0].resource.get_absolute_size_y() * 10)
401401

402402
tecan_liquid_classes = [
403403
get_liquid_class(
@@ -443,7 +443,7 @@ async def pick_up_tips(
443443
x_positions, y_positions, _ = self._liha_positions(ops, use_channels)
444444

445445
# move channels
446-
ys = int(ops[0].resource.get_size_y() * 10)
446+
ys = int(ops[0].resource.get_absolute_size_y() * 10)
447447
x, _ = self._first_valid(x_positions)
448448
y, yi = self._first_valid(y_positions)
449449
assert x is not None and y is not None
@@ -517,7 +517,7 @@ async def move_resource(self, move: Move):
517517

518518
z_range = await self.roma.report_z_param(5)
519519
x, y, z = self._roma_positions(move.resource, move.resource.get_absolute_location(), z_range)
520-
h = int(move.resource.get_size_y() * 10)
520+
h = int(move.resource.get_absolute_size_y() * 10)
521521
xt, yt, zt = self._roma_positions(move.resource, move.destination, z_range)
522522

523523
# move to resource
@@ -778,7 +778,7 @@ def _roma_positions(
778778
or par.roma_z_travel is None or par.roma_z_end is None:
779779
raise ValueError(f"Operation is not supported by resource {par}.")
780780
x_position = int((offset.x - 100)* 10 + par.roma_x)
781-
y_position = int((347.1 - (offset.y + resource.get_size_y())) * 10 + par.roma_y)
781+
y_position = int((347.1 - (offset.y + resource.get_absolute_size_y())) * 10 + par.roma_y)
782782
z_positions = {
783783
"safe": z_range - int(par.roma_z_safe),
784784
"travel": z_range - int(par.roma_z_travel - offset.z * 10),

Diff for: pylabrobot/liquid_handling/liquid_handler.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1378,7 +1378,8 @@ async def aspirate96(
13781378
blow_out_air_volume = float(blow_out_air_volume) if blow_out_air_volume is not None else None
13791379

13801380
if isinstance(resource, Container):
1381-
if resource.get_size_x() < 108.0 or resource.get_size_y() < 70.0: # TODO: analyze as attr
1381+
if resource.get_absolute_size_x() < 108.0 or \
1382+
resource.get_absolute_size_y() < 70.0: # TODO: analyze as attr
13821383
raise ValueError("Container too small to accommodate 96 head")
13831384

13841385
for channel in self.head96.values():
@@ -1519,7 +1520,8 @@ async def dispense96(
15191520
blow_out_air_volume = float(blow_out_air_volume) if blow_out_air_volume is not None else None
15201521

15211522
if isinstance(resource, Container):
1522-
if resource.get_size_x() < 108.0 or resource.get_size_y() < 70.0: # TODO: analyze as attr
1523+
if resource.get_absolute_size_x() < 108.0 or \
1524+
resource.get_absolute_size_y() < 70.0: # TODO: analyze as attr
15231525
raise ValueError("Container too small to accommodate 96 head")
15241526

15251527
for channel in self.head96.values():
@@ -1753,7 +1755,7 @@ async def move_lid(
17531755
to_location = Coordinate(
17541756
x=to_location.x,
17551757
y=to_location.y,
1756-
z=to_location.z + to.get_size_z() - lid.nesting_z_height)
1758+
z=to_location.z + to.get_absolute_size_z() - lid.nesting_z_height)
17571759
elif isinstance(to, ResourceStack):
17581760
assert to.direction == "z", "Only ResourceStacks with direction 'z' are currently supported"
17591761
to_location = to.get_absolute_location(z="top")

0 commit comments

Comments
 (0)