ephys-link 2.0.0b2__tar.gz → 2.0.0b6__tar.gz

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.
Files changed (37) hide show
  1. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/PKG-INFO +10 -11
  2. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/README.md +3 -4
  3. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/pyproject.toml +7 -6
  4. ephys_link-2.0.0b6/scripts/move_tester.py +16 -0
  5. ephys_link-2.0.0b6/src/ephys_link/__about__.py +1 -0
  6. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/__main__.py +1 -1
  7. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/back_end/platform_handler.py +37 -27
  8. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/back_end/server.py +1 -1
  9. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/bindings/fake_bindings.py +8 -3
  10. ephys_link-2.0.0b6/src/ephys_link/bindings/mpm_bindings.py +278 -0
  11. ephys_link-2.0.0b6/src/ephys_link/bindings/ump_3_bindings.py +138 -0
  12. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/bindings/ump_4_bindings.py +27 -20
  13. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/util/base_bindings.py +18 -3
  14. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/util/common.py +10 -10
  15. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/util/console.py +5 -8
  16. ephys_link-2.0.0b2/scripts/move_tester.py +0 -15
  17. ephys_link-2.0.0b2/src/ephys_link/__about__.py +0 -1
  18. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/.gitignore +0 -0
  19. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/LICENSE +0 -0
  20. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/assets/icon.ico +0 -0
  21. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/ephys_link.spec +0 -0
  22. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/qodana.yaml +0 -0
  23. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/scripts/__init__.py +0 -0
  24. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/scripts/logger_test.py +0 -0
  25. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/scripts/server_tester.py +0 -0
  26. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/__init__.py +0 -0
  27. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/back_end/__init__.py +0 -0
  28. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/bindings/__init__.py +0 -0
  29. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/front_end/__init__.py +0 -0
  30. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/front_end/cli.py +0 -0
  31. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/front_end/gui.py +0 -0
  32. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/resources/CP210xManufacturing.dll +0 -0
  33. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/resources/NstMotorCtrl.dll +0 -0
  34. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/resources/SiUSBXp.dll +0 -0
  35. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/resources/libum.dll +0 -0
  36. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/src/ephys_link/util/__init__.py +0 -0
  37. {ephys_link-2.0.0b2 → ephys_link-2.0.0b6}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ephys-link
3
- Version: 2.0.0b2
3
+ Version: 2.0.0b6
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
@@ -26,16 +26,16 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
26
26
  Classifier: Programming Language :: Python :: Implementation :: PyPy
27
27
  Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
28
28
  Requires-Python: <3.13,>=3.10
29
- Requires-Dist: aiohttp==3.9.5
29
+ Requires-Dist: aiohttp==3.10.8
30
30
  Requires-Dist: colorama==0.4.6
31
- Requires-Dist: platformdirs==4.2.2
31
+ Requires-Dist: platformdirs==4.3.6
32
32
  Requires-Dist: pyserial==3.5
33
- Requires-Dist: python-socketio[asyncio-client]==5.11.3
34
- Requires-Dist: pythonnet==3.0.3
33
+ Requires-Dist: python-socketio[asyncio-client]==5.11.4
34
+ Requires-Dist: pythonnet==3.0.4
35
35
  Requires-Dist: requests==2.32.3
36
- Requires-Dist: rich==13.7.1
36
+ Requires-Dist: rich==13.8.1
37
37
  Requires-Dist: sensapex==1.400.1
38
- Requires-Dist: vbl-aquarium==0.0.19
38
+ Requires-Dist: vbl-aquarium==0.0.22
39
39
  Description-Content-Type: text/markdown
40
40
 
41
41
  # Electrophysiology Manipulator Link
@@ -120,13 +120,12 @@ window instead of `localhost`.
120
120
  pip install ephys-link
121
121
  ```
122
122
 
123
- Import the modules you need and launch the server.
123
+ Import main and run (this will launch the setup GUI).
124
124
 
125
125
  ```python
126
- from ephys_link.server import Server
126
+ from ephys_link.__main__ import main
127
127
 
128
- server = Server()
129
- server.launch("sensapex", args.proxy_address, 8081)
128
+ main()
130
129
  ```
131
130
 
132
131
  ## Install for Development
@@ -80,13 +80,12 @@ window instead of `localhost`.
80
80
  pip install ephys-link
81
81
  ```
82
82
 
83
- Import the modules you need and launch the server.
83
+ Import main and run (this will launch the setup GUI).
84
84
 
