|
4 | 4 | from dataclasses import dataclass, field, fields, replace |
5 | 5 | from datetime import date, datetime, time, timezone |
6 | 6 | from enum import IntFlag |
| 7 | +from fnmatch import fnmatch |
7 | 8 | from functools import lru_cache |
8 | 9 | from pathlib import Path |
9 | 10 | from typing import Dict, List, Optional, Union |
|
29 | 30 | "DSSCConditions", |
30 | 31 | "JUNGFRAUConditions", |
31 | 32 | "ShimadzuHPVX2Conditions", |
| 33 | + "DetectorData", |
| 34 | + "PhysicalDetectorUnit" |
32 | 35 | ] |
33 | 36 |
|
34 | 37 | # Default address to connect to, only available internally |
@@ -191,6 +194,9 @@ def detector_by_identifier(self, identifier): |
191 | 194 | return self._get_by_name( |
192 | 195 | "detectors", identifier, name_key="identifier") |
193 | 196 |
|
| 197 | + def instrument_by_name(self, name): |
| 198 | + return self._get_by_name("instruments", name, name_key='identifier') |
| 199 | + |
194 | 200 | def calibration_by_name(self, name): |
195 | 201 | return self._get_by_name("calibrations", name) |
196 | 202 |
|
@@ -824,6 +830,9 @@ def pdu_names(self): |
824 | 830 | May include missing modules.""" |
825 | 831 | return [m["physical_name"] for m in self.module_details] |
826 | 832 |
|
| 833 | + def detector(self): |
| 834 | + return DetectorData.from_identifier(self.detector_name) |
| 835 | + |
827 | 836 | def require_calibrations(self, calibrations) -> "CalibrationData": |
828 | 837 | """Drop any modules missing the specified constant types""" |
829 | 838 | mods = set(self.aggregator_names) |
@@ -1203,6 +1212,199 @@ class ShimadzuHPVX2Conditions(ConditionsBase): |
1203 | 1212 | } |
1204 | 1213 |
|
1205 | 1214 |
|
| 1215 | +@dataclass |
| 1216 | +class PhysicalDetectorUnit: |
| 1217 | + """Physical detector unit (PDU). |
| 1218 | +
|
| 1219 | + PDUs describe the physical modules independent from the detector |
| 1220 | + they may be installed in, or the detector's logical modules they are |
| 1221 | + mapped to. |
| 1222 | + |
| 1223 | + Calibration data is always associated with a PDU rather than a |
| 1224 | + detector or detector module. This allows a PDU to be moved to a |
| 1225 | + different detector installation or place within the detector and |
| 1226 | + carry all its calibration data alongside with it. |
| 1227 | + """ |
| 1228 | + |
| 1229 | + pdu_id: int |
| 1230 | + physical_name: str # PDU identifier independent of detector |
| 1231 | + uuid: int # Universally unique ID used for mapping |
| 1232 | + aggregator: str # Data aggregator the PDU is currently mapped to |
| 1233 | + detector: str # Detector identifier the PDU is currently mapped to |
| 1234 | + virtual_device_name: str # Identifier within the detector, e.g. Q1M2 |
| 1235 | + module_index: int # Enumerated module index within the detector, contiguous and always starts at 0 |
| 1236 | + module_number: int # Module number within the detector, may start at any number and have gaps |
| 1237 | + detector_type: str |
| 1238 | + |
| 1239 | + @property |
| 1240 | + def ccv_params(self): |
| 1241 | + """PDU arguments as needed for write_ccv().""" |
| 1242 | + return self.physical_name, self.uuid, self.detector_type |
| 1243 | + |
| 1244 | + |
| 1245 | +class DetectorData(Mapping): |
| 1246 | + """Detector consisting of one or more modules |
| 1247 | + |
| 1248 | + A detector can house one or more physical detector units, which are |
| 1249 | + mapped to the logical modules of the detector. Calibration data is |
| 1250 | + associated with a PDU rather than a detector. |
| 1251 | +
|
| 1252 | + This object exposes the module mapping in a dict-like interface |
| 1253 | + mapping data aggregators to physical detector units. Alternatively, |
| 1254 | + the module index may also be used as a key. |
| 1255 | + """ |
| 1256 | + |
| 1257 | + def __init__(self, detector_row, module_rows): |
| 1258 | + # Result rows as returned by CalCat. |
| 1259 | + self.detector_row = detector_row |
| 1260 | + self.pdus = [PhysicalDetectorUnit( |
| 1261 | + p['id'], p['physical_name'], p['uuid'], p['karabo_da'], |
| 1262 | + p['detector']['identifier'], p['virtual_device_name'], |
| 1263 | + i, p['module_number'], p['detector_type']['name']) |
| 1264 | + for i, p in enumerate(module_rows)] |
| 1265 | + |
| 1266 | + @classmethod |
| 1267 | + def from_identifier(cls, identifier, pdu_snapshot_at=None, client=None): |
| 1268 | + """Look up a detector and its modules by identifier. |
| 1269 | +
|
| 1270 | + `pdu_snapshot_at` should either be an ISO 8601 compatible string |
| 1271 | + or a datetime-like object. It may also be a DataCollection |
| 1272 | + object from EXtra-data to use the beginning of the run as a |
| 1273 | + point in time. |
| 1274 | + """ |
| 1275 | + |
| 1276 | + client = client or get_client() |
| 1277 | + detector_row = client.detector_by_identifier(identifier) |
| 1278 | + |
| 1279 | + try: |
| 1280 | + module_rows = client.get( |
| 1281 | + 'physical_detector_units/get_all_by_detector', |
| 1282 | + {'detector_id': detector_row['id'], |
| 1283 | + 'pdu_snapshot_at': client.format_time(pdu_snapshot_at)}) |
| 1284 | + except CalCatAPIError as e: |
| 1285 | + if e.status_code == 404: |
| 1286 | + module_rows = [] |
| 1287 | + else: |
| 1288 | + raise e |
| 1289 | + |
| 1290 | + return cls(detector_row, module_rows) |
| 1291 | + |
| 1292 | + @classmethod |
| 1293 | + def from_instrument(cls, instrument, identifier=None, pdu_snapshot_at=None, |
| 1294 | + client=None): |
| 1295 | + """Look up a detector and its modules by instrument. |
| 1296 | +
|
| 1297 | + `identifier` may be a string restricting the result using Unix |
| 1298 | + shell-style glob patterns. |
| 1299 | + |
| 1300 | + `pdu_snapshot_at` should either be an ISO 8601 compatible string |
| 1301 | + or a datetime-like object. It may also be a DataCollection |
| 1302 | + object from EXtra-data to use the beginning of the run as a |
| 1303 | + point in time. |
| 1304 | + """ |
| 1305 | + |
| 1306 | + client = client or get_client() |
| 1307 | + instrument_id = client.instrument_by_name(instrument)['id'] |
| 1308 | + |
| 1309 | + rows = [det for det in client.get( |
| 1310 | + 'detectors/get_all_by_instrument', {'instrument_id': instrument_id} |
| 1311 | + ) if identifier is None or fnmatch(det['identifier'], identifier)] |
| 1312 | + |
| 1313 | + if not rows: |
| 1314 | + raise ValueError(f'No such detector found for {instrument}') |
| 1315 | + elif len(rows) > 1: |
| 1316 | + raise ValueError( |
| 1317 | + f'Multiple such detectors found for {instrument}: ' + |
| 1318 | + ', '.join([detector['identifier'] for detector in rows])) |
| 1319 | + |
| 1320 | + return cls.from_identifier(rows[0]['identifier']) |
| 1321 | + |
| 1322 | + @classmethod |
| 1323 | + def list_by_instrument(cls, instrument, client=None): |
| 1324 | + """List all detectors by instrument.""" |
| 1325 | + |
| 1326 | + client = client or get_client() |
| 1327 | + instrument_id = client.instrument_by_name(instrument)['id'] |
| 1328 | + |
| 1329 | + return [det['identifier'] for det in |
| 1330 | + client.get('detectors/get_all_by_instrument', |
| 1331 | + {'instrument_id': instrument_id})] |
| 1332 | + |
| 1333 | + def __getitem__(self, key): |
| 1334 | + if isinstance(key, int): |
| 1335 | + return self.pdus[key] |
| 1336 | + elif isinstance(key, str): |
| 1337 | + for pdu in self.pdus: |
| 1338 | + if pdu.aggregator == key: |
| 1339 | + return pdu |
| 1340 | + |
| 1341 | + raise KeyError(key) |
| 1342 | + |
| 1343 | + def __iter__(self): |
| 1344 | + return (pdu.aggregator for pdu in self.pdus) |
| 1345 | + |
| 1346 | + def __len__(self): |
| 1347 | + return len(self.pdus) |
| 1348 | + |
| 1349 | + def __repr__(self): |
| 1350 | + return f'<DetectorData: {len(self.pdus)}/{self.number_of_modules} ' \ |
| 1351 | + f'modules of {self.identifier}>' |
| 1352 | + |
| 1353 | + @property |
| 1354 | + def id(self) -> int: |
| 1355 | + """Detector ID in CalCat.""" |
| 1356 | + return self.detector_row['id'] |
| 1357 | + |
| 1358 | + @property |
| 1359 | + def identifier(self) -> str: |
| 1360 | + """Detector identifier.""" |
| 1361 | + return self.detector_row['identifier'] |
| 1362 | + |
| 1363 | + @property |
| 1364 | + def source_name_pattern(self) -> str: |
| 1365 | + """Source name pattern.""" |
| 1366 | + assert self.detector_row['source_name_pattern'] is not None, \ |
| 1367 | + 'incomplete detector entry in CalCat' |
| 1368 | + return self.detector_row['source_name_pattern'] |
| 1369 | + |
| 1370 | + @property |
| 1371 | + def source_names(self) -> list[str]: |
| 1372 | + """Source names of currently mapped PDUs.""" |
| 1373 | + return [self.source_name_pattern.format( |
| 1374 | + modno=pdu.module_number or i + self.first_module_index |
| 1375 | + ) for i, pdu in enumerate(self.pdus)] |
| 1376 | + |
| 1377 | + @property |
| 1378 | + def number_of_modules(self) -> int: |
| 1379 | + """Number of modules for the full detector.""" |
| 1380 | + return self.detector_row['number_of_modules'] |
| 1381 | + |
| 1382 | + @property |
| 1383 | + def first_module_index(self) -> int: |
| 1384 | + """Module index of the first module.""" |
| 1385 | + assert self.detector_row['first_module_index'] is not None, \ |
| 1386 | + 'incomplete detector entry in CalCat' |
| 1387 | + return self.detector_row['first_module_index'] |
| 1388 | + |
| 1389 | + @property |
| 1390 | + def pdu_detector_types(self) -> set[str]: |
| 1391 | + """Detector types of currently installed PDUs.""" |
| 1392 | + return {pdu.detector_type for pdu in self.pdus} |
| 1393 | + |
| 1394 | + @property |
| 1395 | + def detector_type(self) -> str: |
| 1396 | + """Detector type of all PDUs if unique.""" |
| 1397 | + pdu_types = self.pdu_detector_types |
| 1398 | + |
| 1399 | + if len(pdu_types) > 1: |
| 1400 | + raise ValueError('more than one type of PDU: ' + |
| 1401 | + ', '.join(pdu_types)) |
| 1402 | + elif len(pdu_types) == 0: |
| 1403 | + raise ValueError('no mapped PDUs') |
| 1404 | + |
| 1405 | + return pdu_types.pop() |
| 1406 | + |
| 1407 | + |
1206 | 1408 | class BadPixels(IntFlag): |
1207 | 1409 | """Bad pixel reasons, as used in masks in corrected detector data""" |
1208 | 1410 | OFFSET_OUT_OF_THRESHOLD = 1 << 0 |
|
0 commit comments