Near to far field transformation
Near to far field transformation#
To run this notebook from your browser, click this link.
This tutorial will show you how to solve for electromagnetic fields far away from your structure using field information stored on a nearby surface.
This technique is called a ‘near field to far field transformation’ and is very useful for reducing the simulation size needed for structures involving lots of empty space.
As an example, we will simulate a simple zone plate lens with a very thin domain size to get the transmitted fields measured just above the structure. Then, we’ll show how to use a near-field to far-field transformation to extrapolate to the fields at the focal plane above the lens.
# standard python imports import numpy as np import matplotlib.pyplot as plt # tidy3d imports import tidy3d as td import tidy3d.web as web
Below is a rough sketch of the setup of a near field to far field transformation.
The transmitted near fields are measured just above the metalens on the blue line, and the near field to far field transformation is then used to project the fields to the focal plane above at the red line.
Define Simulation Parameters#
As always, we first need to define our simulation parameters. As a reminder, all length units in
tidy3D are specified in microns.
# 1 nanometer in units of microns (for conversion) nm = 1e-3 # free space central wavelength wavelength = 1.0 # numerical aperture NA = 0.8 # height of lens features height_lens = 200 * nm # space between bottom PML and substrate (-z) # and the space between lens structure and top pml (+z) space_below_sub = 1.5 * wavelength # height of substrate (um) height_sub = wavelength / 2 # side length (xy plane) of entire metalens (um) length_xy = 20 * wavelength # Lens and substrate refractive index n_TiO2 = 2.40 n_SiO2 = 1.46 # define material properties air = td.Medium(permittivity=1.0) SiO2 = td.Medium(permittivity=n_SiO2**2) TiO2 = td.Medium(permittivity=n_TiO2**2)
Next we perform some conversions based on these parameters to define the simulation.
# because the wavelength is in microns, use builtin td.C_0 (um/s) to get frequency in Hz f0 = td.C_0 / wavelength # Define PML layers, for this application we surround the whole structure in PML to isolate the fields boundary_spec = td.BoundarySpec.all_sides(boundary=td.PML()) # domain size in z, note, we're just simulating a thin slice: (space -> substrate -> lens height -> space) length_z = space_below_sub + height_sub + height_lens + space_below_sub # construct simulation size array sim_size = (length_xy, length_xy, length_z)
Now we create the ring metalens programatically
# define substrate substrate = td.Structure( geometry=td.Box( center=[0, 0, -length_z/2 + space_below_sub + height_sub / 2.0], size=[td.inf, td.inf, height_sub] ), medium=SiO2 ) # focal length focal_length = length_xy / 2 / NA * np.sqrt(1 - NA**2) # location from center for edge of the n-th inner ring, see https://en.wikipedia.org/wiki/Zone_plate def edge(n): return np.sqrt(n * wavelength * focal_length + n**2 * wavelength**2 / 4) # loop through the ring indeces until it's too big and add each to geometry list n = 1 r = edge(n) rings =  while r < 2 * length_xy: # progressively wider cylinders, material alternating between air and TiO2 cylinder = td.Structure( geometry=td.Cylinder( center=[0,0,-length_z/2 + space_below_sub + height_sub + height_lens / 2], axis=2, radius=r, length=height_lens), medium=TiO2 if n % 2 == 0 else air, ) rings.append(cylinder) n += 1 r = edge(n) # reverse geometry list so that inner, smaller rings are added last and therefore override larger rings. rings.reverse() geometry = [substrate] + rings
Create a plane wave incident from below the metalens
# Bandwidth in Hz fwidth = f0 / 10.0 # Gaussian source offset; the source peak is at time t = offset/fwidth offset = 4. # time dependence of source gaussian = td.GaussianPulse(freq0=f0, fwidth=fwidth) source = td.PlaneWave( center=(0,0,-length_z/2 + space_below_sub / 2), size=(td.inf, td.inf, 0), source_time=gaussian, direction='+', pol_angle=0.0) # Simulation run time run_time = 40 / fwidth
Create a near-to-far field monitor to measure the fields just above the metalens and project them to a Cartesian plane in the far field. We’ll also make a dedicated near-field monitor just to see what the near fields look like.
# place the monitors halfway between top of lens and PML pos_monitor_z = -length_z/2 + space_below_sub + height_sub + height_lens + space_below_sub / 2 # set the points on the observation grid at which fields should be projected num_far = 40 xs_far = 4 * wavelength * np.linspace(-0.5, 0.5, num_far) ys_far = 4 * wavelength * np.linspace(-0.5, 0.5, num_far) monitor_far = td.Near2FarCartesianMonitor( center=[0., 0., pos_monitor_z], # center of the near field surface on which fields are recorded size=[td.inf, td.inf, 0], # size of the near field surface on which fields are recorded normal_dir='+', # normal vector direction of the near field surface on which fields are recorded freqs=[f0], name='farfield', x=xs_far, y=ys_far, plane_axis=2, # normal direction to the observation plane plane_distance=focal_length # signed distance along the normal axis at which the observations grid resides ) monitor_near = td.FieldMonitor( center=[0., 0., pos_monitor_z], size=[td.inf, td.inf, 0], freqs=[f0], name='nearfield' )
Put everything together and define a simulation object. A nonuniform simulation grid is generated automatically based on a given number of cells per wavelength in each material (10 by default), using the frequencies defined in the sources.
simulation = td.Simulation( size=sim_size, grid_spec = td.GridSpec.auto(min_steps_per_wvl=20), structures=geometry, sources=[source], monitors=[monitor_far, monitor_near], run_time=run_time, boundary_spec=boundary_spec )
Let’s take a look and make sure everything is defined properly
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 8)) simulation.plot_eps(x=0, ax=ax1); simulation.plot_eps(z=-length_z/2 + space_below_sub + height_sub + height_lens / 2, ax=ax2);
<Figure size 1440x576 with 4 Axes>
Now we can run the simulation and download the results
import tidy3d.web as web sim_data = web.run(simulation, task_name='near2far', path='data/simulation.hdf5')
near_field_data = sim_data['nearfield'] fig, (ax1, ax2, ax3) = plt.subplots(1, 3, tight_layout=True, figsize=(15, 3.5)) near_field_data.Ex.real.plot(ax=ax1) near_field_data.Ey.real.plot(ax=ax2) near_field_data.Ez.real.plot(ax=ax3) plt.show()
<Figure size 1080x252 with 6 Axes>
Getting Far Field Data#
The Near2FarCartesianMonitor object object ensures that the far field radiation vectors are already computed on the server during the simulation run.
The radiation vectors are building blocks that can be combined in various ways to quickly return various far field quantities such as fields, power, and radar cross section.
For this example, we use
Near2FarCartesianData.fields() to get the fields at the previously-set
far_fields = sim_data[monitor_far.name].fields()
Now we can plot the near and far fields together
# plot everything f, (axes_near, axes_far) = plt.subplots(2, 3, tight_layout=True, figsize=(10, 5)) def pmesh(xs, ys, array, ax, cmap): im = ax.pcolormesh(xs, ys, array.T, cmap=cmap, shading='auto') return im ax1, ax2, ax3 = axes_near im = near_field_data.Ex.real.plot(ax=ax1) im = near_field_data.Ey.real.plot(ax=ax2) im = near_field_data.Ez.real.plot(ax=ax3) ax1, ax2, ax3 = axes_far im = far_fields['Ex'].real.plot(ax=ax1) im = far_fields['Ey'].real.plot(ax=ax2) im = far_fields['Ez'].real.plot(ax=ax3) plt.show()
<Figure size 720x360 with 12 Axes>
We can also use the far field data and plot the field intensity to see the focusing effect.
intensity_far = np.squeeze( np.square(np.abs(far_fields['Ex'].values)) +\ np.square(np.abs(far_fields['Ey'].values)) +\ np.square(np.abs(far_fields['Ez'].values)) ) _, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 5)) im1 = pmesh(xs_far, ys_far, intensity_far, ax=ax1, cmap='magma') im2 = pmesh(xs_far, ys_far, np.sqrt(intensity_far), ax=ax2, cmap='magma') ax1.set_title('$|E(x,y)|^2$') ax2.set_title('$|E(x,y)|$') plt.colorbar(im1, ax=ax1) plt.colorbar(im2, ax=ax2) plt.show()
<Figure size 720x360 with 4 Axes>