85
85
  ```python
86
- from ephys_link.server import Server
86
+ from ephys_link.__main__ import main
87
87
 
88
- server = Server()
89
- server.launch("sensapex", args.proxy_address, 8081)
88
+ main()
90
89
  ```
91
90
 
92
91
  ## Install for Development
@@ -30,16 +30,16 @@ classifiers = [
30
30
  "Topic :: Scientific/Engineering :: Medical Science Apps.",
31
31
  ]
32
32
  dependencies = [
33
- "aiohttp==3.9.5",
33
+ "aiohttp==3.10.8",
34
34
  "colorama==0.4.6",
35
- "platformdirs==4.2.2",
35
+ "platformdirs==4.3.6",
36
36
  "pyserial==3.5",
37
- "python-socketio[asyncio_client]==5.11.3",
38
- "pythonnet==3.0.3",
37
+ "python-socketio[asyncio_client]==5.11.4",
38
+ "pythonnet==3.0.4",
39
39
  "requests==2.32.3",
40
40
  "sensapex==1.400.1",
41
- "rich==13.7.1",
42
- "vbl-aquarium==0.0.19"
41
+ "rich==13.8.1",
42
+ "vbl-aquarium==0.0.22"
43
43
  ]
44
44
 
45
45
  [project.urls]
@@ -61,6 +61,7 @@ exclude = ["/.github", "/.idea"]
61
61
  [tool.hatch.envs.default]
62
62
  python = "3.12"
63
63
  dependencies = [
64
+ "mypy",
64
65
  "coverage[toml]>=6.5",
65
66
  "pytest",
66
67
  ]
@@ -0,0 +1,16 @@
1
+ from asyncio import run
2
+
3
+ from vbl_aquarium.models.ephys_link import EphysLinkOptions, SetDepthRequest
4
+ from vbl_aquarium.models.unity import Vector4
5
+
6
+ from ephys_link.back_end.platform_handler import PlatformHandler
7
+ from ephys_link.util.console import Console
8
+
9
+ c = Console(enable_debug=True)
10
+ p = PlatformHandler(EphysLinkOptions(type="pathfinder-mpm"), c)
11
+ # target = Vector4()
12
+ target = Vector4(x=7.5, y=7.5, z=7.5, w=7.5)
13
+
14
+ # print(run(p.set_position(SetPositionRequest(manipulator_id="A", position=target, speed=5))).to_json_string())
15
+ print(run(p.set_depth(SetDepthRequest(manipulator_id="A", depth=7.5, speed=0.15))).to_json_string())
16
+ print("Done!")
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0b6"
@@ -31,7 +31,7 @@ def main() -> None:
31
31
  console = Console(enable_debug=options.debug)
32
32
 
33
33
  # 3. Instantiate the Platform Handler with the appropriate platform bindings.
34
- platform_handler = PlatformHandler(options.type, console)
34
+ platform_handler = PlatformHandler(options, console)
35
35
 
36
36
  # 4. Instantiate the Emergency Stop service.
37
37
 
