Skip to content

Commit f6cf990

Browse files
authored
uMp-3 support (#389)
1 parent b6a2d49 commit f6cf990

File tree

3 files changed

+141
-2
lines changed

3 files changed

+141
-2
lines changed

src/ephys_link/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.0.0b5"
1+
__version__ = "2.0.0b6"
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""Bindings for Sensapex uMp-3 platform.
2+
3+
Usage: Instantiate Ump4Bindings to interact with the Sensapex uMp-4 platform.
4+
"""
5+
6+
from asyncio import get_running_loop
7+
8+
from sensapex import UMP, SensapexDevice
9+
from vbl_aquarium.models.unity import Vector3, Vector4
10+
11+
from ephys_link.util.base_bindings import BaseBindings
12+
from ephys_link.util.common import (
13+
RESOURCES_PATH,
14+
array_to_vector4,
15+
scalar_mm_to_um,
16+
um_to_mm,
17+
vector4_to_array,
18+
vector_mm_to_um,
19+
)
20+
21+
22+
class Ump3Bindings(BaseBindings):
23+
"""Bindings for UMP-3 platform"""
24+
25+
def __init__(self) -> None:
26+
"""Initialize UMP-3 bindings."""
27+
28+
# Establish connection to Sensapex API (exit if connection fails).
29+
UMP.set_library_path(RESOURCES_PATH)
30+
self._ump = UMP.get_ump()
31+
if self._ump is None:
32+
error_message = "Unable to connect to uMp"
33+
raise ValueError(error_message)
34+
35+
async def get_manipulators(self) -> list[str]:
36+
return list(map(str, self._ump.list_devices()))
37+
38+
async def get_axes_count(self) -> int:
39+
return 3
40+
41+
def get_dimensions(self) -> Vector4:
42+
return Vector4(x=20, y=20, z=20, w=20)
43+
44+
async def get_position(self, manipulator_id: str) -> Vector4:
45+
manipulator_position = self._get_device(manipulator_id).get_pos(1)
46+
47+
# Add the depth axis to the end of the position.
48+
manipulator_position.append(manipulator_position[0])
49+
50+
# Convert and return.
51+
return um_to_mm(array_to_vector4(manipulator_position))
52+
53+
# noinspection PyTypeChecker
54+
async def get_angles(self, _: str) -> Vector3:
55+
"""uMp-3 does not support getting angles so raise an error.
56+
57+
:raises: AttributeError
58+
"""
59+
error_message = "UMP-3 does not support getting angles"
60+
raise AttributeError(error_message)
61+
62+
# noinspection PyTypeChecker
63+
async def get_shank_count(self, _: str) -> int:
64+
"""uMp-3 does not support getting shank count so raise an error.
65+
66+
:raises: AttributeError
67+
"""
68+
error_message = "UMP-3 does not support getting shank count"
69+
raise AttributeError(error_message)
70+
71+
def get_movement_tolerance(self) -> float:
72+
return 0.001
73+
74+
# noinspection DuplicatedCode
75+
async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
76+
# Convert position to micrometers.
77+
target_position_um = vector_mm_to_um(position)
78+
79+
# Request movement.
80+
movement = self._get_device(manipulator_id).goto_pos(
81+
vector4_to_array(target_position_um), scalar_mm_to_um(speed)
82+
)
83+
84+
# Wait for movement to complete.
85+
await get_running_loop().run_in_executor(None, movement.finished_event.wait, None)
86+
87+
# Handle interrupted movement.
88+
if movement.interrupted:
89+
error_message = f"Manipulator {manipulator_id} interrupted: {movement.interrupt_reason}"
90+
raise RuntimeError(error_message)
91+
92+
return um_to_mm(array_to_vector4(movement.last_pos))
93+
94+
async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
95+
# Augment current position with depth.
96+
current_position = await self.get_position(manipulator_id)
97+
new_platform_position = current_position.model_copy(update={"x": depth})
98+
99+
# Make the movement.
100+
final_platform_position = await self.set_position(manipulator_id, new_platform_position, speed)
101+
102+
# Return the final depth.
103+
return float(final_platform_position.w)
104+
105+
async def stop(self, manipulator_id: str) -> None:
106+
self._get_device(manipulator_id).stop()
107+
108+
def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
109+
# unified <- platform
110+
# +x <- +y
111+
# +y <- -x
112+
# +z <- -z
113+
# +d <- +d/x
114+
115+
return Vector4(
116+
x=platform_space.y,
117+
y=self.get_dimensions().x - platform_space.x,
118+
z=self.get_dimensions().z - platform_space.z,
119+
w=platform_space.w,
120+
)
121+
122+
def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
123+
# platform <- unified
124+
# +x <- -y
125+
# +y <- +x
126+
# +z <- -z
127+
# +d/x <- +d
128+
129+
return Vector4(
130+
x=self.get_dimensions().y - unified_space.y,
131+
y=unified_space.x,
132+
z=self.get_dimensions().z - unified_space.z,
133+
w=unified_space.w,
134+
)
135+
136+
# Helper methods.
137+
def _get_device(self, manipulator_id: str) -> SensapexDevice:
138+
return self._ump.get_device(int(manipulator_id))

src/ephys_link/bindings/ump_4_bindings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ async def get_shank_count(self, _: str) -> int:
6565
def get_movement_tolerance(self) -> float:
6666
return 0.001
6767

68+
# noinspection DuplicatedCode
6869
async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
6970
# Convert position to micrometers.
7071
target_position_um = vector_mm_to_um(position)
@@ -75,7 +76,7 @@ async def set_position(self, manipulator_id: str, position: Vector4, speed: floa
7576
)
7677

7778
# Wait for movement to finish.
78-
await get_running_loop().run_in_executor(None, movement.finished_event.wait)
79+
await get_running_loop().run_in_executor(None, movement.finished_event.wait, None)
7980

8081
# Handle interrupted movement.
8182
if movement.interrupted:

0 commit comments

Comments
 (0)