from pathlib import Path
from typing import Any, cast
import numpy as np
from filecache import FCPath
from spindoctor.config import DEFAULT_CONFIG, IMAGE_LOGGER, Config
from spindoctor.support.time import et_to_utc
from spindoctor.support.types import PathLike
from .obs_snapshot_inst import ObsSnapshotInst
[docs]
class ObsCassiniISS(ObsSnapshotInst):
"""Implements an observation of a Cassini ISS image.
This class provides specialized functionality for accessing and analyzing Cassini
ISS image data.
"""
[docs]
@staticmethod
def from_file(
path: PathLike,
*,
config: Config | None = None,
extfov_margin_vu: tuple[int, int] | None = None,
**kwargs: Any,
) -> 'ObsCassiniISS':
"""Creates an ObsCassiniISS from a Cassini ISS image file.
Parameters:
path: Path to the Cassini ISS image file.
config: Configuration object to use. If None, uses the default configuration.
extfov_margin_vu: Optional tuple that overrides the extended field of view margins
found in the config.
**kwargs: Additional keyword arguments:
- fast_distortion: Whether to use a fast distortion model.
- return_all_planets: Whether to return all planets.
Returns:
An ObsCassiniISS object containing the image data and metadata.
"""
import oops.hosts.cassini.iss
config = config or DEFAULT_CONFIG
logger = IMAGE_LOGGER
fast_distortion = kwargs.get('fast_distortion', True)
return_all_planets = kwargs.get('return_all_planets', True)
logger.debug(f'Reading Cassini ISS image {path}')
logger.debug(f' Fast distortion: {fast_distortion}')
logger.debug(f' Return all planets: {return_all_planets}')
obs = oops.hosts.cassini.iss.from_file(
path, fast_distortion=fast_distortion, return_all_planets=return_all_planets
)
fc_path = FCPath(path)
obs.abspath = cast(Path, fc_path.get_local_path()).absolute()
obs.image_url = str(fc_path.absolute())
detector = obs.detector.lower()
# Cassini ISS CALIB products carry pixels in I/F units; the RAW
# and CALIB pipelines have different blank / saturation /
# noisy thresholds. ``_CALIB.IMG`` in the filename selects the
# ``cassini_iss_calib`` config block instead of ``cassini_iss``.
is_calibrated = '_CALIB' in fc_path.name.upper()
inst_section = 'cassini_iss_calib' if is_calibrated else 'cassini_iss'
category_dict = config.category(inst_section)
if not category_dict:
raise ValueError(
f'Cassini ISS config section {inst_section!r} is missing or empty; '
f'expected for detector {detector!r}'
)
if detector not in category_dict:
raise ValueError(
f'Cassini ISS config section {inst_section!r} has no entry for '
f'detector {detector!r}; available detectors: {sorted(category_dict)}'
)
inst_config = category_dict[detector]
if extfov_margin_vu is None:
extfov_margin_vu_entry = inst_config['extfov_margin_vu']
if isinstance(extfov_margin_vu_entry, dict):
extfov_margin_vu = extfov_margin_vu_entry[obs.data.shape[0]]
else:
extfov_margin_vu = extfov_margin_vu_entry
logger.debug(f' Data shape: {obs.data.shape}')
logger.debug(f' Extfov margin vu: {extfov_margin_vu}')
# TODO This is slow when debug turned off
logger.debug(f' Data min: {np.min(obs.data)}, max: {np.max(obs.data)}')
new_obs = ObsCassiniISS(obs, config=config, extfov_margin_vu=extfov_margin_vu)
new_obs._inst_config = inst_config
return new_obs
[docs]
def star_min_usable_vmag(self) -> float:
"""Returns the minimum usable magnitude for stars in this observation.
Returns:
The minimum usable magnitude for stars in this observation.
"""
if self.detector == 'WAC':
return 0.0
return 0.0
[docs]
def star_max_usable_vmag(self) -> float:
"""Returns the maximum usable magnitude for stars in this observation.
Returns:
The maximum usable magnitude for stars in this observation.
"""
# A non-positive exposure time is invalid (np.log would give -inf/nan);
# fall back to the reference-exposure magnitude in that case.
if self.detector == 'WAC':
# This is based on star field image W1580760393 with texp 26 and clear filter.
# This image was not useful beyond mag 10.7.
# We don't try to compensate for non-clear filters.
if self.texp <= 0.0:
return 10.7
return cast(float, 10.7 + np.log(self.texp / 26) / np.log(2.512))
# This is based on star field image N1521881358 with texp 1 and clear filter.
# This image was not useful beyond mag 10.7.
# We don't try to compensate for non-clear filters.
if self.texp <= 0.0:
return 10.5
return cast(float, 10.5 + np.log(self.texp) / np.log(2.512))
@property
def camera(self) -> str:
"""The camera that took this observation.
Returns:
The oops detector name: ``'NAC'`` or ``'WAC'``.
"""
return str(self.detector)