@@ -12,6 +12,7 @@ from uuid import uuid4
12
12
  from vbl_aquarium.models.ephys_link import (
13
13
  AngularResponse,
14
14
  BooleanStateResponse,
15
+ EphysLinkOptions,
15
16
  GetManipulatorsResponse,
16
17
  PositionalResponse,
17
18
  SetDepthRequest,
@@ -25,6 +26,8 @@ from vbl_aquarium.models.unity import Vector4
25
26
 
26
27
  from ephys_link.__about__ import __version__
27
28
  from ephys_link.bindings.fake_bindings import FakeBindings
29
+ from ephys_link.bindings.mpm_bindings import MPMBinding
30
+ from ephys_link.bindings.ump_3_bindings import Ump3Bindings
28
31
  from ephys_link.bindings.ump_4_bindings import Ump4Bindings
29
32
  from ephys_link.util.base_bindings import BaseBindings
30
33
  from ephys_link.util.common import vector4_to_array
@@ -34,21 +37,21 @@ from ephys_link.util.console import Console
34
37
  class PlatformHandler:
35
38
  """Handler for platform commands."""
36
39
 
37
- def __init__(self, platform_type: str, console: Console) -> None:
40
+ def __init__(self, options: EphysLinkOptions, console: Console) -> None:
38
41
  """Initialize platform handler.
39
42
 
40
- :param platform_type: Platform type to initialize bindings from.
41
- :type platform_type: str
43
+ :param options: CLI options.
44
+ :type options: EphysLinkOptions
42
45
  """
43
46
 
44
- # Store the platform type.
45
- self._platform_type = platform_type
47
+ # Store the CLI options.
48
+ self._options = options
46
49
 
47
50
  # Store the console.
48
51
  self._console = console
49
52
 
50
53
  # Define bindings based on platform type.
51
- self._bindings = self._match_platform_type(platform_type)
54
+ self._bindings = self._match_platform_type(options)
52
55
 
53
56
  # Record which IDs are inside the brain.
54
57
  self._inside_brain: set[str] = set()
@@ -56,21 +59,25 @@ class PlatformHandler:
56
59
  # Generate a Pinpoint ID for proxy usage.
57
60
  self._pinpoint_id = str(uuid4())[:8]
58
61
 
59
- def _match_platform_type(self, platform_type: str) -> BaseBindings:
62
+ def _match_platform_type(self, options: EphysLinkOptions) -> BaseBindings:
60
63
  """Match the platform type to the appropriate bindings.
61
64
 
62
- :param platform_type: Platform type.
63
- :type platform_type: str
65
+ :param options: CLI options.
66
+ :type options: EphysLinkOptions
64
67
  :returns: Bindings for the specified platform type.
65
68
  :rtype: :class:`ephys_link.util.base_bindings.BaseBindings`
66
69
  """
67
- match platform_type:
70
+ match options.type:
68
71
  case "ump-4":
69
72
  return Ump4Bindings()
73
+ case "ump-3":
74
+ return Ump3Bindings()
75
+ case "pathfinder-mpm":
76
+ return MPMBinding(options.mpm_port)
70
77
  case "fake":
71
78
  return FakeBindings()
72
79
  case _:
73
- error_message = f'Platform type "{platform_type}" not recognized.'
80
+ error_message = f'Platform type "{options.type}" not recognized.'
74
81
  self._console.critical_print(error_message)
75
82
  raise ValueError(error_message)
76
83
 
@@ -99,7 +106,7 @@ class PlatformHandler:
99
106
  :returns: Platform type config identifier (see CLI options for examples).
100
107
  :rtype: str
101
108
  """
102
- return self._platform_type
109
+ return str(self._options.type)
103
110
 
104
111
  # Manipulator commands.
105
112
 
@@ -111,7 +118,7 @@ class PlatformHandler:
111
118
  """
112
119
  try:
113
120
  manipulators = await self._bindings.get_manipulators()
114
- num_axes = await self._bindings.get_num_axes()
121
+ num_axes = await self._bindings.get_axes_count()
115
122
  dimensions = self._bindings.get_dimensions()
116
123
  except Exception as e:
117
124
  self._console.exception_error_print("Get Manipulators", e)
@@ -198,16 +205,16 @@ class PlatformHandler:
198
205
 
199
206
  # Return error if movement did not reach target within tolerance.
200
207
  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():
208
+ # End once index is the number of axes.
209
+ if index == await self._bindings.get_axes_count():
203
210
  break
204
211
 
205
212
  # Check if the axis is within the movement tolerance.
206
- if abs(axis) > await self._bindings.get_movement_tolerance():
213
+ if abs(axis) > self._bindings.get_movement_tolerance():
207
214
  error_message = (
208
215
  f"Manipulator {request.manipulator_id} did not reach target"
209
216
  f" position on axis {list(Vector4.model_fields.keys())[index]}."
210
- f"Requested: {request.position}, got: {final_unified_position}."
217
+ f" Requested: {request.position}, got: {final_unified_position}."
211
218
  )
212
219
  self._console.error_print("Set Position", error_message)
213
220
  return PositionalResponse(error=error_message)
@@ -226,24 +233,27 @@ class PlatformHandler:
226
233
  :rtype: :class:`vbl_aquarium.models.ephys_link.DriveToDepthResponse`
227
234
  """
228
235
  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
236
  # Move to the new depth.
236
- final_platform_position = await self._bindings.set_position(
237
+ final_platform_depth = await self._bindings.set_depth(
237
238
  manipulator_id=request.manipulator_id,
238
- position=target_platform_position,
239
+ depth=self._bindings.unified_space_to_platform_space(Vector4(w=request.depth)).w,
239
240
  speed=request.speed,
240
241
  )
241
- final_unified_position = self._bindings.platform_space_to_unified_space(final_platform_position)
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)
242
252
  except Exception as e:
243
253
  self._console.exception_error_print("Set Depth", e)
244
254
  return SetDepthResponse(error=self._console.pretty_exception(e))
245
255
  else:
246
- return SetDepthResponse(depth=final_unified_position.w)
256
+ return SetDepthResponse(depth=final_unified_depth)
247
257
 
248
258
  async def set_inside_brain(self, request: SetInsideBrainRequest) -> BooleanStateResponse:
249
259
  """Mark a manipulator as inside the brain or not.
@@ -12,7 +12,7 @@ from vbl_aquarium.models.ephys_link import (
12
12
  SetInsideBrainRequest,
13
13
  SetPositionRequest,
14
14
  )
15
- from vbl_aquarium.models.generic import VBLBaseModel
15
+ from vbl_aquarium.utils.vbl_base_model import VBLBaseModel
16
16
 
17
17
  from ephys_link.back_end.platform_handler import PlatformHandler
18
18
  from ephys_link.util.common import PORT, check_for_updates, server_preamble
@@ -1,6 +1,7 @@
1
1
  from vbl_aquarium.models.unity import Vector3, Vector4
2
2
 
3
3
  from ephys_link.util.base_bindings import BaseBindings
4
+ from ephys_link.util.common import array_to_vector4
4
5
 
5
6
 
6
7
  class FakeBindings(BaseBindings):
@@ -22,11 +23,11 @@ class FakeBindings(BaseBindings):
22
23
  async def get_manipulators(self) -> list[str]:
23
24
  return list(map(str, range(8)))
24
25
 
25
- async def get_num_axes(self) -> int:
26
+ async def get_axes_count(self) -> int:
26
27
  return 4
27
28
 
28
29
  def get_dimensions(self) -> Vector4:
29
- return Vector4(x=20, y=20, z=20, w=20)
30
+ return array_to_vector4([20] * 4)
30
31
 
31
32
  async def get_position(self, manipulator_id: str) -> Vector4:
32
33
  return self._positions[int(manipulator_id)]
@@ -37,13 +38,17 @@ class FakeBindings(BaseBindings):
37
38
  async def get_shank_count(self, _: str) -> int:
38
39
  return 1
39
40
 
40
- async def get_movement_tolerance(self) -> float:
41
+ def get_movement_tolerance(self) -> float:
41
42
  return 0.001
42
43
 
43
44
  async def set_position(self, manipulator_id: str, position: Vector4, _: float) -> Vector4:
44
45
  self._positions[int(manipulator_id)] = position
45
46
  return position
46
47
 
48
+ async def set_depth(self, manipulator_id: str, depth: float, _: float) -> float:
49
+ self._positions[int(manipulator_id)].w = depth
50
+ return depth
51
+
47
52
  async def stop(self, _: str) -> None:
48
53
  pass
49
54
 
@@ -0,0 +1,278 @@
1
+ """Bindings for New Scale Pathfinder MPM HTTP server platform.
2
+
3
+ MPM works slightly differently than the other platforms since it operates in stereotactic coordinates.
4
+ This means exceptions need to be made for its API.
5
+
6
+ Usage: Instantiate MPMBindings to interact with the New Scale Pathfinder MPM HTTP server platform.
7
+ """
8
+
9
+ from asyncio import get_running_loop, sleep
10
+ from json import dumps
11
+ from typing import Any
12
+
13
+ from requests import JSONDecodeError, get, put
14
+ from vbl_aquarium.models.unity import Vector3, Vector4
15
+
16
+ from ephys_link.util.base_bindings import BaseBindings
17
+ from ephys_link.util.common import scalar_mm_to_um, vector4_to_array
18
+
19
+
20
+ class MPMBinding(BaseBindings):
21
+ """Bindings for New Scale Pathfinder MPM HTTP server platform."""
22
+
23
+ # Valid New Scale manipulator IDs
24
+ VALID_MANIPULATOR_IDS = (
25
+ "A",
26
+ "B",
27
+ "C",
28
+ "D",
29
+ "E",
30
+ "F",
31
+ "G",
32
+ "H",
33
+ "I",
34
+ "J",
35
+ "K",
36
+ "L",
37
+ "M",
38
+ "N",
39
+ "O",
40
+ "P",
41
+ "Q",
42
+ "R",
43
+ "S",
44
+ "T",
45
+ "U",
46
+ "V",
47
+ "W",
48
+ "X",
49
+ "Y",
50
+ "Z",
51
+ "AA",
52
+ "AB",
53
+ "AC",
54
+ "AD",
55
+ "AE",
56
+ "AF",
57
+ "AG",
58
+ "AH",
59
+ "AI",
60
+ "AJ",
61
+ "AK",
62
+ "AL",
63
+ "AM",
64
+ "AN",
65
+ )
66
+
67
+ # Movement polling preferences.
68
+ UNCHANGED_COUNTER_LIMIT = 10
69
+ POLL_INTERVAL = 0.1
70
+
71
+ # Speed preferences (mm/s to use coarse mode).
72
+ COARSE_SPEED_THRESHOLD = 0.1
73
+ INSERTION_SPEED_LIMIT = 9_000
74
+
75
+ def __init__(self, port: int) -> None:
76
+ """Initialize connection to MPM HTTP server.
77
+
78
+ :param port: Port number for MPM HTTP server.
79
+ :type port: int
80
+ """
81
+ self._url = f"http://localhost:{port}"
82
+ self._movement_stopped = False
83
+
84
+ async def get_manipulators(self) -> list[str]:
85
+ return [manipulator["Id"] for manipulator in (await self._query_data())["ProbeArray"]]
86
+
87
+ async def get_axes_count(self) -> int:
88
+ return 3
89
+
90
+ def get_dimensions(self) -> Vector4:
91
+ return Vector4(x=15, y=15, z=15, w=15)
92
+
93
+ async def get_position(self, manipulator_id: str) -> Vector4:
94
+ manipulator_data = await self._manipulator_data(manipulator_id)
95
+ stage_z = manipulator_data["Stage_Z"]
96
+
97
+ await sleep(self.POLL_INTERVAL) # Wait for the stage to stabilize.
98
+
99
+ return Vector4(
100
+ x=manipulator_data["Stage_X"],
101
+ y=manipulator_data["Stage_Y"],
102
+ z=stage_z,
103
+ w=stage_z,
104
+ )
105
+
106
+ async def get_angles(self, manipulator_id: str) -> Vector3:
107
+ manipulator_data = await self._manipulator_data(manipulator_id)
108
+
109
+ # Apply PosteriorAngle to Polar to get the correct angle.
110
+ adjusted_polar = manipulator_data["Polar"] - (await self._query_data())["PosteriorAngle"]
111
+
112
+ return Vector3(
113
+ x=adjusted_polar if adjusted_polar > 0 else 360 + adjusted_polar,
114
+ y=manipulator_data["Pitch"],
115
+ z=manipulator_data["ShankOrientation"],
116
+ )
117
+
118
+ async def get_shank_count(self, manipulator_id: str) -> int:
119
+ return int((await self._manipulator_data(manipulator_id))["ShankCount"])
120
+
121
+ def get_movement_tolerance(self) -> float:
122
+ return 0.01
123
+
124
+ async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
125
+ # Keep track of the previous position to check if the manipulator stopped advancing.
126
+ current_position = await self.get_position(manipulator_id)
127
+ previous_position = current_position
128
+ unchanged_counter = 0
129
+
130
+ # Set step mode based on speed.
131
+ await self._put_request(
132
+ {
133
+ "PutId": "ProbeStepMode",
134
+ "Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id),
135
+ "StepMode": 0 if speed > self.COARSE_SPEED_THRESHOLD else 1,
136
+ }
137
+ )
138
+
139
+ # Send move request.
140
+ await self._put_request(
141
+ {
142
+ "PutId": "ProbeMotion",
143
+ "Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id),
144
+ "Absolute": 1,
145
+ "Stereotactic": 0,
146
+ "AxisMask": 7,
147
+ "X": position.x,
148
+ "Y": position.y,
149
+ "Z": position.z,
150
+ }
151
+ )
152
+
153
+ # Wait for the manipulator to reach the target position or be stopped or stuck.
154
+ while (
155
+ not self._movement_stopped
156
+ and not self._is_vector_close(current_position, position)
157
+ and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT
158
+ ):
159
+ # Wait for a short time before checking again.
160
+ await sleep(self.POLL_INTERVAL)
161
+
162
+ # Update current position.
163
+ current_position = await self.get_position(manipulator_id)
164
+
165
+ # Check if manipulator is not moving.
166
+ if self._is_vector_close(previous_position, current_position):
167
+ # Position did not change.
168
+ unchanged_counter += 1
169
+ else:
170
+ # Position changed.
171
+ unchanged_counter = 0
172
+ previous_position = current_position
173
+
174
+ # Reset movement stopped flag.
175
+ self._movement_stopped = False
176
+
177
+ # Return the final position.
178
+ return await self.get_position(manipulator_id)
179
+
180
+ async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
181
+ # Keep track of the previous depth to check if the manipulator stopped advancing unexpectedly.
182
+ current_depth = (await self.get_position(manipulator_id)).w
183
+ previous_depth = current_depth
184
+ unchanged_counter = 0
185
+
186
+ # Send move request.
187
+ # Convert mm/s to um/min and cap speed at the limit.
188
+ await self._put_request(
189
+ {
190
+ "PutId": "ProbeInsertion",
191
+ "Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id),
192
+ "Distance": scalar_mm_to_um(current_depth - depth),
193
+ "Rate": min(scalar_mm_to_um(speed) * 60, self.INSERTION_SPEED_LIMIT),
194
+ }
195
+ )
196
+
197
+ # Wait for the manipulator to reach the target depth or be stopped or get stuck.
198
+ while not self._movement_stopped and not abs(current_depth - depth) <= self.get_movement_tolerance():
199
+ # Wait for a short time before checking again.
200
+ await sleep(self.POLL_INTERVAL)
201
+
202
+ # Get the current depth.
203
+ current_depth = (await self.get_position(manipulator_id)).w
204
+
205
+ # Check if manipulator is not moving.
206
+ if abs(previous_depth - current_depth) <= self.get_movement_tolerance():
207
+ # Depth did not change.
208
+ unchanged_counter += 1
209
+ else:
210
+ # Depth changed.
211
+ unchanged_counter = 0
212
+ previous_depth = current_depth
213
+
214
+ # Reset movement stopped flag.
215
+ self._movement_stopped = False
216
+
217
+ # Return the final depth.
218
+ return float((await self.get_position(manipulator_id)).w)
219
+
220
+ async def stop(self, manipulator_id: str) -> None:
221
+ request = {"PutId": "ProbeStop", "Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id)}
222
+ await self._put_request(request)
223
+ self._movement_stopped = True
224
+
225
+ def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
226
+ # unified <- platform
227
+ # +x <- -x
228
+ # +y <- +z
229
+ # +z <- +y
230
+ # +w <- -w
231
+
232
+ return Vector4(
233
+ x=self.get_dimensions().x - platform_space.x,
234
+ y=platform_space.z,
235
+ z=platform_space.y,
236
+ w=self.get_dimensions().w - platform_space.w,
237
+ )
238
+
239
+ def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
240
+ # platform <- unified
241
+ # +x <- -x
242
+ # +y <- +z
243
+ # +z <- +y
244
+ # +w <- -w
245
+
246
+ return Vector4(
247
+ x=self.get_dimensions().x - unified_space.x,
248
+ y=unified_space.z,
249
+ z=unified_space.y,
250
+ w=self.get_dimensions().w - unified_space.w,
251
+ )
252
+
253
+ # Helper functions.
254
+ async def _query_data(self) -> Any:
255
+ try:
256
+ return (await get_running_loop().run_in_executor(None, get, self._url)).json()
257
+ except ConnectionError as connectionError:
258
+ error_message = f"Unable to connect to MPM HTTP server: {connectionError}"
259
+ raise RuntimeError(error_message) from connectionError
260
+ except JSONDecodeError as jsonDecodeError:
261
+ error_message = f"Unable to decode JSON response from MPM HTTP server: {jsonDecodeError}"
262
+ raise ValueError(error_message) from jsonDecodeError
263
+
264
+ async def _manipulator_data(self, manipulator_id: str) -> Any:
265
+ probe_data = (await self._query_data())["ProbeArray"]
266
+ for probe in probe_data:
267
+ if probe["Id"] == manipulator_id:
268
+ return probe
269
+
270
+ # If we get here, that means the manipulator doesn't exist.
271
+ error_message = f"Manipulator {manipulator_id} not found."
272
+ raise ValueError(error_message)
273
+
274
+ async def _put_request(self, request: dict[str, Any]) -> None:
275
+ await get_running_loop().run_in_executor(None, put, self._url, dumps(request))
276
+
277
+ def _is_vector_close(self, target: Vector4, current: Vector4) -> bool:
278
+ return all(abs(axis) <= self.get_movement_tolerance() for axis in vector4_to_array(target - current)[:3])
@@ -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))
@@ -9,7 +9,14 @@ from sensapex import UMP, SensapexDevice
9
9
  from vbl_aquarium.models.unity import Vector3, Vector4
