ephys-link 2.0.2__py3-none-any.whl → 2.1.0b0__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.
ephys_link/__about__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2.0.2"
1
+ __version__ = "2.1.0b0"
@@ -70,10 +70,22 @@ class PlatformHandler:
70
70
  Returns:
71
71
  Bindings for the specified platform type.
72
72
  """
73
+
74
+ # What the user supplied.
75
+ selected_type = options.type
76
+
73
77
  for binding_type in get_bindings():
74
78
  binding_cli_name = binding_type.get_cli_name()
75
79
 
76
- if binding_cli_name == options.type:
80
+ # Notify deprecation of "ump-4" and "ump-3" CLI options and fix.
81
+ if selected_type in ("ump-4", "ump-3"):
82
+ self._console.error_print(
83
+ "DEPRECATION",
84
+ f"CLI option '{selected_type}' is deprecated and will be removed in v3.0.0. Use 'ump' instead.",
85
+ )
86
+ selected_type = "ump"
87
+
88
+ if binding_cli_name == selected_type:
77
89
  # Pass in HTTP port for Pathfinder MPM.
78
90
  if binding_cli_name == "pathfinder-mpm":
79
91
  return MPMBinding(options.mpm_port)
@@ -0,0 +1,239 @@
1
+ """Bindings for Sensapex uMp platform.
2
+
3
+ Usage: Instantiate UmpBindings to interact with Sensapex uMp-4 and uMp-3 manipulators.
4
+ """
5
+
6
+ from asyncio import get_running_loop
7
+ from typing import NoReturn, final, override
8
+
9
+ from sensapex import UMP, SensapexDevice # pyright: ignore [reportMissingTypeStubs]
10
+ from vbl_aquarium.models.unity import Vector4
11
+
12
+ from ephys_link.utils.base_binding import BaseBinding
13
+ from ephys_link.utils.constants import RESOURCES_DIRECTORY
14
+ from ephys_link.utils.converters import (
15
+ list_to_vector4,
16
+ scalar_mm_to_um,
17
+ um_to_mm,
18
+ vector4_to_array,
19
+ vector_mm_to_um,
20
+ )
21
+
22
+
23
+ @final
24
+ class UmpBinding(BaseBinding):
25
+ """Bindings for uMp platform"""
26
+
27
+ # Number of axes for uMp-3.
28
+ UMP_3_NUM_AXES = 3
29
+
30
+ def __init__(self) -> None:
31
+ """Initialize uMp bindings."""
32
+
33
+ # Establish connection to Sensapex API (exit if connection fails).
34
+ UMP.set_library_path(RESOURCES_DIRECTORY)
35
+ self._ump: UMP = UMP.get_ump() # pyright: ignore [reportUnknownMemberType]
36
+
37
+ # Exit if no manipulators are connected.
38
+ device_ids: list[str] = list(map(str, self._ump.list_devices()))
39
+ if len(device_ids) == 0:
40
+ msg = "No manipulators connected."
41
+ raise RuntimeError(msg)
42
+
43
+ # Currently only supports using uMp-4 XOR uMp-3. Exit if both are connected.
44
+
45
+ # Use the first device as the reference for the number of axes.
46
+ self.num_axes: int = self._get_device(device_ids[0]).n_axes()
47
+
48
+ if any(self._get_device(device_id).n_axes() != self.num_axes for device_id in device_ids): # pyright: ignore [reportUnknownArgumentType, reportUnknownMemberType]
49
+ msg = "uMp-4 and uMp-3 cannot be used at the same time."
50
+ raise RuntimeError(msg)
51
+
52
+ @staticmethod
53
+ @override
54
+ def get_display_name() -> str:
55
+ return "Sensapex uMp"
56
+
57
+ @staticmethod
58
+ @override
59
+ def get_cli_name() -> str:
60
+ return "ump"
61
+
62
+ @override
63
+ async def get_manipulators(self) -> list[str]:
64
+ return list(map(str, self._ump.list_devices()))
65
+
66
+ @override
67
+ async def get_axes_count(self) -> int:
68
+ return self.num_axes
69
+
70
+ @override
71
+ def get_dimensions(self) -> Vector4:
72
+ return Vector4(x=20, y=20, z=20, w=20)
73
+
74
+ @override
75
+ async def get_position(self, manipulator_id: str) -> Vector4:
76
+ # Get the position list from the device.
77
+ position = self._get_device(manipulator_id).get_pos(1) # pyright: ignore [reportUnknownMemberType]
78
+
79
+ # Copy x-coordinate into depth for uMp-3.
80
+ return um_to_mm(list_to_vector4([*position, position[0]] if self._is_ump_3() else position))
81
+
82
+ @override
83
+ async def get_angles(self, manipulator_id: str) -> NoReturn:
84
+ """uMp does not support getting angles so raise an error.
85
+
86
+ Raises:
87
+ AttributeError: uMp does not support getting angles.
88
+ """
89
+ error_message = "uMp does not support getting angles"
90
+ raise AttributeError(error_message)
91
+
92
+ @override
93
+ async def get_shank_count(self, manipulator_id: str) -> NoReturn:
94
+ """uMp does not support getting shank count so raise an error.
95
+
96
+ Raises:
97
+ AttributeError: uMp does not support getting shank count.
98
+ """
99
+ error_message = "uMp does not support getting shank count"
100
+ raise AttributeError(error_message)
101
+
102
+ @staticmethod
103
+ @override
104
+ def get_movement_tolerance() -> float:
105
+ return 0.001
106
+
107
+ @override
108
+ async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
109
+ # Convert position to micrometers array.
110
+ target_position_um = vector4_to_array(vector_mm_to_um(position))
111
+
112
+ # Request movement (clip 4th axis for uMp-3).
113
+ movement = self._get_device(
114
+ manipulator_id
115
+ ).goto_pos( # pyright: ignore [reportUnknownMemberType]
116
+ target_position_um[: self.UMP_3_NUM_AXES] if self._is_ump_3() else target_position_um,
117
+ scalar_mm_to_um(speed),
118
+ )
119
+
120
+ # Wait for movement to finish.
121
+ _ = await get_running_loop().run_in_executor(None, movement.finished_event.wait, None)
122
+
123
+ # Handle interrupted movement.
124
+ if movement.interrupted:
125
+ error_message = f"Manipulator {manipulator_id} interrupted: {movement.interrupt_reason}" # pyright: ignore [reportUnknownMemberType]
126
+ raise RuntimeError(error_message)
127
+
128
+ # Handle empty end position.
129
+ if movement.last_pos is None or len(movement.last_pos) == 0: # pyright: ignore [reportUnknownMemberType, reportUnknownArgumentType]
130
+ error_message = f"Manipulator {manipulator_id} did not reach target position"
131
+ raise RuntimeError(error_message)
132
+
133
+ return um_to_mm(
134
+ list_to_vector4([*movement.last_pos, movement.last_pos[0]] if self._is_ump_3() else list(movement.last_pos)) # pyright: ignore [reportUnknownArgumentType, reportUnknownMemberType]
135
+ )
136
+
137
+ @override
138
+ async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
139
+ # Augment current position with depth.
140
+ current_position = await self.get_position(manipulator_id)
141
+ new_platform_position = current_position.model_copy(update={"x" if self._is_ump_3() else "w": depth})
142
+
143
+ # Make the movement.
144
+ final_platform_position = await self.set_position(manipulator_id, new_platform_position, speed)
145
+
146
+ # Return the final depth.
147
+ return float(final_platform_position.w)
148
+
149
+ @override
150
+ async def stop(self, manipulator_id: str) -> None:
151
+ self._get_device(manipulator_id).stop()
152
+
153
+ @override
154
+ def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
155
+ """
156
+ For uMp-3:
157
+ unified <- platform
158
+ +x <- +y
159
+ +y <- -x
160
+ +z <- -z
161
+ +d <- +d
162
+
163
+ For uMp-4:
164
+ unified <- platform
165
+ +x <- +y
166
+ +y <- -z
167
+ +z <- +x
168
+ +d <- +d
169
+ """
170
+
171
+ return (
172
+ Vector4(
173
+ x=platform_space.y,
174
+ y=self.get_dimensions().x - platform_space.x,
175
+ z=self.get_dimensions().z - platform_space.z,
176
+ w=platform_space.w,
177
+ )
178
+ if self._is_ump_3()
179
+ else Vector4(
180
+ x=platform_space.y,
181
+ y=self.get_dimensions().z - platform_space.z,
182
+ z=platform_space.x,
183
+ w=platform_space.w,
184
+ )
185
+ )
186
+
187
+ @override
188
+ def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
189
+ """
190
+ For uMp-3:
191
+ platform <- unified
192
+ +x <- -y
193
+ +y <- +x
194
+ +z <- -z
195
+ +d <- +d
196
+
197
+ For uMp-4:
198
+ platform <- unified
199
+ +x <- +z
200
+ +y <- +x
201
+ +z <- -y
202
+ +d <- +d
203
+ """
204
+
205
+ return (
206
+ Vector4(
207
+ x=self.get_dimensions().y - unified_space.y,
208
+ y=unified_space.x,
209
+ z=self.get_dimensions().z - unified_space.z,
210
+ w=unified_space.w,
211
+ )
212
+ if self._is_ump_3()
213
+ else Vector4(
214
+ x=unified_space.z,
215
+ y=unified_space.x,
216
+ z=self.get_dimensions().y - unified_space.y,
217
+ w=unified_space.w,
218
+ )
219
+ )
220
+
221
+ # Helper methods.
222
+ def _get_device(self, manipulator_id: str) -> SensapexDevice:
223
+ """Returns the Sensapex device object for the given manipulator ID.
224
+
225
+ Args:
226
+ manipulator_id: Manipulator ID.
227
+ Returns:
228
+ Sensapex device object.
229
+ """
230
+
231
+ return self._ump.get_device(int(manipulator_id)) # pyright: ignore [reportUnknownMemberType]
232
+
233
+ def _is_ump_3(self) -> bool:
234
+ """Check if the current device is uMp-3.
235
+
236
+ Returns:
237
+ True if the device is uMp-3, False otherwise.
238
+ """
239
+ return self.num_axes == self.UMP_3_NUM_AXES
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ephys-link
3
- Version: 2.0.2
3
+ Version: 2.1.0b0
4
4
  Summary: A Python Socket.IO server that allows any Socket.IO-compliant application to communicate with manipulators used in electrophysiology experiments.
5
5
  Project-URL: Documentation, https://virtualbrainlab.org/ephys_link/installation_and_use.html
6
6
  Project-URL: Issues, https://github.com/VirtualBrainLab/ephys-link/issues
@@ -28,7 +28,7 @@ Requires-Dist: keyboard==0.13.5
28
28
  Requires-Dist: packaging==24.2
29
29
  Requires-Dist: platformdirs==4.3.7
30
30
  Requires-Dist: pyserial==3.5
31
- Requires-Dist: python-socketio[asyncio-client]==5.12.1
31
+ Requires-Dist: python-socketio[asyncio-client]==5.13.0
32
32
  Requires-Dist: requests==2.32.3
33
33
  Requires-Dist: rich==14.0.0
34
34
  Requires-Dist: sensapex==1.400.3
@@ -1,13 +1,13 @@
1
- ephys_link/__about__.py,sha256=tATvJM5shAzfspHYjdVwpV2w3-gDA119NlEYi5X2lFY,22
1
+ ephys_link/__about__.py,sha256=72sTjjXH8lKUBOrH9ADJVFplt-JL7YxNRjHxZ7aL25E,24
2
2
  ephys_link/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  ephys_link/__main__.py,sha256=sbFdC6KJjTfXDgRraU_fmGRPcF4I1Ur9PRDiD86dkRI,1449
4
4
  ephys_link/back_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- ephys_link/back_end/platform_handler.py,sha256=PSlBa2_jM073uaF3MvCd-_a7XXEeTfn5KXJ7aPEwMVk,12149
5
+ ephys_link/back_end/platform_handler.py,sha256=gdiO6d0L-DWWLEJOL6eP6685tOC6otffmhfIBtPjhq0,12604
6
6
  ephys_link/back_end/server.py,sha256=mb3K3pXSO-gHaSj1CGJ0v3CSOW5YCi-p0EOKoySRzKQ,10322
7
7
  ephys_link/bindings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  ephys_link/bindings/fake_binding.py,sha256=PI7zYv-SjWsGFEs_FVu5Z5l9gykIqG3C7pQISdbwdoY,2357
9
9
  ephys_link/bindings/mpm_binding.py,sha256=vn7IKqdiZ6_MX91zomqDXX08ONHwVVgWncRuJTxJpOM,10872
10
- ephys_link/bindings/ump_4_binding.py,sha256=NTewhryjJYDDGYXHMiFRICfGBPJqrdStKOMB87dftiY,5421
10
+ ephys_link/bindings/ump_binding.py,sha256=-RwzUggGYsznhV6yiXb96y6WD-e3E7-tcDk6NfesiWU,8080
11
11
  ephys_link/front_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  ephys_link/front_end/cli.py,sha256=isIJs_sZbz7VbNvLgi-HyDlE-TyKD12auDhMTxAkWQU,3099
13
13
  ephys_link/front_end/gui.py,sha256=MDcrTS_Xz9bopAgamh4HknqRC10W8E6eOS3Kss_2ZKQ,6864
@@ -18,8 +18,8 @@ ephys_link/utils/console.py,sha256=52SYvXv_7Fx8QDL3RMFQoggQ1n5W93Yu5aU7uuJQgfg,3
18
18
  ephys_link/utils/constants.py,sha256=1aML7zBNTM5onVSf6NDrYIR33VJy-dIHd1lFORVBGbM,725
19
19
  ephys_link/utils/converters.py,sha256=ZdVmIX-LHCwM__F0SpjN_mfNGGetr1U97xvHd0hf8T0,2038
20
20
  ephys_link/utils/startup.py,sha256=jZVed78tuWjUuZqWVgii_zumDr87T-ikEtOFa6KTE_E,2500
21
- ephys_link-2.0.2.dist-info/METADATA,sha256=goBbmRyz6sCH8A6KRRuIz9Jo2AHT4vkkoRLNcgUvbnA,4607
22
- ephys_link-2.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- ephys_link-2.0.2.dist-info/entry_points.txt,sha256=o8wV3AdnJ9o47vg9ymKxPNVq9pMdPq8UZHE_iyAJx-k,124
24
- ephys_link-2.0.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
25
- ephys_link-2.0.2.dist-info/RECORD,,
21
+ ephys_link-2.1.0b0.dist-info/METADATA,sha256=i3-gX7xuUweGrkbvtoQQ01nNf2jsrd-hYtvLL_zF4tE,4609
22
+ ephys_link-2.1.0b0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ ephys_link-2.1.0b0.dist-info/entry_points.txt,sha256=o8wV3AdnJ9o47vg9ymKxPNVq9pMdPq8UZHE_iyAJx-k,124
24
+ ephys_link-2.1.0b0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
25
+ ephys_link-2.1.0b0.dist-info/RECORD,,
@@ -1,158 +0,0 @@
1
- """Bindings for Sensapex uMp-4 platform.
2
-
3
- Usage: Instantiate Ump4Bindings to interact with the Sensapex uMp-4 platform.
4
- """
5
-
6
- from asyncio import get_running_loop
7
- from typing import NoReturn, final, override
8
-
9
- from sensapex import UMP, SensapexDevice # pyright: ignore [reportMissingTypeStubs]
10
- from vbl_aquarium.models.unity import Vector4
11
-
12
- from ephys_link.utils.base_binding import BaseBinding
13
- from ephys_link.utils.constants import RESOURCES_DIRECTORY
14
- from ephys_link.utils.converters import (
15
- list_to_vector4,
16
- scalar_mm_to_um,
17
- um_to_mm,
18
- vector4_to_array,
19
- vector_mm_to_um,
20
- )
21
-
22
-
23
- @final
24
- class Ump4Binding(BaseBinding):
25
- """Bindings for UMP-4 platform"""
26
-
27
- def __init__(self) -> None:
28
- """Initialize UMP-4 bindings."""
29
-
30
- # Establish connection to Sensapex API (exit if connection fails).
31
- UMP.set_library_path(RESOURCES_DIRECTORY)
32
- self._ump = UMP.get_ump() # pyright: ignore [reportUnknownMemberType]
33
-
34
- @staticmethod
35
- @override
36
- def get_display_name() -> str:
37
- return "Sensapex uMp-4"
38
-
39
- @staticmethod
40
- @override
41
- def get_cli_name() -> str:
42
- return "ump-4"
43
-
44
- @override
45
- async def get_manipulators(self) -> list[str]:
46
- return list(map(str, self._ump.list_devices()))
47
-
48
- @override
49
- async def get_axes_count(self) -> int:
50
- return 4
51
-
52
- @override
53
- def get_dimensions(self) -> Vector4:
54
- return Vector4(x=20, y=20, z=20, w=20)
55
-
56
- @override
57
- async def get_position(self, manipulator_id: str) -> Vector4:
58
- return um_to_mm(list_to_vector4(self._get_device(manipulator_id).get_pos(1))) # pyright: ignore [reportUnknownMemberType]
59
-
60
- @override
61
- async def get_angles(self, manipulator_id: str) -> NoReturn:
62
- """uMp-4 does not support getting angles so raise an error.
63
-
64
- Raises:
65
- AttributeError: uMp-4 does not support getting angles.
66
- """
67
- error_message = "UMP-4 does not support getting angles"
68
- raise AttributeError(error_message)
69
-
70
- @override
71
- async def get_shank_count(self, manipulator_id: str) -> NoReturn:
72
- """uMp-4 does not support getting shank count so raise an error.
73
-
74
- Raises:
75
- AttributeError: uMp-4 does not support getting shank count.
76
- """
77
- error_message = "UMP-4 does not support getting shank count"
78
- raise AttributeError(error_message)
79
-
80
- @staticmethod
81
- @override
82
- def get_movement_tolerance() -> float:
83
- return 0.001
84
-
85
- @override
86
- async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
87
- # Convert position to micrometers.
88
- target_position_um = vector_mm_to_um(position)
89
-
90
- # Request movement.
91
- movement = self._get_device(manipulator_id).goto_pos( # pyright: ignore [reportUnknownMemberType]
92
- vector4_to_array(target_position_um), scalar_mm_to_um(speed)
93
- )
94
-
95
- # Wait for movement to finish.
96
- _ = await get_running_loop().run_in_executor(None, movement.finished_event.wait, None)
97
-
98
- # Handle interrupted movement.
99
- if movement.interrupted:
100
- error_message = f"Manipulator {manipulator_id} interrupted: {movement.interrupt_reason}" # pyright: ignore [reportUnknownMemberType]
101
- raise RuntimeError(error_message)
102
-
103
- # Handle empty end position.
104
- if movement.last_pos is None or len(movement.last_pos) == 0: # pyright: ignore [reportUnknownMemberType, reportUnknownArgumentType]
105
- error_message = f"Manipulator {manipulator_id} did not reach target position"
106
- raise RuntimeError(error_message)
107
-
108
- return um_to_mm(list_to_vector4(movement.last_pos)) # pyright: ignore [reportArgumentType, reportUnknownMemberType]
109
-
110
- @override
111
- async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
112
- # Augment current position with depth.
113
- current_position = await self.get_position(manipulator_id)
114
- new_platform_position = current_position.model_copy(update={"w": depth})
115
-
116
- # Make the movement.
117
- final_platform_position = await self.set_position(manipulator_id, new_platform_position, speed)
118
-
119
- # Return the final depth.
120
- return float(final_platform_position.w)
121
-
122
- @override
123
- async def stop(self, manipulator_id: str) -> None:
124
- self._get_device(manipulator_id).stop()
125
-
126
- @override
127
- def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
128
- # unified <- platform
129
- # +x <- +y
130
- # +y <- -z
131
- # +z <- +x
132
- # +d <- +d
133
-
134
- return Vector4(
135
- x=platform_space.y,
136
- y=self.get_dimensions().z - platform_space.z,
137
- z=platform_space.x,
138
- w=platform_space.w,
139
- )
140
-
141
- @override
142
- def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
143
- # platform <- unified
144
- # +x <- +z
145
- # +y <- +x
146
- # +z <- -y
147
- # +d <- +d
148
-
149
- return Vector4(
150
- x=unified_space.z,
151
- y=unified_space.x,
152
- z=self.get_dimensions().z - unified_space.y,
153
- w=unified_space.w,
154
- )
155
-
156
- # Helper methods.
157
- def _get_device(self, manipulator_id: str) -> SensapexDevice:
158
- return self._ump.get_device(int(manipulator_id)) # pyright: ignore [reportUnknownMemberType]