bluesky / ophyd

hardware abstraction in Python with an emphasis on EPICS
https://blueskyproject.io/ophyd
BSD 3-Clause "New" or "Revised" License
49 stars 78 forks source link

`ophyd.sim.det` issue with configuration #1083

Open mrakitin opened 1 year ago

mrakitin commented 1 year ago

While testing bluesky/databroker#750, we discovered that the simulated detector (ophyd.sim.det) does not work well with databroker v2.

Error

Here is the error while reading the recorded configuration (expand it):

```py [15] ▶ db[-1].primary.config['det'].read() --------------------------------------------------------------------------- WouldBlock Traceback (most recent call last) File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/anyio/streams/memory.py:94, in MemoryObjectReceiveStream.receive(self) 93 try: ---> 94 return self.receive_nowait() 95 except WouldBlock: 96 # Add ourselves in the queue File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/anyio/streams/memory.py:89, in MemoryObjectReceiveStream.receive_nowait(self) 87 raise EndOfStream ---> 89 raise WouldBlock WouldBlock: During handling of the above exception, another exception occurred: EndOfStream Traceback (most recent call last) File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/base.py:77, in BaseHTTPMiddleware.__call__..call_next(request) 76 try: ---> 77 message = await recv_stream.receive() 78 except anyio.EndOfStream: File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/anyio/streams/memory.py:114, in MemoryObjectReceiveStream.receive(self) 113 else: --> 114 raise EndOfStream EndOfStream: During handling of the above exception, another exception occurred: ValueError Traceback (most recent call last) Cell In[15], line 1 ----> 1 db[-1].primary.config['det'].read() File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/client/node.py:313, in Node.__getitem__(self, key, _ignore_inlined_contents) 311 if self_link.endswith("/"): 312 self_link = self_link[:-1] --> 313 content = self.context.get_json( 314 self_link + f"/{key}", 315 ) 316 except ClientError as err: 317 if err.response.status_code == 404: File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/client/context.py:541, in Context.get_json(self, path, stream, **kwargs) 539 def get_json(self, path, stream=False, **kwargs): 540 return msgpack.unpackb( --> 541 self.get_content( 542 path, accept="application/x-msgpack", stream=stream, **kwargs 543 ), 544 timestamp=3, # Decode msgpack Timestamp as datetime.datetime object. 545 ) File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/client/context.py:467, in Context.get_content(self, path, accept, stream, revalidate, **kwargs) 464 return content 465 if self._cache is None: 466 # No cache, so we can use the client straightforwardly. --> 467 response = self.http_client.send(request, stream=stream) 468 handle_error(response) 469 if response.headers.get("content-encoding") == "blosc": File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/httpx/_client.py:902, in Client.send(self, request, stream, auth, follow_redirects) 894 follow_redirects = ( 895 self.follow_redirects 896 if isinstance(follow_redirects, UseClientDefault) 897 else follow_redirects 898 ) 900 auth = self._build_request_auth(request, auth) --> 902 response = self._send_handling_auth( 903 request, 904 auth=auth, 905 follow_redirects=follow_redirects, 906 history=[], 907 ) 908 try: 909 if not stream: File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/httpx/_client.py:930, in Client._send_handling_auth(self, request, auth, follow_redirects, history) 927 request = next(auth_flow) 929 while True: --> 930 response = self._send_handling_redirects( 931 request, 932 follow_redirects=follow_redirects, 933 history=history, 934 ) 935 try: 936 try: File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/httpx/_client.py:967, in Client._send_handling_redirects(self, request, follow_redirects, history) 964 for hook in self._event_hooks["request"]: 965 hook(request) --> 967 response = self._send_single_request(request) 968 try: 969 for hook in self._event_hooks["response"]: File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/httpx/_client.py:1003, in Client._send_single_request(self, request) 998 raise RuntimeError( 999 "Attempted to send an async request with a sync Client instance." 1000 ) 1002 with request_context(request=request): -> 1003 response = transport.handle_request(request) 1005 assert isinstance(response.stream, SyncByteStream) 1007 response.request = request File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/client/_testclient.py:351, in _TestClientTransport.handle_request(self, request) 349 except BaseException as exc: 350 if self.raise_server_exceptions: --> 351 raise exc 353 if self.raise_server_exceptions: 354 assert response_started, "TestClient did not receive any response." File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/client/_testclient.py:348, in _TestClientTransport.handle_request(self, request) 346 with self.portal_factory() as portal: 347 response_complete = portal.call(anyio.Event) --> 348 portal.call(self.app, scope, receive, send) 349 except BaseException as exc: 350 if self.raise_server_exceptions: File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/anyio/from_thread.py:283, in BlockingPortal.call(self, func, *args) 268 def call( 269 self, 270 func: Callable[..., Union[Coroutine[Any, Any, T_Retval], T_Retval]], 271 *args: object 272 ) -> T_Retval: 273 """ 274 Call the given function in the event loop thread. 275 (...) 281 282 """ --> 283 return cast(T_Retval, self.start_task_soon(func, *args).result()) File ~/miniconda3/envs/tiled/lib/python3.10/concurrent/futures/_base.py:458, in Future.result(self, timeout) 456 raise CancelledError() 457 elif self._state == FINISHED: --> 458 return self.__get_result() 459 else: 460 raise TimeoutError() File ~/miniconda3/envs/tiled/lib/python3.10/concurrent/futures/_base.py:403, in Future.__get_result(self) 401 if self._exception: 402 try: --> 403 raise self._exception 404 finally: 405 # Break a reference cycle with the exception in self._exception 406 self = None File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/anyio/from_thread.py:219, in BlockingPortal._call_func(self, func, args, kwargs, future) 216 else: 217 future.add_done_callback(callback) --> 219 retval = await retval 220 except self._cancelled_exc_class: 221 future.cancel() File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/fastapi/applications.py:270, in FastAPI.__call__(self, scope, receive, send) 268 if self.root_path: 269 scope["root_path"] = self.root_path --> 270 await super().__call__(scope, receive, send) File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/applications.py:124, in Starlette.__call__(self, scope, receive, send) 122 async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: 123 scope["app"] = self --> 124 await self.middleware_stack(scope, receive, send) File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/errors.py:184, in ServerErrorMiddleware.__call__(self, scope, receive, send) 179 await response(scope, receive, send) 181 # We always continue to raise the exception. 182 # This allows servers to log the error, or allows test clients 183 # to optionally raise the error within the test case. --> 184 raise exc File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/errors.py:162, in ServerErrorMiddleware.__call__(self, scope, receive, send) 159 await send(message) 161 try: --> 162 await self.app(scope, receive, _send) 163 except Exception as exc: 164 request = Request(scope) File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/cors.py:84, in CORSMiddleware.__call__(self, scope, receive, send) 81 origin = headers.get("origin") 83 if origin is None: ---> 84 await self.app(scope, receive, send) 85 return 87 if method == "OPTIONS" and "access-control-request-method" in headers: File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/base.py:106, in BaseHTTPMiddleware.__call__(self, scope, receive, send) 104 async with anyio.create_task_group() as task_group: 105 request = Request(scope, receive=receive) --> 106 response = await self.dispatch_func(request, call_next) 107 await response(scope, receive, send) 108 response_sent.set() File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/server/app.py:567, in build_app..set_cookies(request, call_next) 565 # Create some Request state, to be (possibly) populated by dependencies. 566 request.state.cookies_to_set = [] --> 567 response = await call_next(request) 568 response.__class__ = PatchedStreamingResponse # tolerate memoryview 569 for params in request.state.cookies_to_set: File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/base.py:80, in BaseHTTPMiddleware.__call__..call_next(request) 78 except anyio.EndOfStream: 79 if app_exc is not None: ---> 80 raise app_exc 81 raise RuntimeError("No response returned.") 83 assert message["type"] == "http.response.start" File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/base.py:69, in BaseHTTPMiddleware.__call__..call_next..coro() 67 async with send_stream: 68 try: ---> 69 await self.app(scope, receive_or_disconnect, send_no_error) 70 except Exception as exc: 71 app_exc = exc File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/base.py:106, in BaseHTTPMiddleware.__call__(self, scope, receive, send) 104 async with anyio.create_task_group() as task_group: 105 request = Request(scope, receive=receive) --> 106 response = await self.dispatch_func(request, call_next) 107 await response(scope, receive, send) 108 response_sent.set() File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/server/app.py:558, in build_app..client_compatibility_check(request, call_next) 547 if parsed_version < MINIMUM_SUPPORTED_PYTHON_CLIENT_VERSION: 548 return JSONResponse( 549 status_code=400, 550 content={ (...) 556 }, 557 ) --> 558 response = await call_next(request) 559 response.__class__ = PatchedStreamingResponse # tolerate memoryview 560 return response File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/base.py:80, in BaseHTTPMiddleware.__call__..call_next(request) 78 except anyio.EndOfStream: 79 if app_exc is not None: ---> 80 raise app_exc 81 raise RuntimeError("No response returned.") 83 assert message["type"] == "http.response.start" File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/base.py:69, in BaseHTTPMiddleware.__call__..call_next..coro() 67 async with send_stream: 68 try: ---> 69 await self.app(scope, receive_or_disconnect, send_no_error) 70 except Exception as exc: 71 app_exc = exc File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/base.py:106, in BaseHTTPMiddleware.__call__(self, scope, receive, send) 104 async with anyio.create_task_group() as task_group: 105 request = Request(scope, receive=receive) --> 106 response = await self.dispatch_func(request, call_next) 107 await response(scope, receive, send) 108 response_sent.set() File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/server/app.py:530, in build_app..double_submit_cookie_csrf_protection(request, call_next) 525 if not secrets.compare_digest(csrf_token, csrf_cookie): 526 return Response( 527 status_code=403, content="Double-submit CSRF tokens do not match" 528 ) --> 530 response = await call_next(request) 531 response.__class__ = PatchedStreamingResponse # tolerate memoryview 532 if not csrf_cookie: File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/base.py:80, in BaseHTTPMiddleware.__call__..call_next(request) 78 except anyio.EndOfStream: 79 if app_exc is not None: ---> 80 raise app_exc 81 raise RuntimeError("No response returned.") 83 assert message["type"] == "http.response.start" File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/base.py:69, in BaseHTTPMiddleware.__call__..call_next..coro() 67 async with send_stream: 68 try: ---> 69 await self.app(scope, receive_or_disconnect, send_no_error) 70 except Exception as exc: 71 app_exc = exc File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/base.py:106, in BaseHTTPMiddleware.__call__(self, scope, receive, send) 104 async with anyio.create_task_group() as task_group: 105 request = Request(scope, receive=receive) --> 106 response = await self.dispatch_func(request, call_next) 107 await response(scope, receive, send) 108 response_sent.set() File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/server/app.py:483, in build_app..capture_metrics(request, call_next) 481 # Record the overall application time. 482 with record_timing(metrics, "app"): --> 483 response = await call_next(request) 484 response.__class__ = PatchedStreamingResponse # tolerate memoryview 485 # Server-Timing specifies times should be in milliseconds. 486 # Prometheus specifies times should be in seconds. 487 # Therefore, we store as seconds and convert to ms for Server-Timing here. 488 # That is what the factor of 1000 below is doing. File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/base.py:80, in BaseHTTPMiddleware.__call__..call_next(request) 78 except anyio.EndOfStream: 79 if app_exc is not None: ---> 80 raise app_exc 81 raise RuntimeError("No response returned.") 83 assert message["type"] == "http.response.start" File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/base.py:69, in BaseHTTPMiddleware.__call__..call_next..coro() 67 async with send_stream: 68 try: ---> 69 await self.app(scope, receive_or_disconnect, send_no_error) 70 except Exception as exc: 71 app_exc = exc File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/server/compression.py:27, in CompressionMiddleware.__call__(self, scope, receive, send) 19 accepted = { 20 item.strip() 21 for item in headers.get("accept-encoding", "").split(",") 22 if item 23 } 24 responder = CompressionResponder( 25 self.app, self.minimum_size, accepted, self.compression_registry 26 ) ---> 27 await responder(scope, receive, send) 28 return 29 await self.app(scope, receive, send) File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/server/compression.py:48, in CompressionResponder.__call__(self, scope, receive, send) 46 self.send = send 47 self.scope = scope ---> 48 await self.app(scope, receive, self.send_compressed) File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/exceptions.py:79, in ExceptionMiddleware.__call__(self, scope, receive, send) 76 handler = self._lookup_exception_handler(exc) 78 if handler is None: ---> 79 raise exc 81 if response_started: 82 msg = "Caught handled exception, but response already started." File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/middleware/exceptions.py:68, in ExceptionMiddleware.__call__(self, scope, receive, send) 65 await send(message) 67 try: ---> 68 await self.app(scope, receive, sender) 69 except Exception as exc: 70 handler = None File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py:21, in AsyncExitStackMiddleware.__call__(self, scope, receive, send) 19 except Exception as e: 20 dependency_exception = e ---> 21 raise e 22 if dependency_exception: 23 # This exception was possibly handled by the dependency but it should 24 # still bubble up so that the ServerErrorMiddleware can return a 500 25 # or the ExceptionMiddleware can catch and handle any other exceptions 26 raise dependency_exception File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py:18, in AsyncExitStackMiddleware.__call__(self, scope, receive, send) 16 scope[self.context_name] = stack 17 try: ---> 18 await self.app(scope, receive, send) 19 except Exception as e: 20 dependency_exception = e File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/routing.py:706, in Router.__call__(self, scope, receive, send) 704 if match == Match.FULL: 705 scope.update(child_scope) --> 706 await route.handle(scope, receive, send) 707 return 708 elif match == Match.PARTIAL and partial is None: File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/routing.py:276, in Route.handle(self, scope, receive, send) 274 await response(scope, receive, send) 275 else: --> 276 await self.app(scope, receive, send) File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/routing.py:66, in request_response..app(scope, receive, send) 64 request = Request(scope, receive=receive, send=send) 65 if is_coroutine: ---> 66 response = await func(request) 67 else: 68 response = await run_in_threadpool(func, request) File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/fastapi/routing.py:225, in get_request_handler..app(request) 221 except Exception as e: 222 raise HTTPException( 223 status_code=400, detail="There was an error parsing the body" 224 ) from e --> 225 solved_result = await solve_dependencies( 226 request=request, 227 dependant=dependant, 228 body=body, 229 dependency_overrides_provider=dependency_overrides_provider, 230 ) 231 values, errors, background_tasks, sub_response, _ = solved_result 232 if errors: File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/fastapi/dependencies/utils.py:535, in solve_dependencies(request, dependant, body, background_tasks, response, dependency_overrides_provider, dependency_cache) 533 solved = await call(**sub_values) 534 else: --> 535 solved = await run_in_threadpool(call, **sub_values) 536 if sub_dependant.name is not None: 537 values[sub_dependant.name] = solved File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/starlette/concurrency.py:41, in run_in_threadpool(func, *args, **kwargs) 38 if kwargs: # pragma: no cover 39 # run_sync doesn't accept 'kwargs', so bind them in here 40 func = functools.partial(func, **kwargs) ---> 41 return await anyio.to_thread.run_sync(func, *args) File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/anyio/to_thread.py:31, in run_sync(func, cancellable, limiter, *args) 10 async def run_sync( 11 func: Callable[..., T_Retval], 12 *args: object, 13 cancellable: bool = False, 14 limiter: Optional[CapacityLimiter] = None 15 ) -> T_Retval: 16 """ 17 Call the given function with the given arguments in a worker thread. 18 (...) 29 30 """ ---> 31 return await get_asynclib().run_sync_in_worker_thread( 32 func, *args, cancellable=cancellable, limiter=limiter 33 ) File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/anyio/_backends/_asyncio.py:937, in run_sync_in_worker_thread(func, cancellable, limiter, *args) 935 context.run(sniffio.current_async_library_cvar.set, None) 936 worker.queue.put_nowait((context, func, args, future)) --> 937 return await future File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/anyio/_backends/_asyncio.py:867, in WorkerThread.run(self) 865 exception: Optional[BaseException] = None 866 try: --> 867 result = context.run(func, *args) 868 except BaseException as exc: 869 exception = exc File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/server/dependencies.py:64, in SecureEntry..inner(path, request, principal, root_tree) 60 entry = filter_for_access( 61 entry, principal, ["read:metadata"], request.state.metrics 62 ) 63 try: ---> 64 entry = entry[segment] 65 except (KeyError, TypeError): 66 raise NoEntry(path_parts) File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/adapters/mapping.py:140, in MapAdapter.__getitem__(self, key) 139 def __getitem__(self, key): --> 140 return self._mapping[key] File ~/miniconda3/envs/tiled/lib/python3.10/site-packages/tiled/utils.py:126, in OneShotCachedMap.__getitem__(self, key) 123 v = self.__mapping[key] 124 if isinstance(v, _OneShotCachedMapWrapper): 125 # TODO handle exceptions? --> 126 v = self.__mapping[key] = v.func() 127 return v File ~/src/mrakitin/DSSI/databroker/databroker/mongo_normalized.py:1457, in MongoAdapter._build_event_stream....(object_name) 1432 object_names = event_descriptors[0]["object_keys"] 1433 run = self[run_start_uid] 1434 mapping = OneShotCachedMap( 1435 { 1436 "data": lambda: DatasetFromDocuments( 1437 run=run, 1438 stream_name=stream_name, 1439 cutoff_seq_num=cutoff_seq_num, 1440 event_descriptors=event_descriptors, 1441 event_collection=self._event_collection, 1442 root_map=self.root_map, 1443 sub_dict="data", 1444 ), 1445 "timestamps": lambda: DatasetFromDocuments( 1446 run=run, 1447 stream_name=stream_name, 1448 cutoff_seq_num=cutoff_seq_num, 1449 event_descriptors=event_descriptors, 1450 event_collection=self._event_collection, 1451 root_map=self.root_map, 1452 sub_dict="timestamps", 1453 ), 1454 "config": lambda: Config( 1455 OneShotCachedMap( 1456 { -> 1457 object_name: lambda object_name=object_name: build_config_xarray( 1458 event_descriptors=event_descriptors, 1459 object_name=object_name, 1460 sub_dict="data", 1461 ) 1462 for object_name in object_names 1463 } 1464 ) 1465 ), 1466 "config_timestamps": lambda: Config( 1467 OneShotCachedMap( 1468 { 1469 object_name: lambda object_name=object_name: build_config_xarray( 1470 event_descriptors=event_descriptors, 1471 object_name=object_name, 1472 sub_dict="timestamps", 1473 ) 1474 for object_name in object_names 1475 } 1476 ) 1477 ), 1478 } 1479 ) 1481 metadata = {"descriptors": event_descriptors, "stream_name": stream_name} 1482 return BlueskyEventStream( 1483 mapping, 1484 metadata=metadata, (...) 1487 run=run, 1488 ) File ~/src/mrakitin/DSSI/databroker/databroker/mongo_normalized.py:1001, in build_config_xarray(event_descriptors, sub_dict, object_name) 996 # otherwise guess! 997 else: 998 numpy_dtype = JSON_DTYPE_TO_MACHINE_DATA_TYPE[ 999 field_metadata["dtype"] 1000 ].to_numpy_dtype() -> 1001 columns[key] = numpy.array(column, dtype=numpy_dtype) 1002 data_arrays = {} 1003 dim_counter = itertools.count() ValueError: invalid literal for int() with base 10: 'none' ```