10
10
 
11
11
  from ephys_link.util.base_bindings import BaseBindings
12
- from ephys_link.util.common import RESOURCES_PATH, array_to_vector4, mm_to_um, mmps_to_umps, um_to_mm, vector4_to_array
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
+ )
13
20
 
14
21
 
15
22
  class Ump4Bindings(BaseBindings):
@@ -28,7 +35,7 @@ class Ump4Bindings(BaseBindings):
28
35
  async def get_manipulators(self) -> list[str]:
29
36
  return list(map(str, self._ump.list_devices()))
30
37
 
31
- async def get_num_axes(self) -> int:
38
+ async def get_axes_count(self) -> int:
32
39
  return 4
33
40
 
34
41
  def get_dimensions(self) -> Vector4:
@@ -55,32 +62,21 @@ class Ump4Bindings(BaseBindings):
55
62
  error_message = "UMP-4 does not support getting shank count"
56
63
  raise AttributeError(error_message)
57
64
 
58
- async def get_movement_tolerance(self) -> float:
65
+ def get_movement_tolerance(self) -> float:
59
66
  return 0.001
60
67
 
68
+ # noinspection DuplicatedCode
61
69
  async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
62
- """Set the position of the manipulator.
63
-
64
- Waits using Asyncio until the movement is finished. This assumes the application is running in an event loop.
65
-
66
- :param manipulator_id: Manipulator ID.
67
- :type manipulator_id: str
68
- :param position: Platform space position to set the manipulator to (mm).
69
- :type position: Vector4
70
- :param speed: Speed to move the manipulator to the position (mm/s).
71
- :type speed: float
72
- :returns: Final position of the manipulator in platform space (mm).
73
- :rtype: Vector4
74
- :raises RuntimeError: If the movement is interrupted.
75
- """
76
70
  # Convert position to micrometers.
