Source code for pyxel.models.photon_collection.illumination

#  Copyright (c) European Space Agency, 2020.
#
#  This file is subject to the terms and conditions defined in file 'LICENCE.txt', which
#  is part of this Pyxel package. No part of the package, including
#  this file, may be copied, modified, propagated, or distributed except according to
#  the terms contained in the file ‘LICENCE.txt’.

"""Pyxel photon generator models."""

from collections.abc import Sequence
from typing import Literal

import numpy as np

from pyxel.detectors import Detector


def rectangular(
    shape: tuple[int, int],
    level: float,
    object_size: Sequence[int] | None = None,
    object_center: Sequence[int] | None = None,
) -> np.ndarray:
    """Calculate an image of a rectangular object.

    Parameters
    ----------
    shape: tuple
        Shape of the output array.
    level: float
        Flux of photon per pixel.
    object_size: list or tuple, optional
        List or tuple of length 2, integers defining the diameters of the rectangular object
        in vertical and horizontal directions.
    object_center: list or tuple, optional
        List or tuple of length 2, two integers (row and column number),
        defining the coordinates of the center of the rectangular object.

    Returns
    -------
    photon_array: ndarray
        Output numpy array.
    """
    if not object_size:
        raise ValueError(
            "object_size argument should be defined for illumination model"
        )
    if object_size and not len(object_size) == 2:
        raise ValueError("Object size should be a sequence of length 2!")
    if object_center and not len(object_center) == 2:
        raise ValueError("Object size should be a sequence of length 2!")

    photon_array = np.zeros(shape, dtype=float)
    if object_center is not None:
        if not (
            (0 <= object_center[0] <= shape[0]) and (0 <= object_center[1] <= shape[1])
        ):
            raise ValueError('Argument "object_center" should be inside Photon array.')
    else:
        object_center = [int(shape[0] / 2), int(shape[1] / 2)]
    p = object_center[0] - int(object_size[0] / 2)
    q = object_center[1] - int(object_size[1] / 2)
    p0 = int(np.clip(p, a_min=0, a_max=shape[0]))
    q0 = int(np.clip(q, a_min=0, a_max=shape[1]))
    photon_array[slice(p0, p + object_size[0]), slice(q0, q + object_size[1])] = level

    return photon_array


def elliptic(
    shape: tuple[int, int],
    level: float,
    object_size: Sequence[int] | None = None,
    object_center: Sequence[int] | None = None,
) -> np.ndarray:
    """Calculate an image of an elliptic object.

    Parameters
    ----------
    shape: tuple
        Shape of the output array.
    level: float
        Flux of photon per pixel.
    object_size: list or tuple, optional
        List or tuple of length 2, integers defining the diameters of the elliptic object
        in vertical and horizontal directions.
    object_center: list or tuple, optional
        List or tuple of length 2, two integers (row and column number),
        defining the coordinates of the center of the elliptic object.

    Returns
    -------
    photon_array: ndarray
        Output numpy array.
    """
    if not object_size:
        raise ValueError(
            "object_size argument should be defined for illumination model"
        )
    if object_size and not len(object_size) == 2:
        raise ValueError("Object size should be a sequence of length 2!")
    if object_center and not len(object_center) == 2:
        raise ValueError("Object size should be a sequence of length 2!")

    photon_array = np.zeros(shape, dtype=float)
    if object_center is not None:
        if not (
            (0 <= object_center[0] <= shape[0]) and (0 <= object_center[1] <= shape[1])
        ):
            raise ValueError(
                f"Argument 'object_center' should be inside Photon array. {object_center=}, {shape=}"
            )
    else:
        object_center = [int(shape[0] / 2), int(shape[1] / 2)]
    y, x = np.ogrid[: shape[0], : shape[1]]
    dist_from_center = np.sqrt(
        ((x - object_center[1]) / float(object_size[1] / 2)) ** 2
        + ((y - object_center[0]) / float(object_size[0] / 2)) ** 2
    )
    photon_array[dist_from_center < 1] = level
    return photon_array


def calculate_illumination(
    shape: tuple[int, int],
    level: float,
    option: Literal["uniform", "rectangular", "elliptic"] = "uniform",
    object_size: Sequence[int] | None = None,
    object_center: Sequence[int] | None = None,
) -> np.ndarray:
    """Calculate the array of photons uniformly over the entire array or over a object.

    Parameters
    ----------
    shape: tuple
        Shape of the output array.
    level: float
        Flux of photon per pixel.
    option: str{'uniform', 'elliptic_hole', 'rectangular_hole'}
        A string indicating the type of illumination:
        - ``uniform``
           Uniformly fill the entire array with photon. (Default)
        - ``elliptic_hole``
           Mask with elliptic object.
        - ``rectangular_hole``
           Mask with rectangular object.
    object_size: list or tuple, optional
        List or tuple of length 2, integers defining the diameters of the elliptic or rectangular object
        in vertical and horizontal directions.
    object_center: list or tuple, optional
        List or tuple of length 2, two integers (row and column number),
        defining the coordinates of the center of the elliptic or rectangular object.

    Returns
    -------
    photon_array: ndarray
        Output numpy array.
    """
    if option == "uniform":
        photon_array = np.ones(shape, dtype=float) * level
    elif option == "rectangular":
        photon_array = rectangular(
            shape=shape,
            object_size=object_size,
            object_center=object_center,
            level=level,
        )
    elif option == "elliptic":
        photon_array = elliptic(
            shape=shape,
            object_size=object_size,
            object_center=object_center,
            level=level,
        )
    else:
        raise NotImplementedError

    return photon_array


[docs] def illumination( detector: Detector, level: float, option: Literal["uniform", "rectangular", "elliptic"] = "uniform", object_size: Sequence[int] | None = None, object_center: Sequence[int] | None = None, time_scale: float = 1.0, ) -> None: """Generate photon uniformly over the entire array or over an elliptic or rectangular object. Parameters ---------- detector : Detector Pyxel Detector object. level : float Flux of photon per pixel. option : str A string indicating the type of illumination: - ``uniform``: Uniformly fill the entire array with photon. (Default) - ``elliptic``: Mask with elliptic object. - ``rectangular``: Mask with rectangular object. object_size : list or tuple, optional List or tuple of length 2, integers defining the diameters of the elliptic or rectangular object in vertical and horizontal directions. object_center : list or tuple, optional List or tuple of length 2, two integers (row and column number), defining the coordinates of the center of the elliptic or rectangular object. time_scale : float Time scale of the photon flux, default is 1 second. 0.001 would be ms. """ shape = (detector.geometry.row, detector.geometry.col) photon_array = calculate_illumination( shape=shape, level=level, option=option, object_size=object_size, object_center=object_center, ) photon_array = photon_array * (detector.time_step / time_scale) detector.photon += photon_array