diff --git a/doc/docs/Python_Tutorials/Cylindrical_Coordinates.md b/doc/docs/Python_Tutorials/Cylindrical_Coordinates.md
index ad0c02549..b6bb5aff9 100644
--- a/doc/docs/Python_Tutorials/Cylindrical_Coordinates.md
+++ b/doc/docs/Python_Tutorials/Cylindrical_Coordinates.md
@@ -468,7 +468,7 @@ Focusing Properties of a Binary-Phase Zone Plate
It is also possible to compute a [near-to-far field transformation](../Python_User_Interface.md#near-to-far-field-spectra) in cylindrical coordinates. This is demonstrated in this example for a binary-phase [zone plate](https://en.wikipedia.org/wiki/Zone_plate) which is a rotationally-symmetric diffractive lens used to focus a normally-incident planewave to a single spot.
-Using [scalar theory](http://zoneplate.lbl.gov/theory), the radius of the $n$th zone can be computed as:
+Using [scalar theory](https://en.wikipedia.org/wiki/Zone_plate#Design_and_manufacture), the radius of the $n$th zone can be computed as:
$$ r_n^2 = n\lambda (f+\frac{n\lambda}{4})$$
@@ -481,110 +481,215 @@ where $n$ is the zone index (1,2,3,...,$N$), $f$ is the focal length, and $\lamb
The simulation script is in [examples/zone_plate.py](https://github.com/NanoComp/meep/blob/master/python/examples/zone_plate.py). The notebook is [examples/zone_plate.ipynb](https://nbviewer.jupyter.org/github/NanoComp/meep/blob/master/python/examples/zone_plate.ipynb).
```py
-import meep as mp
-import numpy as np
import math
+
import matplotlib.pyplot as plt
+import meep as mp
+import numpy as np
-resolution = 25 # pixels/μm
-dpml = 1.0 # PML thickness
-dsub = 2.0 # substrate thickness
-dpad = 2.0 # padding betweeen zone plate and PML
-zh = 0.5 # zone-plate height
-zN = 25 # number of zones (odd zones: π phase shift, even zones: none)
-focal_length = 200 # focal length of zone plate
-spot_length = 100 # far-field line length
-ff_res = 10 # far-field resolution
+resolution_um = 25
-pml_layers = [mp.PML(thickness=dpml)]
+pml_um = 1.0
+substrate_um = 2.0
+padding_um = 2.0
+height_um = 0.5
+focal_length_um = 200
+scan_length_z_um = 100
+farfield_resolution_um = 10
-wvl_cen = 0.5
-frq_cen = 1/wvl_cen
-dfrq = 0.2*frq_cen
+pml_layers = [mp.PML(thickness=pml_um)]
-## radii of zones
-## ref: eq. 7 of http://zoneplate.lbl.gov/theory
-r = [math.sqrt(n*wvl_cen*(focal_length+n*wvl_cen/4)) for n in range(1,zN+1)]
+wavelength_um = 0.5
+frequency = 1 / wavelength_um
+frequench_width = 0.2 * frequency
-sr = r[-1]+dpad+dpml
-sz = dpml+dsub+zh+dpad+dpml
-cell_size = mp.Vector3(sr,0,sz)
+# The number of zones in the zone plate.
+# Odd-numbered zones impart a π phase shift and
+# even-numbered zones impart no phase shift.
+num_zones = 25
-sources = [mp.Source(mp.GaussianSource(frq_cen,fwidth=dfrq,is_integrated=True),
- component=mp.Er,
- center=mp.Vector3(0.5*sr,0,-0.5*sz+dpml),
- size=mp.Vector3(sr)),
- mp.Source(mp.GaussianSource(frq_cen,fwidth=dfrq,is_integrated=True),
- component=mp.Ep,
- center=mp.Vector3(0.5*sr,0,-0.5*sz+dpml),
- size=mp.Vector3(sr),
- amplitude=-1j)]
+# Specify the radius of each zone using the equation
+# from https://en.wikipedia.org/wiki/Zone_plate.
+zone_radius_um = np.zeros(num_zones)
+for n in range(1, num_zones + 1):
+ zone_radius_um[n-1] = math.sqrt(
+ n * wavelength_um *
+ (focal_length_um + n * wavelength_um / 4)
+ )
-glass = mp.Medium(index=1.5)
+size_r_um = zone_radius_um[-1] + padding_um + pml_um
+size_z_um = pml_um + substrate_um + height_um + padding_um + pml_um
+cell_size = mp.Vector3(size_r_um, 0, size_z_um)
+
+# Specify a (linearly polarized) planewave at normal incidence.
+sources = [
+ mp.Source(
+ mp.GaussianSource(
+ frequency,
+ fwidth=frequench_width,
+ is_integrated=True
+ ),
+ component=mp.Er,
+ center=mp.Vector3(0.5 * size_r_um, 0, -0.5 * size_z_um + pml_um),
+ size=mp.Vector3(size_r_um),
+ ),
+ mp.Source(
+ mp.GaussianSource(
+ frequency,
+ fwidth=frequench_width,
+ is_integrated=True
+ ),
+ component=mp.Ep,
+ center=mp.Vector3(0.5 * size_r_um, 0, -0.5 * size_z_um + pml_um),
+ size=mp.Vector3(size_r_um),
+ amplitude=-1j,
+ ),
+]
-geometry = [mp.Block(material=glass,
- size=mp.Vector3(sr,0,dpml+dsub),
- center=mp.Vector3(0.5*sr,0,-0.5*sz+0.5*(dpml+dsub)))]
+glass = mp.Medium(index=1.5)
-for n in range(zN-1,-1,-1):
- geometry.append(mp.Block(material=glass if n % 2 == 0 else mp.vacuum,
- size=mp.Vector3(r[n],0,zh),
- center=mp.Vector3(0.5*r[n],0,-0.5*sz+dpml+dsub+0.5*zh)))
+# Add the substrate.
+geometry = [
+ mp.Block(
+ material=glass,
+ size=mp.Vector3(size_r_um, 0, pml_um + substrate_um),
+ center=mp.Vector3(
+ 0.5 * size_r_um,
+ 0,
+ -0.5 * size_z_um + 0.5 * (pml_um + substrate_um)
+ ),
+ )
+]
-sim = mp.Simulation(cell_size=cell_size,
- boundary_layers=pml_layers,
- resolution=resolution,
- sources=sources,
- geometry=geometry,
- dimensions=mp.CYLINDRICAL,
- m=-1)
+# Add the zone plates starting with the ones with largest radius.
+for n in range(num_zones - 1, -1, -1):
+ geometry.append(
+ mp.Block(
+ material=glass if n % 2 == 0 else mp.vacuum,
+ size=mp.Vector3(zone_radius_um[n], 0, height_um),
+ center=mp.Vector3(
+ 0.5 * zone_radius_um[n],
+ 0,
+ -0.5 * size_z_um + pml_um + substrate_um + 0.5 * height_um
+ ),
+ )
+ )
-## near-field monitor
-n2f_obj = sim.add_near2far(frq_cen,
- 0,
- 1,
- mp.Near2FarRegion(center=mp.Vector3(0.5*(sr-dpml),0,0.5*sz-dpml),
- size=mp.Vector3(sr-dpml)),
- mp.Near2FarRegion(center=mp.Vector3(sr-dpml,0,0.5*sz-dpml-0.5*(dsub+zh+dpad)),
- size=mp.Vector3(z=dsub+zh+dpad)))
+sim = mp.Simulation(
+ cell_size=cell_size,
+ boundary_layers=pml_layers,
+ resolution=resolution_um,
+ sources=sources,
+ geometry=geometry,
+ dimensions=mp.CYLINDRICAL,
+ m=-1,
+)
+
+# Add the near-field monitor (must be entirely in air).
+n2f_monitor = sim.add_near2far(
+ frequency,
+ 0,
+ 1,
+ mp.Near2FarRegion(
+ center=mp.Vector3(
+ 0.5 * (size_r_um - pml_um),
+ 0,
+ 0.5 * size_z_um - pml_um
+ ),
+ size=mp.Vector3(size_r_um - pml_um, 0, 0),
+ ),
+ mp.Near2FarRegion(
+ center=mp.Vector3(
+ size_r_um - pml_um,
+ 0,
+ 0.5 * size_z_um - pml_um - 0.5 * (height_um + padding_um)
+ ),
+ size=mp.Vector3(0, 0, height_um + padding_um),
+ ),
+)
-sim.plot2D()
+fig, ax = plt.subplots()
+sim.plot2D(ax=ax)
if mp.am_master():
- plt.savefig("zone_plate_epsilon.png",bbox_inches='tight',dpi=150)
-
-sim.run(until_after_sources=100)
-
-ff_r = sim.get_farfields(n2f_obj,
- ff_res,
- center=mp.Vector3(0.5*(sr-dpml),0,-0.5*sz+dpml+dsub+zh+focal_length),
- size=mp.Vector3(sr-dpml))
-
-ff_z = sim.get_farfields(n2f_obj,
- ff_res,
- center=mp.Vector3(z=-0.5*sz+dpml+dsub+zh+focal_length),
- size=mp.Vector3(z=spot_length))
+ fig.savefig("zone_plate_layout.png", bbox_inches="tight", dpi=150)
+
+# Timestep the fields until they have sufficiently decayed away.
+sim.run(
+ until_after_sources=mp.stop_when_fields_decayed(
+ 50.0,
+ mp.Er,
+ mp.Vector3(0.5 * size_r_um, 0, 0),
+ 1e-6
+ )
+)
-E2_r = np.absolute(ff_r['Ex'])**2+np.absolute(ff_r['Ey'])**2+np.absolute(ff_r['Ez'])**2
-E2_z = np.absolute(ff_z['Ex'])**2+np.absolute(ff_z['Ey'])**2+np.absolute(ff_z['Ez'])**2
+farfields_r = sim.get_farfields(
+ n2f_monitor,
+ farfield_resolution_um,
+ center=mp.Vector3(
+ 0.5 * (size_r_um - pml_um),
+ 0,
+ -0.5 * size_z_um + pml_um + substrate_um + height_um + focal_length_um
+ ),
+ size=mp.Vector3(size_r_um - pml_um, 0, 0),
+)
+
+farfields_z = sim.get_farfields(
+ n2f_monitor,
+ farfield_resolution_um,
+ center=mp.Vector3(
+ 0,
+ 0,
+ -0.5 * size_z_um + pml_um + substrate_um + height_um + focal_length_um
+ ),
+ size=mp.Vector3(0, 0, scan_length_z_um),
+)
+
+intensity_r = (
+ np.absolute(farfields_r["Ex"]) ** 2
+ + np.absolute(farfields_r["Ey"]) ** 2
+ + np.absolute(farfields_r["Ez"]) ** 2
+)
+intensity_z = (
+ np.absolute(farfields_z["Ex"]) ** 2
+ + np.absolute(farfields_z["Ey"]) ** 2
+ + np.absolute(farfields_z["Ez"]) ** 2
+)
+
+# Plot the intensity data and save the result to disk.
+fig, ax = plt.subplots(ncols=2)
+
+ax[0].semilogy(
+ np.linspace(0, size_r_um - pml_um, intensity_r.size),
+ intensity_r,
+ "bo-"
+)
+ax[0].set_xlim(-2, 20)
+ax[0].set_xticks(np.arange(0, 25, 5))
+ax[0].grid(True, axis="y", which="both", ls="-")
+ax[0].set_xlabel(r"$r$ coordinate (μm)")
+ax[0].set_ylabel(r"energy density of far fields, |E|$^2$")
+
+ax[1].semilogy(
+ np.linspace(
+ focal_length_um - 0.5 * scan_length_z_um,
+ focal_length_um + 0.5 * scan_length_z_um,
+ intensity_z.size,
+ ),
+ intensity_z,
+ "bo-",
+)
+ax[1].grid(True, axis="y", which="both", ls="-")
+ax[1].set_xlabel(r"$z$ coordinate (μm)")
+ax[1].set_ylabel(r"energy density of far fields, |E|$^2$")
+
+fig.suptitle(
+ f"binary-phase zone plate with focal length $z$ = {focal_length_um} μm"
+)
if mp.am_master():
- plt.figure(dpi=200)
- plt.subplot(1,2,1)
- plt.semilogy(np.linspace(0,sr-dpml,len(E2_r)),E2_r,'bo-')
- plt.xlim(-2,20)
- plt.xticks([t for t in np.arange(0,25,5)])
- plt.grid(True,axis="y",which="both",ls="-")
- plt.xlabel(r'$r$ coordinate (μm)')
- plt.ylabel(r'energy density of far fields, |E|$^2$')
- plt.subplot(1,2,2)
- plt.semilogy(np.linspace(focal_length-0.5*spot_length,focal_length+0.5*spot_length,len(E2_z)),E2_z,'bo-')
- plt.grid(True,axis="y",which="both",ls="-")
- plt.xlabel(r'$z$ coordinate (μm)')
- plt.ylabel(r'energy density of far fields, |E|$^2$')
- plt.suptitle(r"Binary-Phase Zone Plate with Focal Length $z$ = {} μm".format(focal_length))
- plt.tight_layout()
- plt.savefig("zone_plate_farfields.png")
+ fig.savefig("zone_plate_farfields.png", dpi=200, bbox_inches="tight")
```
Note that the volume specified in `get_farfields` via `center` and `size` is in cylindrical coordinates. These points must therefore lie in the $\phi = 0$ ($rz = xz$) plane. The fields $E$ and $H$ returned by `get_farfields` can be thought of as either cylindrical ($r$,$\phi$,$z$) or Cartesian ($x$,$y$,$z$) coordinates since these are the same in the $\phi = 0$ plane (i.e., $E_r=E_x$ and $E_\phi=E_y$). Also, `get_farfields` tends to gradually *slow down* as the far-field point gets closer to the near-field monitor. This performance degradation is unavoidable and is due to the larger number of $\phi$ integration points required for accurate convergence of the integral involving the Green's function which diverges as the evaluation point approaches the source point.
diff --git a/python/examples/zone_plate.py b/python/examples/zone_plate.py
index 17db0e0b9..97daa0c34 100644
--- a/python/examples/zone_plate.py
+++ b/python/examples/zone_plate.py
@@ -1,153 +1,185 @@
+"""Computes the diffraction spectra of a zone plate in cylindrical coords."""
+
import math
import matplotlib.pyplot as plt
+import meep as mp
import numpy as np
-import meep as mp
-resolution = 25 # pixels/μm
+resolution_um = 25
-dpml = 1.0 # PML thickness
-dsub = 2.0 # substrate thickness
-dpad = 2.0 # padding betweeen zone plate and PML
-zh = 0.5 # zone-plate height
-zN = 25 # number of zones (odd zones: π phase shift, even zones: none)
-focal_length = 200 # focal length of zone plate
-spot_length = 100 # far-field line length
-ff_res = 10 # far-field resolution
+pml_um = 1.0
+substrate_um = 2.0
+padding_um = 2.0
+height_um = 0.5
+focal_length_um = 200
+scan_length_z_um = 100
+farfield_resolution_um = 10
-pml_layers = [mp.PML(thickness=dpml)]
+pml_layers = [mp.PML(thickness=pml_um)]
-wvl_cen = 0.5
-frq_cen = 1 / wvl_cen
-dfrq = 0.2 * frq_cen
+wavelength_um = 0.5
+frequency = 1 / wavelength_um
+frequench_width = 0.2 * frequency
-## radii of zones
-## ref: eq. 7 of http://zoneplate.lbl.gov/theory
-r = [
- math.sqrt(n * wvl_cen * (focal_length + n * wvl_cen / 4)) for n in range(1, zN + 1)
-]
+# The number of zones in the zone plate.
+# Odd-numbered zones impart a π phase shift and
+# even-numbered zones impart no phase shift.
+num_zones = 25
+
+# Specify the radius of each zone using the equation
+# from https://en.wikipedia.org/wiki/Zone_plate.
+zone_radius_um = np.zeros(num_zones)
+for n in range(1, num_zones + 1):
+ zone_radius_um[n - 1] = math.sqrt(
+ n * wavelength_um * (focal_length_um + n * wavelength_um / 4)
+ )
-sr = r[-1] + dpad + dpml
-sz = dpml + dsub + zh + dpad + dpml
-cell_size = mp.Vector3(sr, 0, sz)
+size_r_um = zone_radius_um[-1] + padding_um + pml_um
+size_z_um = pml_um + substrate_um + height_um + padding_um + pml_um
+cell_size = mp.Vector3(size_r_um, 0, size_z_um)
+# Specify a (linearly polarized) planewave at normal incidence.
sources = [
mp.Source(
- mp.GaussianSource(frq_cen, fwidth=dfrq, is_integrated=True),
+ mp.GaussianSource(frequency, fwidth=frequench_width, is_integrated=True),
component=mp.Er,
- center=mp.Vector3(0.5 * sr, 0, -0.5 * sz + dpml),
- size=mp.Vector3(sr),
+ center=mp.Vector3(0.5 * size_r_um, 0, -0.5 * size_z_um + pml_um),
+ size=mp.Vector3(size_r_um),
),
mp.Source(
- mp.GaussianSource(frq_cen, fwidth=dfrq, is_integrated=True),
+ mp.GaussianSource(frequency, fwidth=frequench_width, is_integrated=True),
component=mp.Ep,
- center=mp.Vector3(0.5 * sr, 0, -0.5 * sz + dpml),
- size=mp.Vector3(sr),
+ center=mp.Vector3(0.5 * size_r_um, 0, -0.5 * size_z_um + pml_um),
+ size=mp.Vector3(size_r_um),
amplitude=-1j,
),
]
glass = mp.Medium(index=1.5)
+# Add the substrate.
geometry = [
mp.Block(
material=glass,
- size=mp.Vector3(sr, 0, dpml + dsub),
- center=mp.Vector3(0.5 * sr, 0, -0.5 * sz + 0.5 * (dpml + dsub)),
+ size=mp.Vector3(size_r_um, 0, pml_um + substrate_um),
+ center=mp.Vector3(
+ 0.5 * size_r_um, 0, -0.5 * size_z_um + 0.5 * (pml_um + substrate_um)
+ ),
)
]
-geometry.extend(
- mp.Block(
- material=glass if n % 2 == 0 else mp.vacuum,
- size=mp.Vector3(r[n], 0, zh),
- center=mp.Vector3(0.5 * r[n], 0, -0.5 * sz + dpml + dsub + 0.5 * zh),
+# Add the zone plates starting with the ones with largest radius.
+for n in range(num_zones - 1, -1, -1):
+ geometry.append(
+ mp.Block(
+ material=glass if n % 2 == 0 else mp.vacuum,
+ size=mp.Vector3(zone_radius_um[n], 0, height_um),
+ center=mp.Vector3(
+ 0.5 * zone_radius_um[n],
+ 0,
+ -0.5 * size_z_um + pml_um + substrate_um + 0.5 * height_um,
+ ),
+ )
)
- for n in range(zN - 1, -1, -1)
-)
sim = mp.Simulation(
cell_size=cell_size,
boundary_layers=pml_layers,
- resolution=resolution,
+ resolution=resolution_um,
sources=sources,
geometry=geometry,
dimensions=mp.CYLINDRICAL,
m=-1,
)
-## near-field monitor
-n2f_obj = sim.add_near2far(
- frq_cen,
+# Add the near-field monitor (must be entirely in air).
+n2f_monitor = sim.add_near2far(
+ frequency,
0,
1,
mp.Near2FarRegion(
- center=mp.Vector3(0.5 * (sr - dpml), 0, 0.5 * sz - dpml),
- size=mp.Vector3(sr - dpml),
+ center=mp.Vector3(0.5 * (size_r_um - pml_um), 0, 0.5 * size_z_um - pml_um),
+ size=mp.Vector3(size_r_um - pml_um, 0, 0),
),
mp.Near2FarRegion(
- center=mp.Vector3(sr - dpml, 0, 0.5 * sz - dpml - 0.5 * (dsub + zh + dpad)),
- size=mp.Vector3(z=dsub + zh + dpad),
+ center=mp.Vector3(
+ size_r_um - pml_um,
+ 0,
+ 0.5 * size_z_um - pml_um - 0.5 * (height_um + padding_um),
+ ),
+ size=mp.Vector3(0, 0, height_um + padding_um),
),
)
-sim.plot2D()
+fig, ax = plt.subplots()
+sim.plot2D(ax=ax)
if mp.am_master():
- plt.savefig("zone_plate_epsilon.png", bbox_inches="tight", dpi=150)
+ fig.savefig("zone_plate_layout.png", bbox_inches="tight", dpi=150)
-sim.run(until_after_sources=100)
+# Timestep the fields until they have sufficiently decayed away.
+sim.run(
+ until_after_sources=mp.stop_when_fields_decayed(
+ 50.0, mp.Er, mp.Vector3(0.5 * size_r_um, 0, 0), 1e-6
+ )
+)
-ff_r = sim.get_farfields(
- n2f_obj,
- ff_res,
+farfields_r = sim.get_farfields(
+ n2f_monitor,
+ farfield_resolution_um,
center=mp.Vector3(
- 0.5 * (sr - dpml), 0, -0.5 * sz + dpml + dsub + zh + focal_length
+ 0.5 * (size_r_um - pml_um),
+ 0,
+ -0.5 * size_z_um + pml_um + substrate_um + height_um + focal_length_um,
),
- size=mp.Vector3(sr - dpml),
+ size=mp.Vector3(size_r_um - pml_um, 0, 0),
)
-ff_z = sim.get_farfields(
- n2f_obj,
- ff_res,
- center=mp.Vector3(z=-0.5 * sz + dpml + dsub + zh + focal_length),
- size=mp.Vector3(z=spot_length),
+farfields_z = sim.get_farfields(
+ n2f_monitor,
+ farfield_resolution_um,
+ center=mp.Vector3(
+ 0, 0, -0.5 * size_z_um + pml_um + substrate_um + height_um + focal_length_um
+ ),
+ size=mp.Vector3(0, 0, scan_length_z_um),
)
-E2_r = (
- np.absolute(ff_r["Ex"]) ** 2
- + np.absolute(ff_r["Ey"]) ** 2
- + np.absolute(ff_r["Ez"]) ** 2
+intensity_r = (
+ np.absolute(farfields_r["Ex"]) ** 2
+ + np.absolute(farfields_r["Ey"]) ** 2
+ + np.absolute(farfields_r["Ez"]) ** 2
)
-E2_z = (
- np.absolute(ff_z["Ex"]) ** 2
- + np.absolute(ff_z["Ey"]) ** 2
- + np.absolute(ff_z["Ez"]) ** 2
+intensity_z = (
+ np.absolute(farfields_z["Ex"]) ** 2
+ + np.absolute(farfields_z["Ey"]) ** 2
+ + np.absolute(farfields_z["Ez"]) ** 2
)
-if mp.am_master():
- plt.figure(dpi=200)
- plt.subplot(1, 2, 1)
- plt.semilogy(np.linspace(0, sr - dpml, len(E2_r)), E2_r, "bo-")
- plt.xlim(-2, 20)
- plt.xticks(list(np.arange(0, 25, 5)))
- plt.grid(True, axis="y", which="both", ls="-")
- plt.xlabel(r"$r$ coordinate (μm)")
- plt.ylabel(r"energy density of far fields, |E|$^2$")
- plt.subplot(1, 2, 2)
- plt.semilogy(
- np.linspace(
- focal_length - 0.5 * spot_length,
- focal_length + 0.5 * spot_length,
- len(E2_z),
- ),
- E2_z,
- "bo-",
- )
- plt.grid(True, axis="y", which="both", ls="-")
- plt.xlabel(r"$z$ coordinate (μm)")
- plt.ylabel(r"energy density of far fields, |E|$^2$")
- plt.suptitle(f"binary-phase zone plate with focal length $z$ = {focal_length} μm")
+# Plot the intensity data and save the result to disk.
+fig, ax = plt.subplots(ncols=2)
+
+ax[0].semilogy(np.linspace(0, size_r_um - pml_um, intensity_r.size), intensity_r, "bo-")
+ax[0].set_xlim(-2, 20)
+ax[0].set_xticks(np.arange(0, 25, 5))
+ax[0].grid(True, axis="y", which="both", ls="-")
+ax[0].set_xlabel(r"$r$ coordinate (μm)")
+ax[0].set_ylabel(r"energy density of far fields, |E|$^2$")
+
+ax[1].semilogy(
+ np.linspace(
+ focal_length_um - 0.5 * scan_length_z_um,
+ focal_length_um + 0.5 * scan_length_z_um,
+ intensity_z.size,
+ ),
+ intensity_z,
+ "bo-",
+)
+ax[1].grid(True, axis="y", which="both", ls="-")
+ax[1].set_xlabel(r"$z$ coordinate (μm)")
+ax[1].set_ylabel(r"energy density of far fields, |E|$^2$")
- plt.tight_layout()
- plt.savefig("zone_plate_farfields.png")
+fig.suptitle(f"binary-phase zone plate with focal length $z$ = {focal_length_um} μm")
+
+if mp.am_master():
+ fig.savefig("zone_plate_farfields.png", dpi=200, bbox_inches="tight")