Source code for visualization.plot_pixels

#!/usr/bin/python3
"""Functions for plotting pixel layout information.

The camera (``simtools.model.camera.Camera``) and its raw pixel data are built by the caller,
so this module does not import the model package.
"""

import logging
from copy import deepcopy

import astropy.units as u
import numpy as np

from simtools.utils import names
from simtools.utils.names import is_two_mirror_telescope
from simtools.visualization import visualize
from simtools.visualization.camera_plot_utils import (
    add_pixel_legend,
    add_pixel_patch_collections,
    create_pixel_patches_by_type,
    setup_camera_axis_properties,
)
from simtools.visualization.matplotlib_backend import pyplot as plt

logger = logging.getLogger(__name__)


[docs] def plot(output_file, camera, raw_pixels, *, rotate_angle=None, pixels_id_to_print=80): """ Plot a pixel layout and save it to file. Parameters ---------- output_file : str or Path Path where to save the plot. camera : Camera Camera with pixel geometry (``pixels``, ``telescope_name``, ``get_edge_pixels``). raw_pixels : dict Unrotated pixel data with keys ``x``, ``y`` and ``rotate_angle`` (radians). rotate_angle : astropy.units.Quantity, optional Rotation applied to the telescope-specific pixel transform (default: from raw_pixels). pixels_id_to_print : int, optional Pixel IDs below this value are printed. """ fig = plot_pixel_layout( camera, raw_pixels, rotate_angle=rotate_angle, pixels_id_to_print=pixels_id_to_print ) visualize.save_figure(fig, output_file) plt.close(fig)
[docs] def raw_pixels_from_configuration(configuration): """ Return unrotated pixel data from resolved camera components. Parameters ---------- configuration : dict Output of :func:`resolve_camera_components`. Returns ------- dict Keys ``x``, ``y`` (cm) and ``rotate_angle`` (radians). """ return { "x": np.asarray([item["x_cm"] for item in configuration["pixels"]]), "y": np.asarray([item["y_cm"] for item in configuration["pixels"]]), "rotate_angle": np.deg2rad(float(configuration.get("rotate", 0.0))), }
[docs] def resolve_camera_components(model_reader, parameters): """Resolve camera component model parameters for plotting.""" return { "rotate": parameters["camera_rotate"]["value"], "pixel_types": deepcopy(parameters["camera_pixel_types"]["value"]), "pixels": _table_records(model_reader, parameters["camera_pixel_layout"]), "triggers": _table_records(model_reader, parameters["camera_trigger_groups"]), "trigger_members": _table_records(model_reader, parameters["camera_trigger_members"]), }
def _table_records(model_reader, parameter): """Convert a model parameter table into scalar records.""" table = model_reader.get_parameter_table(parameter) return [ {column: getattr(row[column], "value", row[column]) for column in table.colnames} for row in table ]
[docs] def plot_pixel_layout(camera, raw_pixels, *, rotate_angle=None, pixels_id_to_print=50): """ Create the pixel layout figure. Parameters ---------- camera : Camera Camera with pixel geometry; its pixel positions are transformed in place. raw_pixels : dict Unrotated pixel data with keys ``x``, ``y`` and ``rotate_angle`` (radians). rotate_angle : astropy.units.Quantity, optional Rotation applied to the telescope-specific pixel transform. pixels_id_to_print : int, optional Pixel IDs below this value are printed. Returns ------- matplotlib.figure.Figure The generated figure. """ logger.info("Plotting pixel layout for %s", camera.telescope_name) _apply_telescope_specific_pixel_transform(camera, raw_pixels, rotate_angle) return _create_pixel_plot(camera, pixels_id_to_print)
def _create_pixel_plot(camera, pixels_id_to_print): """Create and configure the pixel layout plot.""" fig, ax = plt.subplots(figsize=(8, 8)) on_pixels, edge_pixels, off_pixels = create_pixel_patches_by_type(camera) add_pixel_patch_collections(ax, on_pixels, edge_pixels, off_pixels) array_element_type = names.get_array_element_type_from_name(camera.telescope_name) font_size = 2 if "SCT" in array_element_type else 4 for x, y, pix_id in zip(camera.pixels["x"], camera.pixels["y"], camera.pixels["pix_id"]): if pix_id < pixels_id_to_print: plt.text(x, y, pix_id, ha="center", va="center", fontsize=font_size) _configure_plot(ax, camera) add_pixel_legend(ax, on_pixels, off_pixels) return fig def _configure_plot(ax, camera): """Configure titles, labels, limits and the coordinate-axes overlay.""" x_values, y_values = camera.pixels["x"], camera.pixels["y"] padding = 0.1 * max(max(x_values) - min(x_values), max(y_values) - min(y_values)) setup_camera_axis_properties(ax, camera, grid=True, axis_below=True, padding=padding) plt.xlabel("Horizontal scale [cm]", fontsize=18, labelpad=0) plt.ylabel("Vertical scale [cm]", fontsize=18, labelpad=0) ax.set_title("Pixel layout", fontsize=15, y=1.02) plt.tick_params(axis="both", which="major", labelsize=15) rotation = camera.pixels.get("plot_rotate_angle", camera.pixels["rotate_angle"]) * u.rad _add_coordinate_axes(ax, rotation) x_text = min(x_values) - (max(x_values) - min(x_values)) * 0.05 y_text = min(y_values) - (max(y_values) - min(y_values)) * 0.05 ax.text( x_text, y_text, "For an observer facing the camera", fontsize=10, ha="left", va="bottom" ) def _add_coordinate_axes(ax, rotation=0 * u.deg): """Add the Az/Alt and pixel coordinate axis arrows to the plot.""" x_min, x_max = ax.get_xlim() y_min, y_max = ax.get_ylim() axis_length = min(x_max - x_min, y_max - y_min) * 0.08 x_origin = x_max - axis_length * 1.0 y_origin_az = y_min + axis_length * 2.5 y_origin_pix = y_min + axis_length * 1.2 arrow_style = { "head_width": axis_length * 0.15, "head_length": axis_length * 0.15, "width": axis_length * 0.02, } arrow_length = 0.6 is_sst = abs(rotation - (90.0 * u.deg)).value < 1.0 az_direction = 1 if is_sst else -1 def add_arrow_label(ox, oy, dx, dy, label, offset, color="black", ha="center", va="center"): """Add an arrow with its label.""" ax.arrow(ox, oy, dx, dy, fc=color, ec=color, **arrow_style) if np.sqrt(dx**2 + dy**2) > 0: dir_unit = np.sqrt(dx**2 + dy**2) ax.text( ox + dx + dx / dir_unit * axis_length * offset, oy + dy + dy / dir_unit * axis_length * offset, label, ha=ha, va=va, color=color, fontsize=10, fontweight="bold", ) # Az-Alt axes add_arrow_label( x_origin, y_origin_az, az_direction * axis_length * arrow_length, 0, "Az", 0.25, "red", ha="left" if az_direction > 0 else "right", ) add_arrow_label( x_origin, y_origin_az, 0, -axis_length * arrow_length, "Alt", 0.25, "red", va="top" ) # Pixel coordinate axes rot_angle = rotation.to(u.rad).value x_direction = -1 if is_sst else 1 x_dir = x_direction * axis_length * arrow_length * np.cos(rot_angle) y_dir = x_direction * axis_length * arrow_length * np.sin(rot_angle) add_arrow_label(x_origin, y_origin_pix, x_dir, y_dir, "$\\mathrm{x}_\\mathrm{pix}$", 0.45) y_dx = axis_length * arrow_length * np.sin(rot_angle) y_dy = -axis_length * arrow_length * np.cos(rot_angle) add_arrow_label(x_origin, y_origin_pix, y_dx, y_dy, "$\\mathrm{y}_\\mathrm{pix}$", 0.45) def _apply_telescope_specific_pixel_transform(camera, raw_pixels, rotate_angle=None): """Apply the telescope-specific pixel transform to ``camera.pixels`` in place.""" x_pos = np.asarray(raw_pixels["x"], dtype=float) y_pos = np.asarray(raw_pixels["y"], dtype=float) two_mirror = is_two_mirror_telescope(camera.telescope_name) if not two_mirror: y_pos = -y_pos if rotate_angle is None: raw_rotate_angle = raw_pixels.get("rotate_angle") rotate_angle = 0.0 * u.deg if raw_rotate_angle is None else raw_rotate_angle * u.rad if isinstance(rotate_angle, (int, float, np.floating)): rotate_angle = float(rotate_angle) * u.deg total_rotation = (90 if two_mirror else -90) * u.deg - rotate_angle.to(u.deg) rot_angle = total_rotation.to(u.rad).value camera.pixels["x"] = x_pos * np.cos(rot_angle) - y_pos * np.sin(rot_angle) camera.pixels["y"] = y_pos * np.cos(rot_angle) + x_pos * np.sin(rot_angle) if camera.pixels.get("pixel_shape") in (1, 3): base_orientation = 30 if camera.pixels["pixel_shape"] == 3 else 0 camera.pixels["orientation"] = base_orientation + total_rotation.to_value(u.deg) camera.pixels["plot_rotate_angle"] = float(rot_angle)