From b1b76179a2b9606620cc5cf894ef0c53550aecdd Mon Sep 17 00:00:00 2001 From: Andy Kee Date: Sun, 26 Nov 2023 19:42:47 -0800 Subject: [PATCH] Rework API docs --- .gitignore | 2 +- docs/_img/python/focus_images.py | 60 ----- docs/_img/python/tilt_images.py | 50 ---- docs/_static/css/lentil.css | 16 +- docs/_templates/autosummary/attribute.rst | 7 + docs/_templates/autosummary/class.rst | 49 ++-- docs/_templates/autosummary/property.rst | 12 + docs/_templates/index.html | 2 +- docs/_templates/indexcontent.html | 82 ------- docs/conf.py | 28 +-- docs/dev/tech_notes/dft_caching.rst | 86 ------- docs/dev/tech_notes/index.rst | 1 - docs/docs.rst | 16 +- docs/examples/index.rst | 1 - docs/examples/tutorial.rst | 86 ------- docs/ref/detector.rst | 47 ++++ docs/ref/imaging_artifacts.rst | 13 ++ docs/ref/index.rst | 28 +++ docs/ref/internals.rst | 42 ++++ docs/ref/planes.rst | 37 +++ docs/ref/propagate.rst | 10 + docs/ref/radiometry.rst | 23 ++ docs/ref/util.rst | 36 +++ docs/ref/wavefront.rst | 10 + docs/ref/wavefront_errors.rst | 13 ++ docs/ref/zernike.rst | 15 ++ docs/reference.rst | 36 --- docs/user/basics.coordinates.rst | 10 +- docs/user/basics.diffraction.rst | 37 ++- docs/user/basics.optical_systems.rst | 2 - docs/user/basics.planes.rst | 21 +- docs/user/basics.wavefront_error.rst | 60 ++--- .../images}/propagate_npix_prop.png | Bin docs/user/plots/focus_images.py | 47 ++++ docs/{_img/python => user/plots}/npix_prop.py | 19 +- docs/user/plots/tilt_images.py | 45 ++++ docs/user/quickstart.rst | 122 +++++----- lentil/field.py | 33 ++- lentil/plane.py | 216 +++++++++++++----- lentil/wavefront.py | 85 +++++-- 40 files changed, 812 insertions(+), 693 deletions(-) delete mode 100644 docs/_img/python/focus_images.py delete mode 100644 docs/_img/python/tilt_images.py create mode 100644 docs/_templates/autosummary/attribute.rst create mode 100644 docs/_templates/autosummary/property.rst delete mode 100644 docs/_templates/indexcontent.html delete mode 100644 docs/dev/tech_notes/dft_caching.rst delete mode 100644 docs/examples/tutorial.rst create mode 100644 docs/ref/detector.rst create mode 100644 docs/ref/imaging_artifacts.rst create mode 100644 docs/ref/index.rst create mode 100644 docs/ref/internals.rst create mode 100644 docs/ref/planes.rst create mode 100644 docs/ref/propagate.rst create mode 100644 docs/ref/radiometry.rst create mode 100644 docs/ref/util.rst create mode 100644 docs/ref/wavefront.rst create mode 100644 docs/ref/wavefront_errors.rst create mode 100644 docs/ref/zernike.rst delete mode 100644 docs/reference.rst rename docs/{_static/img => user/images}/propagate_npix_prop.png (100%) create mode 100644 docs/user/plots/focus_images.py rename docs/{_img/python => user/plots}/npix_prop.py (69%) create mode 100644 docs/user/plots/tilt_images.py diff --git a/.gitignore b/.gitignore index ff67362..5416535 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,7 @@ cover/ # Sphinx documentation docs/_build/ -docs/generated +docs/ref/generated # PyBuilder .pybuilder/ diff --git a/docs/_img/python/focus_images.py b/docs/_img/python/focus_images.py deleted file mode 100644 index cceac3c..0000000 --- a/docs/_img/python/focus_images.py +++ /dev/null @@ -1,60 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -import lentil - -import matplotlib as mpl -mpl.rcParams['figure.figsize'] = (4, 4) - -amp = np.zeros((256, 256)) -amp += lentil.circlemask((256, 256), 64, shift=(32, 8)) -amp -= lentil.circlemask((256, 256), 40, shift=(32, 8)) -amp[:, 0:128+8] = 0 -amp[32:225, 104-16:128-16] = 1 -amp[96:120, 128-16:144-8] = 1 -amp[201:225, 128-16:144-8] = 1 - -focus = lentil.zernike(mask=np.ones((256, 256)), index=4) - -pupil_neg = lentil.Pupil(amplitude=amp, pixelscale=1/240, focal_length=10) -pupil_neg.phase = -6e-6 * focus -w_neg = lentil.Wavefront(650e-9) -w_neg *= pupil_neg -w_neg = lentil.propagate_dft(w_neg, pixelscale=5e-6, shape=200, oversample=2) - -pupil_pos = lentil.Pupil(amplitude=amp, pixelscale=1/240, focal_length=10) -pupil_pos.phase = 6e-6 * focus -w_pos = lentil.Wavefront(650e-9) -w_pos *= pupil_pos -w_pos = lentil.propagate_dft(w_pos, pixelscale=5e-6, shape=200, oversample=2) - -plt.subplot(1, 2, 1) -plt.imshow(w_pos.intensity, origin='lower') -plt.title('Image plane (+focus)') -plt.xticks(np.linspace(0, 400, 5), labels=np.linspace(-1, 1, 5)) -plt.yticks(np.linspace(0, 400, 5), labels=np.linspace(-1, 1, 5)) -plt.xlabel('[m]') - -plt.subplot(1, 2, 2) -plt.imshow(w_neg.intensity, origin='lower') -plt.title('Image plane (-focus)') -plt.xticks(np.linspace(0, 400, 5), labels=np.linspace(-1, 1, 5)) -plt.yticks(np.linspace(0, 400, 5), labels=np.linspace(-1, 1, 5)) -plt.xlabel('[m]') - -#fig, axs = plt.subplots(1, 2) -# -#ax1 = axs[0] -#ax1.imshow(w_pos.intensity, origin='lower') -#ax1.set_title('Image plane (+focus)') -#ax1.set_xticks(np.linspace(0, 400, 5), labels=np.linspace(-1, 1, 5)) -#ax1.set_yticks(np.linspace(0, 400, 5), labels=np.linspace(-1, 1, 5)) -#ax1.set_xlabel('[mm]') -# -#ax2 = axs[1] -#ax2.imshow(w_neg.intensity, origin='lower') -##ax2.set_title('Image plane (-focus)') -#ax2.set_xticks(np.linspace(0, 400, 5), labels=np.linspace(-1, 1, 5)) -#ax2.set_yticks(np.linspace(0, 400, 5), labels=np.linspace(-1, 1, 5)) -#ax2.set_xlabel('[mm]') - -plt.tight_layout() diff --git a/docs/_img/python/tilt_images.py b/docs/_img/python/tilt_images.py deleted file mode 100644 index 7a387af..0000000 --- a/docs/_img/python/tilt_images.py +++ /dev/null @@ -1,50 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -import lentil - -import matplotlib as mpl -mpl.rcParams['figure.figsize'] = (3.5, 3.5) - -amp = lentil.circle((256, 256), 120) -x_tilt = 8e-6 * lentil.zernike(amp, 3) # +x tilt -y_tilt = 8e-6 * lentil.zernike(amp, 2) # +y tilt - -py = lentil.Pupil(focal_length=10, pixelscale=1 / 240, amplitude=amp, phase=y_tilt) -wy = lentil.Wavefront(650e-9) -wy *= py -wy = lentil.propagate_dft(wy, pixelscale=5e-6, shape=200, oversample=5) - -px = lentil.Pupil(focal_length=10, pixelscale=1 / 240, amplitude=amp, phase=x_tilt) -wx = lentil.Wavefront(650e-9) -wx *= px -wx = lentil.propagate_dft(wx, pixelscale=5e-6, shape=200, oversample=5) - -plt.subplot(2, 2, 1) -plt.imshow(x_tilt, origin='lower') -plt.title('Pupil plane ($+R_x$)') -plt.xticks(np.linspace(0, 256, 5), labels=np.linspace(-0.5, 0.5, 5)) -plt.yticks(np.linspace(0, 256, 5), labels=np.linspace(-0.5, 0.5, 5)) -plt.xlabel('[m]') - -plt.subplot(2, 2, 2) -plt.imshow(y_tilt, origin='lower') -plt.title('Pupil plane ($+R_y$)') -plt.xticks(np.linspace(0, 256, 5), labels=np.linspace(-0.5, 0.5, 5)) -plt.yticks(np.linspace(0, 256, 5), labels=np.linspace(-0.5, 0.5, 5)) -plt.xlabel('[m]') - -plt.subplot(2, 2, 3) -plt.imshow(wx.intensity ** 0.2, origin='lower') -plt.title('Image plane ($+R_x$)') -plt.xticks(np.linspace(0, 200 * 5, 5), labels=np.linspace(-1, 1, 5)) -plt.yticks(np.linspace(0, 200 * 5, 5), labels=np.linspace(-1, 1, 5)) -plt.xlabel('[mm]') - -plt.subplot(2, 2, 4) -plt.imshow(wy.intensity ** 0.2, origin='lower') -plt.title('Image plane ($+R_y$)') -plt.xticks(np.linspace(0, 200 * 5, 5), labels=np.linspace(-1, 1, 5)) -plt.yticks(np.linspace(0, 200 * 5, 5), labels=np.linspace(-1, 1, 5)) -plt.xlabel('[mm]') - -plt.tight_layout() diff --git a/docs/_static/css/lentil.css b/docs/_static/css/lentil.css index 7c3af04..a456efd 100644 --- a/docs/_static/css/lentil.css +++ b/docs/_static/css/lentil.css @@ -31,15 +31,12 @@ a:hover code{ text-decoration-thickness: 1px; } -.sig-name { - --pst-color-inline-code: #5E81AC; +a.headerlink { + color: #ee9040; } -pre { - background-color: #f8f8f8; - border: 0px; - box-shadow: none; - padding: 20px; +.sig-name { + --pst-color-inline-code: #5E81AC; } h1, h2, h3, h4, h5, h6 { @@ -89,6 +86,11 @@ a.hederlink:hover { color: #ee9040 } +/* Disable API reference name highlighting */ +dt:target, span.highlighted { + background-color: transparent; +} + :target:before{ content:""; display:block; diff --git a/docs/_templates/autosummary/attribute.rst b/docs/_templates/autosummary/attribute.rst new file mode 100644 index 0000000..f1baeaf --- /dev/null +++ b/docs/_templates/autosummary/attribute.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ fullname | replace("lentil.", "lentil::") }} diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst index c13da30..5a887a9 100644 --- a/docs/_templates/autosummary/class.rst +++ b/docs/_templates/autosummary/class.rst @@ -1,26 +1,31 @@ -{% extends "!autosummary/class.rst" %} +{{ fullname | escape | underline}} -{% block methods %} -{% if methods %} - .. HACK -- the point here is that we don't want this to appear in the output, but the autosummary should still generate the pages. - .. autosummary:: - :toctree: - {% for item in all_methods %} - {%- if not item.startswith('_') or item in ['__call__'] %} - {{ name }}.{{ item }} - {%- endif -%} - {%- endfor %} -{% endif %} -{% endblock %} +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} -{% block attributes %} {% if attributes %} - .. HACK -- the point here is that we don't want this to appear in the output, but the autosummary should still generate the pages. - .. autosummary:: - {% for item in all_attributes %} - {%- if not item.startswith('_') %} - {{ name }}.{{ item }} - {%- endif -%} - {%- endfor %} +.. rubric:: {{ _('Attributes') }} + +.. autosummary:: + :toctree: + {% for item in attributes %} + {%- if not item.startswith('_') %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} -{% endblock %} \ No newline at end of file + +{% if methods %} +.. rubric:: {{ _('Methods') }} + +.. autosummary:: + :toctree: + {% for item in methods %} + {%- if not item.startswith('_') or item in ['__call__'] %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + +{% endif %} \ No newline at end of file diff --git a/docs/_templates/autosummary/property.rst b/docs/_templates/autosummary/property.rst new file mode 100644 index 0000000..de272bd --- /dev/null +++ b/docs/_templates/autosummary/property.rst @@ -0,0 +1,12 @@ +:orphan: + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoattribute:: {{ fullname | replace("lentil.", "lentil::") }} + +{# Normally this line would read + .. auto{{ objtype }}:: {{ fullname | replace("lentil.", "lentil::") }} +but we've explicitly called autoattribute so that properties are rendered +in a way that is indistinguishable from attributes #} \ No newline at end of file diff --git a/docs/_templates/index.html b/docs/_templates/index.html index 156134f..e15fbbd 100644 --- a/docs/_templates/index.html +++ b/docs/_templates/index.html @@ -63,7 +63,7 @@

