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 ObsGalileoSSI(ObsSnapshotInst):
"""Implements an observation of a Galileo SSI image.
This class provides specialized functionality for accessing and analyzing Galileo
SSI image data.
"""
[docs]
@staticmethod
def from_file(
path: PathLike,
*,
config: Config | None = None,
extfov_margin_vu: tuple[int, int] | None = None,
**_kwargs: Any,
) -> 'ObsGalileoSSI':
"""Creates an ObsGalileoSSI from a Galileo SSI image file.
Parameters:
path: Path to the Galileo SSI 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 (none for this instrument).
Returns:
An ObsGalileoSSI object containing the image data and metadata.
"""
import oops.hosts.galileo.ssi
config = config or DEFAULT_CONFIG
logger = IMAGE_LOGGER
logger.debug(f'Reading Galileo SSI image {path}')
# Galileo SSI navigates in raw DN: there is no I/F-calibrated SSI
# product and there never will be. The navigation pipeline treats
# image brightness scale-invariantly (NCC correlation, image-derived
# MAD noise thresholds, magnitude-based star gate), so no photometric
# calibration is required here.
obs = oops.hosts.galileo.ssi.from_file(path, full_fov=True)
fc_path = FCPath(path)
obs.abspath = cast(Path, fc_path.get_local_path()).absolute()
obs.image_url = str(fc_path.absolute())
inst_config = config.category('galileo_ssi')
if extfov_margin_vu is None:
if isinstance(inst_config.extfov_margin_vu, dict):
# TODO Do this a better way
extfov_margin_vu = inst_config.extfov_margin_vu[obs.data.shape[0]]
else:
extfov_margin_vu = inst_config.extfov_margin_vu
logger.debug(f' Data shape: {obs.data.shape}')
logger.debug(f' Extfov margin vu: {extfov_margin_vu}')
logger.debug(f' Data min: {np.min(obs.data)}, max: {np.max(obs.data)}')
new_obs = ObsGalileoSSI(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.
Mirrors the Cassini ISS reference implementation, which imposes no
bright-end cutoff (saturation of bright stars is handled elsewhere).
Returns:
The minimum usable magnitude for stars in this observation.
"""
return 0.0
[docs]
def star_max_usable_vmag(self) -> float:
"""Returns the maximum usable magnitude for stars in this observation.
The limiting magnitude follows the Cassini Pogson-ratio form,
star_max_usable_vmag(texp) = anchor + log(texp) / log(2.512)
where ``anchor`` is the limiting magnitude at a 1 s exposure (each
2.512x increase in exposure buys +1 mag of depth).
The anchor is scaled from the Cassini NAC anchor (10.5 mag at 1 s,
aperture D = 0.19 m) by collecting-area. Galileo SSI uses a CCD, so
no detector-sensitivity penalty is applied. These are nominal optics
values; the term is approximate and pending calibration against real
Galileo star fields.
anchor = 10.5 + 5*log10(0.176/0.19) (CCD) ~= 10.3
Returns:
The maximum usable magnitude for stars in this observation.
"""
# Anchor (limiting mag at texp = 1 s) derived above; rounded to 0.1.
anchor = 10.3
if self.texp <= 0:
return anchor
return cast(float, anchor + np.log(self.texp) / np.log(2.512))
@property
def camera(self) -> str:
"""The camera that took this observation.
Returns:
Always ``'SSI'``; Galileo carries a single camera.
"""
return 'SSI'