diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c5949e4..615c774 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -45,7 +45,11 @@ jobs: uses: actions/upload-pages-artifact@v2 with: # Upload entire repository +<<<<<<< HEAD path: 'docs/_build/html' +======= + path: '_build/html' +>>>>>>> docs - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v2 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/Makefile b/docs/Makefile index 26dd64e..5e7ac96 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -16,7 +16,7 @@ help: clean: -rm -rf $(BUILDDIR) - -rm -rf generated + -rm -rf ref/generated # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). diff --git a/docs/_img/logo/Metropolis-Light.otf b/docs/_img/logo/Metropolis-Light.otf new file mode 100644 index 0000000..b8b917c Binary files /dev/null and b/docs/_img/logo/Metropolis-Light.otf differ diff --git a/docs/_img/logo/favicon.tex b/docs/_img/logo/favicon.tex new file mode 100644 index 0000000..b2cd0a9 --- /dev/null +++ b/docs/_img/logo/favicon.tex @@ -0,0 +1,17 @@ +\documentclass[tikz, margin=0mm]{standalone} +\definecolor{nord}{HTML}{3B4252} + +\begin{document} +\begin{tikzpicture} + +\def \w {1.5mm} + +\draw[fill, nord](0,0) circle (2mm); +\node[draw, circle, nord, line width=\w, minimum size=0.65cm] at (0, 0) {}; +\node[draw, circle, nord, line width=\w, minimum size=1.025cm] at (0, 0) {}; +\node[draw, circle, nord, line width=\w, minimum size=1.4cm] at (0, 0) {}; +\node[draw, circle, nord, line width=\w, minimum size=1.775cm] at (0, 0) {}; +\node[draw, circle, nord, line width=\w, minimum size=2.15cm] at (0, 0) {}; + +\end{tikzpicture} +\end{document} \ No newline at end of file diff --git a/docs/_img/logo/logo-dark.tex b/docs/_img/logo/logo-dark.tex new file mode 100644 index 0000000..d4ca942 --- /dev/null +++ b/docs/_img/logo/logo-dark.tex @@ -0,0 +1,11 @@ +\documentclass[tikz, margin=0mm]{standalone} +%\usepackage{tikz} +%\usetikzlibrary{calc} + +\usepackage{fontspec} +\setmainfont{Metropolis-Light.otf} +\definecolor{nord}{HTML}{E5E9F0} + +\begin{document} +\include{logo.tex} +\end{document} diff --git a/docs/_img/logo/logo-light.tex b/docs/_img/logo/logo-light.tex new file mode 100644 index 0000000..3c38ed5 --- /dev/null +++ b/docs/_img/logo/logo-light.tex @@ -0,0 +1,11 @@ +\documentclass[tikz, margin=0mm]{standalone} +%\usepackage{tikz} +%\usetikzlibrary{calc} + +\usepackage{fontspec} +\setmainfont{Metropolis-Light.otf} +\definecolor{nord}{HTML}{3B4252} + +\begin{document} +\include{logo.tex} +\end{document} diff --git a/docs/_img/logo/logo.sh b/docs/_img/logo/logo.sh new file mode 100644 index 0000000..163b45e --- /dev/null +++ b/docs/_img/logo/logo.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +# note: this requires poppler to be installed (brew install poppler) +xelatex logo-dark.tex +pdftocairo -svg logo-dark.pdf logo-dark.svg + +xelatex logo-light.tex +pdftocairo -svg logo-light.pdf logo-light.svg + +rm *.pdf +rm *.log +rm *.aux \ No newline at end of file diff --git a/docs/_img/logo/logo.tex b/docs/_img/logo/logo.tex new file mode 100644 index 0000000..2b713c7 --- /dev/null +++ b/docs/_img/logo/logo.tex @@ -0,0 +1,15 @@ +\begin{tikzpicture} + +\def \w {1.5mm} + +\draw[fill, nord](0,0) circle (2mm); +\draw[fill, white] (-1.5,0) circle (0.01mm); % pad left edge +\node[draw, circle, nord, line width=\w, minimum size=0.65cm] at (0, 0) {}; +\node[draw, circle, nord, line width=\w, minimum size=1.025cm] at (0, 0) {}; +\node[draw, circle, nord, line width=\w, minimum size=1.4cm] at (0, 0) {}; +\node[draw, circle, nord, line width=\w, minimum size=1.775cm] at (0, 0) {}; +\node[draw, circle, nord, line width=\w, minimum size=2.15cm] at (0, 0) {}; +\node [scale=6, right, nord, inner sep=1pt] at (2,0) {L E N T I L}; +%\node [scale=9, right, nord] at (1.25,0) {L E N T I L}; + +\end{tikzpicture} \ No newline at end of file diff --git a/docs/_img/python/focus_images.py b/docs/_img/python/focus_images.py deleted file mode 100644 index 7e85810..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 = w_neg.propagate_image(pixelscale=5e-6, npix=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 = w_pos.propagate_image(pixelscale=5e-6, npix=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/jitter.py b/docs/_img/python/jitter.py index e926006..3e48386 100644 --- a/docs/_img/python/jitter.py +++ b/docs/_img/python/jitter.py @@ -9,7 +9,7 @@ pupil = lentil.Pupil(amplitude=mask, phase=phase, focal_length=10, pixelscale=1 / 240) w = lentil.Wavefront(650e-9) w *= pupil -w = w.propagate_image(pixelscale=5e-6, npix=64, oversample=5) +w = lentil.propagate_dft(w, pixelscale=5e-6, shape=64, oversample=5) psf = w.intensity psf_jitter = lentil.jitter(psf, scale=2, oversample=5) diff --git a/docs/_img/python/npix_prop.py b/docs/_img/python/npix_prop.py deleted file mode 100644 index 20e24d3..0000000 --- a/docs/_img/python/npix_prop.py +++ /dev/null @@ -1,27 +0,0 @@ -import matplotlib.pyplot as plt -import lentil - -import matplotlib as mpl -mpl.rcParams['figure.figsize'] = (4.5, 4.5) - -amp = lentil.circle((256, 256), 120) -opd = lentil.zernike(amp, 4) * 1e-6 -pupil = lentil.Pupil(amplitude=amp, phase=opd, pixelscale=1 / 240, focal_length=10) - -w1 = lentil.Wavefront(wavelength=500e-9) -w1 *= pupil -w1 = w1.propagate_image(pixelscale=5e-6, npix=128, npix_prop=128, oversample=5) - -w2 = lentil.Wavefront(wavelength=500e-9) -w2 *= pupil -w2 = w2.propagate_image(pixelscale=5e-6, npix=128, npix_prop=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') diff --git a/docs/_img/python/pixelate.py b/docs/_img/python/pixelate.py index 2923ec6..b30ad69 100644 --- a/docs/_img/python/pixelate.py +++ b/docs/_img/python/pixelate.py @@ -9,7 +9,7 @@ pupil = lentil.Pupil(amplitude=mask, phase=phase, focal_length=10, pixelscale=1 / 240) w = lentil.Wavefront(650e-9) w *= pupil -w = w.propagate_image(pixelscale=5e-6, npix=64, oversample=5) +w = lentil.propagate_dft(w, pixelscale=5e-6, shape=64, oversample=5) psf = w.intensity psf_pixelate = lentil.detector.pixelate(psf, oversample=5) diff --git a/docs/_img/python/smear.py b/docs/_img/python/smear.py index 4597ed4..31d5f6f 100644 --- a/docs/_img/python/smear.py +++ b/docs/_img/python/smear.py @@ -9,7 +9,7 @@ pupil = lentil.Pupil(amplitude=mask, phase=phase, focal_length=10, pixelscale=1 / 240) w = lentil.Wavefront(650e-9) w *= pupil -w = w.propagate_image(pixelscale=5e-6, npix=64, oversample=5) +w = lentil.propagate_dft(w, pixelscale=5e-6, shape=64, oversample=5) psf = w.intensity psf_smear = lentil.smear(psf, distance=5e-5, diff --git a/docs/_img/python/smear_directional.py b/docs/_img/python/smear_directional.py index be77279..fbe5281 100644 --- a/docs/_img/python/smear_directional.py +++ b/docs/_img/python/smear_directional.py @@ -9,7 +9,7 @@ pupil = lentil.Pupil(amplitude=mask, phase=phase, focal_length=10, pixelscale=1 / 240) w = lentil.Wavefront(650e-9) w *= pupil -w = w.propagate_image(pixelscale=5e-6, npix=64, oversample=5) +w = lentil.propagate_dft(w, pixelscale=5e-6, shape=64, oversample=5) psf = w.intensity psf_smear = lentil.smear(psf, distance=25, angle=30, diff --git a/docs/_img/python/tilt_images.py b/docs/_img/python/tilt_images.py deleted file mode 100644 index 9700a22..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 = wy.propagate_image(pixelscale=5e-6, npix=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 = wx.propagate_image(pixelscale=5e-6, npix=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 7b16b46..a456efd 100644 --- a/docs/_static/css/lentil.css +++ b/docs/_static/css/lentil.css @@ -1,41 +1,59 @@ @import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;0,900;1,400;1,700;1,900&&family=Open+Sans:ital,wght@0,400;0,600;1,400;1,600&family=IBM+Plex+Mono:wght@300;400;500;600&display=swap'); +:root { + --pst-color-secondary: #5E81AC; /* nord blue */ + + --sd-color-secondary: #e83e8c; +} + body { font-family: 'Open Sans', sans-serif; } pre, code { - font-family: 'IBM Plex Mono', monospace; + font-family: 'SFMono-Regular', 'Menlo', 'IBM Plex Mono', monospace; font-size: 90%; line-height: 150%; + --pst-color-inline-code-links: #5E81AC; /* nord blue */ + --pst-color-inline-code: #e83e8c; } -pre { - background-color: #f8f8f8; +code.literal { + background-color: transparent; border: 0px; - box-shadow: none; - padding: 20px; + padding: 0; + font-size: 94%; +} + +a:hover code{ + color: #ee9040; + text-decoration: underline; + text-decoration-thickness: 1px; +} + +a.headerlink { + color: #ee9040; +} + +.sig-name { + --pst-color-inline-code: #5E81AC; } h1, h2, h3, h4, h5, h6 { font-family: 'Lato', sans-serif; } h1 { - color: #013243; /* warm black */ - font-weight: 500; - letter-spacing: -.04em; - margin-bottom: 2rem; - /*font-size: 3rem;*/ + color: #2E3440; /* nord black */ } h2 { - color: #4d77cf; /* han blue */ + color: #5E81AC; /* nord blue */ letter-spacing: -.03em; } h3 { - color: #013243; /* warm black */ + color: #2E3440; /* nord black */ letter-spacing: -.03em; } @@ -43,10 +61,36 @@ img { margin-bottom: 1.125rem; } -img.logo { +.navbar-brand img { margin-bottom: 0; } +a { + text-decoration: none; +} + +a:hover { + text-decoration: underline; + text-decoration-thickness: 1px; +} + +a.hederlink { + color: #5E81AC +} + +a.hederlink { + color: #5E81AC +} + +a.hederlink:hover { + color: #ee9040 +} + +/* Disable API reference name highlighting */ +dt:target, span.highlighted { + background-color: transparent; +} + :target:before{ content:""; display:block; @@ -55,3 +99,52 @@ img.logo { height:80px; margin:-80px 0 0; } + +.bd-header { + --pst-color-primary: #5E81AC; + --pst-color-link-hover: #5E81AC; +} + +.bd-header .navbar-nav li a.nav-link:hover { + text-decoration: none; +} + +.bd-header .navbar-nav>.current>.nav-link { + border-bottom: 0px; +} + +.bd-sidebar { + --pst-color-primary: #5E81AC; + --pst-color-link-hover: #5E81AC; +} + +/*.bd-sidebar a:hover { + text-decoration: none; +}*/ + +nav.bd-links li>a:hover { + text-decoration: none; +} + +/* remove left vertical bar on active link on left sidebar */ +nav.bd-links .current>a { + box-shadow: none; +} + +.bd-content { + --pst-color-link: #5E81AC; + --pst-color-link-hover: #ee9040; +} + +.toc-entry a.nav-link:hover { + text-decoration: none; + color: #5E81AC; +} + +#pst-back-to-top { + background-color: #2E3440; +} + +.form-control:focus { + outline: 2px solid var(--bs-border-color); +} diff --git a/docs/_static/css/lentil_index.css b/docs/_static/css/lentil_index.css new file mode 100644 index 0000000..633c5e9 --- /dev/null +++ b/docs/_static/css/lentil_index.css @@ -0,0 +1,52 @@ +.bd-main .bd-content .bd-article-container { + max-width: 80em; +} + +.biglinks { + --pst-color-link: var(--pst-color-text-base); + --pst-color-link-hover: var(--pst-color-text-base); +} + +.biglinks img { + width: 5em; + height: 5em; +} + +.bd-footer { + background-color: #3B4252; + color: white; +} + +.bd-footer a { + color: white; +} + +.bd-footer a:hover { + color: #ee9040; +} + +.bd-footer h2 { + color: white; + margin: 0; +} + +.bd-footer ul { + list-style-type: none; +} + +.bd-footer ul li { + margin-bottom: 10px; + font-weight: bold; +} + +.bd-footer dt { + text-transform: uppercase; + color: #E5E9F0; +} + + +.sd-btn-primary:hover, .sd-btn-primary:focus { + color: red !important; + background-color: blue !important; + border-color: green !important; + } diff --git a/docs/_static/documentation.svg b/docs/_static/documentation.svg new file mode 100644 index 0000000..2304118 --- /dev/null +++ b/docs/_static/documentation.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + diff --git a/docs/_static/examples.svg b/docs/_static/examples.svg new file mode 100644 index 0000000..8191e27 --- /dev/null +++ b/docs/_static/examples.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + diff --git a/docs/_static/favicon/apple-touch-icon.png b/docs/_static/favicon/apple-touch-icon.png new file mode 100644 index 0000000..ab222a4 Binary files /dev/null and b/docs/_static/favicon/apple-touch-icon.png differ diff --git a/docs/_static/favicon/favicon-16x16.png b/docs/_static/favicon/favicon-16x16.png new file mode 100644 index 0000000..79ba4d7 Binary files /dev/null and b/docs/_static/favicon/favicon-16x16.png differ diff --git a/docs/_static/favicon/favicon-32x32.png b/docs/_static/favicon/favicon-32x32.png new file mode 100644 index 0000000..fc01996 Binary files /dev/null and b/docs/_static/favicon/favicon-32x32.png differ diff --git a/docs/_static/favicon/favicon-48x48.png b/docs/_static/favicon/favicon-48x48.png new file mode 100644 index 0000000..50a7c67 Binary files /dev/null and b/docs/_static/favicon/favicon-48x48.png differ diff --git a/docs/_static/img/lentil.png b/docs/_static/img/lentil.png deleted file mode 100644 index 0e454d5..0000000 Binary files a/docs/_static/img/lentil.png and /dev/null differ diff --git a/docs/_static/img/logo.png b/docs/_static/img/logo.png new file mode 100644 index 0000000..a371fd1 Binary files /dev/null and b/docs/_static/img/logo.png differ diff --git a/docs/_static/img/psf.png b/docs/_static/img/psf.png new file mode 100644 index 0000000..a4f341d Binary files /dev/null and b/docs/_static/img/psf.png differ diff --git a/docs/_static/installation.svg b/docs/_static/installation.svg new file mode 100644 index 0000000..28b194d --- /dev/null +++ b/docs/_static/installation.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + diff --git a/docs/_static/js/copybutton.js b/docs/_static/js/copybutton.js deleted file mode 100644 index 331cdd4..0000000 --- a/docs/_static/js/copybutton.js +++ /dev/null @@ -1,65 +0,0 @@ -$(document).ready(function() { - /* Add a [>>>] button on the top-right corner of code samples to hide - * the >>> and ... prompts and the output and thus make the code - * copyable. */ - var div = $('.highlight-python .highlight,' + - '.highlight-python3 .highlight,' + - '.highlight-pycon .highlight,' + - '.highlight-default .highlight') - var pre = div.find('pre'); - - // get the styles from the current theme - pre.parent().parent().css('position', 'relative'); - var hide_text = 'Hide the prompts and output'; - var show_text = 'Show the prompts and output'; - //var border_width = pre.css('border-top-width'); - var border_width = '0px'; - var border_style = pre.css('border-top-style'); - //var border_color = pre.css('border-top-color'); - var border_color = 'rgba(0,0,0,0.4)'; - var button_styles = { - 'cursor':'pointer', 'position': 'absolute', 'top': '0', 'right': '0', - 'border-color': border_color, 'border-style': border_style, - 'border-width': border_width, 'color': border_color, 'text-size': '65%', - 'font-family': 'monospace', 'padding-left': '0.2em', 'padding-right': '0.2em', - 'border-radius': '0 3px 0 0' - } - - // create and add the button to all the code blocks that contain >>> - div.each(function(index) { - var jthis = $(this); - if (jthis.find('.gp').length > 0) { - var button = $('>>>'); - button.css(button_styles) - button.attr('title', hide_text); - button.data('hidden', 'false'); - jthis.prepend(button); - } - // tracebacks (.gt) contain bare text elements that need to be - // wrapped in a span to work with .nextUntil() (see later) - jthis.find('pre:has(.gt)').contents().filter(function() { - return ((this.nodeType == 3) && (this.data.trim().length > 0)); - }).wrap(''); - }); - - // define the behavior of the button when it's clicked - $('.copybutton').click(function(e){ - e.preventDefault(); - var button = $(this); - if (button.data('hidden') === 'false') { - // hide the code output - button.parent().find('.go, .gp, .gt').hide(); - button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'hidden'); - button.css('text-decoration', 'line-through'); - button.attr('title', show_text); - button.data('hidden', 'true'); - } else { - // show the code output - button.parent().find('.go, .gp, .gt').show(); - button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'visible'); - button.css('text-decoration', 'none'); - button.attr('title', hide_text); - button.data('hidden', 'false'); - } - }); -}); diff --git a/docs/_static/logo-dark.svg b/docs/_static/logo-dark.svg new file mode 100644 index 0000000..8a79e69 --- /dev/null +++ b/docs/_static/logo-dark.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/logo-light.svg b/docs/_static/logo-light.svg new file mode 100644 index 0000000..32bded4 --- /dev/null +++ b/docs/_static/logo-light.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/reference.svg b/docs/_static/reference.svg new file mode 100644 index 0000000..78d2af9 --- /dev/null +++ b/docs/_static/reference.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + 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 0735953..5a887a9 100644 --- a/docs/_templates/autosummary/class.rst +++ b/docs/_templates/autosummary/class.rst @@ -4,13 +4,12 @@ .. autoclass:: {{ objname }} - {% if attributes %} -**Attributes** +.. rubric:: {{ _('Attributes') }} .. autosummary:: :toctree: - {% for item in all_attributes %} + {% for item in attributes %} {%- if not item.startswith('_') %} ~{{ name }}.{{ item }} {%- endif -%} @@ -19,14 +18,14 @@ {% endif %} {% if methods %} -**Methods** +.. rubric:: {{ _('Methods') }} .. autosummary:: :toctree: - {% for item in all_methods %} + {% for item in methods %} {%- if not item.startswith('_') or item in ['__call__'] %} ~{{ name }}.{{ item }} {%- endif -%} {%- endfor %} -{% endif %} +{% 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 new file mode 100644 index 0000000..e15fbbd --- /dev/null +++ b/docs/_templates/index.html @@ -0,0 +1,121 @@ +{# + Loosely inspired by the deprecated sphinx/themes/basic/defindex.html + #} + {%- extends "layout.html" %} + + {% block css %} + {{ super() }} + + {% endblock %} + + {% set title = _('Overview') %} + {% block body %} + +
+
+
+
+