Step to reproduce:

RE = RunEngine({}) db = temp() RE.subscribe(db.v1.insert) bec = BestEffortCallback() RE.subscribe(bec)

det.kind = 'hinted' motor.kind = 'hinted' motor.delay = 0.1

RE(bp.scan([det], motor, -1, 1, 3)) det.read_configuration() db[-1].primary.config['det'].read()

tacaswell commented 1 year ago

This looks like a broken enum...

In [2]: det.read_configuration()
Out[2]:
OrderedDict([('det_Imax', {'value': 1, 'timestamp': 1670611553.7255466}),
             ('det_center', {'value': 0, 'timestamp': 1670611553.725544}),
             ('det_sigma', {'value': 1, 'timestamp': 1670611553.725551}),
             ('det_noise', {'value': 'none', 'timestamp': 1670611553.7255316}),
             ('det_noise_multiplier',
              {'value': 1, 'timestamp': 1670611553.7255402})])

In [3]: det.describe_configuration()
Out[3]:
OrderedDict([('det_Imax',
              {'source': 'SIM:det_Imax', 'dtype': 'integer', 'shape': []}),
             ('det_center',
              {'source': 'SIM:det_center', 'dtype': 'integer', 'shape': []}),
             ('det_sigma',
              {'source': 'SIM:det_sigma', 'dtype': 'integer', 'shape': []}),
             ('det_noise',
              {'source': 'SIM:det_noise',
               'dtype': 'integer',
               'shape': [],
               'enum_strs': ('none', 'poisson', 'uniform')}),
             ('det_noise_multiplier',
              {'source': 'SIM:det_noise_multiplier',
               'dtype': 'integer',
               'shape': []})])

https://github.com/bluesky/ophyd/blob/60d52b79a5513883d66d09d56e86e98a3a106a21/ophyd/sim.py#L589-L594

which says there is something wrong with the EnumSignal:

In [6]: es = EnumSignal(value='a', enum_strings=['a', 'b', 'c'], name='bob')

In [7]: es.read()
Out[7]: {'bob': {'value': 'a', 'timestamp': 1670611752.3272095}}

In [8]: es.describe()
Out[8]:
{'bob': {'source': 'SIM:bob',
  'dtype': 'integer',
  'shape': [],
  'enum_strs': ('a', 'b', 'c')}}