|
1 | 1 | import warnings |
2 | 2 | from pathlib import Path |
| 3 | +import numpy as np |
| 4 | +from datetime import datetime |
| 5 | +from astropy.time import Time |
| 6 | +from asdf.tags.core import NDArrayType |
| 7 | + |
3 | 8 | from stdatamodels.jwst.datamodels.util import open as datamodels_open |
4 | 9 | from stdatamodels.jwst.datamodels import read_metadata |
5 | | -from stpipe.library import AbstractModelLibrary, NoGroupID |
| 10 | +from stpipe.library import AbstractModelLibrary, NoGroupID, BorrowError |
6 | 11 |
|
7 | 12 | from jwst.associations import AssociationNotValidError, load_asn |
8 | 13 | from jwst.datamodels.utils import attrs_to_group_id |
@@ -218,25 +223,161 @@ def get_crds_parameters(self): |
218 | 223 | ) |
219 | 224 | idx = 0 |
220 | 225 |
|
221 | | - # find model in _loaded_models, temp_filenames, or asn_dir |
222 | | - if self._on_disk: |
223 | | - if idx in self._temp_filenames: |
224 | | - # if model has been modified, find its temp filename |
225 | | - filename = self._temp_filenames[idx] |
226 | | - else: |
227 | | - # otherwise, find the filename in the asn_dir |
228 | | - member = self._members[idx] |
229 | | - filename = Path(self._asn_dir) / member["expname"] |
| 226 | + return self.read_metadata(idx) |
| 227 | + |
| 228 | + def read_metadata(self, idx, flatten=True): |
| 229 | + """ |
| 230 | + Read metadata for a model at the given index. |
| 231 | +
|
| 232 | + Parameters |
| 233 | + ---------- |
| 234 | + idx : int |
| 235 | + Index of the model in the library. |
| 236 | +
|
| 237 | + Returns |
| 238 | + ------- |
| 239 | + dict |
| 240 | + The metadata dictionary for the model. |
| 241 | + """ |
| 242 | + # if model was already borrowed, calling code should just read the metadata |
| 243 | + # from the model itself, so we raise an error here |
| 244 | + if idx in self._ledger: |
| 245 | + raise BorrowError("Attempt to read metadata from model that is already borrowed") |
| 246 | + |
| 247 | + # find model in _loaded_models, temp_filenames, or asn_dir |
| 248 | + if self._on_disk: |
| 249 | + if idx in self._temp_filenames: |
| 250 | + # if model has been modified, find its temp filename |
| 251 | + filename = self._temp_filenames[idx] |
230 | 252 | else: |
231 | | - if idx in self._loaded_models: |
232 | | - # if this model is in memory, retrieve parameters from it directly |
233 | | - model = self._loaded_models[idx] |
234 | | - return model.get_crds_parameters() |
235 | | - else: |
236 | | - # otherwise, find the filename in the asn_dir |
237 | | - member = self._members[idx] |
238 | | - filename = Path(self._asn_dir) / member["expname"] |
| 253 | + # otherwise, find the filename in the asn_dir |
| 254 | + member = self._members[idx] |
| 255 | + filename = Path(self._asn_dir) / member["expname"] |
| 256 | + else: |
| 257 | + if idx in self._loaded_models: |
| 258 | + # if this model is in memory, retrieve parameters from it directly |
| 259 | + model = self._loaded_models[idx] |
| 260 | + return _read_meta_from_open_model(model, flatten=flatten) |
| 261 | + else: |
| 262 | + # otherwise, find the filename in the asn_dir |
| 263 | + member = self._members[idx] |
| 264 | + filename = Path(self._asn_dir) / member["expname"] |
239 | 265 |
|
240 | | - meta = read_metadata(filename, flatten=True) |
| 266 | + meta = read_metadata(filename, flatten=flatten) |
| 267 | + meta = self._assign_member_to_meta(meta, self._members[idx], flatten) |
| 268 | + return meta |
241 | 269 |
|
| 270 | + def _assign_member_to_meta(self, meta, member, flatten): |
| 271 | + """ |
| 272 | + Update meta dict with asn-related attributes, similar to _assign_member_to_model. |
| 273 | +
|
| 274 | + Parameters |
| 275 | + ---------- |
| 276 | + meta : dict |
| 277 | + The metadata dictionary to update. |
| 278 | + member : dict |
| 279 | + The member dictionary containing association attributes. |
| 280 | + flatten : bool |
| 281 | + If True, the metadata will be flattened to a single level. |
| 282 | + If False, the metadata will be nested. |
| 283 | +
|
| 284 | + Returns |
| 285 | + ------- |
| 286 | + dict |
| 287 | + The updated metadata dictionary with association attributes. |
| 288 | + """ |
| 289 | + if flatten: |
| 290 | + meta["meta.asn.exptype"] = member["exptype"] |
| 291 | + for attr in ("group_id", "tweakreg_catalog"): |
| 292 | + if attr in member: |
| 293 | + meta["meta." + attr] = member[attr] |
| 294 | + if "table_name" in self.asn.keys(): |
| 295 | + meta["meta.asn.table_name"] = self.asn["table_name"] |
| 296 | + if "asn_pool" in self.asn.keys(): |
| 297 | + meta["meta.asn.pool_name"] = self.asn["asn_pool"] |
| 298 | + else: |
| 299 | + meta["meta"].setdefault("asn", {}) |
| 300 | + meta["meta"]["asn"]["exptype"] = member["exptype"] |
| 301 | + for attr in ("group_id", "tweakreg_catalog"): |
| 302 | + if attr in member: |
| 303 | + meta["meta"][attr] = member[attr] |
| 304 | + if "table_name" in self.asn.keys(): |
| 305 | + meta["meta"]["asn"]["table_name"] = self.asn["table_name"] |
| 306 | + if "asn_pool" in self.asn.keys(): |
| 307 | + meta["meta"]["asn"]["pool_name"] = self.asn["asn_pool"] |
242 | 308 | return meta |
| 309 | + |
| 310 | + |
| 311 | +def _read_meta_from_open_model(model, flatten): |
| 312 | + """ |
| 313 | + Read metadata from an open model. |
| 314 | +
|
| 315 | + Parameters |
| 316 | + ---------- |
| 317 | + model : DataModel |
| 318 | + The model from which to read metadata. |
| 319 | + flatten : bool |
| 320 | + If True, the metadata will be flattened to a single level. |
| 321 | + If False, the metadata will be nested. |
| 322 | +
|
| 323 | + Returns |
| 324 | + ------- |
| 325 | + dict |
| 326 | + The metadata dictionary for the model. |
| 327 | + """ |
| 328 | + if flatten: |
| 329 | + return model.get_crds_parameters() |
| 330 | + |
| 331 | + def convert_val(val): |
| 332 | + if isinstance(val, datetime): |
| 333 | + return val.isoformat() |
| 334 | + elif isinstance(val, Time): |
| 335 | + return str(val) |
| 336 | + return val |
| 337 | + |
| 338 | + def recurse(tree): |
| 339 | + """ |
| 340 | + Walk the tree and add metadata to a new dictionary. |
| 341 | +
|
| 342 | + Do all conversions and exclusions model.get_crds_parameters does, |
| 343 | + but without flattening. |
| 344 | +
|
| 345 | + Parameters |
| 346 | + ---------- |
| 347 | + tree : dict |
| 348 | + An asdf-like nested dictionary structure, e.g. model._instance. |
| 349 | +
|
| 350 | + Returns |
| 351 | + ------- |
| 352 | + dict |
| 353 | + A new nested dictionary containing the metadata. |
| 354 | + """ |
| 355 | + new_tree = {} |
| 356 | + for key, val in tree.items(): |
| 357 | + if key == "wcs": |
| 358 | + continue |
| 359 | + val = convert_val(val) |
| 360 | + if isinstance(val, dict): |
| 361 | + new_tree[key] = recurse(val) |
| 362 | + elif isinstance(val, list): |
| 363 | + new_list = [] |
| 364 | + for item in val: |
| 365 | + if isinstance(item, dict): |
| 366 | + new_list.append(recurse(item)) |
| 367 | + elif isinstance(item, list): |
| 368 | + # handle nested lists |
| 369 | + new_list.append(recurse({"_idx": item})["_idx"]) # placeholder key |
| 370 | + elif isinstance(item, (np.ndarray, NDArrayType)): |
| 371 | + continue |
| 372 | + else: |
| 373 | + new_list.append(convert_val(item)) |
| 374 | + new_tree[key] = new_list |
| 375 | + elif isinstance(val, (np.ndarray, NDArrayType)): |
| 376 | + continue |
| 377 | + elif isinstance(val, (str, int, float, complex, bool)): |
| 378 | + new_tree[key] = val |
| 379 | + else: |
| 380 | + continue # skip unsupported types |
| 381 | + return new_tree |
| 382 | + |
| 383 | + return recurse(model._instance) # noqa: SLF001 |
0 commit comments