Lentil: Fast optical propagation

+ +

+ Lentil is an Python package for developing high-performance diffraction simulations. + Lentil provides an easy to use framework for modeling optical systems and simulating + the wave propagation of light through them. Lentil was originally developed at NASA's + Jet Propulsion Library and is now available as open-source software. + + +

+
+ +
+
+ +
+ +
+
+
+ + + +
+ +{% endblock %} + +{% block footer %} + + + +{% endblock %} + + + 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/cite.rst b/docs/cite.rst new file mode 100644 index 0000000..e848207 --- /dev/null +++ b/docs/cite.rst @@ -0,0 +1,33 @@ +.. _cite: + +************* +Citing Lentil +************* + +If Lentil has been significant in your research and you would like to +acknowledge the project in a publication, we suggest citing the following +paper: + +* Kee, Andrew G., Troy, Mitchell, Nissly, Carl et al. *Lentil: An open-source + library for fast optical propagation* + +*In BibTeX format:* + +.. code-block:: bibtex + + @Article{ kee2024lentil, + title = {Lentil: An open-source library for fast optical + propagation}, + author = {Andrew G. Kee and Mitchell Troy and Carl Nissly and + Jonathan Tesch and Siddarayappa Bikkannavar and David + Redding}, + year = {2024}, + month = jun, + journal = {}, + volume = {}, + number = {}, + pages = {--}, + doi = {}, + publisher = {}, + url = {} + } diff --git a/docs/conf.py b/docs/conf.py index 2b09594..ca3131d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,11 +16,12 @@ project = 'Lentil' author = 'Andy Kee' -copyright = f'{datetime.now().year} California Institute of Technology' +copyright = f'2017-{datetime.now().year} California Institute of Technology' 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 --------------------------------------------------- @@ -30,10 +31,13 @@ 'sphinx.ext.napoleon', 'sphinx.ext.viewcode', 'sphinx_remove_toctrees', - 'matplotlib.sphinxext.plot_directive'] + 'sphinx_copybutton', + 'sphinx_design', + 'matplotlib.sphinxext.plot_directive']#, + #'numpydoc'] templates_path = ['_templates'] source_suffix = '.rst' -master_doc = 'index' +master_doc = 'docs' exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] @@ -42,13 +46,47 @@ html_theme = 'pydata_sphinx_theme' html_theme_options = { 'show_prev_next': False, - 'google_analytics_id': 'UA-180546240-1', - 'github_url': 'https://github.com/andykee/lentil' + 'github_url': 'https://github.com/andykee/lentil', + "logo": { + "link": "docs", + "image_light": "_static/logo-light.svg", + "image_dark": "_static/logo-dark.svg", + }, + "collapse_navigation": True, + "navbar_persistent": ["search-button"], + "pygment_light_style": "tango", + "pygment_dark_style": "nord", + "favicons": [ + { + "rel": "icon", + "sizes": "16x16", + "href": "favicon/favicon-16x16.png", + }, + { + "rel": "icon", + "sizes": "32x32", + "href": "favicon/favicon-32x32.png", + }, + { + "rel": "icon", + "sizes": "48x48", + "href": "favicon/favicon-48x48.png", + }, + { + "rel": "apple-touch-icon", + "sizes": "180x180", + "href": "favicon/apple-touch-icon-180x180.png", + "color": "#000000", + }, + ] } -html_logo = '_static/img/lentil.png' html_additional_pages = { - 'index': 'indexcontent.html' + 'index': 'index.html' +} + +html_sidebars = { + 'index': [] } html_static_path = ['_static'] @@ -56,22 +94,22 @@ html_show_sourcelink = False html_scaled_image_link = False -html_js_files = ['js/copybutton.js'] -html_css_files = ['css/lentil.css', 'css/syntax-highlighting.css'] - -pygments_style = 'default' +html_css_files = ['css/lentil.css'] # 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': 'alphabetical', - 'exclude-members': '__init__, __weakref__, __dict__, __module__' + 'exclude-members': '__init__, __weakref__, __dict__, __module__', } autosummary_generate = True + #remove_from_toctrees = ["generated/*"] # -- Plot config ------------------------------------------------------------- @@ -103,6 +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) """ + diff --git a/docs/dev/contributing.rst b/docs/dev/contributing.rst deleted file mode 100644 index 24ee089..0000000 --- a/docs/dev/contributing.rst +++ /dev/null @@ -1,377 +0,0 @@ -.. _contributing: - -********************** -Contributing to Lentil -********************** - -.. note:: - - This contribution guide is very heavily influenced by (and in many places - directly copied from) the `Pandas development guide - `_. - -Bug reports and enhancement requests - -Bug reports and enhancement requests should be filed using Lentil's -`issue tracker `__. - -Working with the source code -============================ - -Version control, Git, and GitHub --------------------------------- -Lentil's source code is hosted on `GitHub `_. -To contribute you'll need `an account `_. Local -version control is handled by `Git `_. - -`GitHub has instructions `_ for -installing Git, setting up your SSH key, and configuring Git. All these steps -need to be completed before you can work seamlessly between your local repository -and GitHub. - -Some useful resources for learning Git: - -* `Github Docs `_ -* Matthew Brett's `Pydagouge `_ - notes on Git -* `Oh Shit, Git? `_ for when things go horribly wrong - -.. _contributing.forking: - -Forking -------- -You will need your own fork to work on the code. Go to the Lentil GitHub page and -hit the ``Fork`` button. You will want to clone your fork to your machine:: - - git clone https://github.com/your-user-name/lentil.git lentil-yourname - cd lentil-yourname - git remote add upstream https://github.com/andykee/lentil.git - -This creates the directory `lentil-yourname` and connects your repository to the -upstream (main project) Lentil repository. - -Creating a Python environment ------------------------------ -To test out code changes, you’ll need to build install from source, which -requires a suitable Python environment. To create an isolated Lentil development -environment: - -* Install either `Anaconda `_ or `miniconda - `_ -* Make sure your conda is up to date (``conda update conda``) -* Make sure that you have :ref:`cloned the repository ` -* ``cd`` to the Lentil source directory - -We can now create a development environment and install Lentil:: - - # Create and activate the build environment: - conda env create -f environment.yml - conda activate lentil-dev - - # or with older versions of Anaconda: - source activate lentil-dev - - # Install Lentil and its dependencies - python -m pip install -e . --no-build-isolation --no-use-pep517 - -You should now be able to import Lentil in your development environment:: - - $ python - >>> import lentil - -To view your environments:: - - conda info -e - -To return to your root environment:: - - conda deactivate - -See the full conda docs `here `_. - -Contributing to the code base -============================= - -Creating a branch ------------------ -You want your master branch to reflect only production-ready code, so create a feature -branch for making your changes. For example:: - - git branch shiny-new-feature - git checkout shiny-new-feature - -The above can be simplified to:: - - git checkout -b shiny-new-feature - -This changes your working directory to the shiny-new-feature branch. Keep any changes -in this branch specific to one bug or feature so it is clear what the branch brings to -Lentil. You can have many shiny-new-features and switch in between them using the git -checkout command. - -When creating this branch, make sure your master branch is up to date with the latest -upstream master version. To update your local master branch, you can do:: - - git checkout master - git pull upstream master --ff-only - -When you want to update the feature branch with changes in master after you created the -branch, check the section on :ref:`updating a PR `. - -.. _contributing.commit-code: - -Committing your code --------------------- -Once you've made changes, you can see them by typing:: - - git status - -If you have created a new file, it is not being tracked by git. Add it by typing:: - - git add path/to/file-to-be-added.py - -Doing 'git status' again should give something like:: - - # On branch shiny-new-feature - # - # modified: /relative/path/to/file-you-added.py - # - -Finally, commit your changes to your local repository with an explanatory message. -Lentil uses a convention for commit message prefixes and layout. Here are -some common prefixes along with general guidelines for when to use them: - -* ENH: Enhancement, new functionality -* BUG: Bug fix -* DOC: Additions/updates to documentation -* TEST: Additions/updates to tests -* PERF: Performance improvement -* CLN: Code cleanup - -The following defines how a commit message should be structured. Please reference the -relevant GitHub issues in your commit message using #1234. - -* a subject line with ``< 80`` chars. -* One blank line. -* Optionally, a commit message body. - -Now you can commit your changes in your local repository:: - - git commit -m - -.. _contributing.push-code: - -Squashing commits ------------------ -It's possible to combine (or squash) a number of smaller commits into one larger -commit. This helps to keep the project history more concise and readable. The -easiest wat to squash commits is by using interactive rebase. To consider the -most recent ``n`` commits:: - - git rebase -i HEAD~ - -To instead consider all commits including and after a specific commit:: - - git rebase -i - -The interactive rebase interface provides additional syntax details. - -Pushing your changes --------------------- - -When you want your changes to appear publicly on your GitHub page, push your -forked feature branch's commits:: - - git push origin shiny-new-feature - -Here ``origin`` is the default name given to your remote repository on GitHub. -You can see the remote repositories:: - - git remote -v - -If you added the upstream repository as described above you will see something -like:: - - origin git@github.com:yourname/lentil.git (fetch) - origin git@github.com:yourname/lentil.git (push) - upstream git://github.com/andykee/lentil.git (fetch) - upstream git://github.com/andykee/lentil.git (push) - -Now your code is on GitHub, but it is not yet a part of the Lentil project. For that to -happen, a pull request needs to be submitted on GitHub. - -Review your code ----------------- - -When you're ready to ask for a code review, file a pull request. Before you do, once -again make sure that you have followed all the guidelines outlined in this document -regarding code style, tests, and documentation. You should also double check your -branch changes against the branch it was based on: - -#. Navigate to your repository on GitHub -- https://github.com/your-user-name/lentil -#. Click on ``Branches`` -#. Click on the ``Compare`` button for your feature branch -#. Select the ``base`` and ``compare`` branches, if necessary. This will be ``master`` and - ``shiny-new-feature``, respectively. - -Make a pull request -------------------- - -If everything looks good, you are ready to make a pull request. A pull request is how -code from a local repository becomes available to the GitHub community and can be looked -at and eventually merged into the master version. This pull request and its associated -changes will eventually be committed to the master branch and available in the next -release. To submit a pull request: - -#. Navigate to your repository on GitHub -#. Click on the ``Pull Request`` button -#. You can then click on ``Commits`` and ``Files Changed`` to make sure everything looks - okay one last time -#. Write a description of your changes in the ``Preview Discussion`` tab -#. Click ``Send Pull Request``. - -This request then goes to the repository maintainers, and they will review -the code. - -.. _contributing.update-pr: - -Upadting a pull request ------------------------ - -Based on the review you get on your pull request, you will probably need to make -some changes to the code. In that case, you can make them in your branch, -add a new commit to that branch, push it to GitHub, and the pull request will be -automatically updated. Pushing them to GitHub again is done by:: - - git push origin shiny-new-feature - -Another reason you might need to update your pull request is to solve conflicts -with changes that have been merged into the master branch since you opened your -pull request. - -To do this, you need to "merge upstream master" in your branch:: - - git checkout shiny-new-feature - git fetch upstream - git merge upstream/master - -If there are no conflicts (or they could be fixed automatically), a file with a -default commit message will open, and you can simply save and quit this file. - -If there are merge conflicts, you need to solve those conflicts. See for -example at https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line/ -for an explanation on how to do this. -Once the conflicts are merged and the files where the conflicts were solved are -added, you can run ``git commit`` to save those fixes. - -If you have uncommitted changes at the moment you want to update the branch with -master, you will need to ``stash`` them prior to updating (see the -`stash docs `__). -This will effectively store your changes and they can be reapplied after updating. - -After the feature branch has been update locally, you can now update your pull -request by pushing to the branch on GitHub:: - - git push origin shiny-new-feature - -Delete your merged branch (optional) ------------------------------------- - -Once your feature branch is accepted into upstream, you'll probably want to get rid of -the branch. First, merge upstream master into your branch so git knows it is safe to -delete your branch:: - - git fetch upstream - git checkout master - git merge upstream/master - -Then you can do:: - - git branch -d shiny-new-feature - -Make sure you use a lower-case ``-d``, or else git won't warn you if your feature -branch has not actually been merged. - -The branch will still exist on GitHub, so to delete it there do:: - - git push origin --delete shiny-new-feature - -.. _run_tests: - -Running the test suite ----------------------- - -.. note:: - - Running the tests requires `pytest `_. - -The tests can then be run directly inside your Git clone by typing:: - - pytest tests - -Documenting your code ---------------------- -Changes should be reflected in the release notes located in ``CHANGES.rst``. This -file contains an ongoing change log for each release. Add an entry to this file to -document your fix, enhancement or (unavoidable) breaking change. Make sure to include -the GitHub issue number when adding your entry (using ``:issue:`1234``` where ``1234`` -is the issue/pull request number). - -If your code is an enhancement, it is most likely necessary to add usage examples to -the existing documentation. Further, to let users know when this feature was added, -the ``versionadded`` directive is used. The sphinx syntax for that is:: - - .. versionadded:: 1.1.0 - -This will put the text New in version 1.1.0 wherever you put the sphinx directive. This -should also be put in the docstring when adding a new function or method or a new keyword -argument. - -Building the documentation --------------------------- - -.. note:: - - Building the documentation requires `Sphinx `_, - `PyData Sphinx Theme `_, - and the ``sphinx-remove-toctrees`` extension. - -To build the documentation, navigate to your local ``docs/`` directory and run:: - - make html - -The HTML documentation will be written to ``docs/_build/html``. - -If you want to do a full clean build, do:: - - make clean && make html - -Publishing to PyPi -================== - -Prepare the release -------------------- -1. Increment the version number appropriately in `lentil/__init__.py - `_ according to - `PEP 440 `_. -2. Document contents of the release in `CHANGES.rst - `_ -3. Commit the changes from above on the master branch with a commit message equal to the - short version name (i.e. ``v0.3.2`` or ``v0.5.0b2``). -4. Push the updated master branch and create a new release. - -Build the release ------------------ -.. code:: bash - - $ python setup.py sdist bdist_wheel - -Upload the release ------------------- -.. code:: bash - - $ twine upload dist/* - -.. note:: - - Only core-team members are able to publish new releases to PyPi. diff --git a/docs/dev/index.rst b/docs/dev/index.rst index b93bf6d..ac8889d 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -4,17 +4,23 @@ Development *********** +This page provides resources for Lentil developers + + + .. toctree:: - :maxdepth: 3 + :maxdepth: 2 - contributing + tech_notes/index .. toctree:: :maxdepth: 3 - tech_notes/index + verification/index .. toctree:: :maxdepth: 1 + install + publishing changes diff --git a/docs/dev/install.rst b/docs/dev/install.rst new file mode 100644 index 0000000..aa50397 --- /dev/null +++ b/docs/dev/install.rst @@ -0,0 +1,77 @@ +.. _development.install: + +****************************** +Install Lentil for development +****************************** + + +Forking the Lentil repo +======================= +Matplotlib is hosted at `andykee/lentil.git `_. +If you plan on solving issues or submitting pull requests to the main Lentil +repository, you should first fork this repository by visiting +`andykee/lentil.git `_ and clicking on the +``Fork`` button on the top right of the page. See the +`GitHub documentation `_ +for more details. + +Installing from source +====================== +If you want to build from source in order to work on Lentil itself, first +clone the Lentil repository: + +.. code-block:: bash + + git clone https://github.com/andykee/lentil.git + +If you forked the Lentil repository, you should clone the Lentil repository +from your fork instead (replacing ```` with your GitHub +username): + +.. code-block:: bash + + git clone https://github.com//lentil.git + +Now you can install Lentil in editable mode from the ``lentil`` directory: + +.. code-block:: bash + + pip install -e . + +Development dependencies +======================== +Lentil uses the `pytest `_ framework for +testing. Install it with + +.. code-block:: bash + + pip install pytest + +The additional Python packages required to build the documentation are +listed in ``docs/requirements.txt`` and can be installed using + +.. code-block:: bash + + pip install -r docs/requirements.txt + +Running the test suite +====================== +To run the tests, in the root directory of your development repository run: + +.. code-block:: bash + + pytest tests + + +Building the documentation +========================== +The documentation source is found in the ``docs/`` directory. The +configuration file for Sphinx is ``docs/conf.py``. It controls which +directories Sphinx parses, how the docs are built, and how the extensions are +used. To build the documentation in html format, cd into ``docs/`` and run: + +.. code-block:: bash + + make html + +The built docs will be placed in the folder ``docs/_build/html``. \ No newline at end of file diff --git a/docs/dev/publishing.rst b/docs/dev/publishing.rst new file mode 100644 index 0000000..893471c --- /dev/null +++ b/docs/dev/publishing.rst @@ -0,0 +1,31 @@ +.. _publishing: + +Publishing to PyPi +================== + +Prepare the release +------------------- +1. Increment the version number appropriately in `lentil/__init__.py + `_ according to + `PEP 440 `_. +2. Document contents of the release in `CHANGES.rst + `_ +3. Commit the changes from above on the master branch with a commit message equal to the + short version name (i.e. ``v0.3.2`` or ``v0.5.0b2``). +4. Push the updated master branch and create a new release. + +Build the release +----------------- +.. code:: bash + + $ python setup.py sdist bdist_wheel + +Upload the release +------------------ +.. code:: bash + + $ twine upload dist/* + +.. note:: + + Only core-team members are able to publish new releases to PyPi. diff --git a/docs/dev/tech_notes/diffraction/dft_sampling.rst b/docs/dev/tech_notes/dft_sampling.rst similarity index 100% rename from docs/dev/tech_notes/diffraction/dft_sampling.rst rename to docs/dev/tech_notes/dft_sampling.rst diff --git a/docs/dev/tech_notes/diffraction/dft2_caching.rst b/docs/dev/tech_notes/diffraction/dft2_caching.rst deleted file mode 100644 index 4fc4dfc..0000000 --- a/docs/dev/tech_notes/diffraction/dft2_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/diffraction/index.rst b/docs/dev/tech_notes/diffraction/index.rst deleted file mode 100644 index 2c878f0..0000000 --- a/docs/dev/tech_notes/diffraction/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. _tech_notes.diffraction: - -*********** -Diffraction -*********** - -.. toctree:: - :maxdepth: 1 - - prop_algorithm - dft_sampling - dft2_caching \ No newline at end of file diff --git a/docs/dev/tech_notes/index.rst b/docs/dev/tech_notes/index.rst index aac8014..d25ae27 100644 --- a/docs/dev/tech_notes/index.rst +++ b/docs/dev/tech_notes/index.rst @@ -5,6 +5,7 @@ Technical Notes *************** .. toctree:: - :maxdepth: 2 + :maxdepth: 1 - diffraction/index + dft_sampling + prop_algorithm diff --git a/docs/dev/tech_notes/diffraction/prop_algorithm.rst b/docs/dev/tech_notes/prop_algorithm.rst similarity index 100% rename from docs/dev/tech_notes/diffraction/prop_algorithm.rst rename to docs/dev/tech_notes/prop_algorithm.rst diff --git a/docs/user_guide/verification/index.rst b/docs/dev/verification/index.rst similarity index 100% rename from docs/user_guide/verification/index.rst rename to docs/dev/verification/index.rst diff --git a/docs/user_guide/verification/pixel_mtf.rst b/docs/dev/verification/pixel_mtf.rst similarity index 97% rename from docs/user_guide/verification/pixel_mtf.rst rename to docs/dev/verification/pixel_mtf.rst index 305a3ea..b5aebb4 100644 --- a/docs/user_guide/verification/pixel_mtf.rst +++ b/docs/dev/verification/pixel_mtf.rst @@ -61,7 +61,7 @@ with the analytic result for the same imaging system: amp = lentil.circle((npix, npix), npix_pup_rad) alpha = (dx*du)/(wave*focal_length*oversample) - psf = np.abs(lentil.fourier.dft2(amp, alpha, npix=npix)**2) + psf = np.abs(lentil.fourier.dft2(amp, alpha, shape=npix)**2) psf = psf/np.max(psf) # Compute the optical MTF from a Lentil-generated PSF diff --git a/docs/docs.rst b/docs/docs.rst new file mode 100644 index 0000000..c6476f1 --- /dev/null +++ b/docs/docs.rst @@ -0,0 +1,96 @@ +.. _docs: + +.. toctree:: + :hidden: + + User Guide + Examples + API Reference + Development + +******************** +Lentil Documentation +******************** + + +**Version**: |version| + +**Useful links**: +:doc:`Installation ` | +`Source Repository `_ | +`Issue Tracker `_ | + +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 + :doc:`release notes `. + +.. grid:: 2 + :gutter: 5 + + .. grid-item-card:: :octicon:`file;1em;` User guide + + The user guide provides in-depth information on the + key concepts of NumPy with useful background information + and explanation. + + +++ + + .. button-ref:: user/index + :expand: + :color: info + :click-parent: + + To the user guide + + .. grid-item-card:: :octicon:`code-square;1em;` Examples + + New to NumPy? Check out the Absolute Beginner's Guide. It contains an + introduction to NumPy's main concepts and links to additional tutorials. + + +++ + + .. button-ref:: examples/index + :expand: + :color: info + :click-parent: + + To the examples + + .. grid-item-card:: :octicon:`repo;1em;` API reference + + The reference guide contains a detailed description of the functions, + modules, and objects included in NumPy. The reference describes how the + methods work and which parameters can be used. It assumes that you have an + understanding of the key concepts. + + +++ + + .. button-ref:: ref/index + :expand: + :color: info + :click-parent: + + To the reference guide + + .. grid-item-card:: :octicon:`terminal;1em;` Development guide + + Want to add to the codebase? Can help add translation or a flowchart to the + documentation? The contributing guidelines will guide you through the + process of improving NumPy. + + +++ + + .. button-ref:: dev/index + :expand: + :color: info + :click-parent: + + To the development guide diff --git a/docs/patterns/general/attributes.rst b/docs/examples/general/attributes.rst similarity index 100% rename from docs/patterns/general/attributes.rst rename to docs/examples/general/attributes.rst diff --git a/docs/patterns/general/large.rst b/docs/examples/general/large.rst similarity index 100% rename from docs/patterns/general/large.rst rename to docs/examples/general/large.rst diff --git a/docs/patterns/general/simple.rst b/docs/examples/general/simple.rst similarity index 100% rename from docs/patterns/general/simple.rst rename to docs/examples/general/simple.rst diff --git a/docs/patterns/index.rst b/docs/examples/index.rst similarity index 90% rename from docs/patterns/index.rst rename to docs/examples/index.rst index 08037d5..0bc04e9 100644 --- a/docs/patterns/index.rst +++ b/docs/examples/index.rst @@ -1,8 +1,8 @@ -.. _patterns: +.. _examples: -************** -Model Patterns -************** +******** +Examples +******** General ======= diff --git a/docs/patterns/matlab/matlab_interface.rst b/docs/examples/matlab/matlab_interface.rst similarity index 100% rename from docs/patterns/matlab/matlab_interface.rst rename to docs/examples/matlab/matlab_interface.rst diff --git a/docs/patterns/planes/filter_wheel.rst b/docs/examples/planes/filter_wheel.rst similarity index 100% rename from docs/patterns/planes/filter_wheel.rst rename to docs/examples/planes/filter_wheel.rst diff --git a/docs/patterns/planes/rb_element.rst b/docs/examples/planes/rb_element.rst similarity index 100% rename from docs/patterns/planes/rb_element.rst rename to docs/examples/planes/rb_element.rst diff --git a/docs/patterns/planes/translation_stage.rst b/docs/examples/planes/translation_stage.rst similarity index 100% rename from docs/patterns/planes/translation_stage.rst rename to docs/examples/planes/translation_stage.rst diff --git a/docs/patterns/radiometry/complex_sources.rst b/docs/examples/radiometry/complex_sources.rst similarity index 100% rename from docs/patterns/radiometry/complex_sources.rst rename to docs/examples/radiometry/complex_sources.rst diff --git a/docs/patterns/radiometry/propagation.rst b/docs/examples/radiometry/propagation.rst similarity index 94% rename from docs/patterns/radiometry/propagation.rst rename to docs/examples/radiometry/propagation.rst index 7d62cef..135e22e 100644 --- a/docs/patterns/radiometry/propagation.rst +++ b/docs/examples/radiometry/propagation.rst @@ -33,7 +33,7 @@ and multiply the source irradiance by the collecting area to get photons/second. >>> for wl, wt in zip(source.wave, source.value): ... w = lentil.Wavefront(wl*1e-9) ... w = w * pupil - ... w = w.propagate_image(pixelscale=5e-6, npix=32, oversample=2) + ... w = lentil.propagate_dft(w, pixelscale=5e-6, shape=(32,32), oversample=2) ... psf += (w.intensity * (np.pi*(pupil_diameter/2)**2)) >>> plt.imshow(psf, origin='lower') @@ -57,7 +57,7 @@ fine features present in the source's spectral response. >>> for wl, wt in zip(binned_wave, binned_flux): ... w = lentil.Wavefront(wl*1e-9) ... w = w * pupil - ... w = w.propagate_image(pixelscale=5e-6, npix=32, oversample=2) + ... w = lentil.propagate_dft(w, pixelscale=5e-6, shape=(32,32), oversample=2) ... psf += (w.intensity * (np.pi*(pupil_diameter/2)**2)) >>> plt.imshow(psf, origin='lower') @@ -100,7 +100,7 @@ runs 50 times faster. wave = bandpass.wave trans = bandpass.value - return lentil.propagate(self.planes, wave*1e-9, trans, npix, npix_chip, + return lentil.propagate_dft(self.planes, wave*1e-9, trans, npix, npix_chip, oversample, rebin, tilt, flatten=True) If we would like to render an image as read out by the detector, we add light_flux and @@ -139,7 +139,7 @@ image methods to the Model class: binned_flux = flux.bin(wave, waveunit=flux.waveunit) # do the propagation - img = lentil.propagate(self.planes, + img = lentil.propagate_dft(self.planes, wave=wave * 1e-9, weight=binned_flux, npix=npix, diff --git a/docs/patterns/radiometry/self_emission.rst b/docs/examples/radiometry/self_emission.rst similarity index 100% rename from docs/patterns/radiometry/self_emission.rst rename to docs/examples/radiometry/self_emission.rst diff --git a/docs/patterns/radiometry/source_coupling.rst b/docs/examples/radiometry/source_coupling.rst similarity index 100% rename from docs/patterns/radiometry/source_coupling.rst rename to docs/examples/radiometry/source_coupling.rst diff --git a/docs/patterns/radiometry/source_defocus.rst b/docs/examples/radiometry/source_defocus.rst similarity index 100% rename from docs/patterns/radiometry/source_defocus.rst rename to docs/examples/radiometry/source_defocus.rst diff --git a/docs/getting_started/index.rst b/docs/getting_started/index.rst deleted file mode 100644 index e972dc1..0000000 --- a/docs/getting_started/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -*************** -Getting started -*************** - -.. toctree:: - :maxdepth: 1 - - install - overview - quickstart diff --git a/docs/getting_started/install.rst b/docs/getting_started/install.rst deleted file mode 100644 index 754dd06..0000000 --- a/docs/getting_started/install.rst +++ /dev/null @@ -1,35 +0,0 @@ -.. _installation: - -############ -Installation -############ - -The easiest way to install Lentil is with ``pip``: - -.. code-block:: bash - - pip install lentil - -Python version support -====================== -Lentil requires Python 3.6 and above - -Dependencies -============ - -================================== ========================== -Package Minimum supported version -================================== ========================== -`NumPy `__ 1.16 -`SciPy `__ 1.4 -================================== ========================== - -Installing from source -====================== -See the :ref:`contributing guide ` for instructions on installing from -the source code. - -Running the test suite -====================== -Lentil comes with a suite of units tests. See :ref:`running the tests ` -for instructions on running the tests. \ No newline at end of file diff --git a/docs/getting_started/quickstart.rst b/docs/getting_started/quickstart.rst deleted file mode 100644 index c7f2e8c..0000000 --- a/docs/getting_started/quickstart.rst +++ /dev/null @@ -1,134 +0,0 @@ -.. |Plane| replace:: :class:`~lentil.Plane` -.. |Pupil| replace:: :class:`~lentil.Pupil` -.. |Image| replace:: :class:`~lentil.Image` -.. |Wavefront| replace:: :class:`~lentil.Wavefront` - -.. _quickstart: - -********** -Quickstart -********** -This is a short introduction to Lentil, mainly written for new users. More -complex recipes are available in the Cookbook. - -First, we import Lentil: - -.. code-block:: pycon - - >>> import lentil - -We'll also import `Matplotlib `_ to visualize results: - -.. code-block:: pycon - - >>> 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: - -.. code-block:: pycon - - >>> 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') - -.. plot:: - :scale: 50 - :context: reset - - 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') - -Note the diameter is 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) - - -.. Wavefront error -.. =============== - -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 -in meters: - -.. code-block:: pycon - - >>> w = lentil.Wavefront(wavelength=650e-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 - - >>> w = w * pupil - -Finally, we'll propagate the wavefront to a discreetely sampled image plane -using :func:`~lentil.Wavefront.propagate_image`. In this case, we'll sample -the result every 5e-6 meters and perform the propagation 3 times oversampled: - -.. code-block:: pycon - - >>> w = w.propagate_image(npix=64, pixelscale=5e-6, oversample=5) - -The resulting intensity (point spread function) can now be observed: - -.. code-block:: pycon - - >>> plt.imshow(w.intensity, origin='lower') - -.. plot:: - :context: close-figs - :scale: 50 - - w = lentil.Wavefront(wavelength=650e-9) - w *= pupil - w = w.propagate_image(npix=64, pixelscale=5e-6, oversample=5) - plt.imshow(w.intensity, origin='lower') - -.. Multi-plane propagation -.. ----------------------- - -.. Free-space propagation -.. ---------------------- - - -Focal planes -============ - - -Radiometry -========== - - diff --git a/docs/getting_started/tutorial.rst b/docs/getting_started/tutorial.rst deleted file mode 100644 index 0e9e75d..0000000 --- a/docs/getting_started/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/index.rst b/docs/index.rst deleted file mode 100644 index 9ba4054..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. toctree:: - :hidden: - - getting_started/index - user_guide/index - reference - dev/index diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 0000000..d6f334a --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,8 @@ +.. _license: + +******* +License +******* + +.. include:: ../LICENSE.rst + :literal: \ No newline at end of file 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..491e0aa --- /dev/null +++ b/docs/ref/propagate.rst @@ -0,0 +1,11 @@ +.. _api.propagate: + +*********** +Propagation +*********** + +.. autosummary:: + :toctree: generated/ + + lentil.propagate_dft + lentil.propagate_fft \ 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 a2f4d7f..0000000 --- a/docs/reference.rst +++ /dev/null @@ -1,230 +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:: - :toctree: generated/ - - lentil.Plane - lentil.Pupil - lentil.Image - lentil.Detector - lentil.Grism - lentil.Tilt - lentil.Rotate - lentil.Flip - -.. _api.wavefront: - -Wavefront -========= - -.. autosummary:: - :toctree: generated/ - - lentil.Wavefront - -.. .. _api.propagation: - -.. Numerical diffraction propagation -.. ================================= - -.. .. autosummary:: -.. :toctree: generated/ - -.. lentil.propagate - -.. _api.zernike: - -Zernike polynomials -=================== -.. autosummary:: - :toctree: generated/ - - lentil.zernike - lentil.zernike_basis - lentil.zernike_compose - lentil.zernike_coordinates - lentil.zernike_fit - lentil.zernike_remove - -.. _api.wfe: - -Wavefront errors -================ -.. autosummary:: - :toctree: generated/ - - lentil.power_spectrum - lentil.translation_defocus - -.. _api.imaging: - -Imaging artifacts -================= -.. autosummary:: - :toctree: generated/ - - lentil.jitter - lentil.smear - -.. _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 - lentil.sanitize_shape - lentil.sanitize_bandpass - -.. _api.detector: - -Detector module -=============== - -Charge collection ------------------ -.. autosummary:: - :toctree: generated/ - - lentil.detector.collect_charge - lentil.detector.collect_charge_bayer - -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 - -.. _api.radiometry: - -Radiometry module -================= - -.. autosummary:: - :toctree: generated/ - - lentil.radiometry.Spectrum - lentil.radiometry.Blackbody - lentil.radiometry.Material - lentil.radiometry.planck_radiance - lentil.radiometry.planck_exitance - lentil.radiometry.vegaflux - lentil.radiometry.path_transmission - lentil.radiometry.path_emission - - -.. _apt.internal: - -Internals -========= - -.. warning:: - - The ``lentil.field``, ``lentil.fourier``, and ``lentil.helper`` top-level - modules are intended for internal use. Stable functionality in these - modules is not guaranteed. - -Field ------ -.. autosummary:: - :toctree: generated/ - - lentil.field.Field - lentil.field.NDField - lentil.field.extent - lentil.field.overlap - lentil.field.boundary - lentil.field.insert - lentil.field.merge - lentil.field.multiply - lentil.field.reduce - -Fourier transforms ------------------- -.. autosummary:: - :toctree: generated/ - - lentil.fourier.dft2 - lentil.fourier.idft2 - -Helper functions ----------------- -.. autosummary:: - :toctree: generated/ - - lentil.helper.boundary_slice - lentil.helper.gaussian2d - lentil.helper.mesh - lentil.helper.slice_offset - diff --git a/docs/release.rst b/docs/release.rst new file mode 100644 index 0000000..8b0e791 --- /dev/null +++ b/docs/release.rst @@ -0,0 +1,3 @@ +.. _release: + +.. include:: ../CHANGES.rst \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 01d0049..d059c04 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,6 @@ sphinx>=3.0 pydata-sphinx-theme -sphinx_remove_toctrees +sphinx-remove-toctrees +sphinx-coppybutton +sphinx-design matplotlib diff --git a/docs/user_guide/coordinates.rst b/docs/user/basics.coordinates.rst similarity index 80% rename from docs/user_guide/coordinates.rst rename to docs/user/basics.coordinates.rst index c0d3655..a4df4a3 100644 --- a/docs/user_guide/coordinates.rst +++ b/docs/user/basics.coordinates.rst @@ -1,4 +1,4 @@ -.. _user_guide.coordinate_system: +.. _user.coordinate_system: ***************** Coordinate system @@ -25,8 +25,10 @@ positive x-axis pointing to the right and the positive y-axis pointing up. Additional details on the sign conventions for representing wavefront error and of the complex exponential in the Fourier kernel are provided below: -* :ref:`user_guide.wavefront_error.sign` -* :ref:`user_guide.diffraction.sign` +* :ref:`user.wavefront_error.sign` +* :ref:`user.diffraction.sign` + +.. _user.coordinate_system.origin: .. note:: @@ -34,10 +36,10 @@ of the complex exponential in the Fourier kernel are provided below: 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_guide/diffraction.rst b/docs/user/basics.diffraction.rst similarity index 89% rename from docs/user_guide/diffraction.rst rename to docs/user/basics.diffraction.rst index 43577a4..6ea92ec 100644 --- a/docs/user_guide/diffraction.rst +++ b/docs/user/basics.diffraction.rst @@ -1,4 +1,4 @@ -.. _user_guide.diffraction: +.. _user.diffraction: .. |Wavefront| replace:: :class:`~lentil.Wavefront` .. |Plane| replace:: :class:`~lentil.Plane` @@ -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: @@ -86,7 +83,7 @@ follows the same basic flow: .. note:: Additional details on the plane-wavefront interaction can be found in - :ref:`user_guide.optical_systems.plane_wavefront`. + :ref:`user.optical_systems.plane_wavefront`. 3. **Propagate the wavefront to the next plane in the optical system** - the |Wavefront| object provides a number of methods to propagate between planes. The appropriate method @@ -106,9 +103,9 @@ follows the same basic flow: * :attr:`pixelscale` - the spatial sampling of the output plane * :attr:`npix` - the shape of the output plane * :attr:`npix_prop` - the shape of the propagation plane. See - :ref:`user_guide.diffraction.npix` for additional details. + :ref:`user.diffraction.npix` for additional details. * :attr:`oversample` - the number of times to oversample the output plane. - See the section on :ref:`user_guide.diffraction.sampling` for more + See the section on :ref:`user.diffraction.sampling` for more details. @@ -119,8 +116,8 @@ follows the same basic flow: :include-source: :scale: 50 - >>> w2.propagate_image(pixelscale=5e-6, npix=64, oversample=5) - >>> plt.imshow(w2.intensity**0.1, origin='lower') + >>> w2 = lentil.propagate_dft(w2, pixelscale=5e-6, shape=(64,64), oversample=5) + >>> 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) @@ -155,10 +148,10 @@ different wavelengths and accumulates the resulting image plane intensity: for wl in wavelengths: w = lentil.Wavefront(wl) w = w * pupil - w.propagate_image(pixelscale=5e-6, npix=64, oversample=5) + 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``. @@ -178,10 +171,10 @@ wavefront intensity given by ``npix`` * ``oversample``. for wl in wavelengths: w = lentil.Wavefront(wl) w = w * pupil - w.propagate_image(pixelscale=5e-6, npix=64, oversample=5) + w = lentil.propagate_dft(w, pixelscale=5e-6, shape=(64,64), oversample=5) img = w.insert(img) -.. _user_guide.diffraction.npix: +.. _user.diffraction.npix: ``npix`` vs ``npix_prop`` ------------------------- @@ -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 @@ -234,7 +227,7 @@ The chirp Z-transform provides additional efficiency when transforming large arr Lentil selects the most appropriate DFT method automatically based on the plane size and sampling requirements. -.. _user_guide.diffraction.sign: +.. _user.diffraction.sign: Sign of the DFT complex exponential ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -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_guide/optical_systems.rst b/docs/user/basics.optical_systems.rst similarity index 94% rename from docs/user_guide/optical_systems.rst rename to docs/user/basics.optical_systems.rst index cdbce4b..aa2cd7f 100644 --- a/docs/user_guide/optical_systems.rst +++ b/docs/user/basics.optical_systems.rst @@ -1,4 +1,4 @@ -.. _user_guide.optical_systems: +.. _user.optical_systems: .. currentmodule:: lentil @@ -26,7 +26,7 @@ Lentil uses |Plane| objects to represent discretely sampled planes in an optical and |Wavefront| objects to represent discretely sampled electromagnetic fields as they propagate through an optical system. -.. _user_guide.optical_systems.plane_wavefront: +.. _user.optical_systems.plane_wavefront: How a plane affects a wavefront =============================== @@ -66,7 +66,7 @@ the wavefront's complex data array: Planes in a simple optical system ================================= Most optical systems can be adequately modeled by a single far-field propagation -between a :ref:`user_guide.planes.pupil` and image plane. This includes most cameras, +between a :ref:`user.planes.pupil` and image plane. This includes most cameras, telescopes, and imaging instruments. In these models, all of the optics in a system are represented by a single |Pupil| plane: @@ -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]) @@ -99,7 +97,7 @@ with a segmented aperture, as depicted below: This modification is not necessary to achieve accurate propagations, but can greatly improve performance. For additional details, see -:ref:`user_guide.diffraction.segmented`. +:ref:`user.diffraction.segmented`. More complicated optical systems diff --git a/docs/user_guide/planes.rst b/docs/user/basics.planes.rst similarity index 95% rename from docs/user_guide/planes.rst rename to docs/user/basics.planes.rst index f7a36b2..8251202 100644 --- a/docs/user_guide/planes.rst +++ b/docs/user/basics.planes.rst @@ -1,4 +1,4 @@ -.. _user_guide.planes: +.. _user.planes: .. |Plane| replace:: :class:`~lentil.Plane` .. |Pupil| replace:: :class:`~lentil.Pupil` @@ -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') @@ -121,10 +117,10 @@ to the plane's :attr:`~lentil.Plane.tilt` attribute. The default behavior is to perform this operation on a copy of the plane, but it is possible to operate in-place by setting ``inplace=True``. -See :ref:`user_guide.diffraction.tilt` for additional information on when to use +See :ref:`user.diffraction.tilt` for additional information on when to use this method. -.. _user_guide.planes.pupil: +.. _user.planes.pupil: Pupil ===== @@ -217,7 +213,7 @@ operation. Details of this algorithm are available in the :ref:`technical-notes` field information. Because of this, |Detector| planes can only be used as the final plane in a Lentil model. -.. _user_guide.planes.tilt: +.. _user.planes.tilt: Tilt ==== @@ -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 = w.propagate_image(pixelscale=5e-6, npix=(64,64), oversample=2) - >>> plt.imshow(w.intensity, origin='lower') + >>> w = lentil.propagate_dft(w, pixelscale=5e-6, shape=(64,64), oversample=2) + >>> plt.imshow(w.intensity) It is simple to see the effect of introducing a tilted wavefront into the system: @@ -247,17 +241,20 @@ 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) >>> w = lentil.Wavefront(650e-9) >>> w *= pupil >>> w *= tilt - >>> w = w.propagate_image(pixelscale=5e-6, npix=(64,64), oversample=2) + >>> 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.rst b/docs/user/basics.rst new file mode 100644 index 0000000..86bbc6e --- /dev/null +++ b/docs/user/basics.rst @@ -0,0 +1,14 @@ +.. _user.basics: + +******************* +Lentil fundamentals +******************* + +.. toctree:: + :maxdepth: 1 + + basics.coordinates + basics.planes + basics.wavefront_error + basics.optical_systems + basics.diffraction \ No newline at end of file diff --git a/docs/user_guide/wavefront_error.rst b/docs/user/basics.wavefront_error.rst similarity index 85% rename from docs/user_guide/wavefront_error.rst rename to docs/user/basics.wavefront_error.rst index 289db0c..6d85c37 100644 --- a/docs/user_guide/wavefront_error.rst +++ b/docs/user/basics.wavefront_error.rst @@ -1,4 +1,4 @@ -.. _user_guide.wavefront_error: +.. _user.wavefront_error: **************************** Representing wavefront error @@ -17,7 +17,7 @@ Lentil to a Numpy array. For :class:`~lentil.Pupil` planes, the :attr:`~lentil.Pupil.phase` attribute represents the optical path difference (OPD) relative to the pupil's reference sphere. -.. _user_guide.wavefront_error.sign: +.. _user.wavefront_error.sign: Wavefront error sign convention =============================== @@ -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) @@ -202,7 +188,7 @@ Defining custom Zernike coordinates ----------------------------------- By default, all of Lentil's Zernike functions place the center of the coordinate system at the centroid of the supplied mask with its axes aligned with Lentil's -:ref:`user_guide.coordinate_system`. This works as expected for the vast majority of +:ref:`user.coordinate_system`. This works as expected for the vast majority of needs, but in some cases it may be desirable to manually define the coordinate system. This is accomplished by using :func:`~lentil.zernike_coordinates` to compute ``rho`` and ``theta``, and providing these definitions to the appropriate Zernike function. For @@ -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/user_guide/image_sensors.rst b/docs/user/image_sensors.rst similarity index 99% rename from docs/user_guide/image_sensors.rst rename to docs/user/image_sensors.rst index c1bc8d4..f3e241c 100644 --- a/docs/user_guide/image_sensors.rst +++ b/docs/user/image_sensors.rst @@ -1,4 +1,4 @@ -.. _user_guide.image_sensors: +.. _user.image_sensors: ************* Image sensors 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/index.rst b/docs/user/index.rst new file mode 100644 index 0000000..b4e7d6d --- /dev/null +++ b/docs/user/index.rst @@ -0,0 +1,42 @@ +.. _user: + +********** +User Guide +********** + +The User Guide provides documentation for all of Lentil's features and capabilities. + +.. toctree:: + :caption: Getting started + :maxdepth: 1 + + overview + install + quickstart + +.. toctree:: + :caption: Fundamentals + :maxdepth: 2 + + basics + +.. toctree:: + :maxdepth: 1 + + radiometry + image_sensors + matlab + +.. toctree:: + :caption: Advanced usage + :maxdepth: 1 + + performance + +.. toctree:: + :hidden: + :caption: Extras + + ../release + ../cite + ../license \ No newline at end of file diff --git a/docs/user/install.rst b/docs/user/install.rst new file mode 100644 index 0000000..cd8c2b9 --- /dev/null +++ b/docs/user/install.rst @@ -0,0 +1,15 @@ +.. _user.install: + +############ +Installation +############ + +The easiest way to install Lentil is with ``pip``: + +.. code-block:: bash + + pip install lentil + + +For instructions on setting up Lentil for development, see +:ref:`development.install`. \ No newline at end of file diff --git a/docs/user_guide/matlab.rst b/docs/user/matlab.rst similarity index 99% rename from docs/user_guide/matlab.rst rename to docs/user/matlab.rst index 04f6dea..a813bca 100644 --- a/docs/user_guide/matlab.rst +++ b/docs/user/matlab.rst @@ -1,3 +1,5 @@ +.. _user.matlab: + ************************ Using Lentil with MATLAB ************************ @@ -9,7 +11,7 @@ Calling Python libraries from MATLAB is as simple as configuring MATLAB to use t appropriate Python implementation and prepending the Python command with ``py.``. Before using Lentil in MATLAB, be sure that you you have :ref:`configured MATLAB to use the correct version of Python installed on your system -` +` .. _numpy-matlab: @@ -68,7 +70,7 @@ Finally, a few links that may be helpful when developing a MATLAB interface: -.. _configuring-matlab: +.. _user.matlab.config: Configuring MATLAB to use the correct version of Python ======================================================= diff --git a/docs/getting_started/overview.rst b/docs/user/overview.rst similarity index 59% rename from docs/getting_started/overview.rst rename to docs/user/overview.rst index 14d5888..ff63a75 100644 --- a/docs/getting_started/overview.rst +++ b/docs/user/overview.rst @@ -1,4 +1,4 @@ -.. _package-overview: +.. _user.overview: **************** Package overview @@ -53,21 +53,3 @@ Getting help ============ The best place to ask for help on subjects not covered in this documentation or suggest new features/ideas is by opening a ticket on `Github `__. - -License -======= -Copyright (c) 2021, California Institute of Technology ("Caltech"). U.S. Government sponsorship acknowledged. - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - - - - diff --git a/docs/user_guide/performance.rst b/docs/user/performance.rst similarity index 99% rename from docs/user_guide/performance.rst rename to docs/user/performance.rst index 42e0dcf..ae67e02 100644 --- a/docs/user_guide/performance.rst +++ b/docs/user/performance.rst @@ -1,3 +1,5 @@ +.. _user.performance: + ********************** Optimizing Performance ********************** 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/user/plots/npix_prop.py b/docs/user/plots/npix_prop.py new file mode 100644 index 0000000..69a062a --- /dev/null +++ b/docs/user/plots/npix_prop.py @@ -0,0 +1,28 @@ +import matplotlib.pyplot as plt +import lentil + +import matplotlib as mpl +mpl.rcParams['figure.figsize'] = (4.5, 4.5) + +amp = lentil.circle((256, 256), 120) +opd = lentil.zernike(amp, 4) * 1e-6 +pupil = lentil.Pupil(amplitude=amp, phase=opd, pixelscale=1 / 240, focal_length=10) + +w1 = lentil.Wavefront(wavelength=500e-9) +w1 *= pupil +w1 = lentil.propagate_dft(w1, pixelscale=5e-6, shape=128, prop_shape=128, oversample=5) + +w2 = lentil.Wavefront(wavelength=500e-9) +w2 *= pupil +w2 = lentil.propagate_dft(w2, pixelscale=5e-6, shape=128, prop_shape=40, oversample=5) + + +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 new file mode 100644 index 0000000..32a477d --- /dev/null +++ b/docs/user/quickstart.rst @@ -0,0 +1,138 @@ +.. _user.quickstart: + +.. |Plane| replace:: :class:`~lentil.Plane` +.. |Pupil| replace:: :class:`~lentil.Pupil` +.. |Image| replace:: :class:`~lentil.Image` +.. |Wavefront| replace:: :class:`~lentil.Wavefront` + +********** +Quickstart +********** +This is a short introduction to Lentil, mainly written for new users. More +detailed examples are available :ref:`here`. + +First, we import Lentil: + +.. code-block:: pycon + + >>> import lentil + +We'll also import `Matplotlib `_ to visualize results: + +.. code-block:: pycon + + >>> import matplotlib.pyplot as plt + +Creating planes +=============== +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: + +.. plot:: + :context: reset + :include-source: + :scale: 50 + + >>> amp = lentil.circle(shape=(256,256), radius=120) + >>> 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:: + :context: close-figs + :include-source: + + >>> pupil = lentil.Pupil(amplitude=amp, phase=opd, pixelscale=1/240, + ... focal_length=20) + +Note the diameter is implicitly defined via the +:attr:`~lentil.Pupil.pixelscale` attribute: + +.. image:: /_static/img/pixelscale.png + :width: 500px + :align: center + +.. 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. + +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 500 nm, again represented +in meters: + +.. plot:: + :context: + :include-source: + + >>> 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|: + +.. plot:: + :context: + :include-source: + + >>> 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 on a grid with spacing of 5e-6 meters and perform the propagation +2 times oversampled: + +.. plot:: + :context: + :include-source: + + >>> w2 = lentil.propagate_dft(w1, shape=(64,64), pixelscale=5e-6, oversample=2) + +The resulting intensity (point spread function) can now be observed: + +.. plot:: + :context: + :include-source: + :scale: 50 + + >>> plt.imshow(w2.intensity) + +Finally, we will rescale the oversampled image to native sampling and include the +blurring effects due to the pixel MTF: + +.. plot:: + :context: close-figs + :include-source: + :scale: 50 + + >>> img = lentil.detector.pixelate(w2.intensity, oversample=2) + >>> plt.imshow(img) + +.. Focal planes +.. ============ + + +.. Radiometry +.. ========== + + diff --git a/docs/user_guide/radiometry.rst b/docs/user/radiometry.rst similarity index 99% rename from docs/user_guide/radiometry.rst rename to docs/user/radiometry.rst index dcf42a1..f039e3f 100644 --- a/docs/user_guide/radiometry.rst +++ b/docs/user/radiometry.rst @@ -1,9 +1,11 @@ +.. _user.radiometry: + +.. currentmodule:: lentil + ************************ Computational radiometry ************************ - .. currentmodule:: lentil - Computational radiometry is used to model the propagation of radiant energy through an optical system. It uses geometry and known optical and imaging properties to compute the irradiance from an observed scene at a detector. Lentil's diff --git a/docs/user_guide/index.rst b/docs/user_guide/index.rst deleted file mode 100644 index 19ad775..0000000 --- a/docs/user_guide/index.rst +++ /dev/null @@ -1,36 +0,0 @@ -.. _user-guide: - -********** -User Guide -********** - -The User Guide provides documentation for all of Lentil's features and capabilities. It -is generally organized by topic area. - -Brand new users should start with the :ref:`package-overview` and :ref:`quickstart`. - -Detailed information on any specific class or method can be found in the :ref:`api`. - -.. note:: - Many of the topics covered in this user guide and the API reference require some - understanding of the underlying physics that govern the phsyical and optical - processes modeled by Lentil. - - This documentation does not attempt to be a guide to these topics but does provide - links and references to supporting materials where applicable. - - -.. toctree:: - :maxdepth: 2 - - coordinates - planes - wavefront_error - optical_systems - diffraction - radiometry - image_sensors - ../patterns/index - performance - matlab - verification/index 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.