imdclient 0.1.3__py3-none-any.whl → 0.2.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.
Files changed (48) hide show
  1. imdclient/IMDClient.py +43 -12
  2. imdclient/IMDProtocol.py +1 -0
  3. imdclient/__init__.py +0 -5
  4. imdclient/data/gromacs/md/gromacs_v3_nst1.mdp +3 -3
  5. imdclient/data/namd/md/namd3 +0 -0
  6. imdclient/data/namd/md/namd_v3_nst_1.namd +1 -1
  7. imdclient/tests/base.py +108 -83
  8. imdclient/tests/conftest.py +0 -39
  9. imdclient/tests/datafiles.py +16 -1
  10. imdclient/tests/docker_testing/docker.md +1 -1
  11. imdclient/tests/hpc_testing/gromacs/README.md +112 -0
  12. imdclient/tests/hpc_testing/gromacs/gmx_gpu_test.mdp +58 -0
  13. imdclient/tests/hpc_testing/gromacs/gmx_gpu_test.top +11764 -0
  14. imdclient/tests/hpc_testing/gromacs/struct.gro +21151 -0
  15. imdclient/tests/hpc_testing/gromacs/validate_gmx.sh +90 -0
  16. imdclient/tests/hpc_testing/lammps/README.md +62 -0
  17. imdclient/tests/hpc_testing/lammps/lammps_v3_nst_1.in +71 -0
  18. imdclient/tests/hpc_testing/lammps/topology_after_min.data +8022 -0
  19. imdclient/tests/hpc_testing/lammps/validate_lmp.sh +66 -0
  20. imdclient/tests/hpc_testing/namd/README.md +147 -0
  21. imdclient/tests/hpc_testing/namd/alanin.params +402 -0
  22. imdclient/tests/hpc_testing/namd/alanin.pdb +77 -0
  23. imdclient/tests/hpc_testing/namd/alanin.psf +206 -0
  24. imdclient/tests/hpc_testing/namd/namd_v3_nst_1.namd +59 -0
  25. imdclient/tests/hpc_testing/namd/validate_namd.sh +71 -0
  26. imdclient/tests/minimalreader.py +86 -0
  27. imdclient/tests/server.py +6 -14
  28. imdclient/tests/test_gromacs.py +15 -3
  29. imdclient/tests/test_imdclient.py +26 -7
  30. imdclient/tests/test_lammps.py +22 -19
  31. imdclient/tests/test_manual.py +224 -66
  32. imdclient/tests/test_namd.py +39 -16
  33. imdclient/tests/test_utils.py +31 -0
  34. imdclient/utils.py +50 -17
  35. {imdclient-0.1.3.dist-info → imdclient-0.2.0b0.dist-info}/METADATA +60 -39
  36. imdclient-0.2.0b0.dist-info/RECORD +53 -0
  37. {imdclient-0.1.3.dist-info → imdclient-0.2.0b0.dist-info}/WHEEL +1 -1
  38. {imdclient-0.1.3.dist-info → imdclient-0.2.0b0.dist-info/licenses}/AUTHORS.md +4 -1
  39. {imdclient-0.1.3.dist-info → imdclient-0.2.0b0.dist-info/licenses}/LICENSE +3 -1
  40. imdclient/IMD.py +0 -130
  41. imdclient/backends.py +0 -352
  42. imdclient/results.py +0 -332
  43. imdclient/streamanalysis.py +0 -1056
  44. imdclient/streambase.py +0 -199
  45. imdclient/tests/test_imdreader.py +0 -658
  46. imdclient/tests/test_stream_analysis.py +0 -61
  47. imdclient-0.1.3.dist-info/RECORD +0 -42
  48. {imdclient-0.1.3.dist-info → imdclient-0.2.0b0.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,10 @@
1
- from imdclient.IMD import IMDReader
2
- import pytest
1
+ from .minimalreader import MinimalReader
3
2
  import MDAnalysis as mda
4
- from MDAnalysisTests.coordinates.base import assert_timestep_almost_equal
5
- from numpy.testing import (
6
- assert_allclose,
7
- )
3
+ from numpy.testing import assert_allclose
8
4
  import numpy as np
9
- from .base import assert_allclose_with_logging
10
5
  from pathlib import Path
11
-
6
+ import time
7
+ import argparse
12
8
  import logging
13
9
 
14
10
  logger = logging.getLogger("imdclient.IMDClient")
@@ -20,74 +16,236 @@ file_handler.setFormatter(formatter)
20
16
  logger.addHandler(file_handler)
21
17
  logger.setLevel(logging.DEBUG)
22
18
 
19
+ """
20
+ Tool for running IMDv3 integration tests via the command line.
23
21
 
24
- class TestIMDv3Manual:
25
- """
26
- Tool for running IMDv3 integration tests via the command line.
22
+ To use, start the simulation, wait for it to be ready for an IMD connection,
23
+ and then run this command relative to the root of the cloned respository:
27
24
 
28
- To use, start the simulation, wait for it to be ready for an IMD connection,
29
- and then run this command relative to the root of the cloned respository:
25
+ python imdclient/tests/test_manual.py \
26
+ --topol_path <path/to/topology> \
27
+ --traj_path <path/to/trajectory> \
28
+ --first_frame <first frame to compare> \
29
+ --tmp_path <temporary directory for IMD trajectory>
30
30
 
31
- pytest -s imdclient/tests/test_manual.py \
32
- --topol_path_arg <path/to/topology> \
33
- --traj_path_arg <path/to/trajectory> \
34
- --first_frame_arg <first traj frame to compare to IMD>
31
+ Where the topology is the same topology as the IMD system, the trajectory is the path where
32
+ the trajectory of the running simulation is being written, and the first frame is the first frame of the
33
+ trajectory which should be compared to IMD data read from the socket (0 for GROMACS and NAMD, 1 for LAMMPS)
34
+ """
35
+
36
+
37
+ def assert_allclose_with_logging(a, b, rtol=1e-07, atol=0, equal_nan=False):
38
+ """
39
+ Custom function to compare two arrays element-wise, similar to np.testing.assert_allclose,
40
+ but logs all non-matching values.
35
41
 
36
- Where the topology is the same topology as the IMD system, the trajectory is the path where
37
- the trajectory of the running simulation is being written, and the first frame is the first frame of the
38
- trajectory which should be compared to IMD data read from the socket (0 for GROMACS and NAMD, 1 for LAMMPS)
42
+ Parameters:
43
+ a, b : array_like
44
+ Input arrays to compare.
45
+ rtol : float
46
+ Relative tolerance.
47
+ atol : float
48
+ Absolute tolerance.
49
+ equal_nan : bool
50
+ Whether to compare NaNs as equal.
39
51
  """
52
+ # Convert inputs to numpy arrays
53
+ a = np.asarray(a)
54
+ b = np.asarray(b)
55
+
56
+ # Compute the absolute difference
57
+ diff = np.abs(a - b)
58
+
59
+ # Check if values are within tolerance
60
+ not_close = diff > (atol + rtol * np.abs(b))
61
+
62
+ # Check if there are any NaNs and handle them if necessary
63
+ if equal_nan:
64
+ nan_mask = np.isnan(a) & np.isnan(b)
65
+ not_close &= ~nan_mask
66
+
67
+ # Log all the values that are not close
68
+ if np.any(not_close):
69
+ print("The following values do not match within tolerance:")
70
+ for idx in np.argwhere(not_close):
71
+ logger.debug(
72
+ f"a[{tuple(idx)}]: {a[tuple(idx)]}, b[{tuple(idx)}]: {b[tuple(idx)]}, diff: {diff[tuple(idx)]}"
73
+ )
74
+ # Optionally raise an error after logging if you want it to behave like assert
75
+ raise AssertionError("Arrays are not almost equal.")
76
+ else:
77
+ print("All values are within tolerance.")
78
+
40
79
 
41
- @pytest.fixture()
42
- def true_u(self, imd_u, topol_path_arg, traj_path_arg):
43
- return mda.Universe(topol_path_arg, traj_path_arg)
44
-
45
- @pytest.fixture()
46
- def imd_u(self, topol_path_arg, tmp_path):
47
- tmp_u = mda.Universe(topol_path_arg, "imd://localhost:8888")
48
- with mda.Writer(
49
- f"{tmp_path.as_posix()}/imd_test_traj.trr", tmp_u.atoms.n_atoms
50
- ) as w:
51
- for ts in tmp_u.trajectory:
52
- w.write(tmp_u.atoms)
53
- imd_u = mda.Universe(
54
- topol_path_arg, f"{tmp_path.as_posix()}/imd_test_traj.trr"
80
+ def load_true_universe(topol_path, traj_path):
81
+ if topol_path.endswith(".data"):
82
+ return mda.Universe(
83
+ topol_path,
84
+ traj_path,
85
+ atom_style="id type x y z",
86
+ convert_units=False,
55
87
  )
56
- yield imd_u
88
+ return mda.Universe(topol_path, traj_path)
57
89
 
58
- def test_compare_imd_to_true_traj(self, true_u, imd_u, first_frame_arg):
59
90
 
60
- for i in range(first_frame_arg, len(true_u.trajectory)):
91
+ def load_imd_universe(topol_path, tmp_path):
92
+ # Pass atom_style (ignored if not using LAMMPS topol)
93
+ n_atoms = mda.Universe(
94
+ topol_path,
95
+ atom_style="id type x y z",
96
+ convert_units=False,
97
+ ).atoms.n_atoms
98
+ tmp_u = MinimalReader(
99
+ f"imd://localhost:8888", n_atoms=n_atoms, process_stream=True
100
+ )
101
+ return tmp_u
102
+
103
+
104
+ def test_compare_imd_to_true_traj_vel(imd_u, true_u_vel, first_frame):
105
+ for i in range(first_frame, len(true_u_vel.trajectory)):
106
+ # Manually convert unit
107
+ assert_allclose_with_logging(
108
+ true_u_vel.trajectory[i].positions * 20.45482706,
109
+ imd_u.trajectory[i - first_frame].velocities,
110
+ atol=1e-03,
111
+ )
112
+
113
+
114
+ def test_compare_imd_to_true_traj_forces(imd_u, true_u_force, first_frame):
115
+ for i in range(first_frame, len(true_u_force.trajectory)):
116
+ assert_allclose_with_logging(
117
+ true_u_force.trajectory[i].positions,
118
+ imd_u.trajectory[i - first_frame].forces,
119
+ atol=1e-03,
120
+ )
121
+
122
+
123
+ def test_compare_imd_to_true_traj(
124
+ imd_u, true_u, first_frame, vel, force, dt, step
125
+ ):
126
+ for i in range(first_frame, len(true_u.trajectory)):
127
+ assert_allclose(
128
+ true_u.trajectory[i].time,
129
+ imd_u.trajectory[i - first_frame].time,
130
+ atol=1e-03,
131
+ )
132
+ # Issue #63
133
+ # if dt:
134
+ # assert_allclose(
135
+ # true_u.trajectory[i].dt,
136
+ # imd_u.trajectory[i - first_frame].dt,
137
+ # atol=1e-03,
138
+ # )
139
+ if step:
61
140
  assert_allclose(
62
- true_u.trajectory[i].time,
63
- imd_u.trajectory[i - first_frame_arg].time,
141
+ true_u.trajectory[i].data["step"],
142
+ imd_u.trajectory[i - first_frame].data["step"],
143
+ )
144
+ assert_allclose_with_logging(
145
+ true_u.trajectory[i].dimensions,
146
+ imd_u.trajectory[i - first_frame].dimensions,
147
+ atol=1e-03,
148
+ )
149
+ assert_allclose_with_logging(
150
+ true_u.trajectory[i].positions,
151
+ imd_u.trajectory[i - first_frame].positions,
152
+ atol=1e-03,
153
+ )
154
+ if vel:
155
+ assert_allclose_with_logging(
156
+ true_u.trajectory[i].velocities,
157
+ imd_u.trajectory[i - first_frame].velocities,
64
158
  atol=1e-03,
65
159
  )
66
- assert_allclose(
67
- true_u.trajectory[i].data["step"],
68
- imd_u.trajectory[i - first_frame_arg].data["step"],
160
+ if force:
161
+ assert_allclose_with_logging(
162
+ true_u.trajectory[i].forces,
163
+ imd_u.trajectory[i - first_frame].forces,
164
+ atol=1e-03,
165
+ )
166
+
167
+
168
+ def main():
169
+ parser = argparse.ArgumentParser(description="IMDv3 Integration Test Tool")
170
+ parser.add_argument(
171
+ "--topol_path", required=True, help="Path to topology file"
172
+ )
173
+ parser.add_argument(
174
+ "--traj_path", required=True, help="Path to trajectory file"
175
+ )
176
+ parser.add_argument(
177
+ "--vel_path",
178
+ required=False,
179
+ help="Path to velocities trajectory file (NAMD only)",
180
+ )
181
+ parser.add_argument(
182
+ "--force_path",
183
+ required=False,
184
+ help="Path to forces trajectory file (NAMD only)",
185
+ )
186
+ parser.add_argument(
187
+ "--first_frame",
188
+ type=int,
189
+ required=True,
190
+ help="First frame to compare (0 for GROMACS and NAMD, 1 for LAMMPS)",
191
+ )
192
+ parser.add_argument(
193
+ "--tmp_path", default=".", help="Temporary directory for IMD traj"
194
+ )
195
+
196
+ args = parser.parse_args()
197
+
198
+ print(
199
+ "Writing IMD trajectory to temporary directory...\n===================="
200
+ )
201
+ imd_u = load_imd_universe(args.topol_path, args.tmp_path)
202
+
203
+ print("Loading source of truth trajectory...\n====================")
204
+ true_u = load_true_universe(args.topol_path, args.traj_path)
205
+
206
+ try:
207
+ print("Comparing trajectories...\n====================")
208
+ vel_in_trr = args.vel_path is None
209
+ force_in_trr = args.force_path is None
210
+ dt_in_trr = not args.topol_path.endswith(".data")
211
+ # True when not using DCDReader
212
+ step_in_trr = not args.traj_path.endswith(".coor")
213
+
214
+ test_compare_imd_to_true_traj(
215
+ imd_u,
216
+ true_u,
217
+ args.first_frame,
218
+ vel_in_trr,
219
+ force_in_trr,
220
+ dt_in_trr,
221
+ step_in_trr,
222
+ )
223
+
224
+ if args.vel_path is not None:
225
+ print(
226
+ "Loading source of truth velocity trajectory...\n===================="
227
+ )
228
+ true_vel = load_true_universe(args.topol_path, args.vel_path)
229
+ print("Comparing velocities...")
230
+ test_compare_imd_to_true_traj_vel(imd_u, true_vel, args.first_frame)
231
+
232
+ if args.force_path is not None:
233
+ print(
234
+ "Loading source of truth force trajectory...\n===================="
69
235
  )
70
- if true_u.trajectory[i].dimensions is not None:
71
- assert_allclose_with_logging(
72
- true_u.trajectory[i].dimensions,
73
- imd_u.trajectory[i - first_frame_arg].dimensions,
74
- atol=1e-03,
75
- )
76
- if true_u.trajectory[i].has_positions:
77
- assert_allclose_with_logging(
78
- true_u.trajectory[i].positions,
79
- imd_u.trajectory[i - first_frame_arg].positions,
80
- atol=1e-03,
81
- )
82
- if true_u.trajectory[i].has_velocities:
83
- assert_allclose_with_logging(
84
- true_u.trajectory[i].velocities,
85
- imd_u.trajectory[i - first_frame_arg].velocities,
86
- atol=1e-03,
87
- )
88
- if true_u.trajectory[i].has_forces:
89
- assert_allclose_with_logging(
90
- true_u.trajectory[i].forces,
91
- imd_u.trajectory[i - first_frame_arg].forces,
92
- atol=1e-03,
93
- )
236
+ true_force = load_true_universe(args.topol_path, args.force_path)
237
+ print("Comparing forces...")
238
+ test_compare_imd_to_true_traj_forces(
239
+ imd_u, true_force, args.first_frame
240
+ )
241
+
242
+ print("All tests passed!")
243
+
244
+ except AssertionError as e:
245
+ logger.error("Comparison failed!")
246
+ print(f"Test failed: {e}")
247
+ raise e
248
+
249
+
250
+ if __name__ == "__main__":
251
+ main()
@@ -1,6 +1,13 @@
1
- import MDAnalysis as mda
2
- import pytest
3
1
  import logging
2
+ from pathlib import Path
3
+ import re
4
+
5
+ import pytest
6
+ from numpy.testing import (
7
+ assert_allclose,
8
+ )
9
+ import MDAnalysis as mda
10
+
4
11
  from .base import IMDv3IntegrationTest, assert_allclose_with_logging
5
12
  from .datafiles import (
6
13
  NAMD_TOPOL,
@@ -9,10 +16,6 @@ from .datafiles import (
9
16
  NAMD_PARAMS,
10
17
  NAMD_PSF,
11
18
  )
12
- from pathlib import Path
13
- from numpy.testing import (
14
- assert_allclose,
15
- )
16
19
 
17
20
  logger = logging.getLogger("imdclient.IMDClient")
18
21
  file_handler = logging.FileHandler("namd_test.log")
@@ -26,6 +29,10 @@ logger.setLevel(logging.DEBUG)
26
29
 
27
30
  class TestIMDv3NAMD(IMDv3IntegrationTest):
28
31
 
32
+ @pytest.fixture()
33
+ def container_name(self):
34
+ return "ghcr.io/becksteinlab/streaming-namd-docker:main-common-cpu"
35
+
29
36
  @pytest.fixture(params=[NAMD_CONF_NST_1, NAMD_CONF_NST_8])
30
37
  def inp(self, request):
31
38
  return request.param
@@ -42,6 +49,18 @@ class TestIMDv3NAMD(IMDv3IntegrationTest):
42
49
  def topol(self):
43
50
  return Path(NAMD_TOPOL).name
44
51
 
52
+ @pytest.fixture()
53
+ def dt(self, inp):
54
+ pattern = re.compile(r"^\s*timestep\s*(\S+)")
55
+ with open(inp, "r") as file:
56
+ for line in file:
57
+ match = pattern.match(line)
58
+ if match:
59
+ # NAMD timestep is in femtoseconds, convert to picoseconds
60
+ # as IMDv3 expects dt in ps, 1fs = 0.001ps
61
+ return float(match.group(1)) * 0.001
62
+ raise ValueError(f"No dt found in {inp}")
63
+
45
64
  @pytest.fixture()
46
65
  def true_u(self, topol, imd_u, tmp_path):
47
66
  u = mda.Universe(
@@ -75,40 +94,43 @@ class TestIMDv3NAMD(IMDv3IntegrationTest):
75
94
  return 0
76
95
 
77
96
  # Compare coords, box, time, dt, step
78
- def test_compare_imd_to_true_traj(self, imd_u, true_u, first_frame):
97
+ def test_compare_imd_to_true_traj(self, imd_u, true_u, first_frame, dt):
79
98
  for i in range(first_frame, len(true_u.trajectory)):
99
+
80
100
  assert_allclose(
81
101
  true_u.trajectory[i].time,
82
102
  imd_u.trajectory[i - first_frame].time,
83
103
  atol=1e-03,
84
104
  )
105
+
85
106
  assert_allclose(
86
- true_u.trajectory[i].dt,
107
+ dt,
87
108
  imd_u.trajectory[i - first_frame].dt,
88
109
  atol=1e-03,
89
110
  )
90
- assert_allclose(
91
- true_u.trajectory[i].data["step"],
92
- imd_u.trajectory[i - first_frame].data["step"],
93
- )
111
+ # step in DCDReader is frame index, not integration step
112
+ # don't compare step
113
+
94
114
  assert_allclose_with_logging(
95
115
  true_u.trajectory[i].dimensions,
96
116
  imd_u.trajectory[i - first_frame].dimensions,
97
117
  atol=1e-03,
98
118
  )
119
+
99
120
  assert_allclose_with_logging(
100
121
  true_u.trajectory[i].positions,
101
122
  imd_u.trajectory[i - first_frame].positions,
102
123
  atol=1e-03,
103
124
  )
104
125
 
126
+ # Since NAMD does not write velocities, forces to the DCD file, we need to do so seperately by extracting that info from their respective DCD files
105
127
  # Compare velocities
106
- def test_compare_imd_to_true_traj_vel(
107
- self, imd_u, true_u_vel, first_frame
108
- ):
128
+ def test_compare_imd_to_true_traj_vel(self, imd_u, true_u_vel, first_frame):
109
129
  for i in range(first_frame, len(true_u_vel.trajectory)):
130
+
110
131
  assert_allclose_with_logging(
111
- true_u_vel.trajectory[i].positions,
132
+ # Unit conversion
133
+ true_u_vel.trajectory[i].positions * 20.45482706,
112
134
  imd_u.trajectory[i - first_frame].velocities,
113
135
  atol=1e-03,
114
136
  )
@@ -118,6 +140,7 @@ class TestIMDv3NAMD(IMDv3IntegrationTest):
118
140
  self, imd_u, true_u_force, first_frame
119
141
  ):
120
142
  for i in range(first_frame, len(true_u_force.trajectory)):
143
+
121
144
  assert_allclose_with_logging(
122
145
  true_u_force.trajectory[i].positions,
123
146
  imd_u.trajectory[i - first_frame].forces,
@@ -0,0 +1,31 @@
1
+ # tests/test_utils.py
2
+ import pytest
3
+ from imdclient.utils import parse_host_port
4
+
5
+
6
+ @pytest.mark.parametrize(
7
+ "server_address,host_port",
8
+ [
9
+ ("imd://localhost:8888", ("localhost", 8888)),
10
+ ("imd://example.com:12345", ("example.com", 12345)),
11
+ ],
12
+ )
13
+ def test_parse_host_port_valid(server_address, host_port):
14
+ assert parse_host_port(server_address) == host_port
15
+
16
+
17
+ @pytest.mark.parametrize(
18
+ "server_address",
19
+ [
20
+ "", # empty
21
+ "http://localhost:80", # wrong protocol prefix
22
+ "imd://", # missing host and port
23
+ "imd://localhost:", # missing port
24
+ "imd://:8080", # missing host
25
+ "imd://host:notaport", # port not integer
26
+ "imd://host:80:90", # too many segments
27
+ ],
28
+ )
29
+ def test_parse_host_port_invalid(server_address):
30
+ with pytest.raises(ValueError):
31
+ parse_host_port(server_address)
imdclient/utils.py CHANGED
@@ -43,23 +43,6 @@ class timeit(object):
43
43
  # always propagate exceptions forward
44
44
  return False
45
45
 
46
- # NOTE: think of other edge cases as well- should be robust
47
- def parse_host_port(filename):
48
- if not filename.startswith("imd://"):
49
- raise ValueError("IMDReader: URL must be in the format 'imd://host:port'")
50
-
51
- # Check if the format is correct
52
- parts = filename.split("imd://")[1].split(":")
53
- if len(parts) == 2:
54
- host = parts[0]
55
- try:
56
- port = int(parts[1])
57
- return (host, port)
58
- except ValueError:
59
- raise ValueError("IMDReader: Port must be an integer")
60
- else:
61
- raise ValueError("IMDReader: URL must be in the format 'imd://host:port'")
62
-
63
46
 
64
47
  def approximate_timestep_memsize(
65
48
  n_atoms, energies, dimensions, positions, velocities, forces
@@ -116,3 +99,53 @@ def sock_contains_data(sock, timeout) -> bool:
116
99
  [sock], [], [], timeout
117
100
  )
118
101
  return sock in ready_to_read
102
+
103
+
104
+ def parse_host_port(filename):
105
+ """
106
+ Parses a URL in the format 'imd://host:port' and returns the host and port.
107
+ Parameters
108
+ ----------
109
+ filename : str
110
+ The URL to parse, must be in the format 'imd://host:port'.
111
+
112
+ Returns
113
+ -------
114
+ tuple[str, int]
115
+ A 2-tuple ``(host, port)`` where `host` is the host server name
116
+ and `port` is the TCP port number.
117
+ Raises
118
+ ------
119
+ ValueError
120
+ If the URL is not in the correct format or if the host or port is invalid.
121
+
122
+ Examples
123
+ --------
124
+ >>> parse_host_port("imd://localhost:8888")
125
+ ('localhost', 8888)
126
+ >>> parse_host_port("invalid://localhost:12345")
127
+ Traceback (most recent call last):
128
+ ... ValueError: IMDClient: URL must be in the format 'imd://host:port'
129
+ """
130
+ if not filename.startswith("imd://"):
131
+ raise ValueError(
132
+ "IMDClient: URL must be in the format 'imd://host:port'"
133
+ )
134
+
135
+ # Check if the format is correct
136
+ parts = filename.split("imd://")[1].split(":")
137
+ if len(parts) == 2:
138
+ host = parts[0]
139
+ if not host:
140
+ raise ValueError("IMDClient: Host cannot be empty")
141
+ if not parts[1]:
142
+ raise ValueError("IMDClient: Port cannot be empty")
143
+ try:
144
+ port = int(parts[1])
145
+ return (host, port)
146
+ except ValueError:
147
+ raise ValueError("IMDClient: Port must be an integer")
148
+ else:
149
+ raise ValueError(
150
+ "IMDClient: URL must be in the format 'imd://host:port'"
151
+ )