77
- target_position_um = mm_to_um(position)
71
+ target_position_um = vector_mm_to_um(position)
78
72
 
79
73
  # Request movement.
80
- movement = self._get_device(manipulator_id).goto_pos(vector4_to_array(target_position_um), mmps_to_umps(speed))
74
+ movement = self._get_device(manipulator_id).goto_pos(
75
+ vector4_to_array(target_position_um), scalar_mm_to_um(speed)
76
+ )
81
77
 
82
78
  # Wait for movement to finish.
83
- 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)
84
80
 
85
81
  # Handle interrupted movement.
86
82
  if movement.interrupted:
@@ -89,6 +85,17 @@ class Ump4Bindings(BaseBindings):
89
85
 
90
86
  return um_to_mm(array_to_vector4(movement.last_pos))
91
87
 
88
+ async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
89
+ # Augment current position with depth.
90
+ current_position = await self.get_position(manipulator_id)
91
+ new_platform_position = current_position.model_copy(update={"w": depth})
92
+
93
+ # Make the movement.
94
+ final_platform_position = await self.set_position(manipulator_id, new_platform_position, speed)
95
+
96
+ # Return the final depth.
97
+ return float(final_platform_position.w)
98
+
92
99
  async def stop(self, manipulator_id: str) -> None:
