Skip to content

Commit

Permalink
More shapes and their documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
andykee committed Jan 16, 2024
1 parent aac2efb commit bd1b0af
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 20 deletions.
1 change: 1 addition & 0 deletions docs/ref/util.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Shapes
lentil.circle
lentil.hexagon
lentil.rectangle
lentil.spider

Array manipulation
------------------
Expand Down
135 changes: 125 additions & 10 deletions docs/user/fundamentals/apertures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,157 @@ Apertures and masks
Lentil expects apertures and masks to be described in a discretely sampled
array-like format - most commonly in the form of a NumPy array. Portions of
the array with values greater than one are assued to be transmissive while
portions of the array that are zero are assumed to be blocking.
portions of the array that are zero are assumed to be opaque.

Aperture or mask shapes can be loaded from a file, created manually, or
constructed using one or more of Lentil's functions that draw common shapes
in an array.
constructed using one or more of Lentil's functions for drawing common
shapes in an array.

Core shapes
===========
Lentil provides a number of functions for drawing basic shapes in arrays.
Multiple arrays can be combined to create more complicated shapes.

The core shape functions support shifting the center of the shape arbitrarily
relative to the center of the array. The shift is defined in terms of
(row, col).

.. plot:: user/fundamentals/plots/shape_shift.py
.. plot:: user/fundamentals/plots/apertures.py
:scale: 50


Basic antialiasing is also supported (and is enabled by default):
Basic antialiasing is supported (and is enabled by default):

.. plot:: user/fundamentals/plots/shape_antialias.py
:scale: 50

The core shape functions also support shifting the center of the shape
arbitrarily relative to the center of the array. The shift is defined
in terms of (row, col).

.. plot:: user/fundamentals/plots/shape_shift.py
:scale: 50


Circle
------
Draw a circle with :func:`~lentil.circle`:

.. plot::
:include-source:
:scale: 50

>>> circ = lentil.circle(shape=(256,256), radius=100)
>>> plt.imshow(circ, cmap='gray')

Rectangle
---------
Draw a rectangle with :func:`~lentil.rectangle`:

.. plot::
:include-source:
:scale: 50

>>> rect = lentil.rectangle(shape=(256,256), width=200, height=100)
>>> plt.imshow(rect, cmap='gray')

Hexagon
-------
Draw a hexagon with :func:`~lentil.hexagon`:

.. plot::
:include-source:
:scale: 50

>>> hex = lentil.hexagon(shape=(256,256), radius=75)
>>> plt.imshow(hex, cmap='gray')

Spider
------
Draw a spider with :func:`~lentil.spider`:

.. plot::
:include-source:
:scale: 50

>>> spider = lentil.spider(shape=(256,256), width=3, angle=30)
>>> plt.imshow(spider, cmap='gray')


Composite apertures
===================
Because the core shape functions simply return NumPy arrays, it is possible
to create more complicated shapes by combining multiple arrays together. Below
is an example of how to draw the Hubble mask:

.. plot::
:include-source:
:scale: 50

# dimensions from Tiny Tim (Krist & Hook 2011)
outer_diam = 2.4
central_obsc = .33
spider = 0.0264
mount_diam = 0.13
mount_dist = 0.8921

shape = (256, 256)
pixelscale = 0.01

npix_outer = outer_diam/pixelscale
npix_inner = (outer_diam * central_obsc)/pixelscale
npix_spider = spider/pixelscale
npix_mount = mount_diam/pixelscale
npix_mount_dist = npix_outer * mount_dist / 2

# primary mirror
hubble_outer = lentil.circle(shape, radius=npix_outer/2)
hubble_inner = lentil.circle(shape, radius=npix_inner/2)
hubble = hubble_outer - hubble_inner

# secondary spiders
for angle in (45, 135, 225, 315):
hubble *= lentil.spider(shape, width=npix_spider, angle=angle)
# primary mirror mounting pads
for angle in (75, 195, 315):
mount_shift = (npix_mount_dist * -np.sin(np.deg2rad(angle)),
npix_mount_dist * np.cos(np.deg2rad(angle)))
hubble *= 1 - lentil.circle(shape, npix_mount/2, shift=mount_shift)
plt.imshow(hubble, cmap='gray')


Hex segmented apertures
=======================
The :func:`~lentil.hex_segments` function constructs an aperture made up of
a number of concentric rings of hexagonal segments. An example showing how
to construct the James Webb Space Telescope aperture is below:

.. plot::
:include-source:
:scale: 50

# dimensions from WebbPSF (STScI)
segment_diam = 1.524
segment_gap = 0.0075
spider = 0.083

# pixelscale is selected to provide at least 2 samples across
# the smallest feature (segment gap)
pixelscale = 0.003

npix_seg = segment_diam/pixelscale
npix_gap = segment_gap/pixelscale
npix_spider = spider/pixelscale

jwst = lentil.hex_segments(rings=2, seg_radius=npix_seg/2,
seg_gap=npix_gap, flatten=True)