Lentil: Fast optical propagation

- +
Reference
diff --git a/docs/_templates/indexcontent.html b/docs/_templates/indexcontent.html deleted file mode 100644 index 207d801..0000000 --- a/docs/_templates/indexcontent.html +++ /dev/null @@ -1,82 +0,0 @@ -{# - Loosely inspired by the deprecated sphinx/themes/basic/defindex.html - #} - {%- extends "layout.html" %} - {% set title = _('Overview') %} - {% block body %} -

Lentil documentation

-

Version: {{ release|e }}

- {% if last_updated %}

Date: {{ last_updated|e }}

{% endif %} - -

- Lentil is a Python library for modeling the imaging chain of an optical system. - It was originally developed at NASA's Jet Propulsion Lab by the Wavefront Sensing and - Control group (383E) to provide an easy to use framework for simulating point spread - functions of segmented aperture telescopes. -

- -
-

Note

-

Lentil is still under active development and new features continue to be added. - Until Lentil reaches version 1.0, the API is not guaranteed to be stable, but - changes breaking backwards compatibility will be noted in the release notes.

-
- -

Getting started

- - -
- - - -
- -

User guide

- - -
- - - - - - - - - - -
- -

API reference

- - -
- -
- - -

Developer guide

- - -
- - -
- - {% endblock %} diff --git a/docs/conf.py b/docs/conf.py index c764f0a..ca3131d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,6 +21,7 @@ with open(os.path.normpath(os.path.join(path, '..', 'lentil', '__init__.py'))) as f: version = release = re.search("__version__ = '(.*?)'", f.read()).group(1) +today_fmt = '%B %d, %Y' # -- General configuration --------------------------------------------------- @@ -32,8 +33,8 @@ 'sphinx_remove_toctrees', 'sphinx_copybutton', 'sphinx_design', - 'matplotlib.sphinxext.plot_directive', - 'numpydoc'] + 'matplotlib.sphinxext.plot_directive']#, + #'numpydoc'] templates_path = ['_templates'] source_suffix = '.rst' master_doc = 'docs' @@ -53,6 +54,8 @@ }, "collapse_navigation": True, "navbar_persistent": ["search-button"], + "pygment_light_style": "tango", + "pygment_dark_style": "nord", "favicons": [ { "rel": "icon", @@ -93,20 +96,20 @@ html_css_files = ['css/lentil.css'] -pygments_style = 'default' - # if true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True +add_function_parentheses = True + autodoc_default_options = { - 'member-order': 'bysource', - 'undoc-members': None -# 'exclude-members': '__init__, __weakref__, __dict__, __module__' + 'member-order': 'alphabetical', + 'exclude-members': '__init__, __weakref__, __dict__, __module__', } autosummary_generate = True + #remove_from_toctrees = ["generated/*"] # -- Plot config ------------------------------------------------------------- @@ -138,16 +141,9 @@ plot_html_show_formats = False plot_formats = [('png', dpi*2)] plot_pre_code = """ +import lentil +import matplotlib.pyplot as plt import numpy as np np.random.seed(12345) """ - - - -#def fix_attributes(app, pagename, templatename, context, doctree): -# if 'generated' in pagename: -# context['body'] = context['body'].replace('Variables', 'Attributes') - -#def setup(app): -# app.connect("html-page-context", fix_attributes) \ No newline at end of file diff --git a/docs/dev/tech_notes/dft_caching.rst b/docs/dev/tech_notes/dft_caching.rst deleted file mode 100644 index 4fc4dfc..0000000 --- a/docs/dev/tech_notes/dft_caching.rst +++ /dev/null @@ -1,86 +0,0 @@ -***************************** -Caching in ``fourier.dft2()`` -***************************** - -:func:`~lentil.fourier.dft2` uses the matrix triple product (MTP) approach to -compute Fourier transforms with variable input and output plane sampling and -sizes [1]_. - -The Fourier transform of an array :math:`f` is given by - -.. math:: - - F = E_1 f E_2 - -where the Fourier kernels :math:`E_1` and :math:`E_2` are constructed as - -.. math:: - - E_1 = \exp{(-2\pi i \alpha_y Y V)} - - E_2 = \exp{(-2\pi i \alpha_x X U)} - -given vectors of input plane coordinates :math:`X` and :math:`Y`, vectors of output -plane coordinates :math:`U` and :math:`V`, and sampling coefficients :math:`\alpha_x` -and :math:`\alpha_y`. - -Fourier kernel caching ----------------------- -If the input and output array sizes and sampling are held constant, :math:`E_1` and -:math:`E_2` can be computed once and reused providing increased performance for all -subsequent evaluations. This is achieved by employing Python's ``modeltools.lru_cache`` -decorator on a method which computes these matrices. The maximum cache size is 32 -entries. - -Caching with variable output plane shifts ------------------------------------------ -Repeated calls to :func:`~lentil.fourier.dft2` with a static :attr:`shift` term will -hit the Fourier kernel cache described above. In the event that :attr:`shift` varies -on repeated calls to :func:`~lentil.fourier.dft2` we can still achieve some performance -gains by caching the :math:`X`, :math:`Y`, :math:`U`, and :math:`V` vectors. - -Implementation --------------- -The MTP described above is imlemented in Lentil using a two stage LRU caching approach -that caches the calculation of ``X``, ``Y``, ``U``, and ``V`` in all cases and caches -the calculation of ``E1`` and ``E2`` for when ``shift`` does not vary. - -.. code:: python - - @functools.lru_cache(maxsize=32) - def _dft2_coords(m, n, M, N): - # Y and X are (r,c) coordinates in the (m x n) input plane f - # V and U are (r,c) coordinates in the (M x N) ourput plane F - - X = np.arange(n) - np.floor(n/2.0) - Y = np.arange(m) - np.floor(m/2.0) - U = np.arange(N) - np.floor(N/2.0) - V = np.arange(M) - np.floor(M/2.0) - - return X, Y, U, V - - - @functools.lru_cache(maxsize=32) - def _dft2_matrices(m, n, M, N, ax, ay, sx, sy): - X, Y, U, V = _dft2_coords(m, n, M, N) - E1 = np.exp(-2.0 * np.pi * 1j * ay * np.outer(Y-sy, V-sy)).T - E2 = np.exp(-2.0 * np.pi * 1j * ax * np.outer(X-sx, U-sx)) - return E1, E2 - - - def dft2(f, alpha, npix=None, shift=(0, 0)): - - ... - - E1, E2 = _dft2_matrices(m, n, M, N, ax, ay, sx, sy) - F = np.dot(E1.dot(f), E2) - - ... - - return F - - -References ----------- - -.. [1] Jurling et. al., *Techniques for arbitrary sampling in two-dimensional Fourier transforms*. \ No newline at end of file diff --git a/docs/dev/tech_notes/index.rst b/docs/dev/tech_notes/index.rst index 5a6dec3..d25ae27 100644 --- a/docs/dev/tech_notes/index.rst +++ b/docs/dev/tech_notes/index.rst @@ -8,5 +8,4 @@ Technical Notes :maxdepth: 1 dft_sampling - dft_caching prop_algorithm diff --git a/docs/docs.rst b/docs/docs.rst index dc6713e..c6476f1 100644 --- a/docs/docs.rst +++ b/docs/docs.rst @@ -3,10 +3,10 @@ .. toctree:: :hidden: - user/index - examples/index - reference - dev/index + User Guide + Examples + API Reference + Development ******************** Lentil Documentation @@ -45,7 +45,7 @@ functions of segmented aperture telescopes. .. button-ref:: user/index :expand: - :color: muted + :color: info :click-parent: To the user guide @@ -59,7 +59,7 @@ functions of segmented aperture telescopes. .. button-ref:: examples/index :expand: - :color: light + :color: info :click-parent: To the examples @@ -73,9 +73,9 @@ functions of segmented aperture telescopes. +++ - .. button-ref:: reference + .. button-ref:: ref/index :expand: - :color: dark + :color: info :click-parent: To the reference guide diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 90a3788..0bc04e9 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -9,7 +9,6 @@ General .. toctree:: :maxdepth: 1 - tutorial general/simple general/large general/attributes diff --git a/docs/examples/tutorial.rst b/docs/examples/tutorial.rst deleted file mode 100644 index 0e9e75d..0000000 --- a/docs/examples/tutorial.rst +++ /dev/null @@ -1,86 +0,0 @@ -.. _tutorial: - -************************ -Getting started tutorial -************************ - -In this short example, we'll walk through the steps to develop a very simple Lentil -model of an imaging system with a single pupil. We'll then propagate light through -the imaging system to an image plane and view the resulting point spread function -(PSF). The imaging system has a 1m diameter primary mirror, a secondary mirror -obscuration of 0.33m centered over the primary, a focal length of 10m, and a focal -plane with 5um pixels. - -First, we'll import Lentil and matplotlib: - -.. code-block:: pycon - - >>> import matplotlib.pyplot as plt - >>> import lentil - -Now we can define the system amplitude and plot it: - -.. code-block:: pycon - - >>> amplitude = lentil.circle(shape=(256, 256), radius=128) - - ... lentil.circle(shape=(256, 256), radius=128/3) - >>> plt.imshow(amplitude) - -.. image:: /_static/img/getting_started_amp.png - :width: 350px - -We'll create some wavefront error constructed from a combination of Zernikes: - -.. code-block:: pycon - - >>> coeffs = [0, 0, 0, 300e-9, 50e-9, -100e-9, 50e-9] - >>> opd = lentil.zernike_compose(mask=amplitude, coeffs=coeffs) - >>> plt.imshow(opd) - -.. image:: /_static/img/getting_started_opd.png - :width: 350px - -Next we'll define the system's pupil plane. Note that the -:attr:`~lentil.Pupil.pixelscale` attribute represents the physical sampling of each -pixel in the pupil (in meters/pixel). Because our amplitude has a diameter of 256 pixels -and the system diameter was specified as 1m, the pixelscale is 1/256. - -.. code-block:: pycon - - >>> pupil = lentil.Pupil(amplitude=amplitude, phase=opd, focal_length=10, - ... pixelscale=1/256) - -We'll create a monochromatic :class:`~lentil.Wavefront` with wavelength of 650nm and -propagate it "through" the pupil plane. This operation is represented by multiplying the -wavefront by the pupil: - -propagate the wavefront through the pupil plane, and finally on to an image plane with -5um pixels. We'll also oversample the image plane by a factor of 10. - -.. code-block:: pycon - - >>> w = lentil.Wavefront(wavelength=650e-9) - >>> w = w * pupil - -Now we can propagate the wavefront from the pupil plane to an image plane with 5 micron -pixels. We'll perform this propagation 10x oversampled and look at the resulting intensity -pattern: - -.. code-block:: pycon - - >>> w = lentil.propagate_image(w, pixelscale=5e-6, npix=32, oversample=10) - >>> plt.imshow(w.intensity) - -.. image:: /_static/img/getting_started_psf_oversample.png - :width: 350px - -Finally, we will rescale the oversampled image to native sampling and include the -blurring effects of the pixel MTF: - -.. code-block:: pycon - - >>> img = lentil.detector.pixellate(w.intensity, oversample=10) - >>> plt.imshow(img) - -.. image:: /_static/img/getting_started_psf_detector.png - :width: 350px diff --git a/docs/ref/detector.rst b/docs/ref/detector.rst new file mode 100644 index 0000000..c890d37 --- /dev/null +++ b/docs/ref/detector.rst @@ -0,0 +1,47 @@ +.. _api.detector: + +****************************** +Detector (``lentil.detector``) +****************************** + +Charge collection +----------------- +.. autosummary:: + :toctree: generated/ + + lentil.detector.collect_charge + lentil.detector.collect_charge + +Pixel effects +------------- +.. autosummary:: + :toctree: generated/ + + lentil.detector.pixel + lentil.detector.pixelate + +Noise +----- +.. autosummary:: + :toctree: generated/ + + lentil.detector.shot_noise + lentil.detector.read_noise + lentil.detector.charge_diffusion + lentil.detector.dark_current + lentil.detector.rule07_dark_current + +Readout +------- +.. autosummary:: + :toctree: generated/ + + lentil.detector.adc + +Cosmic rays +----------- +.. autosummary:: + :toctree: generated/ + + lentil.detector.cosmic_rays + \ No newline at end of file diff --git a/docs/ref/imaging_artifacts.rst b/docs/ref/imaging_artifacts.rst new file mode 100644 index 0000000..174569b --- /dev/null +++ b/docs/ref/imaging_artifacts.rst @@ -0,0 +1,13 @@ +.. _api.imaging_artifacts: + +***************** +Imaging artifacts +***************** + +.. autosummary:: + :toctree: generated/ + + lentil.jitter + lentil.smear + + diff --git a/docs/ref/index.rst b/docs/ref/index.rst new file mode 100644 index 0000000..921ca70 --- /dev/null +++ b/docs/ref/index.rst @@ -0,0 +1,28 @@ +.. _api: + +.. module:: lentil + +**************** +Lentil reference +**************** + +:Release: |version| +:Date: |today| + +This reference manual gives an overview of all public functions, modules, and +objects included in Lentil. For learning how to use Lentil, see the +:ref:`user guide `. + +.. toctree:: + :maxdepth: 1 + + planes + wavefront + propagate + zernike + wavefront_errors + imaging_artifacts + radiometry + detector + util + internals diff --git a/docs/ref/internals.rst b/docs/ref/internals.rst new file mode 100644 index 0000000..cdf0414 --- /dev/null +++ b/docs/ref/internals.rst @@ -0,0 +1,42 @@ +.. _api.internals: + +********* +Internals +********* + +Field +----- +.. autosummary:: + :toctree: generated/ + + lentil.field.Field + lentil.field.boundary + lentil.field.extent + lentil.field.insert + lentil.field.merge + lentil.field.overlap + lentil.field.reduce + +Tilt interface +-------------- +.. autosummary:: + :toctree: generated/ + + lentil.plane.TiltInterface + +Fourier transforms +------------------ +.. autosummary:: + :toctree: generated/ + + lentil.fourier.dft2 + +Helper functions +---------------- +.. autosummary:: + :toctree: generated/ + + lentil.helper.mesh + lentil.helper.gaussian2d + lentil.helper.boundary_slice + lentil.helper.slice_offset diff --git a/docs/ref/planes.rst b/docs/ref/planes.rst new file mode 100644 index 0000000..3ac62cb --- /dev/null +++ b/docs/ref/planes.rst @@ -0,0 +1,37 @@ +.. _api.planes: + +************* +Plane objects +************* + +.. autosummary:: + :toctree: generated/ + + lentil.Plane + lentil.Pupil + lentil.Image + lentil.Detector + +Plane transformations +--------------------- +.. autosummary:: + :toctree: generated/ + + lentil.Tilt + lentil.Rotate + lentil.Flip + +Dispersive Planes +----------------- +.. autosummary:: + :toctree: generated/ + + lentil.DispersiveTilt + + +Deprecated +---------- +.. autosummary:: + :toctree: generated/ + + lentil.Grism diff --git a/docs/ref/propagate.rst b/docs/ref/propagate.rst new file mode 100644 index 0000000..66e05cd --- /dev/null +++ b/docs/ref/propagate.rst @@ -0,0 +1,10 @@ +.. _api.propagate: + +*********** +Propagation +*********** + +.. autosummary:: + :toctree: generated/ + + lentil.propagate_dft \ No newline at end of file diff --git a/docs/ref/radiometry.rst b/docs/ref/radiometry.rst new file mode 100644 index 0000000..5ff20ce --- /dev/null +++ b/docs/ref/radiometry.rst @@ -0,0 +1,23 @@ +.. _api.radiometry: + +********************************** +Radiometry (``lentil.radiometry``) +********************************** + +.. autosummary:: + :toctree: generated/ + + lentil.radiometry.Spectrum + lentil.radiometry.Blackbody + lentil.radiometry.Material + +Utilities +--------- +.. autosummary:: + :toctree: generated/ + + lentil.radiometry.path_transmission + lentil.radiometry.path_emission + lentil.radiometry.planck_radiance + lentil.radiometry.planck_exitance + lentil.radiometry.vegaflux \ No newline at end of file diff --git a/docs/ref/util.rst b/docs/ref/util.rst new file mode 100644 index 0000000..0f25c08 --- /dev/null +++ b/docs/ref/util.rst @@ -0,0 +1,36 @@ +.. _api.util: + +********* +Utilities +********* + +Shapes +------ +.. autosummary:: + :toctree: generated/ + + lentil.circle + lentil.circlemask + lentil.hexagon + lentil.slit + +Array manipulation +------------------ +.. autosummary:: + :toctree: generated/ + + lentil.boundary + lentil.centroid + lentil.pad + lentil.window + lentil.rebin + lentil.rescale + +Miscellaneous +------------- +.. autosummary:: + :toctree: generated/ + + lentil.min_sampling + lentil.pixelscale_nyquist + lentil.normalize_power diff --git a/docs/ref/wavefront.rst b/docs/ref/wavefront.rst new file mode 100644 index 0000000..f455efa --- /dev/null +++ b/docs/ref/wavefront.rst @@ -0,0 +1,10 @@ +.. _api.wavefront: + +********* +Wavefront +********* + +.. autosummary:: + :toctree: generated/ + + lentil.Wavefront \ No newline at end of file diff --git a/docs/ref/wavefront_errors.rst b/docs/ref/wavefront_errors.rst new file mode 100644 index 0000000..becf1ba --- /dev/null +++ b/docs/ref/wavefront_errors.rst @@ -0,0 +1,13 @@ +.. _api.wfe: + +**************** +Wavefront errors +**************** + +.. autosummary:: + :toctree: generated/ + + lentil.power_spectrum + lentil.translation_defocus + + diff --git a/docs/ref/zernike.rst b/docs/ref/zernike.rst new file mode 100644 index 0000000..860f909 --- /dev/null +++ b/docs/ref/zernike.rst @@ -0,0 +1,15 @@ +.. _api.zernike: + +******************* +Zernike polynomials +******************* + +.. autosummary:: + :toctree: generated/ + + lentil.zernike + lentil.zernike_compose + lentil.zernike_fit + lentil.zernike_remove + lentil.zernike_basis + lentil.zernike_coordinates \ No newline at end of file diff --git a/docs/reference.rst b/docs/reference.rst deleted file mode 100644 index 52725ac..0000000 --- a/docs/reference.rst +++ /dev/null @@ -1,36 +0,0 @@ -.. _api: - -.. currentmodule:: lentil - -************* -API reference -************* - -This page gives an overview of all public functions, modules, and objects included -in Lentil. All classes and functions exposed in the ``lentil`` namespace are public. - -Some subpackages are public including ``lentil.radiometry`` and -``lentil.detector``. - -.. _api.planes: - -Planes -====== - -.. autosummary:: - :caption: Planes - :toctree: generated/ - - lentil.Plane - lentil.Pupil - lentil.Image - - -Wavefront -========= - -.. autosummary:: - :caption: Wavefront - :toctree: generated/ - - lentil.Wavefront \ No newline at end of file diff --git a/docs/user/basics.coordinates.rst b/docs/user/basics.coordinates.rst index 9b010a0..a4df4a3 100644 --- a/docs/user/basics.coordinates.rst +++ b/docs/user/basics.coordinates.rst @@ -28,16 +28,18 @@ of the complex exponential in the Fourier kernel are provided below: * :ref:`user.wavefront_error.sign` * :ref:`user.diffraction.sign` +.. _user.coordinate_system.origin: + .. note:: Matplotlib's ``imshow()`` method (and MATLAB's ``imagesc()`` method) place the origin in the upper left corner of the plotted image by default. This presents arrays in the standard (row, column) ordering. The result is that the direction of y-axis is flipped relative to Lentil's coordinate system. This doesn't necessarily - present a problem as long as results are consistently plotted "incorrectly", but - to be completely correct (particularly when comparing model-generated images against - intuition or measured data) the origin should be located in the lower left corner - of the displayed image. + present a problem as long as results are consistently plotted, but to be completely + correct (particularly when comparing model-generated images against intuition or + measured data) the origin should be located in the lower left corner of the + displayed image. .. image:: /_static/img/coordinate_system_plot.png :width: 700px diff --git a/docs/user/basics.diffraction.rst b/docs/user/basics.diffraction.rst index c03a9aa..6ea92ec 100644 --- a/docs/user/basics.diffraction.rst +++ b/docs/user/basics.diffraction.rst @@ -60,20 +60,17 @@ follows the same basic flow: .. code-block:: pycon - >>> plt.imshow(np.abs(w2.field), origin='lower') + >>> plt.imshow(np.abs(w2.field)) .. plot:: :context: reset :scale: 50 - import matplotlib.pyplot as plt - import lentil - pupil = lentil.Pupil(amplitude=lentil.circle((256, 256), 120), pixelscale=1/240, focal_length=10) w1 = lentil.Wavefront(650e-9) w2 = w1 * pupil - plt.imshow(np.abs(w2.field), origin='lower') + plt.imshow(np.abs(w2.field)) Additionally, because ``w2`` was propagated through a |Pupil| plane, it has inherited the pupil's focal length: @@ -120,7 +117,7 @@ follows the same basic flow: :scale: 50 >>> w2 = lentil.propagate_dft(w2, pixelscale=5e-6, shape=(64,64), oversample=5) - >>> plt.imshow(w2.intensity**0.1, origin='lower') + >>> plt.imshow(w2.intensity, norm='log') .. note:: @@ -142,10 +139,6 @@ different wavelengths and accumulates the resulting image plane intensity: :scale: 50 :include-source: - import matplotlib.pyplot as plt - import numpy as np - import lentil - pupil = lentil.Pupil(amplitude=lentil.circle((256, 256), 120), pixelscale=1/240, focal_length=10) @@ -158,7 +151,7 @@ different wavelengths and accumulates the resulting image plane intensity: w = lentil.propagate_dft(w, pixelscale=5e-6, shape=(64,64), oversample=5) img += w.intensity - plt.imshow(img**0.1, origin='lower') + plt.imshow(img, norm='log') Keep in mind the output ``img`` array must be sized to accommodate the oversampled wavefront intensity given by ``npix`` * ``oversample``. @@ -195,7 +188,7 @@ appropriate location in the (potentially larger) output plane when a |Wavefront| :attr:`~lentil.Wavefront.field` or :attr:`~lentil.Wavefront.intensity` attribute is accessed. -.. image:: /_static/img/propagate_npix_prop.png +.. image:: images/propagate_npix_prop.png :width: 450px :align: center @@ -203,7 +196,7 @@ It can be advantageous to specify ``npix_prop`` < ``npix`` for performance reasons, although care must be taken to ensure needed data is not accidentally left out: -.. plot:: _img/python/npix_prop.py +.. plot:: user/plots/npix_prop.py :scale: 50 For most pupil to image plane propagations, setting ``npix_prop`` to 128 or 256 @@ -253,10 +246,10 @@ optics texts. The implications of this choice are as follows: :math:`\exp\left[i\frac{k}{2z} (x^2 + y^2)\right]` -.. .. _user_guide.diffraction.sampling: +.. _user.diffraction.sampling: -.. Sampling considerations -.. ======================= +Sampling considerations +======================= .. .. plot:: _img/python/dft_discrete_Q_sweep.py .. :scale: 50 @@ -269,10 +262,10 @@ optics texts. The implications of this choice are as follows: .. :width: 550px .. :align: center -.. .. _user_guide.diffraction.tilt: +.. _user.diffraction.tilt: -.. Working with large tilts -.. ======================== +Working with large tilts +======================== .. .. image:: /_static/img/propagate_tilt_phase.png .. :width: 450px .. :align: center @@ -289,10 +282,10 @@ optics texts. The implications of this choice are as follows: .. :width: 600px .. :align: center -.. .. _user_guide.diffraction.segmented: +.. _user.diffraction.segmented: -.. Differences for segmented apertures -.. =================================== +Differences for segmented apertures +=================================== diff --git a/docs/user/basics.optical_systems.rst b/docs/user/basics.optical_systems.rst index b45aca1..aa2cd7f 100644 --- a/docs/user/basics.optical_systems.rst +++ b/docs/user/basics.optical_systems.rst @@ -74,8 +74,6 @@ represented by a single |Pupil| plane: :scale: 50 :include-source: - >>> import matplotlib.pyplot as plt - >>> import lentil >>> amplitude = lentil.circle(shape=(256, 256), radius=120) >>> opd = lentil.zernike_compose(mask=amplitude, ... coeffs=[0, 0, 0, 100e-9, 300e-9, 0, -100e-9]) diff --git a/docs/user/basics.planes.rst b/docs/user/basics.planes.rst index 718c469..8251202 100644 --- a/docs/user/basics.planes.rst +++ b/docs/user/basics.planes.rst @@ -15,8 +15,8 @@ Lentil plane types All Lentil planes are derived from the |Plane| class. This base class defines the interface to represent any discretely sampled plane in an optical model. It can also be used directly in a model. Planes typically have some influence on the propagation -of a wavefront though this is not strictly required and some models may use *dummy* -or *reference* planes as needed. +of a wavefront though this is not strictly required and models may use *dummy* or +*reference* planes as needed. Lentil provides several general planes that are the building blocks for most optical models: @@ -74,7 +74,7 @@ plane. A plane is defined by the following parameters: .. note:: All Plane attributes have sensible default values that have no effect on - propagations when not defined. + propagations when not specified. Create a new Plane with @@ -83,8 +83,6 @@ Create a new Plane with :include-source: :scale: 50 - >>> import matplotlib.pyplot as plt - >>> import lentil >>> p = lentil.Plane(amplitude=lentil.util.circle((256,256), 120)) >>> plt.imshow(p.amplitude, origin='lower') @@ -94,8 +92,6 @@ Once a Plane is defined, its attributes can be modified at any time: :include-source: :scale: 50 - >>> import matplotlib.pyplot as plt - >>> import lentil >>> p = lentil.Plane(amplitude=lentil.util.circle((256,256), 120)) >>> p.phase = 2e-6 * lentil.zernike(p.mask, index=4) >>> plt.imshow(p.phase, origin='lower') @@ -232,14 +228,12 @@ Given the following |Pupil| and |Detector| planes: :include-source: :scale: 50 - >>> import matplotlib.pyplot as plt - >>> import lentil >>> pupil = lentil.Pupil(amplitude=lentil.util.circle((256, 256), 120), ... focal_length=10, pixelscale=1/250) >>> w = lentil.Wavefront(650e-9) >>> w *= pupil >>> w = lentil.propagate_dft(w, pixelscale=5e-6, shape=(64,64), oversample=2) - >>> plt.imshow(w.intensity, origin='lower') + >>> plt.imshow(w.intensity) It is simple to see the effect of introducing a tilted wavefront into the system: @@ -247,8 +241,6 @@ It is simple to see the effect of introducing a tilted wavefront into the system :include-source: :scale: 50 - >>> import matplotlib.pyplot as plt - >>> import lentil >>> pupil = lentil.Pupil(amplitude=lentil.util.circle((256, 256), 120), ... focal_length=10, pixelscale=1/250) >>> tilt = lentil.Tilt(x=10e-6, y=-5e-6) @@ -258,6 +250,11 @@ It is simple to see the effect of introducing a tilted wavefront into the system >>> w = lentil.propagate_dft(w, pixelscale=5e-6, shape=(64,64), oversample=2) >>> plt.imshow(w.intensity, origin='lower') +.. note:: + + Notice the use of ``origin='lower'`` in the plot above. For an explanation, see + the note :ref:`here `. + .. .. _user_guide.planes.transformations: .. Plane transformations diff --git a/docs/user/basics.wavefront_error.rst b/docs/user/basics.wavefront_error.rst index 00f6f59..6d85c37 100644 --- a/docs/user/basics.wavefront_error.rst +++ b/docs/user/basics.wavefront_error.rst @@ -32,7 +32,7 @@ in a shift in the image plane in the positive y direction. A positive y-tilt rotates the xz plane clockwise about the y-axis resulting in a shift in the image plane in the negative x direction. -.. plot:: _img/python/tilt_images.py +.. plot:: user/plots/tilt_images.py :scale: 50 Focus @@ -53,7 +53,7 @@ image to be flipped about both axes relative to the aperture (consistent with observing the image after passing through focus). The results of this exercise are presented below: -.. plot:: _img/python/focus_images.py +.. plot:: user/plots/focus_images.py :scale: 50 Static Errors @@ -88,36 +88,32 @@ polynomials `_. Lentil uses the Noll indexing scheme for defining Zernike polynomials [1]_. Wavefront error maps are easily computed using either the :func:`~lentil.zernike` or -:func:`~lentil.zernike_compose` functions. For example, we can represent 100 nm of focus over a -circular aperture with :func:`~lentil.zernike`: +:func:`~lentil.zernike_compose` functions. For example, we can represent 100 nm of +astigmatism over a circular aperture with :func:`~lentil.zernike`: .. plot:: :include-source: :scale: 50 - >>> import matplotlib.pyplot as plt - >>> import lentil >>> mask = lentil.circlemask((256,256), 120) - >>> z4 = 100e-9 * lentil.zernike(mask, index=4) - >>> plt.imshow(z4, origin='lower') + >>> astig = 100e-9 * lentil.zernike(mask, index=6) + >>> plt.imshow(astig, origin='lower') -Any combination of Zernike polynomials can be combined by providing a list of coefficients -to the :func:`~lentil.zernike_compose` function. For example, we can represent 200 nm of -focus and -100 nm of astigmatism as: +Any arbitrary combination of Zernike polynomials can be represented by providing a +list of coefficients to the :func:`~lentil.zernike_compose` function: .. plot:: :include-source: :scale: 50 - >>> import matplotlib.pyplot as plt - >>> import lentil >>> mask = lentil.circlemask((256,256), 120) - >>> coefficients = [0, 0, 0, 200e-9, 0, -100e-9] - >>> z = lentil.zernike_compose(mask, coefficients) + >>> coeff = np.random.uniform(low=-200e-9, high=200e-9, size=10) + >>> z = lentil.zernike_compose(mask, coeff) >>> plt.imshow(z, origin='lower') -Note that the coefficients list is ordered according to the Noll indexing scheme so the +Note that the coefficients list is ordered according to the `Noll indexing scheme +`_ so the first entry in the list represents piston, the second represents, tilt, and so on. For models requiring many random trials, it may make more sense to pre-compute the @@ -127,36 +123,30 @@ each independent term using Numpy's `einsum `_ function. Note that in this case we are only computing the Zernike modes we intend to use (Noll -indices 4 and 6) so now the first entry in ``coefficients`` corresponds to focus and the +indices 4 and 6) so now the first entry in ``coeff`` corresponds to focus and the second corresponds to astigmatism. .. plot:: :include-source: :scale: 50 - >>> import matplotlib.pyplot as plt - >>> import numpy as np - >>> import lentil >>> mask = lentil.circlemask((256,256), 120) - >>> coefficients = [200e-9, -100e-9] + >>> coeff = [200e-9, -100e-9] >>> basis = lentil.zernike_basis(mask, modes=(4,6)) - >>> z = np.einsum('ijk,i->jk', basis, coefficients) + >>> z = np.einsum('ijk,i->jk', basis, coeff) >>> plt.imshow(z, origin='lower') -If you don't love ``einsum``, it's possible to achieve the same result with Numpy's +It's also possible to achieve the same result using Numpy's `tensordot `_: .. plot:: :include-source: :scale: 50 - >>> import matplotlib.pyplot as plt - >>> import numpy as np - >>> import lentil >>> mask = lentil.circlemask((256,256), 120) - >>> coefficients = [200e-9, -100e-9] + >>> coeff = [200e-9, -100e-9] >>> basis = lentil.zernike_basis(mask, modes=(4,6)) - >>> z = np.tensordot(basis, coefficients, axes=(0,0)) + >>> z = np.tensordot(basis, coeff, axes=(0,0)) >>> plt.imshow(z, origin='lower') Normalization @@ -174,8 +164,6 @@ the error magnitude: .. code-block:: pycon - >>> import numpy as np - >>> import lentil >>> mask = lentil.circlemask((256,256), 128) >>> z4 = 100e-9 * lentil.zernike(mask, mode=4, normalize=True) >>> np.std(z4[np.nonzero(z4)]) @@ -188,8 +176,6 @@ the discretely sampled mode spans [-0.5 0.5] before multiplying by the error mag .. code-block:: pycon - >>> import numpy as np - >>> import lentil >>> mask = lentil.circlemask((256,256), 128) >>> z4 = lentil.zernike(mask, mode=4) >>> z4 /= np.max(z4) - np.min(z4) @@ -213,8 +199,6 @@ the center of the defined array: :include-source: :scale: 50 - >>> import matplotlib.pyplot as plt - >>> import lentil >>> mask = lentil.circlemask((256,256), radius=50, shift=(0,60)) >>> rho, theta = lentil.zernike_coordinates(mask, shift=(0,60)) >>> z4 = lentil.zernike(mask, 4, rho=rho, theta=theta) @@ -226,8 +210,6 @@ If we wish to align a tilt mode with one side of a hexagon: :include-source: :scale: 50 - >>> import matplotlib.pyplot as plt - >>> import lentil >>> mask = lentil.hexagon((256,256), radius=120) >>> rho, theta = lentil.zernike_coordinates(mask, shift=(0,0), rotate=30) >>> z2 = lentil.zernike(mask, 2, rho=rho, theta=theta) @@ -281,11 +263,9 @@ wavefront error map given a PSD: :include-source: :scale: 50 - >>> import matplotlib.pyplot as plt - >>> import lentil >>> mask = lentil.circle((256, 256), 120) - >>> w = lentil.power_spectrum(mask, pixelscale=1/120, rms=25e-9, half_power_freq=8, - ... exp=3) + >>> w = lentil.power_spectrum(mask, pixelscale=1/120, rms=25e-9, + ... half_power_freq=8, exp=3) >>> plt.imshow(w, origin='lower') diff --git a/docs/_static/img/propagate_npix_prop.png b/docs/user/images/propagate_npix_prop.png similarity index 100% rename from docs/_static/img/propagate_npix_prop.png rename to docs/user/images/propagate_npix_prop.png diff --git a/docs/user/plots/focus_images.py b/docs/user/plots/focus_images.py new file mode 100644 index 0000000..e8492d7 --- /dev/null +++ b/docs/user/plots/focus_images.py @@ -0,0 +1,47 @@ +import matplotlib.pyplot as plt +import numpy as np +import lentil + +amp = np.zeros((256, 256)) +amp += lentil.circlemask((256, 256), 64, shift=(32, 8)) +amp -= lentil.circlemask((256, 256), 40, shift=(32, 8)) +amp[:, 0:128+8] = 0 +amp[32:225, 104-16:128-16] = 1 +amp[96:120, 128-16:144-8] = 1 +amp[201:225, 128-16:144-8] = 1 + +focus = lentil.zernike(mask=np.ones((256, 256)), index=4) + +pupil_neg = lentil.Pupil(amplitude=amp, pixelscale=1/240, focal_length=10) +pupil_neg.phase = -6e-6 * focus +w_neg = lentil.Wavefront(650e-9) +w_neg *= pupil_neg +w_neg = lentil.propagate_dft(w_neg, pixelscale=5e-6, shape=200, oversample=2) + +pupil_pos = lentil.Pupil(amplitude=amp, pixelscale=1/240, focal_length=10) +pupil_pos.phase = 6e-6 * focus +w_pos = lentil.Wavefront(650e-9) +w_pos *= pupil_pos +w_pos = lentil.propagate_dft(w_pos, pixelscale=5e-6, shape=200, oversample=2) + +fig, (ax1,ax2,ax3) = plt.subplots(nrows=1, ncols=3, figsize=(5, 3)) + +ax1.imshow(amp, origin='lower') +ax1.set_title('Aperture') +ax1.set_xticks(np.linspace(0, 256, 5), labels=np.linspace(-1, 1, 5)) +ax1.set_yticks(np.linspace(0, 256, 5), labels=np.linspace(-1, 1, 5)) +ax1.set_xlabel('[m]') + +ax2.imshow(w_pos.intensity, origin='lower') +ax2.set_title('Image plane (+focus)') +ax2.set_xticks(np.linspace(0, 400, 5), labels=np.linspace(-1, 1, 5)) +ax2.set_yticks(np.linspace(0, 400, 5), labels=np.linspace(-1, 1, 5)) +ax2.set_xlabel('[mm]') + +ax3.imshow(w_neg.intensity, origin='lower') +ax3.set_title('Image plane (-focus)') +ax3.set_xticks(np.linspace(0, 400, 5), labels=np.linspace(-1, 1, 5)) +ax3.set_yticks(np.linspace(0, 400, 5), labels=np.linspace(-1, 1, 5)) +ax3.set_xlabel('[mm]') + +plt.tight_layout() diff --git a/docs/_img/python/npix_prop.py b/docs/user/plots/npix_prop.py similarity index 69% rename from docs/_img/python/npix_prop.py rename to docs/user/plots/npix_prop.py index 843ea9c..69a062a 100644 --- a/docs/_img/python/npix_prop.py +++ b/docs/user/plots/npix_prop.py @@ -16,12 +16,13 @@ w2 *= pupil w2 = lentil.propagate_dft(w2, pixelscale=5e-6, shape=128, prop_shape=40, oversample=5) -plt.subplot(1, 2, 1) -plt.imshow(w1.intensity, origin='lower') -plt.title('npix_prop ok') -plt.axis('off') - -plt.subplot(1, 2, 2) -plt.imshow(w2.intensity, origin='lower') -plt.title('npix_prop too small') -plt.axis('off') + +fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(4, 4)) + +ax1.imshow(w1.intensity, origin='lower') +ax1.set_title('npix_prop ok') +ax1.axis('off') + +ax2.imshow(w2.intensity, origin='lower') +ax2.set_title('npix_prop too small') +ax2.axis('off') diff --git a/docs/user/plots/tilt_images.py b/docs/user/plots/tilt_images.py new file mode 100644 index 0000000..a116a31 --- /dev/null +++ b/docs/user/plots/tilt_images.py @@ -0,0 +1,45 @@ +import matplotlib.pyplot as plt +import numpy as np +import lentil + +amp = lentil.circle((256, 256), 120) +x_tilt = 8e-6 * lentil.zernike(amp, 3) # +x tilt +y_tilt = 8e-6 * lentil.zernike(amp, 2) # +y tilt + +py = lentil.Pupil(focal_length=10, pixelscale=1 / 240, amplitude=amp, phase=y_tilt) +wy = lentil.Wavefront(650e-9) +wy *= py +wy = lentil.propagate_dft(wy, pixelscale=5e-6, shape=200, oversample=5) + +px = lentil.Pupil(focal_length=10, pixelscale=1 /240, amplitude=amp, phase=x_tilt) +wx = lentil.Wavefront(650e-9) +wx *= px +wx = lentil.propagate_dft(wx, pixelscale=5e-6, shape=200, oversample=5) + +fig, ((ax1,ax2),(ax3,ax4)) = plt.subplots(nrows=2, ncols=2, figsize=(3.5, 3.5)) + +ax1.imshow(x_tilt, origin='lower') +ax1.set_title('Pupil plane ($+R_x$)') +ax1.set_xticks(np.linspace(0, 256, 5), labels=np.linspace(-0.5, 0.5, 5)) +ax1.set_yticks(np.linspace(0, 256, 5), labels=np.linspace(-0.5, 0.5, 5)) +ax1.set_xlabel('[m]') + +ax2.imshow(y_tilt, origin='lower') +ax2.set_title('Pupil plane ($+R_y$)') +ax2.set_xticks(np.linspace(0, 256, 5), labels=np.linspace(-0.5, 0.5, 5)) +ax2.set_yticks(np.linspace(0, 256, 5), labels=np.linspace(-0.5, 0.5, 5)) +ax2.set_xlabel('[m]') + +ax3.imshow(wx.intensity ** 0.2, origin='lower') +ax3.set_title('Image plane ($+R_x$)') +ax3.set_xticks(np.linspace(0, 200 * 5, 5), labels=np.linspace(-1, 1, 5)) +ax3.set_yticks(np.linspace(0, 200 * 5, 5), labels=np.linspace(-1, 1, 5)) +ax3.set_xlabel('[mm]') + +ax4.imshow(wy.intensity ** 0.2, origin='lower') +ax4.set_title('Image plane ($+R_y$)') +ax4.set_xticks(np.linspace(0, 200 * 5, 5), labels=np.linspace(-1, 1, 5)) +ax4.set_yticks(np.linspace(0, 200 * 5, 5), labels=np.linspace(-1, 1, 5)) +ax4.set_xlabel('[mm]') + +fig.tight_layout() diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 9934ca7..32a477d 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -9,7 +9,7 @@ Quickstart ********** This is a short introduction to Lentil, mainly written for new users. More -complex recipes are available in the Cookbook. +detailed examples are available :ref:`here`. First, we import Lentil: @@ -23,55 +23,54 @@ We'll also import `Matplotlib `_ to visualize results: >>> import matplotlib.pyplot as plt -.. note:: - - Lentil is "unitless" in the sense that it doesn't enforce a specific base - unit. All calculations are well behaved for both metric and imperial units. - It is important that units are consistent however, and this task is left to - the user. - - That being said, it is recommended that all calculations be performed in - terms of either meters, millimeters, or microns. - Creating planes =============== -Most Lentil models can be constructed using |Pupil| and |Image| planes. We'll -create a circular |Pupil| with a focal length of 10 meters and a diameter of -1 meter: +Most simple diffraction simulations can be represented by a single far-field +propagation from a pupil plane to an image plane. First, we'll create a +pupil amplitude map and corresponding optical path difference (OPD) map which +represents the wavefront error of the system. We'll construct the OPD map from +a combination of Zernike modes: -.. code-block:: pycon +.. plot:: + :context: reset + :include-source: + :scale: 50 >>> amp = lentil.circle(shape=(256,256), radius=120) - >>> pupil = lentil.Pupil(amplitude=amp, pixelscale=1/240, focal_length=10) - >>> plt.imshow(pupil.amplitude, origin='lower') + >>> coef = [0, 0, 0, 300e-9, 50e-9, -100e-9, 50e-9] + >>> opd = lentil.zernike_compose(mask=amp, coeffs=coef) + >>> fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(5, 4)) + >>> ax1.imshow(amp) + >>> ax1.set_title('Amplitude') + >>> ax2.imshow(opd) + >>> ax2.set_title('OPD') + +Now we can use the amplitude and OPD maps to construct a |Pupil| plane with +a focal length of 20 meters and a diameter of 1 meter: .. plot:: - :scale: 50 - :context: reset + :context: close-figs + :include-source: - import matplotlib.pyplot as plt - import lentil - amp = lentil.circle(shape=(256,256), radius=120) - pupil = lentil.Pupil(amplitude=amp, pixelscale=1/240, focal_length=10) - plt.imshow(pupil.amplitude, origin='lower') + >>> pupil = lentil.Pupil(amplitude=amp, phase=opd, pixelscale=1/240, + ... focal_length=20) -Note the diameter is defined via the :attr:`~lentil.Pupil.pixelscale` -attribute: +Note the diameter is implicitly defined via the +:attr:`~lentil.Pupil.pixelscale` attribute: .. image:: /_static/img/pixelscale.png :width: 500px :align: center -Here, we'll create an |Image| plane with spatial sampling of 5 microns, -represented here in trems of meters: - -.. code-block:: pycon - - >>> image = lentil.Image(pixelscale=5e-6) +.. note:: + Lentil is "unitless" in the sense that it doesn't enforce a specific base + unit. All calculations are well behaved for both metric and imperial units. + It is important that units are consistent however, and this task is left to + the user. -.. Wavefront error -.. =============== + That being said, it is recommended that all calculations be performed in + terms of either meters, millimeters, or microns. Diffraction =========== @@ -79,56 +78,61 @@ Diffraction Pupil to image plane propagation -------------------------------- The simplest diffraction propagation is from a pupil to image plane. Here, we -construct a |Wavefront| with wavelength of 650 nanometers, again represented +construct a |Wavefront| with wavelength of 500 nm, again represented in meters: -.. code-block:: pycon +.. plot:: + :context: + :include-source: - >>> w = lentil.Wavefront(wavelength=650e-9) + >>> w0 = lentil.Wavefront(wavelength=500e-9) Next, we'll propagate the wavefront through the pupil plane we defined above. Lentil uses multiplication represent the interaction between a |Plane| and |Wavefront|: -.. code-block:: pycon +.. plot:: + :context: + :include-source: - >>> w = w * pupil + >>> w1 = w0 * pupil Finally, we'll propagate the wavefront to a discreetely sampled image plane using :func:`~lentil.propagate_dft`. In this case, we'll sample -the result every 5e-6 meters and perform the propagation 3 times oversampled: +the result on a grid with spacing of 5e-6 meters and perform the propagation +2 times oversampled: -.. code-block:: pycon +.. plot:: + :context: + :include-source: - >>> w = lentil.propagate_dft(w, shape=(64,64), pixelscale=5e-6, oversample=5) + >>> w2 = lentil.propagate_dft(w1, shape=(64,64), pixelscale=5e-6, oversample=2) The resulting intensity (point spread function) can now be observed: -.. code-block:: pycon - - >>> plt.imshow(w.intensity, origin='lower') - .. plot:: - :context: close-figs + :context: + :include-source: :scale: 50 - w = lentil.Wavefront(wavelength=650e-9) - w *= pupil - w = lentil.propagate_dft(w, shape=(64,64), pixelscale=5e-6, oversample=5) - plt.imshow(w.intensity, origin='lower') + >>> plt.imshow(w2.intensity) -.. Multi-plane propagation -.. ----------------------- +Finally, we will rescale the oversampled image to native sampling and include the +blurring effects due to the pixel MTF: -.. Free-space propagation -.. ---------------------- +.. plot:: + :context: close-figs + :include-source: + :scale: 50 + >>> img = lentil.detector.pixelate(w2.intensity, oversample=2) + >>> plt.imshow(img) -Focal planes -============ +.. Focal planes +.. ============ -Radiometry -========== +.. Radiometry +.. ========== diff --git a/lentil/field.py b/lentil/field.py index e60ecc6..980423c 100644 --- a/lentil/field.py +++ b/lentil/field.py @@ -25,18 +25,32 @@ class Field: and return an updated x and y shift. If None (default), tilt = []. - Attributes - ---------- - extent : tuple - Array indices defining the extent of the offset Field. """ __slots__ = ('data', 'offset', 'tilt', 'pixelscale', 'extent') def __init__(self, data, pixelscale=None, offset=None, tilt=None): - self.data = np.asarray(data, dtype=complex) + #: ndarray : Complex field data + self.data = np.asarray(data, dtype=complex) + self.pixelscale = pixelscale + """Spatial sampling of data + + If None (default), the Field is assumed to be broadcastable to any + legal shape without interpolation. + + Returns + ------- + tuple of ints or None + """ + + #: tuple of ints : Field offset from (0, 0). self.offset = offset if offset is not None else [0, 0] + + #: list : List of objects that implement the tilt interface defined + # in :class:`~lentil.plane.TiltInterface` self.tilt = tilt if tilt else [] + + #: tuple of ints : Extent of ``data`` self.extent = extent(self.shape, self.offset) @property @@ -46,7 +60,7 @@ def shape(self): Returns ------- - shape : tuple + tuple of ints """ return self.data.shape @@ -57,7 +71,7 @@ def size(self): Returns ------- - size : int + int """ return self.data.size @@ -232,7 +246,8 @@ def extent(shape, offset): def insert(field, out, intensity=False, weight=1): - """ + """Insert a field into an array. + Parameters ---------- field : :class:`~lentil.field.Field` @@ -247,7 +262,7 @@ def insert(field, out, intensity=False, weight=1): Returns ------- - out : ndarray + ndarray """ #if indexing not in ('xy', 'ij'): diff --git a/lentil/plane.py b/lentil/plane.py index fc9fddb..094fe98 100644 --- a/lentil/plane.py +++ b/lentil/plane.py @@ -38,24 +38,21 @@ class Plane: Physical sampling of each pixel in the plane. If ``pixelscale`` is a scalar, uniform sampling in x and y is assumed. If None (default), ``pixelscale`` is left undefined. - - Attributes - ---------- - tilt : list - List of :class:`~lentil.Tilt` terms associated wirth this Plane - + diameter : float, optional + Outscribing diameter around mask. If not provided (default), it is computed + from the boundary of :attr:`mask`. + ptype : ptype object + Plane type + """ def __init__(self, amplitude=1, phase=0, mask=None, pixelscale=None, diameter=None, ptype=None): self.amplitude = np.asarray(amplitude) self.phase = np.asarray(phase) self.mask = mask - self.pixelscale = None if pixelscale is None else np.broadcast_to(pixelscale, (2,)) + self.pixelscale = pixelscale self.diameter = diameter self.ptype = lentil.ptype(ptype) - - self._mask = np.asarray(mask) if mask is not None else None - self._slice = _plane_slice(self._mask) self.tilt = [] @@ -63,8 +60,44 @@ def __init__(self, amplitude=1, phase=0, mask=None, pixelscale=None, diameter=No def __repr__(self): return f'{self.__class__.__name__}()' + @property + def amplitude(self): + """Electric field amplitude transmission + + Returns + ------- + ndarray + + """ + return self._amplitude + + @amplitude.setter + def amplitude(self, value): + self._amplitude = np.asarray(value) + + @property + def phase(self): + """Electric field phase + + Returns + ------- + ndarray + + """ + return self._phase + + @phase.setter + def phase(self, value): + self._phase = np.asarray(value) + @property def mask(self): + """Binary transmission mask + + Returns + ------- + ndarray + """ if self._mask is not None: return self._mask else: @@ -84,19 +117,47 @@ def mask(self, value): @property def global_mask(self): """ - Flattened view of :attr:`mask` + Flattened view of :attr:`~mask` Returns ------- - mask : ndarray + ndarray + """ - if self.depth < 2: + if self.size < 2: return self.mask else: return np.sum(self.mask, axis=0) + @property + def pixelscale(self): + """Physical sampling of each pixel in the plane + + Returns + ------- + tuple of ints or None + + """ + return self._pixelscale + + @pixelscale.setter + def pixelscale(self, value): + self._pixelscale = None if value is None else np.broadcast_to(value, (2,)) + @property def diameter(self): + """Plane diameter + + Notes + ----- + If :attr:`diameter` was no provided during Plane creation, it is + autocomputed if possible. If it is not possible, None is returned. + + Returns + ------- + float or None + + """ if self._diameter is None: [rmin, rmax, cmin, cmax] = lentil.boundary(self.global_mask) # since pixelscale has shape=(2,), we need to return the overall @@ -114,22 +175,27 @@ def shape(self): """ Plane dimensions computed from :attr:`mask`. - Returns (mask.shape[1], mask.shape[2]) if mask has ``ndim == 3``. Returns + Returns (mask.shape[1], mask.shape[2]) if :attr:`size: > 1. Returns None if :attr:`mask` is None. + + Returns + ------- + tuple of ints """ - if self.depth == 1: + if self.size == 1: return self.mask.shape else: return self.mask.shape[1], self.mask.shape[2] @property - def depth(self): + def size(self): """ Number of independent masks (segments) in :attr:`mask` Returns ------- - depth : int + int + """ if self.mask.ndim in (0, 1, 2): return 1 @@ -141,11 +207,11 @@ def ptt_vector(self): """ 2D vector representing piston and tilt in x and y. - Planes with no mask have ``ptt_vector = None``. + Planes with no mask have :attr:`ptt_vector` = None. Returns ------- - ptt_vector : ndarray or None + ndarray or None """ # if there's no mask, we just set ptt_vector to None and move on @@ -164,14 +230,14 @@ def ptt_vector(self): unmasked_ptt_vector = np.einsum('ij,i->ij', [np.ones(r.size), r.ravel(), -c.ravel()], [1, self.pixelscale[0], self.pixelscale[1]]) - if self.depth == 1: + if self.size == 1: ptt_vector = np.einsum('ij,j->ij', unmasked_ptt_vector, self.mask.ravel()) else: # prepare empty ptt_vector - ptt_vector = np.empty((self.depth * 3, np.prod(self.shape))) + ptt_vector = np.empty((self.size * 3, np.prod(self.shape))) # loop over the masks and fill in the masked ptt_vectors - for mask in np.arange(self.depth): + for mask in np.arange(self.size): ptt_vector[3*mask:3*mask+3] = unmasked_ptt_vector * self.mask[mask].ravel() return ptt_vector @@ -182,7 +248,7 @@ def copy(self): Returns ------- - copy : :class:`~lentil.Plane` + :class:`~lentil.Plane` """ return copy.deepcopy(self) @@ -199,7 +265,7 @@ def fit_tilt(self, inplace=False): Returns ------- - plane : :class:`~lentil.Plane` + :class:`~lentil.Plane` """ if inplace: plane = self @@ -213,31 +279,30 @@ def fit_tilt(self, inplace=False): if ptt_vector is None or plane.phase.size == 1: return plane - if self.depth == 1: + if self.size == 1: t = np.linalg.lstsq(ptt_vector.T, plane.phase.ravel(), rcond=None)[0] phase_tilt = np.einsum('ij,i->j', ptt_vector[1:3], t[1:3]) plane.phase -= phase_tilt.reshape(plane.phase.shape) plane.tilt.append(Tilt(x=t[1], y=t[2])) else: - t = np.empty((self.depth, 3)) - phase_no_tilt = np.empty((self.depth, plane.phase.shape[0], plane.phase.shape[1])) + t = np.empty((self.size, 3)) + phase_no_tilt = np.empty((self.size, plane.phase.shape[0], plane.phase.shape[1])) # iterate over the segments and compute the tilt term - for seg in np.arange(self.depth): + for seg in np.arange(self.size): t[seg] = np.linalg.lstsq(ptt_vector[3 * seg:3 * seg + 3].T, plane.phase.ravel(), rcond=None)[0] seg_tilt = np.einsum('ij,i->j', ptt_vector[3 * seg + 1:3 * seg + 3], t[seg, 1:3]) phase_no_tilt[seg] = (plane.phase - seg_tilt.reshape(plane.phase.shape)) * self.mask[seg] plane.phase = np.sum(phase_no_tilt, axis=0) - plane.tilt.extend([Tilt(x=t[seg, 1], y=t[seg, 2]) for seg in range(self.depth)]) + plane.tilt.extend([Tilt(x=t[seg, 1], y=t[seg, 2]) for seg in range(self.size)]) return plane def rescale(self, scale): - """ - Rescale a plane via interpolation. + """Rescale a plane via interpolation. The following Plane attributes are resampled: @@ -257,8 +322,8 @@ def rescale(self, scale): ------- plane : :class:`Plane` - Note - ---- + Notes + ----- All interpolation is performed via `scipy.ndimage.map_coordinates` See Also @@ -318,13 +383,13 @@ def resample(self, pixelscale): plane : :class:`Plane` Resampled Plane. - Note - ---- + Notes + ----- All interpolation is performed via `scipy.ndimage.map_coordinates` See Also -------- - * :func:`Plane.rescale` + Plane.rescale """ if not self.pixelscale: @@ -342,8 +407,8 @@ def multiply(self, wavefront): wavefront : :class:`~lentil.wavefront.Wavefront` object Wavefront to be multiplied - Note - ---- + Notes + ----- It is possible to customize the way multiplication is performed by creating a subclass and overloading its ``multiply`` method. @@ -374,7 +439,7 @@ def multiply(self, wavefront): for n, s in enumerate(self._slice): # We have to multiply amplitude[s] by mask[n][s] because the requested # slice of the amplitude array may contain parts of adjacent segments - mask = self.mask if self.depth == 1 else self.mask[n] + mask = self.mask if self.size == 1 else self.mask[n] amp = self.amplitude if self.amplitude.size == 1 else self.amplitude[s] * mask[s] phase = self.phase if self.phase.size == 1 else self.phase[s] @@ -459,11 +524,12 @@ def _plane_slice(mask): slices : list List of slices corresponding to the data extent defined by ``mask``. - See also + See Also -------- - * :func:`~lentil.helper.boundary_slice` - * :func:`~lentil.Plane.slice_offset` - """ + helper.boundary_slice + Plane.slice_offset + + """ # self.mask may still return None so we catch that here if mask is None: @@ -507,8 +573,8 @@ class Pupil(Plane): .. plot:: _img/python/segmask.py :scale: 50 - Note - ---- + Notes + ----- By definition, a pupil is represented by a spherical wavefront. Any aberrations in the optical system appear as deviations from this perfect sphere. The primary use of :class:`Pupil` is to represent these aberrations @@ -548,8 +614,8 @@ class Image(Plane): :class:`Image` is assumed to be square with nrows = ncols = shape. Default is None. - Note - ---- + Notes + ----- If image plane intensity is desired, significant performance improvements can be realized by using a :class:`Detector` plane instead. @@ -600,7 +666,7 @@ class Detector(Image): ---------- pixelscale : float, optional Pixel size in meters. Pixels are assumed to be square. Default is None. - shape : {int, (2,) array_like}, optional + shape : tuple of ints, optional Number of pixels as (rows, cols). If a single value is provided, :class:`Image` is assumed to be square with nrows = ncols = shape. Default is None. @@ -613,10 +679,25 @@ class Detector(Image): pass -class TiltInterfcace(Plane): - # Utility class for holding some common logic shared by - # classes that implement the Tilt interface +class TiltInterface(Plane): + """Utility class for holding common lofic shared by classes that need to + implement the tilt interface. + + Other Parameters + ---------------- + **kwargs : :class:`~lentil.Plane` parameters + Keyword arguments passed to :class:`~lentil.Plane` constructor + Notes + ----- + If :attr:`ptype` is not provided, it defaults to `lentil.tilt`. + + See Also + -------- + Tilt + DispersiveTilt + + """ def __init__(self, **kwargs): # if ptype is provided as a kwarg use that, otherwise default # to lentil.tilt @@ -626,16 +707,37 @@ def __init__(self, **kwargs): super().__init__(ptype=ptype, **kwargs) def multiply(self, wavefront): + """Multiply with a wavefront. This is a custom implementation + supporting the tilt interface. + + Notes + ----- + This method performs the following actions: + + .. code:: python + + wavefront = super().multiply(wavefront) + for field in wavefront.data: + field.tilt.append(self) + return wavefront + + Returns + ------- + :class:`~lentil.Wavefront` + + """ wavefront = super().multiply(wavefront) for field in wavefront.data: field.tilt.append(self) return wavefront def shift(self, wavelength, x0, y0, **kwargs): + """TODO + """ raise NotImplementedError -class Tilt(TiltInterfcace): +class Tilt(TiltInterface): """Object for representing tilt in terms of angle Parameters @@ -674,7 +776,7 @@ def shift(self, xs=0, ys=0, z=0, **kwargs): return x, y -class DispersiveTilt(TiltInterfcace): +class DispersiveTilt(TiltInterface): r"""Class for representing spectral dispersion that appears as a tilt. Light is dispersed along a line called the the spectral trace. The position @@ -883,8 +985,8 @@ class Grism(DispersiveTilt): and should return units of meters of wavelength provided an input distance along the spectral trace. - Note - ---- + Notes + ----- Lentil supports trace and dispersion functions with any arbitrary polynomial order. While a simple analytic solution exists for modeling first-order trace and/or dispersion, there is no general solution for higher order functions. @@ -936,8 +1038,8 @@ class Rotate(Plane): The order of the spline interpolation (if needed), default is 3. The order has to be in the range 0-5. - Note - ---- + Notes + ----- If the angle is an even multiple of 90 degrees, ``numpy.rot90`` is used to perform the rotation rather than ``scipy.ndimage.rotate``. In this case, the order parameter is irrelevant because no interpolation occurs. diff --git a/lentil/wavefront.py b/lentil/wavefront.py index 772f597..3c6aade 100644 --- a/lentil/wavefront.py +++ b/lentil/wavefront.py @@ -23,33 +23,32 @@ class Wavefront: focal_length : float or np.inf, optional Wavefront focal length. A plane wave (default) has an infinite focal length (``np.inf``). - tilt: list_like, optional - - shape : (2,) array_like + tilt: (2,) array_like, optional + Radians of wavefront tilt about the x and y axes provided as + ``[rx, ry]``. Default is ``[0, 0]`` (no tilt). + shape : (2,) array_like, optional Wavefront shape. If ``shape`` is None (default), the wavefront is assumed to be infinite (broadcastable to any shape). - ptype : lentil.ptype - Plane type. - - Attributes - ---------- - data : list_like - Wavefront data. Default is [1+0j] (a plane wave). + ptype : lentil.ptype, optional + Plane type. Default is ``lentil.none``. """ - __slots__ = ('wavelength', 'pixelscale', 'focal_length', 'diameter', - 'focal_length', '_ptype', 'shape', 'data') - def __init__(self, wavelength, pixelscale=None, diameter=None, focal_length=None, tilt=None, ptype=None): - - self.wavelength = wavelength - self.pixelscale = None if pixelscale is None else np.broadcast_to(pixelscale, (2,)) + + #: float: Wavefront focal length self.focal_length = focal_length if focal_length else np.inf + + #: float: Wavefront diameter self.diameter = diameter - self.ptype = lentil.ptype(ptype) + + #: tuple of ints: Wavefront shape self.shape = () + self._wavelength = wavelength + self._pixelscale = None if pixelscale is None else np.broadcast_to(pixelscale, (2,)) + self.ptype = lentil.ptype(ptype) + if tilt is not None: if len(tilt) != 2: raise ValueError('tilt must be specified as [rx, ry]') @@ -65,8 +64,34 @@ def __mul__(self, plane): def __rmul__(self, other): return self.__mul__(other) + @property + def wavelength(self): + """Wavefront wavelength + + Returns + ------- + float + """ + return self._wavelength + + @property + def pixelscale(self): + """Physical sampling of wavefront + + Returns + ------- + tuple of floats + """ + return self._pixelscale + @property def ptype(self): + """Wavefront plane type + + Returns + ------- + ptype object + """ return self._ptype @ptype.setter @@ -78,7 +103,12 @@ def ptype(self, value): @property def field(self): - """Wavefront complex field""" + """Wavefront complex field + + Returns + ------- + ndarray + """ out = np.zeros(self.shape, dtype=complex) for field in self.data: out = lentil.field.insert(field, out) @@ -86,7 +116,12 @@ def field(self): @property def intensity(self): - """Wavefront intensity""" + """Wavefront intensity + + Returns + ------- + ndarray + """ out = np.zeros(self.shape, dtype=float) for field in lentil.field.reduce(self.data): out = lentil.field.insert(field, out, intensity=True) @@ -95,14 +130,20 @@ def intensity(self): @classmethod def empty(cls, wavelength, pixelscale=None, diameter=None, focal_length=None, tilt=None, shape=None, ptype=None): + """Create an empty Wavefront + + The resulting wavefront will have an empty :attr:`data` attribute. + + Parameters + ---------- + + """ w = cls(wavelength=wavelength, pixelscale=pixelscale, diameter=diameter, focal_length=focal_length, tilt=tilt, ptype=ptype) w.data = [] w.shape = () if shape is None else shape return w - - def copy(self): - return copy.deepcopy(self) + def insert(self, out, weight=1): """Directly insert wavefront intensity data into an output array.