{ "cells": [ { "cell_type": "raw", "id": "0", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "Prime Focus Telescope Tutorial\n", "==============================\n", "\n", "The `prime focus telescope `_\n", "is one of the simplest telescope designs since it has only one reflection and no\n", "obscuration surfaces (ignoring the small obscuration from the sensor).\n", "It works by placing a sensor at the focus of a parabolic primary mirror.\n", "This tutorial will demonstrate how to model a prime focus telescope using\n", ":mod:`optika` and investigate its performance" ] }, { "cell_type": "code", "execution_count": null, "id": "1", "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import astropy.units as u\n", "import astropy.visualization\n", "import named_arrays as na\n", "import optika" ] }, { "cell_type": "raw", "id": "2", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "Primary mirror\n", "--------------\n", "\n", "We'll start by defining the primary mirror aperture radius" ] }, { "cell_type": "code", "execution_count": null, "id": "3", "metadata": {}, "outputs": [], "source": [ "radius_aperture = 100 * u.mm" ] }, { "cell_type": "raw", "id": "4", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "and the :math:`f`-number of the primary mirror" ] }, { "cell_type": "code", "execution_count": null, "id": "5", "metadata": {}, "outputs": [], "source": [ "f_number = 5" ] }, { "cell_type": "markdown", "id": "6", "metadata": {}, "source": [ "So the focal length of the primary is then" ] }, { "cell_type": "code", "execution_count": null, "id": "7", "metadata": {}, "outputs": [], "source": [ "focal_length = f_number * radius_aperture\n", "focal_length" ] }, { "cell_type": "raw", "id": "8", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "We can specify a parabolic sag profile for our primary mirror using the :class:`optika.sags.ParabolicSag` class" ] }, { "cell_type": "code", "execution_count": null, "id": "9", "metadata": {}, "outputs": [], "source": [ "sag_primary = optika.sags.ParabolicSag(-focal_length)" ] }, { "cell_type": "raw", "id": "10", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "For simplicity, we will consider the primary mirror to be a perfectly-reflective mirror,\n", "which we can reprsent with :class:`optika.materials.Mirror`." ] }, { "cell_type": "code", "execution_count": null, "id": "11", "metadata": {}, "outputs": [], "source": [ "material_primary = optika.materials.Mirror()" ] }, { "cell_type": "raw", "id": "12", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "To specify a circular apeture for our primary mirror, we can use :class:`optika.apertures.CircularAperture`." ] }, { "cell_type": "code", "execution_count": null, "id": "13", "metadata": {}, "outputs": [], "source": [ "aperture_primary = optika.apertures.CircularAperture(radius_aperture)" ] }, { "cell_type": "raw", "id": "14", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "Finally, we can specify the position and orientation of the primary mirror using the\n", ":mod:`named_arrays.transformations` module.\n", "To translate the primary mirror one focal length away from the origin, we use :class:`named_arrays.transformations.Cartesian3dTranslation`." ] }, { "cell_type": "code", "execution_count": null, "id": "15", "metadata": {}, "outputs": [], "source": [ "translation_primary = na.transformations.Cartesian3dTranslation(z=focal_length)" ] }, { "cell_type": "raw", "id": "16", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "We can combine the sag profile, aperture shape, material type, and coordinate transformation into one object to represent the primary mirror,\n", "an instance of :class:`optika.surfaces.Surface`." ] }, { "cell_type": "code", "execution_count": null, "id": "17", "metadata": {}, "outputs": [], "source": [ "primary = optika.surfaces.Surface(\n", " name=\"primary\",\n", " sag=sag_primary,\n", " material=material_primary,\n", " aperture=aperture_primary,\n", " transformation=translation_primary,\n", " is_pupil_stop=True,\n", ")" ] }, { "cell_type": "raw", "id": "18", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "The ``is_pupil_stop=True`` statement in the previous cell sets the primary mirror as the pupil stop,\n", "the surface that controls the size of the entrance pupil." ] }, { "cell_type": "raw", "id": "19", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "Sensor\n", "------\n", "\n", "If the size of each pixel in the sensor is" ] }, { "cell_type": "code", "execution_count": null, "id": "20", "metadata": {}, "outputs": [], "source": [ "width_pixel = 15 * u.um" ] }, { "cell_type": "raw", "id": "21", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "The number of pixels along each axis is" ] }, { "cell_type": "code", "execution_count": null, "id": "22", "metadata": {}, "outputs": [], "source": [ "num_pixel = na.Cartesian2dVectorArray(256, 256)" ] }, { "cell_type": "raw", "id": "23", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "The name of each axis of the pixel array is" ] }, { "cell_type": "code", "execution_count": null, "id": "24", "metadata": {}, "outputs": [], "source": [ "axis_pixel = na.Cartesian2dVectorArray(\"detector_x\", \"detector_y\")" ] }, { "cell_type": "raw", "id": "25", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "In :mod:`optika`, the surface normal should be antiparallel to the incident light.\n", "To accomplish this the sensor needs to be rotated 180 degrees." ] }, { "cell_type": "code", "execution_count": null, "id": "26", "metadata": {}, "outputs": [], "source": [ "rotation_sensor = na.transformations.Cartesian3dRotationY(180 * u.deg)" ] }, { "cell_type": "raw", "id": "27", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "Putting it all together, we can represent the sensor using :class:`optika.sensors.ImagingSensor`" ] }, { "cell_type": "code", "execution_count": null, "id": "28", "metadata": {}, "outputs": [], "source": [ "sensor = optika.sensors.ImagingSensor(\n", " name=\"sensor\",\n", " width_pixel=width_pixel,\n", " axis_pixel=axis_pixel,\n", " num_pixel=num_pixel,\n", " transformation=rotation_sensor,\n", " is_field_stop=True,\n", ")" ] }, { "cell_type": "raw", "id": "29", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "The ``is_field_stop=True`` statement in the previous cell sets the sensor as the field stop,\n", "the surface that controls the size of the field of view\n", "\n", "Input rays\n", "----------\n", "\n", "The position and direction of the input rays is specified in terms\n", "of normalized coordinates.\n", "We can specify a uniform normlized pupil grid withSS" ] }, { "cell_type": "code", "execution_count": null, "id": "30", "metadata": {}, "outputs": [], "source": [ "pupil = na.Cartesian2dVectorLinearSpace(\n", " start=-1,\n", " stop=1,\n", " axis=na.Cartesian2dVectorArray(\"px\", \"py\"),\n", " num=5,\n", " centers=True,\n", ")" ] }, { "cell_type": "raw", "id": "31", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "a normalized field grid with" ] }, { "cell_type": "code", "execution_count": null, "id": "32", "metadata": {}, "outputs": [], "source": [ "field = na.Cartesian2dVectorLinearSpace(\n", " start=-1,\n", " stop=1,\n", " axis=na.Cartesian2dVectorArray(\"fx\", \"fy\"),\n", " num=5,\n", " centers=True,\n", ")" ] }, { "cell_type": "raw", "id": "33", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "and assume a constant wavelength" ] }, { "cell_type": "code", "execution_count": null, "id": "34", "metadata": {}, "outputs": [], "source": [ "wavelength = 500 * u.nm" ] }, { "cell_type": "raw", "id": "35", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ ":mod:`optika` provides a vector type for representing a point in wavelength/field/pupil space,\n", ":class:`optika.vectors.ObjectVectorArray`" ] }, { "cell_type": "code", "execution_count": null, "id": "36", "metadata": {}, "outputs": [], "source": [ "grid_input = optika.vectors.ObjectVectorArray(\n", " wavelength=wavelength,\n", " field=field,\n", " pupil=pupil,\n", ")" ] }, { "cell_type": "raw", "id": "37", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "Building the system\n", "-------------------\n", "\n", ":class:`optika.systems.SequentialSystem`\n", "is a composition of the optical surfaces and the input ray grid." ] }, { "cell_type": "code", "execution_count": null, "id": "38", "metadata": {}, "outputs": [], "source": [ "system = optika.systems.SequentialSystem(\n", " surfaces=[\n", " primary,\n", " ],\n", " sensor=sensor,\n", " grid_input=grid_input,\n", ")" ] }, { "cell_type": "raw", "id": "39", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "We can plot the system using the :meth:`optika.systems.SequentialSystem.plot` method." ] }, { "cell_type": "code", "execution_count": null, "id": "40", "metadata": {}, "outputs": [], "source": [ "# plot the system\n", "with astropy.visualization.quantity_support():\n", " fig, ax = plt.subplots(constrained_layout=True)\n", " ax.set_aspect(\"equal\")\n", " system.plot(\n", " ax=ax,\n", " components=(\"z\", \"y\"),\n", " kwargs_rays=dict(\n", " color=\"tab:blue\",\n", " ),\n", " color=\"black\",\n", " zorder=10,\n", " )" ] }, { "cell_type": "raw", "id": "41", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "Spot Diagrams\n", "-------------\n", "\n", "We can plot a spot diagram for each field angle using the\n", ":meth:`~optika.systems.SequentialSystem.spot_diagram` method of\n", ":class:`~optika.systems.SequentialSystem`." ] }, { "cell_type": "code", "execution_count": null, "id": "42", "metadata": {}, "outputs": [], "source": [ "fig, ax = system.spot_diagram()" ] } ], "metadata": { "celltoolbar": "Raw Cell Format", "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.3" } }, "nbformat": 4, "nbformat_minor": 5 }