93
100
  self._get_device(manipulator_id).stop()
94
101
 
@@ -25,7 +25,7 @@ class BaseBindings(ABC):
25
25
  """
26
26
 
27
27
  @abstractmethod
28
- async def get_num_axes(self) -> int:
28
+ async def get_axes_count(self) -> int:
29
29
  """Get the number of axes for the current platform.
30
30
 
31
31
  :returns: Number of axes.
@@ -76,7 +76,7 @@ class BaseBindings(ABC):
76
76
  """
77
77
 
78
78
  @abstractmethod
79
- async def get_movement_tolerance(self) -> float:
79
+ def get_movement_tolerance(self) -> float:
80
80
  """Get the tolerance for how close the final position must be to the target position in a movement (mm).
81
81
 
82
82
  :returns: Movement tolerance (mm).
@@ -88,7 +88,6 @@ class BaseBindings(ABC):
88
88
  """Set the position of a manipulator.
89
89
 
90
90
  This will directly set the position in the original platform space.
91
- Unified space coordinates will need to be converted to platform space.
92
91
  For 3-axis manipulators, the first 3 values of the position will be used.
93
92
 
94
93
  :param manipulator_id: Manipulator ID.
@@ -101,6 +100,22 @@ class BaseBindings(ABC):
101
100
  :rtype: Vector4
102
101
  """
