ephys-link 2.0.0__py3-none-any.whl → 2.0.0b1__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.
@@ -1,315 +1,298 @@
1
- """Manipulator platform handler.
2
-
3
- Responsible for performing the various manipulator commands.
4
- Instantiates the appropriate bindings based on the platform type and uses them to perform the commands.
5
-
6
- Usage:
7
- Instantiate PlatformHandler with the platform type and call the desired command.
8
- """
9
-
10
- from typing import final
11
- from uuid import uuid4
12
-
13
- from vbl_aquarium.models.ephys_link import (
14
- AngularResponse,
15
- BooleanStateResponse,
16
- EphysLinkOptions,
17
- GetManipulatorsResponse,
18
- PlatformInfo,
19
- PositionalResponse,
20
- SetDepthRequest,
21
- SetDepthResponse,
22
- SetInsideBrainRequest,
23
- SetPositionRequest,
24
- ShankCountResponse,
25
- )
26
- from vbl_aquarium.models.unity import Vector4
27
-
28
- from ephys_link.bindings.mpm_binding import MPMBinding
29
- from ephys_link.utils.base_binding import BaseBinding
30
- from ephys_link.utils.console import Console
31
- from ephys_link.utils.converters import vector4_to_array
32
- from ephys_link.utils.startup import get_bindings
33
-
34
-
35
- @final
36
- class PlatformHandler:
37
- """Handler for platform commands."""
38
-
39
- def __init__(self, options: EphysLinkOptions, console: Console) -> None:
40
- """Initialize platform handler.
41
-
42
- Args:
43
- options: CLI options.
44
- console: Console instance.
45
- """
46
- # Store the CLI options.
47
- self._options = options
48
-
49
- # Store the console.
50
- self._console = console
51
-
52
- # Define bindings based on platform type.
53
- self._bindings = self._get_binding_instance(options)
54
-
55
- # Record which IDs are inside the brain.
56
- self._inside_brain: set[str] = set()
57
-
58
- # Generate a Pinpoint ID for proxy usage.
59
- self._pinpoint_id = str(uuid4())[:8]
60
-
61
- def _get_binding_instance(self, options: EphysLinkOptions) -> BaseBinding:
62
- """Match the platform type to the appropriate bindings.
63
-
64
- Args:
65
- options: CLI options.
66
-
67
- Raises:
68
- ValueError: If the platform type is not recognized.
69
-
70
- Returns:
71
- Bindings for the specified platform type.
72
- """
73
- for binding_type in get_bindings():
74
- binding_cli_name = binding_type.get_cli_name()
75
-
76
- if binding_cli_name == options.type:
77
- # Pass in HTTP port for Pathfinder MPM.
78
- if binding_cli_name == "pathfinder-mpm":
79
- return MPMBinding(options.mpm_port)
80
-
81
- # Otherwise just return the binding.
82
- return binding_type()
83
-
84
- # Raise an error if the platform type is not recognized.
85
- error_message = f'Platform type "{options.type}" not recognized.'
86
- self._console.critical_print(error_message)
87
- raise ValueError(error_message)
88
-
89
- # Platform metadata.
90
-
91
- def get_display_name(self) -> str:
92
- """Get the display name for the platform.
93
-
94
- Returns:
95
- Display name for the platform.
96
- """
97
- return self._bindings.get_display_name()
98
-
99
- async def get_platform_info(self) -> PlatformInfo:
100
- """Get the manipulator platform type connected to Ephys Link.
101
-
102
- Returns:
103
- Platform type config identifier (see CLI options for examples).
104
- """
105
- return PlatformInfo(
106
- name=self._bindings.get_display_name(),
107
- cli_name=self._bindings.get_cli_name(),
108
- axes_count=await self._bindings.get_axes_count(),
109
- dimensions=self._bindings.get_dimensions(),
110
- )
111
-
112
- # Manipulator commands.
113
-
114
- async def get_manipulators(self) -> GetManipulatorsResponse:
115
- """Get a list of available manipulators on the current handler.
116
-
117
- Returns:
118
- List of manipulator IDs or an error message if any.
119
- """
120
- try:
121
- manipulators = await self._bindings.get_manipulators()
122
- except Exception as e: # noqa: BLE001
123
- self._console.exception_error_print("Get Manipulators", e)
124
- return GetManipulatorsResponse(error=self._console.pretty_exception(e))
125
- else:
126
- return GetManipulatorsResponse(manipulators=manipulators)
127
-
128
- async def get_position(self, manipulator_id: str) -> PositionalResponse:
129
- """Get the current translation position of a manipulator in unified coordinates (mm).
130
-
131
- Args:
132
- manipulator_id: Manipulator ID.
133
-
134
- Returns:
135
- Current position of the manipulator and an error message if any.
136
- """
137
- try:
138
- unified_position = self._bindings.platform_space_to_unified_space(
139
- await self._bindings.get_position(manipulator_id)
140
- )
141
- except Exception as e: # noqa: BLE001
142
- self._console.exception_error_print("Get Position", e)
143
- return PositionalResponse(error=str(e))
144
- else:
145
- return PositionalResponse(position=unified_position)
146
-
147
- async def get_angles(self, manipulator_id: str) -> AngularResponse:
148
- """Get the current rotation angles of a manipulator in Yaw, Pitch, Roll (degrees).
149
-
150
- Args:
151
- manipulator_id: Manipulator ID.
152
-
153
- Returns:
154
- Current angles of the manipulator and an error message if any.
155
- """
156
- try:
157
- angles = await self._bindings.get_angles(manipulator_id)
158
- except Exception as e: # noqa: BLE001
159
- self._console.exception_error_print("Get Angles", e)
160
- return AngularResponse(error=self._console.pretty_exception(e))
161
- else:
162
- return AngularResponse(angles=angles)
163
-
164
- async def get_shank_count(self, manipulator_id: str) -> ShankCountResponse:
165
- """Get the number of shanks on a manipulator.
166
-
167
- Args:
168
- manipulator_id: Manipulator ID.
169
-
170
- Returns:
171
- Number of shanks on the manipulator and an error message if any.
172
- """
173
- try:
174
- shank_count = await self._bindings.get_shank_count(manipulator_id)
175
- except Exception as e: # noqa: BLE001
176
- self._console.exception_error_print("Get Shank Count", e)
177
- return ShankCountResponse(error=self._console.pretty_exception(e))
178
- else:
179
- return ShankCountResponse(shank_count=shank_count)
180
-
181
- async def set_position(self, request: SetPositionRequest) -> PositionalResponse:
182
- """Move a manipulator to a specified translation position in unified coordinates (mm).
183
-
184
- Args:
185
- request: Request to move a manipulator to a specified position.
186
-
187
- Returns:
188
- Final position of the manipulator and an error message if any.
189
- """
190
- try:
191
- # Disallow setting manipulator position while inside the brain.
192
- if request.manipulator_id in self._inside_brain:
193
- error_message = 'Can not move manipulator while inside the brain. Set the depth ("set_depth") instead.'
194
- self._console.error_print("Set Position", error_message)
195
- return PositionalResponse(error=error_message)
196
-
197
- # Move to the new position.
198
- final_platform_position = await self._bindings.set_position(
199
- manipulator_id=request.manipulator_id,
200
- position=self._bindings.unified_space_to_platform_space(request.position),
201
- speed=request.speed,
202
- )
203
- final_unified_position = self._bindings.platform_space_to_unified_space(final_platform_position)
204
-
205
- # Return error if movement did not reach target within tolerance.
206
- for index, axis in enumerate(vector4_to_array(final_unified_position - request.position)):
207
- # End once index is the number of axes.
208
- if index == await self._bindings.get_axes_count():
209
- break
210
-
211
- # Check if the axis is within the movement tolerance.
212
- if abs(axis) > self._bindings.get_movement_tolerance():
213
- error_message = (
214
- f"Manipulator {request.manipulator_id} did not reach target"
215
- f" position on axis {list(Vector4.model_fields.keys())[index]}."
216
- f" Requested: {request.position}, got: {final_unified_position}."
217
- )
218
- self._console.error_print("Set Position", error_message)
219
- return PositionalResponse(error=error_message)
220
- except Exception as e: # noqa: BLE001
221
- self._console.exception_error_print("Set Position", e)
222
- return PositionalResponse(error=self._console.pretty_exception(e))
223
- else:
224
- return PositionalResponse(position=final_unified_position)
225
-
226
- async def set_depth(self, request: SetDepthRequest) -> SetDepthResponse:
227
- """Move a manipulator's depth translation stage to a specific value (mm).
228
-
229
- Args:
230
- request: Request to move a manipulator to a specified depth.
231
-
232
- Returns:
233
- Final depth of the manipulator and an error message if any.
234
- """
235
- try:
236
- # Move to the new depth.
237
- final_platform_depth = await self._bindings.set_depth(
238
- manipulator_id=request.manipulator_id,
239
- depth=self._bindings.unified_space_to_platform_space(Vector4(w=request.depth)).w,
240
- speed=request.speed,
241
- )
242
- final_unified_depth = self._bindings.platform_space_to_unified_space(Vector4(w=final_platform_depth)).w
243
-
244
- # Return error if movement did not reach target within tolerance.
245
- if abs(final_unified_depth - request.depth) > self._bindings.get_movement_tolerance():
246
- error_message = (
247
- f"Manipulator {request.manipulator_id} did not reach target depth."
248
- f" Requested: {request.depth}, got: {final_unified_depth}."
249
- )
250
- self._console.error_print("Set Depth", error_message)
251
- return SetDepthResponse(error=error_message)
252
- except Exception as e: # noqa: BLE001
253
- self._console.exception_error_print("Set Depth", e)
254
- return SetDepthResponse(error=self._console.pretty_exception(e))
255
- else:
256
- return SetDepthResponse(depth=final_unified_depth)
257
-
258
- async def set_inside_brain(self, request: SetInsideBrainRequest) -> BooleanStateResponse:
259
- """Mark a manipulator as inside the brain or not.
260
-
261
- This should restrict the manipulator's movement to just the depth axis.
262
-
263
- Args:
264
- request: Request to set
265
-
266
- Returns:
267
- Inside brain state of the manipulator and an error message if any.
268
- """
269
- try:
270
- if request.inside:
271
- self._inside_brain.add(request.manipulator_id)
272
- else:
273
- self._inside_brain.discard(request.manipulator_id)
274
- except Exception as e: # noqa: BLE001
275
- self._console.exception_error_print("Set Inside Brain", e)
276
- return BooleanStateResponse(error=self._console.pretty_exception(e))
277
- else:
278
- return BooleanStateResponse(state=request.inside)
279
-
280
- async def stop(self, manipulator_id: str) -> str:
281
- """Stop a manipulator.
282
-
283
- Args:
284
- manipulator_id: Manipulator ID.
285
-
286
- Returns:
287
- Error message if any.
288
- """
289
- try:
290
- await self._bindings.stop(manipulator_id)
291
- except Exception as e: # noqa: BLE001
292
- self._console.exception_error_print("Stop", e)
293
- return self._console.pretty_exception(e)
294
- else:
295
- return ""
296
-
297
- async def stop_all(self) -> str:
298
- """Stop all manipulators.
299
-
300
- Returns:
301
- Error message if any.
302
- """
303
- try:
304
- for manipulator_id in await self._bindings.get_manipulators():
305
- await self._bindings.stop(manipulator_id)
306
- except Exception as e: # noqa: BLE001
307
- self._console.exception_error_print("Stop", e)
308
- return self._console.pretty_exception(e)
309
- else:
310
- return ""
311
-
312
- async def emergency_stop(self) -> None:
313
- """Stops all manipulators with a message."""
314
- self._console.critical_print("Emergency Stopping All Manipulators...")
315
- _ = await self.stop_all()
1
+ # ruff: noqa: BLE001
2
+ """Manipulator platform handler.
3
+
4
+ Responsible for performing the various manipulator commands.
5
+ Instantiates the appropriate bindings based on the platform type and uses them to perform the commands.
6
+
7
+ Usage: Instantiate PlatformHandler with the platform type and call the desired command.
8
+ """
9
+
10
+ from uuid import uuid4
11
+
12
+ from vbl_aquarium.models.ephys_link import (
13
+ AngularResponse,
14
+ BooleanStateResponse,
15
+ GetManipulatorsResponse,
16
+ PositionalResponse,
17
+ SetDepthRequest,
18
+ SetDepthResponse,
19
+ SetInsideBrainRequest,
20
+ SetPositionRequest,
21
+ ShankCountResponse,
22
+ )
23
+ from vbl_aquarium.models.proxy import PinpointIdResponse
24
+ from vbl_aquarium.models.unity import Vector4
25
+
26
+ from ephys_link.__about__ import __version__
27
+ from ephys_link.bindings.fake_bindings import FakeBindings
28
+ from ephys_link.bindings.ump_4_bindings import Ump4Bindings
29
+ from ephys_link.util.base_bindings import BaseBindings
30
+ from ephys_link.util.common import vector4_to_array
31
+ from ephys_link.util.console import Console
32
+
33
+
34
+ class PlatformHandler:
35
+ """Handler for platform commands."""
36
+
37
+ def __init__(self, platform_type: str, console: Console) -> None:
38
+ """Initialize platform handler.
39
+
40
+ :param platform_type: Platform type to initialize bindings from.
41
+ :type platform_type: str
42
+ """
43
+
44
+ # Store the platform type.
45
+ self._platform_type = platform_type
46
+
47
+ # Store the console.
48
+ self._console = console
49
+
50
+ # Define bindings based on platform type.
51
+ self._bindings = self._match_platform_type(platform_type)
52
+
53
+ # Record which IDs are inside the brain.
54
+ self._inside_brain: set[str] = set()
55
+
56
+ # Generate a Pinpoint ID for proxy usage.
57
+ self._pinpoint_id = str(uuid4())[:8]
58
+
59
+ def _match_platform_type(self, platform_type: str) -> BaseBindings:
60
+ """Match the platform type to the appropriate bindings.
61
+
62
+ :param platform_type: Platform type.
63
+ :type platform_type: str
64
+ :returns: Bindings for the specified platform type.
65
+ :rtype: :class:`ephys_link.util.base_bindings.BaseBindings`
66
+ """
67
+ match platform_type:
68
+ case "ump-4":
69
+ return Ump4Bindings()
70
+ case "fake":
71
+ return FakeBindings()
72
+ case _:
73
+ error_message = f'Platform type "{platform_type}" not recognized.'
74
+ self._console.labeled_error_print("PLATFORM", error_message)
75
+ raise ValueError(error_message)
76
+
77
+ # Ephys Link metadata.
78
+
79
+ @staticmethod
80
+ def get_version() -> str:
81
+ """Get Ephys Link's version.
82
+
83
+ :returns: Ephys Link's version.
84
+ :rtype: str
85
+ """
86
+ return __version__
87
+
88
+ def get_pinpoint_id(self) -> PinpointIdResponse:
89
+ """Get the Pinpoint ID for proxy usage.
90
+
91
+ :returns: Pinpoint ID response.
92
+ :rtype: :class:`vbl_aquarium.models.ephys_link.PinpointIDResponse`
93
+ """
94
+ return PinpointIdResponse(pinpoint_id=self._pinpoint_id, is_requester=False)
95
+
96
+ def get_platform_type(self) -> str:
97
+ """Get the manipulator platform type connected to Ephys Link.
98
+
99
+ :returns: Platform type config identifier (see CLI options for examples).
100
+ :rtype: str
101
+ """
102
+ return self._platform_type
103
+
104
+ # Manipulator commands.
105
+
106
+ async def get_manipulators(self) -> GetManipulatorsResponse:
107
+ """Get a list of available manipulators on the current handler.
108
+
109
+ :returns: List of manipulator IDs, number of axes, dimensions of manipulators (mm), and an error message if any.
110
+ :rtype: :class:`vbl_aquarium.models.ephys_link.GetManipulatorsResponse`
111
+ """
112
+ try:
113
+ manipulators = await self._bindings.get_manipulators()
114
+ num_axes = await self._bindings.get_num_axes()
115
+ dimensions = self._bindings.get_dimensions()
116
+ except Exception as e:
117
+ self._console.exception_error_print("Get Manipulators", e)
118
+ return GetManipulatorsResponse(error=self._console.pretty_exception(e))
119
+ else:
120
+ return GetManipulatorsResponse(
121
+ manipulators=manipulators,
122
+ num_axes=num_axes,
123
+ dimensions=dimensions,
124
+ )
125
+
126
+ async def get_position(self, manipulator_id: str) -> PositionalResponse:
127
+ """Get the current translation position of a manipulator in unified coordinates (mm).
128
+
129
+ :param manipulator_id: Manipulator ID.
130
+ :type manipulator_id: str
131
+ :returns: Current position of the manipulator and an error message if any.
132
+ :rtype: :class:`vbl_aquarium.models.ephys_link.PositionalResponse`
133
+ """
134
+ try:
135
+ unified_position = self._bindings.platform_space_to_unified_space(
136
+ await self._bindings.get_position(manipulator_id)
137
+ )
138
+ except Exception as e:
139
+ self._console.exception_error_print("Get Position", e)
140
+ return PositionalResponse(error=str(e))
141
+ else:
142
+ return PositionalResponse(position=unified_position)
143
+
144
+ async def get_angles(self, manipulator_id: str) -> AngularResponse:
145
+ """Get the current rotation angles of a manipulator in Yaw, Pitch, Roll (degrees).
146
+
147
+ :param manipulator_id: Manipulator ID.
148
+ :type manipulator_id: str
149
+ :returns: Current angles of the manipulator and an error message if any.
150
+ :rtype: :class:`vbl_aquarium.models.ephys_link.AngularResponse`
151
+ """
152
+ try:
153
+ angles = await self._bindings.get_angles(manipulator_id)
154
+ except Exception as e:
155
+ self._console.exception_error_print("Get Angles", e)
156
+ return AngularResponse(error=self._console.pretty_exception(e))
157
+ else:
158
+ return AngularResponse(angles=angles)
159
+
160
+ async def get_shank_count(self, manipulator_id: str) -> ShankCountResponse:
161
+ """Get the number of shanks on a manipulator.
162
+
163
+ :param manipulator_id: Manipulator ID.
164
+ :type manipulator_id: str
165
+ :returns: Number of shanks on the manipulator and an error message if any.
166
+ :rtype: :class:`vbl_aquarium.models.ephys_link.ShankCountResponse`
167
+ """
168
+ try:
169
+ shank_count = await self._bindings.get_shank_count(manipulator_id)
170
+ except Exception as e:
171
+ self._console.exception_error_print("Get Shank Count", e)
172
+ return ShankCountResponse(error=self._console.pretty_exception(e))
173
+ else:
174
+ return ShankCountResponse(shank_count=shank_count)
175
+
176
+ async def set_position(self, request: SetPositionRequest) -> PositionalResponse:
177
+ """Move a manipulator to a specified translation position in unified coordinates (mm).
178
+
179
+ :param request: Request to move a manipulator to a specified position.
180
+ :type request: :class:`vbl_aquarium.models.ephys_link.GotoPositionRequest`
181
+ :returns: Final position of the manipulator and an error message if any.
182
+ :rtype: :class:`vbl_aquarium.models.ephys_link.Position`
183
+ """
184
+ try:
185
+ # Disallow setting manipulator position while inside the brain.
186
+ if request.manipulator_id in self._inside_brain:
187
+ error_message = 'Can not move manipulator while inside the brain. Set the depth ("set_depth") instead.'
188
+ self._console.error_print(error_message)
189
+ return PositionalResponse(error=error_message)
190
+
191
+ # Move to the new position.
192
+ final_platform_position = await self._bindings.set_position(
193
+ manipulator_id=request.manipulator_id,
194
+ position=self._bindings.unified_space_to_platform_space(request.position),
195
+ speed=request.speed,
196
+ )
197
+ final_unified_position = self._bindings.platform_space_to_unified_space(final_platform_position)
198
+
199
+ # Return error if movement did not reach target within tolerance.
200
+ for index, axis in enumerate(vector4_to_array(final_unified_position - request.position)):
201
+ # End once index is greater than the number of axes.
202
+ if index >= await self._bindings.get_num_axes():
203
+ break
204
+
205
+ # Check if the axis is within the movement tolerance.
206
+ if abs(axis) > await self._bindings.get_movement_tolerance():
207
+ error_message = (
208
+ f"Manipulator {request.manipulator_id} did not reach target"
209
+ f" position on axis {list(Vector4.model_fields.keys())[index]}."
210
+ f"Requested: {request.position}, got: {final_unified_position}."
211
+ )
212
+ self._console.error_print(error_message)
213
+ return PositionalResponse(error=error_message)
214
+ except Exception as e:
215
+ self._console.exception_error_print("Set Position", e)
216
+ return PositionalResponse(error=self._console.pretty_exception(e))
217
+ else:
218
+ return PositionalResponse(position=final_unified_position)
219
+
220
+ async def set_depth(self, request: SetDepthRequest) -> SetDepthResponse:
221
+ """Move a manipulator's depth translation stage to a specific value (mm).
222
+
223
+ :param request: Request to move a manipulator to a specified depth.
224
+ :type request: :class:`vbl_aquarium.models.ephys_link.DriveToDepthRequest`
225
+ :returns: Final depth of the manipulator and an error message if any.
226
+ :rtype: :class:`vbl_aquarium.models.ephys_link.DriveToDepthResponse`
227
+ """
228
+ try:
229
+ # Create a position based on the new depth.
230
+ current_platform_position = await self._bindings.get_position(request.manipulator_id)
231
+ current_unified_position = self._bindings.platform_space_to_unified_space(current_platform_position)
232
+ target_unified_position = current_unified_position.model_copy(update={"w": request.depth})
233
+ target_platform_position = self._bindings.unified_space_to_platform_space(target_unified_position)
234
+
235
+ # Move to the new depth.
236
+ final_platform_position = await self._bindings.set_position(
237
+ manipulator_id=request.manipulator_id,
238
+ position=target_platform_position,
239
+ speed=request.speed,
240
+ )
241
+ final_unified_position = self._bindings.platform_space_to_unified_space(final_platform_position)
242
+ except Exception as e:
243
+ self._console.exception_error_print("Set Depth", e)
244
+ return SetDepthResponse(error=self._console.pretty_exception(e))
245
+ else:
246
+ return SetDepthResponse(depth=final_unified_position.w)
247
+
248
+ async def set_inside_brain(self, request: SetInsideBrainRequest) -> BooleanStateResponse:
249
+ """Mark a manipulator as inside the brain or not.
250
+
251
+ This should restrict the manipulator's movement to just the depth axis.
252
+
253
+ :param request: Request to set a manipulator's inside brain state.
254
+ :type request: :class:`vbl_aquarium.models.ephys_link.InsideBrainRequest`
255
+ :returns: Inside brain state of the manipulator and an error message if any.
256
+ :rtype: :class:`vbl_aquarium.models.ephys_link.BooleanStateResponse`
257
+ """
258
+ try:
259
+ if request.inside:
260
+ self._inside_brain.add(request.manipulator_id)
261
+ else:
262
+ self._inside_brain.discard(request.manipulator_id)
263
+ except Exception as e:
264
+ self._console.exception_error_print("Set Inside Brain", e)
265
+ return BooleanStateResponse(error=self._console.pretty_exception(e))
266
+ else:
267
+ return BooleanStateResponse(state=request.inside)
268
+
269
+ async def stop(self, manipulator_id: str) -> str:
270
+ """Stop a manipulator.
271
+
272
+ :param manipulator_id: Manipulator ID.
273
+ :type manipulator_id: str
274
+ :returns: Error message if any.
275
+ :rtype: str
276
+ """
277
+ try:
278
+ await self._bindings.stop(manipulator_id)
279
+ except Exception as e:
280
+ self._console.exception_error_print("Stop", e)
281
+ return self._console.pretty_exception(e)
282
+ else:
283
+ return ""
284
+
285
+ async def stop_all(self) -> str:
286
+ """Stop all manipulators.
287
+
288
+ :returns: Error message if any.
289
+ :rtype: str
290
+ """
291
+ try:
292
+ for manipulator_id in await self._bindings.get_manipulators():
293
+ await self._bindings.stop(manipulator_id)
294
+ except Exception as e:
295
+ self._console.exception_error_print("Stop", e)
296
+ return self._console.pretty_exception(e)
297
+ else:
298
+ return ""