Source code for opticks.imaging_model.imager

# opticks Models and analysis tools for optical system engineering
#
# Copyright (C) Egemen Imre
#
# Licensed under GNU GPL v3.0. See LICENSE.md for more info.

from pathlib import Path
from typing import Iterable

import numpy as np
from astropy.units import Quantity

from opticks import u
from opticks.imaging_model.detector import Detector
from opticks.imaging_model.optics import Optics
from opticks.imaging_model.rw_electronics import RWElectronics


[docs] class Imager: """ Class containing generic imager parameters. An Imager is made up of two necessary and one optional part: Optics, Detector and Read-out / Write Electronics (Optional) Parameters ---------- optics_data : Optics Optics data object detector_data : Detector Detector data object rw_electronics_data : RWElectronics Read-out/Write Electronics data object (optional) """ def __init__( self, optics_data: Optics, detector_data: Detector, rw_electronics_data: RWElectronics | None = None, ): self.optics = optics_data self.detector = detector_data if rw_electronics_data: self.rw_electronics = rw_electronics_data else: self.rw_electronics = None
[docs] @classmethod def from_yaml_file( cls, optics_data: Path, detector_data: Path, rw_electronics_data: Path | None = None, ) -> "Imager": """ Initialises an Imager from components in YAML files. Parameters ---------- optics_data : Path Filepath containing optics design data file (YAML) detector_data : Path Filepath containing detector design data file (YAML) rw_electronics_data : Path Filepath containing read-write electronics design data file (YAML) Returns ------- Imager Imager object from the input data """ optics = Optics.from_yaml_file(optics_data) detector = Detector.from_yaml_file(detector_data) rw_electronics = None if rw_electronics_data: rw_electronics = RWElectronics.from_yaml_file(rw_electronics_data) # return a new instance of Imager return cls(optics, detector, rw_electronics)
[docs] @classmethod def from_yaml_text( cls, optics_data: str, detector_data: str, rw_electronics_data: str | None = None, ) -> "Imager": """ Initialises an Imager from components in YAML text. Parameters ---------- optics_data : str Text containing optics design data (YAML) detector_data : str Text containing detector design data (YAML) rw_electronics_data : str Text containing read-write electronics design data (YAML) Returns ------- Imager Imager object from the input data """ optics = Optics.from_yaml_text(optics_data) detector = Detector.from_yaml_text(detector_data) rw_electronics = None if rw_electronics_data: rw_electronics = RWElectronics.from_yaml_text(rw_electronics_data) # return a new instance of Imager return cls(optics, detector, rw_electronics)
# ---------- begin modelling functions ----------
[docs] @u.quantity_input(wavelength="length") def q_factor( self, wavelength: Quantity, band_id: str, with_binning=True, ): """ Computes the Q Factor. Q = wavelength x f_number / pixel pitch Parameters ---------- wavelength : Quantity | ArrayLike[Quantity] Wavelength at which Q is computed band_id : str band ID (to select the correct band or channel) with_binning : bool, optional Return the value with binning or not Returns ------- float Q factor value """ # select the correct channel channel = self.detector.channels[band_id] pixel_pitch = channel.pixel_pitch(with_binning) q = (wavelength * self.optics.f_number / pixel_pitch).decompose() # type: ignore[union-attr] return q.value
[docs] def horizontal_fov(self, band_id: str) -> Quantity: """ Computes the full field of view in the horizontal direction. Used pixels only. Parameters ---------- band_id : str band ID (to select the correct band or channel) Returns ------- Quantity Horizontal FOV angle """ # select the correct channel channel = self.detector.get_channel(band_id) return 2 * np.arctan( ( ( channel.pixel_pitch(with_binning=False) * channel.horizontal_pixels / 2.0 ) / self.optics.focal_length ) ).to(u.deg, copy=False)
[docs] def vertical_fov(self, band_id: str) -> Quantity: """ Computes the full field of view in the vertical direction. For a pushbroom detector, this nominally corresponds to "read blocks" (to compensate for the slow detector readout rate) multiplied by TDI stages. For a "full-frame" detector, this corresponds to the entire detector area in the vertical direction. Parameters ---------- band_id : str band ID (to select the correct band or channel) Returns ------- Quantity Vertical FOV angle """ # select the correct channel channel = self.detector.get_channel(band_id) return 2 * np.arctan( ( ( channel.pixel_pitch(with_binning=False) * channel.vertical_pixels / 2.0 ) / self.optics.focal_length ) ).to(u.deg, copy=False)
[docs] def ifov(self, band_id: str, with_binning=True) -> Quantity: """ Computes the average instantaneous field of view per effective pixel (works in vertical and horizontal). The IFoV is computed simply as the FoV divided by the number of (binned or unbinned) pixels. Parameters ---------- band_id : str band ID (to select the correct band or channel) with_binning : bool, optional Return the value with binning or not Returns ------- Quantity IFOV angle """ # select the correct channel channel = self.detector.get_channel(band_id) # horizontal pixels with binning included horiz_pixels = ( channel.pixel_count_line(with_binning).to(u.pixel, copy=False).value ) return (self.horizontal_fov(band_id) / horiz_pixels).to(u.mdeg, copy=False)
[docs] def pix_solid_angle(self, band_id: str, with_binning=True) -> Quantity: """ Pixel solid angle (of a pyramid). Parameters ---------- band_id : str band ID (to select the correct band or channel) with_binning : bool, optional Return the value with binning or not Returns ------- Quantity Pixel solid angle in steradians """ pix_solid_angle = 4 * np.arcsin( np.sin(self.ifov(band_id, with_binning) / 2.0) * np.sin(self.ifov(band_id, with_binning) / 2.0) ) # correct the unit from rad to sr (or rad**2) return (pix_solid_angle * u.rad).to(u.steradian, copy=False)
[docs] def data_write_rate( self, band_id: str | Iterable[str], with_binning: bool = True, with_compression: bool = True, with_read_blocks: bool = True, ) -> Quantity: """ Data write rate with or without compression. For a pushbroom only a single line is computed. For a full-frame, the entire frame is computed. If a list of channels is given, then the returned result is the sum of all the requested channels. Note that the unused pixels are also read, this assumes that the detector does not have ROI functionality. Parameters ---------- band_id : str or Iterable[str] band ID (to select the correct band or channel) with_binning : bool, optional Return the value with binning or not with_compression : bool Return the value with compression or not with_read_blocks : bool Return the value with read blocks or not (valid for pushbroom only) Returns ------- Quantity Pixel read rate with or without binning (Mbit/s) """ # TDI data is processed but not written unless raw data is needed with_tdi = False # data rate after encoding enc_data_rate = ( self.detector.pix_read_rate( band_id, with_binning, with_tdi, with_read_blocks ) * self.rw_electronics.pixel_encoding # type: ignore[union-attr] ) # data rate after compression and other processing if with_compression: process_output_data_rate = ( enc_data_rate / self.rw_electronics.compression_ratio # type: ignore[union-attr] ) else: process_output_data_rate = enc_data_rate # data rate after overheads write_data_rate = process_output_data_rate * ( 1 + self.rw_electronics.data_write_overhead # type: ignore[union-attr] ) return write_data_rate.to("Mbit/s") # type: ignore[union-attr]