103
102
 
103
+ @abstractmethod
104
+ async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
105
+ """Set the depth of a manipulator.
106
+
107
+ This will directly set the depth stage in the original platform space.
108
+
109
+ :param manipulator_id: Manipulator ID.
110
+ :type manipulator_id: str
111
+ :param depth: Depth to set the manipulator to (mm).
112
+ :type depth: float
113
+ :param speed: Speed to move the manipulator to the depth (mm/s).
114
+ :type speed: float
115
+ :returns: Final depth of the manipulator in platform space (mm).
116
+ :rtype: float
117
+ """
118
+
104
119
  @abstractmethod
105
120
  async def stop(self, manipulator_id: str) -> None:
106
121
  """Stop a manipulator."""
@@ -53,23 +53,23 @@ def check_for_updates() -> None:
53
53
  # Unit conversions
54
54
 
55
55
 
56
- def mmps_to_umps(mmps: float) -> float:
57
- """Convert millimeters per second to micrometers per second.
56
+ def scalar_mm_to_um(mm: float) -> float:
57
+ """Convert scalar values of millimeters to micrometers.
58
58
 
59
- :param mmps: Speed in millimeters per second.
60
- :type mmps: float
61
- :returns: Speed in micrometers per second.
59
+ :param mm: Scalar value in millimeters.
60
+ :type mm: float
61
+ :returns: Scalar value in micrometers.
62
62
  :rtype: float