# secondary spiders
for angle in (90, 240, 300):
jwst *= lentil.spider(jwst.shape, width=npix_spider, angle=angle)
jwst *= 1-lentil.rectangle(jwst.shape, width=36, height=60, shift=(-750,0))
jwst *= 1-lentil.rectangle(jwst.shape, width=36, height=20, shift=(-825,0))
plt.imshow(jwst, cmap='gray')





21 changes: 21 additions & 0 deletions docs/user/fundamentals/plots/apertures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import lentil
import matplotlib.pyplot as plt

fig, (ax1,ax2,ax3, ax4) = plt.subplots(nrows=1, ncols=4, figsize=(5, 3))

ax1.imshow(lentil.circle((256,256), 100), cmap='gray')
ax1.axis('off')

ax2.imshow(lentil.rectangle((256,256), 100, 150), cmap='gray')
ax2.axis('off')

ax3.imshow(lentil.hexagon((256,256), 110), cmap='gray')
ax3.axis('off')

c4 = lentil.circle((256,256), 100) - lentil.circle((256,256), 25)
c4 *= lentil.spider((256,256), 4)
c4 *= lentil.spider((256,256), 4, angle=90)
c4 *= lentil.spider((256,256), 4, angle=180)
c4 *= lentil.spider((256,256), 4, angle=270)
ax4.imshow(c4, cmap='gray')
ax4.axis('off')
3 changes: 2 additions & 1 deletion lentil/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
from lentil.shape import (
circle,
hexagon,
rectangle
rectangle,
spider
)

from lentil import util
Expand Down
15 changes: 8 additions & 7 deletions lentil/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@

import lentil

def mesh(shape, shift=(0, 0)):
def mesh(shape, shift=(0, 0), angle=0):
"""Generate a standard mesh."""

nr = shape[0]
nc = shape[1]

r = np.arange(nr) - np.floor(nr/2.0) - shift[0]
c = np.arange(nc) - np.floor(nc/2.0) - shift[1]

return np.meshgrid(r, c, indexing='ij')
rr, cc = np.meshgrid(np.arange(nr) - np.floor(nr/2.0) - shift[0],
np.arange(nc) - np.floor(nc/2.0) - shift[1],
indexing='ij')
angle = np.deg2rad(angle)
r = rr * np.cos(angle) + cc * np.sin(angle)
c = rr * -np.sin(angle) + cc * np.cos(angle)
return r, c


def gaussian2d(size, sigma):
Expand Down
38 changes: 36 additions & 2 deletions lentil/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def hexagon(shape, radius, shift=(0, 0), rotate=False, antialias=True):
return mask


def rectangle(shape, width, height, shift=(0,0), antialias=True):
def rectangle(shape, width, height, shift=(0,0), angle=0, antialias=True):
"""Draw a rectangle
Parameters
Expand All @@ -88,6 +88,9 @@ def rectangle(shape, width, height, shift=(0,0), antialias=True):
Height of rectangle in pixels
shift : tuple of floats, optional
How far to shift center in (rows, cols). Default is (0, 0).
angle : float, optional
Rotation of rectangle in degrees counterclockwise from horizontal.
Default is 0.
antialias : bool, optional
If True (default), the shape edges are antialiased.
Expand All @@ -97,7 +100,7 @@ def rectangle(shape, width, height, shift=(0,0), antialias=True):
"""
shape = np.broadcast_to(shape, (2,))
rr, cc = lentil.helper.mesh(shape, shift)
rr, cc = lentil.helper.mesh(shape, shift, angle)
rect = np.ones(shape)

width_clip = np.clip(0.5 + (width/2) - np.abs(cc), 0, 1)
Expand All @@ -109,3 +112,34 @@ def rectangle(shape, width, height, shift=(0,0), antialias=True):
rect[rect > 0] = 1

return rect


def spider(shape, width, angle=0, shift=(0,0), antialias=True):
"""Draw a spider
Parameters
----------
shape : array_like
Size of output in pixels (nrows, ncols)
width : float
Width of rectangle in pixels
angle : float, optional
Rotation of spider in degrees counterclockwise from horizontal.
Default is 0.
shift : tuple of floats, optional
How far to shift center in (rows, cols). Default is (0, 0).
antialias : bool, optional
If True (default), the spider edges are antialiased.
Returns
-------
ndarray
"""
shape = np.broadcast_to(shape, (2,))
len = np.sqrt(2) * np.max(shape)/2 # max length when angle is a multiple of 45 deg
shift_dist = len / 2
shift_row = -shift_dist * np.sin(np.deg2rad(angle))
shift_col = shift_dist * np.cos(np.deg2rad(angle))
shift = (shift[0] + shift_row, shift[1] + shift_col)
return 1 - lentil.rectangle(shape, len, width, shift, angle, antialias)

0 comments on commit bd1b0af

Please sign in to comment.