63
63
  """
64
- return mmps * 1_000
64
+ return mm * 1_000
65
65
 
66
66
 
67
- def mm_to_um(mm: Vector4) -> Vector4:
68
- """Convert millimeters to micrometers.
67
+ def vector_mm_to_um(mm: Vector4) -> Vector4:
68
+ """Convert vector values of millimeters to micrometers.
69
69
 
70
- :param mm: Length in millimeters.
70
+ :param mm: Vector in millimeters.
71
71
  :type mm: Vector4
72
- :returns: Length in micrometers.
72
+ :returns: Vector in micrometers.
73
73
  :rtype: Vector4
74
74
  """
75
75
  return mm * 1_000
@@ -25,12 +25,12 @@ class Console:
25
25
 
26
26
  # Config logger.
27
27
  basicConfig(
28
- level=DEBUG if enable_debug else INFO,
29
28
  format="%(message)s",
30
29
  datefmt="[%I:%M:%S %p]",
31
- handlers=[RichHandler(rich_tracebacks=True)],
30
+ handlers=[RichHandler(rich_tracebacks=True, markup=True)],
32
31
  )
33
32
  self._log = getLogger("rich")
33
+ self._log.setLevel(DEBUG if enable_debug else INFO)
34
34
 
35
35
  # Install Rich traceback.
36
36
  install()
@@ -71,7 +71,7 @@ class Console:
71
71
  :param msg: Critical message to print.
72
72
  :type msg: str
73
73
  """
74
- self._log.critical(f"[b i red]{msg}", extra={"markup": True})
74
+ self._log.critical(f"[b i red]{msg}")
75
75
 
76
76
  @staticmethod
77
77
  def pretty_exception(exception: Exception) -> str:
@@ -92,9 +92,7 @@ class Console:
92
92
  :param exception: Exception to print.
93
93
  :type exception: Exception
94
94
  """
95
- self._log.exception(
96
- f"[b magenta]{label}:[/] [magenta]{Console.pretty_exception(exception)}", extra={"markup": True}
97
- )
95
+ self._log.exception(f"[b magenta]{label}:[/] [magenta]{Console.pretty_exception(exception)}")
98
96
 
99
97
  # Helper methods.
100
98
  def _repeatable_log(self, log_type: int, label: str, message: str) -> None:
@@ -124,10 +122,9 @@ class Console:
124
122
  self._log.log(
125
123
  self._last_message[0],
126
124
  f"{self._last_message[1]}:[/] {self._last_message[2]}[/] x {self._repeat_counter}",
127
- extra={"markup": True},
128
125
  )
129
126
  self._repeat_counter = 0
130
127
 
131
128
  # Log new message.
132
- self._log.log(log_type, f"{label}:[/] {message}", extra={"markup": True})
129
+ self._log.log(log_type, f"{label}:[/] {message}")
133
130
  self._last_message = message_set
@@ -1,15 +0,0 @@
1
- from asyncio import run
2
-
3
- from vbl_aquarium.models.ephys_link import SetPositionRequest
4
- from vbl_aquarium.models.unity import Vector4
5
-
6
- from ephys_link.back_end.platform_handler import PlatformHandler
7
- from ephys_link.util.console import Console
8
-
9
- c = Console(enable_debug=True)
10
- p = PlatformHandler("ump-4", c)
11
- target = Vector4()
12
- # target = Vector4(x=10, y=10, z=10, w=10)
13
-
14
- print(run(p.set_position(SetPositionRequest(manipulator_id="6", position=target, speed=5))).to_json_string())
15
- print("Done!")
@@ -1 +0,0 @@
1
- __version__ = "2.0.0b2"
File without changes
File without changes
File without changes