mirror of
https://github.com/edoverg/modeconverter.git
synced 2026-10-14 23:06:39 +00:00
repo cleanup
This commit is contained in:
File diff suppressed because one or more lines are too long
-49
@@ -1,49 +0,0 @@
|
||||
#visualize fields from hdf5 files
|
||||
import h5py
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.animation as animation
|
||||
import glob
|
||||
import numpy as np
|
||||
|
||||
input_files = sorted(glob.glob("metapeg_v2-ex-*.h5"))
|
||||
|
||||
data = []
|
||||
times = []
|
||||
|
||||
for filename in input_files:
|
||||
with h5py.File(filename, "r") as f:
|
||||
Ex = f["ex"][:]
|
||||
# Extract time from filename (e.g., "metapeg_v2-ex-000050.00.h5" → 50.00)
|
||||
time = float(filename.split("-")[-1].split(".h5")[0])
|
||||
times.append(time)
|
||||
data.append(Ex[:,0,:])
|
||||
print(f"Loaded {filename}, time={time}")
|
||||
print(len(data))
|
||||
# Create animation
|
||||
fig, ax = plt.subplots()
|
||||
x = np.linspace(-4, 4) #assuming x is the last dimension
|
||||
z = np.linspace(-3, 3) #assuming z is the first dimension
|
||||
max_clim = np.max([np.abs(data[i]) for i in range(len(data))])
|
||||
min_clim = np.min([np.abs(data[i]) for i in range(len(data))])
|
||||
print(max_clim)
|
||||
def animate(frame):
|
||||
ax.clear()
|
||||
|
||||
im = ax.imshow(np.rot90(np.abs(data[frame][:,0,:]),k=3), origin="lower",
|
||||
extent=[z.min(), z.max(),x.min(), x.max(),],
|
||||
|
||||
cmap="viridis",
|
||||
vmin=min_clim, vmax=max_clim)
|
||||
|
||||
ax.set_xlabel("x (um)")
|
||||
ax.set_ylabel("z (um)")
|
||||
ax.set_title(f"Electric field (Ex) at t={times[frame]:.2f}")
|
||||
#plt.colorbar(im, ax=ax, label="Ex (a.u.)")
|
||||
return im,
|
||||
|
||||
anim = animation.FuncAnimation(fig, animate, frames=len(data),
|
||||
interval=100, blit=True, repeat=True)
|
||||
anim.save("results/Ex_animation.gif", writer="ffmpeg", fps=2)
|
||||
plt.close()
|
||||
print("Animation saved to results/Ex_animation.gif")
|
||||
|
||||
-654
@@ -1,654 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
" This code implements an inverse designed metasurface that is used to recreate a given intenstity\n",
|
||||
"\n",
|
||||
" pattern. It is based on meep and tidy3d tutorials."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import meep as mp\n",
|
||||
"import meep.adjoint as mpa\n",
|
||||
"import numpy as np\n",
|
||||
"from autograd import numpy as npa\n",
|
||||
"from autograd import tensor_jacobian_product, grad\n",
|
||||
"import nlopt\n",
|
||||
"from matplotlib import pyplot as plt\n",
|
||||
"from matplotlib.patches import Circle\n",
|
||||
"import scipy as sp\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"mp.verbosity(2)\n",
|
||||
"\n",
|
||||
"#setup a results folder\n",
|
||||
"if not os.path.exists(\"results\"):\n",
|
||||
" os.makedirs(\"results\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######PHYSICS SETUP######\n",
|
||||
"subs_index = 3.4\n",
|
||||
"subs_perm = subs_index**2\n",
|
||||
"\n",
|
||||
"Si = mp.Medium(index = subs_index)\n",
|
||||
"Air = mp.Medium(index = 1.0)\n",
|
||||
"\n",
|
||||
"wavelength = 1.55\n",
|
||||
"nf = 1 #number of frequencies to test\n",
|
||||
"frequencies = np.array([1 / 1.55])\n",
|
||||
"\n",
|
||||
"#distance between PML and source/monitor\n",
|
||||
"buffer = 2.5\n",
|
||||
"\n",
|
||||
"######PML######\n",
|
||||
"pml_size = 1.0\n",
|
||||
"pml_layers = [mp.PML(pml_size)]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######FILTER######\n",
|
||||
"minimum_length = 0.09 # minimum length scale (microns)\n",
|
||||
"eta_i = (\n",
|
||||
" 0.5 # blueprint (or intermediate) design field thresholding point (between 0 and 1)\n",
|
||||
")\n",
|
||||
"eta_e = 0.55 # erosion design field thresholding point (between 0 and 1)\n",
|
||||
"eta_d = 1 - eta_e # dilation design field thresholding point (between 0 and 1)\n",
|
||||
"filter_radius = mpa.get_conic_radius_from_eta_e(minimum_length, eta_e)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######DESIGN REGION######\n",
|
||||
"design_region_width = 4.0\n",
|
||||
"design_region_length = 4.0\n",
|
||||
"design_region_height = (wavelength/np.sqrt(subs_index))/2 #half-wavelength in the substrate\n",
|
||||
"\n",
|
||||
"resolution = 20 # pixels/um\n",
|
||||
"design_region_resolution = int(resolution)\n",
|
||||
"\n",
|
||||
"#number of pixels in the design region\n",
|
||||
"Nx = int(design_region_width * design_region_resolution) + 1\n",
|
||||
"Ny = int(design_region_length * design_region_resolution) + 1\n",
|
||||
"\n",
|
||||
"design_variables = mp.MaterialGrid(mp.Vector3(Nx, Ny), Air, Si, grid_type=\"U_MEAN\")\n",
|
||||
"\n",
|
||||
"design_region = mpa.DesignRegion(\n",
|
||||
" design_variables,\n",
|
||||
" volume=mp.Volume(\n",
|
||||
" center = mp.Vector3(0,0,design_region_height / 2),\n",
|
||||
" size = mp.Vector3(\n",
|
||||
" design_region_width, \n",
|
||||
" design_region_length, \n",
|
||||
" design_region_height),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"######CELL GEOMETRY######\n",
|
||||
"Sx = 2 * pml_size + design_region_width\n",
|
||||
"Sy = 2 * pml_size + design_region_length\n",
|
||||
"Sz = 2 * pml_size + design_region_height + 4*buffer\n",
|
||||
"cell_size = mp.Vector3(Sx, Sy, Sz)\n",
|
||||
"\n",
|
||||
"substrate_thickness = Sz / 2\n",
|
||||
"substrate_size = mp.Vector3(design_region_width + 2 * pml_size, \n",
|
||||
" design_region_length + 2 * pml_size, \n",
|
||||
" substrate_thickness)\n",
|
||||
"\n",
|
||||
"substrate_center = mp.Vector3(0, 0, - substrate_thickness / 2)\n",
|
||||
"\n",
|
||||
"substrate_block = mp.Block(\n",
|
||||
" center = substrate_center,\n",
|
||||
" size = substrate_size,\n",
|
||||
" material = Si,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"design_region_block = mp.Block(\n",
|
||||
" center = design_region.center, \n",
|
||||
" size = design_region.size, \n",
|
||||
" material = design_variables,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"geometry = [#combine geometries\n",
|
||||
" substrate_block,\n",
|
||||
" design_region_block,\n",
|
||||
"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######SOURCE######\n",
|
||||
"fcen = 1 / wavelength\n",
|
||||
"width = 0.2\n",
|
||||
"fwidth = width * fcen\n",
|
||||
"source_center = [0, 0, -3]\n",
|
||||
"source_size = mp.Vector3(design_region_width, design_region_length, 0)\n",
|
||||
"src = mp.GaussianSource(frequency=fcen, fwidth=fwidth)\n",
|
||||
"source = [mp.Source(src, component=mp.Ex, size=source_size, center=source_center)]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######OBSERVATION PLANES######\n",
|
||||
"#planes used for references in 2D plots\n",
|
||||
"output_plane = mp.Volume(\n",
|
||||
" center=mp.Vector3(0,0,0), \n",
|
||||
" size=mp.Vector3(Sx,0,Sz))\n",
|
||||
"\n",
|
||||
"output_plane_xy = mp.Volume(\n",
|
||||
" center=mp.Vector3(0,0,design_region_height / 2),\n",
|
||||
" size=mp.Vector3(Sx,Sy,0)\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######SIMULATION######\n",
|
||||
"kpoint = mp.Vector3()\n",
|
||||
"sim = mp.Simulation(\n",
|
||||
" cell_size = cell_size,\n",
|
||||
" boundary_layers = pml_layers,\n",
|
||||
" geometry = geometry,\n",
|
||||
" sources = source,\n",
|
||||
" default_material = Air,\n",
|
||||
" resolution = resolution,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"sim.plot2D(output_plane=output_plane,show_sources=True, show_boundaries=True, show_geometry=True)\n",
|
||||
"plt.savefig(\"results/simulation_geometry.pdf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import xarray as xr\n",
|
||||
"from PIL import Image\n",
|
||||
"\n",
|
||||
"logo_name = \"misc/peg.png\"\n",
|
||||
"\n",
|
||||
"def get_logo() -> np.ndarray:\n",
|
||||
" logo = Image.open(logo_name).convert(\"L\") #L mode stands for grayscale\n",
|
||||
" logo = np.array(logo).astype(float)\n",
|
||||
" #logo is normalized between 0 and 1\n",
|
||||
" logo -= np.min(logo)\n",
|
||||
" logo /= np.max(logo)\n",
|
||||
" #in general, logo has any shape. At a later stage it will\n",
|
||||
" #be rescaled to fit the design region\n",
|
||||
" print(\"Logo shape: \", logo.shape)\n",
|
||||
" return logo\n",
|
||||
"\n",
|
||||
"def intensity_desired_fn_logo(xs: list, ys: list, rescale: float = 0.5) -> np.ndarray:\n",
|
||||
" '''return the value of the logo as a function of the x,y coordinates, \n",
|
||||
" with a rescale factor\n",
|
||||
" Parameters:\n",
|
||||
" xs: list of x coordinates\n",
|
||||
" ys: list of y coordinates\n",
|
||||
" Returns:\n",
|
||||
" logo_interp: 2D array of the same shape as the design region (nx,ny)\n",
|
||||
" '''\n",
|
||||
" logo_values = get_logo()\n",
|
||||
" logo_values = 1 - np.rot90(np.rot90(np.rot90(logo_values))) #rotate the logo by 90 degrees to match the orientation of the simulation\n",
|
||||
" \n",
|
||||
" nx,ny = logo_values.shape\n",
|
||||
" xs_logo = np.linspace(rescale*min(xs), rescale*max(xs), nx)\n",
|
||||
" ys_logo = np.linspace(rescale*min(ys), rescale*max(ys), ny)\n",
|
||||
" logo_dataarray = xr.DataArray(logo_values, coords=dict(x=xs_logo, y=ys_logo))\n",
|
||||
"\n",
|
||||
" logo_interp = logo_dataarray.interp(x=xs, y=ys)\n",
|
||||
"\n",
|
||||
" return np.nan_to_num(logo_interp.values, nan=np.min(logo_interp))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"xs = ys = np.linspace(-design_region_width / 2, design_region_width / 2, Nx)\n",
|
||||
"intensity_desired = intensity_desired_fn_logo(xs,ys)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"plt.figure()\n",
|
||||
"plt.pcolormesh(xs, ys, intensity_desired.T, cmap=\"magma\")\n",
|
||||
"plt.gca().set_aspect(\"equal\")\n",
|
||||
"plt.xlabel(\"x\")\n",
|
||||
"plt.ylabel(\"y\")\n",
|
||||
"plt.title(\"desired intensity pattern\")\n",
|
||||
"plt.colorbar()\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######MONITOR######\n",
|
||||
"far_field_obs_point_z_coord = 10\n",
|
||||
"\n",
|
||||
"plane_points_array = np.array([[x_p,y_p] for x_p in xs for y_p in ys])\n",
|
||||
"far_z_plane = [mp.Vector3(x_p,y_p,far_field_obs_point_z_coord) for x_p in xs for y_p in ys] \n",
|
||||
"plane_points_matrix = plane_points_array.reshape(Nx,Ny,2)\n",
|
||||
"#make a plot of the sampling coordinates\n",
|
||||
"plt.scatter(plane_points_array[:,0], plane_points_array[:,1], s=1)\n",
|
||||
"plt.xlabel(\"x\")\n",
|
||||
"plt.ylabel(\"y\")\n",
|
||||
"plt.title(\"sampling coordinates for near-to-far transformation\")\n",
|
||||
"\n",
|
||||
"#define the near-field observation plane (that spans the whole design region)\n",
|
||||
"NearRegions = [mp.Near2FarRegion(\n",
|
||||
" center=mp.Vector3(0,0, design_region_height + wavelength),\n",
|
||||
" size=mp.Vector3(design_region_width,design_region_length,0),\n",
|
||||
" weight=+1,\n",
|
||||
")]\n",
|
||||
"\n",
|
||||
"#mpa.Near2FarFields return a numpy array with shape (num_of_points, nfreq, 6) where the \n",
|
||||
"#third axis are the field components Ex (0),Ey (1),Ez (2),Hx (3),Hy (4),Hz (5)\n",
|
||||
"FarFields = mpa.Near2FarFields(sim, NearRegions, far_z_plane)\n",
|
||||
"ob_list = [FarFields]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######INTENSITIES######\n",
|
||||
"def get_intensities(FF:np.ndarray) -> tuple[np.ndarray, np.ndarray]:\n",
|
||||
" '''Utility function to compute target and measured intensities given far field data.\n",
|
||||
" Input is a numpy array with shape (num_of_points, nfreq, 6) where the \n",
|
||||
" third axis are the field components Ex, Ey, Ez, Hx, Hy, Hz. num_of_points represents the number\n",
|
||||
" of far-field points that were passed in the list far_z_plane when defining the Near2FarRegions.\n",
|
||||
" \n",
|
||||
" Parameters:\n",
|
||||
" FF: np.ndarray with shape (num_of_points, nfreq, 6)\n",
|
||||
" '''\n",
|
||||
" #first, retrieve the target intensities from the target\n",
|
||||
" target_intensities = intensity_desired_fn_logo(xs,ys)\n",
|
||||
" #second, compute the measured intensities from the far-field\n",
|
||||
" #TODO need to check if the measured matrix needs to be flipped up/down or\n",
|
||||
" #other transformations due to how it is constructed.\n",
|
||||
" measured_intensities = npa.reshape(npa.abs(FF[:,:,0])**2, (Nx,Ny))\n",
|
||||
" \n",
|
||||
" return target_intensities, measured_intensities"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######NORMALIZATION######\n",
|
||||
"#compute the initial far-field intensity without \n",
|
||||
"#the presence of the metasurface to use as normalization\n",
|
||||
"norm_sim = mp.Simulation(\n",
|
||||
" cell_size = cell_size,\n",
|
||||
" boundary_layers = pml_layers,\n",
|
||||
" geometry = geometry,\n",
|
||||
" sources = source,\n",
|
||||
" default_material = Air,\n",
|
||||
" resolution = resolution,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"norm_near2far = norm_sim.add_near2far(fcen,0,1,*NearRegions)\n",
|
||||
"\n",
|
||||
"norm_sim.plot2D(output_plane=output_plane,show_sources=True, show_boundaries=True, show_geometry=True)\n",
|
||||
"plt.savefig(\"results/normalization_simulation_geometry.pdf\")\n",
|
||||
"\n",
|
||||
"norm_sim.run(until=1500)\n",
|
||||
"ref_fields = np.array([norm_sim.get_farfield(norm_near2far, point) for point in far_z_plane])\n",
|
||||
"#TODO this todo is linked to the one above. Need to check if the far-field need to be flipped or something\n",
|
||||
"#at this time, the two definition are consistent with each other.\n",
|
||||
"#NOTE: read carefully the docs for get_farfield.\n",
|
||||
"#it returns a list of the type [Ex1,Ey1,Ez1,Hx1,Hy1,Hz1,Ex2,Ey2,Ez2,Hx2,Hy2,Hz2....]\n",
|
||||
"#for frequencies 1,2...\n",
|
||||
"intensity_norm_mean = npa.mean(npa.abs(ref_fields[:,0])**2) #get the Ex component for all elements in the the list\n",
|
||||
"print(\"intensity_norm_mean shape (should be 1)\",intensity_norm_mean.shape)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"#this the objective to function\n",
|
||||
"#the objective is to minimize the distance between the measured intenstity\n",
|
||||
"# and the target.\n",
|
||||
"intensity_range = int_min, int_max = (0.0, 1.0)\n",
|
||||
"\n",
|
||||
"def J1(FF:np.ndarray):\n",
|
||||
" '''Computes the objective function for the given input field\n",
|
||||
" Parameters: \n",
|
||||
" FF: np.ndarray with shape (num_of_points, nfreq, 6)\n",
|
||||
" Returns:\n",
|
||||
" the value of the objective function, a float\n",
|
||||
" '''\n",
|
||||
" print(\"FF shape\", FF.shape)\n",
|
||||
" target_intensity, measured_intensity = get_intensities(FF)\n",
|
||||
"\n",
|
||||
" measured_intensity = measured_intensity * npa.mean(target_intensity) / intensity_norm_mean\n",
|
||||
"\n",
|
||||
" int_range_magnitude = npa.abs(int_max - int_min)\n",
|
||||
" measured_intensity = npa.clip(measured_intensity, int_min, int_max)\n",
|
||||
" target_intensity = int_range_magnitude * intensity_desired + int_min\n",
|
||||
"\n",
|
||||
" difference = measured_intensity - target_intensity\n",
|
||||
" difference_denominator = npa.full(target_intensity.shape, int_range_magnitude)\n",
|
||||
"\n",
|
||||
" #obj_to_return = npa.mean(npa.abs(FF[0,:,0]) ** 2)\n",
|
||||
"\n",
|
||||
" return npa.linalg.norm(difference) / npa.linalg.norm(difference_denominator)\n",
|
||||
"#returns the first point of all frequencies of the Ex component"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######OPTIMIZATION PROBLEM######\n",
|
||||
"'''\n",
|
||||
"An OptimizationProblem class object knows how to do one basic thing: \n",
|
||||
"Given an input vector of design variables, compute the objective function value \n",
|
||||
"(forward calculation) and optionally its gradient (adjoint calculation)\n",
|
||||
"The gradient of the objective function is computed against its arguments, which are\n",
|
||||
"the FarFields.\n",
|
||||
"'''\n",
|
||||
"opt = mpa.OptimizationProblem(\n",
|
||||
" simulation=sim, #the optimizer uses this simulation object\n",
|
||||
" objective_functions=[J1], #the optimizer call this objective function\n",
|
||||
" objective_arguments=ob_list, #the optimizer passes this list of arguments to the objective function\n",
|
||||
" design_regions=[design_region], #the optimizer updates the design variables in this design region\n",
|
||||
" frequencies=frequencies, #the optimizer runs the simulation at these frequencies\n",
|
||||
" maximum_run_time=100, #maximum optimizer run time\n",
|
||||
")\n",
|
||||
"######CHECK PLOT######\n",
|
||||
"figure = opt.plot2D(True, output_plane=output_plane)\n",
|
||||
"plt.savefig(\"results/simulation_setup.pdf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def mapping(x,eta,beta):\n",
|
||||
" '''\n",
|
||||
" Arguments:\n",
|
||||
" x: design variables (grey scale)\n",
|
||||
" eta: thresholding point for projection (between 0 and 1)\n",
|
||||
" beta: binarization strength for projection (positive)\n",
|
||||
" Returns:\n",
|
||||
" projected_field: the filtered and projected design variables\n",
|
||||
" '''\n",
|
||||
" #mapping is necessary to reach a physical design.\n",
|
||||
" #we start from the design variables (grey scale), which are values in the range (0,1).\n",
|
||||
" #typically, we set an initial value of 0.5.\n",
|
||||
" #filtering makes a local point behave more like its neighbourhood, avoiding checker patterns\n",
|
||||
" filtered_field = mpa.conic_filter(\n",
|
||||
" x,\n",
|
||||
" filter_radius,\n",
|
||||
" design_region_width,\n",
|
||||
" design_region_length,\n",
|
||||
" [design_region_resolution],\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" #projection\n",
|
||||
" #this is an hyperbolic tangent projection\n",
|
||||
" #after filtering, the design is still blurry, so we project it\n",
|
||||
" #using an tanh projection method to push the values towards the upper/lower bound\n",
|
||||
" #If a filtered value is >0.5, then we push it to 1, otherwise, if <0.5, we push it to 0.\n",
|
||||
" #parameter beta represents the degree of binarization,\n",
|
||||
" #parameter eta is threshold point\n",
|
||||
" projected_field = mpa.tanh_projection(filtered_field,beta,eta)\n",
|
||||
" \n",
|
||||
" #the following lines introduce symmetry. Need to understand if necessary \n",
|
||||
" #projected_field = (\n",
|
||||
" # npa.flipud(projected_field) + projected_field\n",
|
||||
" #)/2\n",
|
||||
"\n",
|
||||
" #interpolate to actual materials\n",
|
||||
" return projected_field.flatten()\n",
|
||||
"\n",
|
||||
"evaluation_history = []\n",
|
||||
"cur_iter = [0]\n",
|
||||
"\n",
|
||||
"def f(v, gradient, cur_beta):\n",
|
||||
" print(\"current iteration: {}\".format(cur_iter[0]+1))\n",
|
||||
"\n",
|
||||
" #value and grad\n",
|
||||
" f0, dJ_du = opt([mapping(v,eta_i,cur_beta)])\n",
|
||||
" if gradient.size > 0:\n",
|
||||
" # NOTE: gradient is assigned in-place! No need to return.\n",
|
||||
" gradient[...] = tensor_jacobian_product(mapping, 0)(\n",
|
||||
" v, eta_i, cur_beta, dJ_du #NOTE: here we are using a single frequency!!\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" evaluation_history.append(np.real(f0))\n",
|
||||
" \n",
|
||||
" fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(18, 5))\n",
|
||||
" \n",
|
||||
" opt.plot2D(\n",
|
||||
" False, \n",
|
||||
" ax=ax1,\n",
|
||||
" output_plane=output_plane,\n",
|
||||
" show_sources = False,\n",
|
||||
" show_monitors = False,\n",
|
||||
" show_boundary_layers = False,\n",
|
||||
" )\n",
|
||||
" circ1 = Circle((2,2), minimum_length/2)\n",
|
||||
" ax1.add_patch(circ1)\n",
|
||||
" \n",
|
||||
" opt.plot2D(\n",
|
||||
" False, \n",
|
||||
" ax=ax2,\n",
|
||||
" output_plane=output_plane_xy,\n",
|
||||
" show_sources = False,\n",
|
||||
" show_monitors = False,\n",
|
||||
" show_boundary_layers = False,\n",
|
||||
" )\n",
|
||||
" circ2 = Circle((2,2), minimum_length/2)\n",
|
||||
" ax2.add_patch(circ2)\n",
|
||||
" \n",
|
||||
" ax3.plot(evaluation_history)\n",
|
||||
" ax3.set_xlabel(\"iteration\")\n",
|
||||
" ax3.set_ylabel(\"objective function value\")\n",
|
||||
" ax3.set_title(\"objective function history\")\n",
|
||||
"\n",
|
||||
" plt.savefig(\"results/iter_{}.pdf\".format(cur_iter[0]+1))\n",
|
||||
"\n",
|
||||
" cur_iter[0] += 1\n",
|
||||
"\n",
|
||||
" return np.real(f0)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"algorithm = nlopt.LD_MMA\n",
|
||||
"n = Nx * Ny #number of parameters to optimize\n",
|
||||
"\n",
|
||||
"x = np.ones((n,)) * 0.5 #initial guess\n",
|
||||
"#x = np.insert(x, 0, -1)\n",
|
||||
"#lower and upper bounds for design variables\n",
|
||||
"lb = np.zeros((Nx * Ny,))\n",
|
||||
"ub = np.ones((Nx * Ny,))\n",
|
||||
"\n",
|
||||
"cur_beta = 4 #starting beta\n",
|
||||
"beta_scale = 2 #beta scaling (update) factor\n",
|
||||
"num_betas = 6 #number of betas to test\n",
|
||||
"update_factor = 12 #number of iterations per beta\n",
|
||||
"#total number of simulations = num_betas * update_factor * 2 (forward + adjoint)\n",
|
||||
"ftol = 1e-5\n",
|
||||
"\n",
|
||||
"optimization_start = True\n",
|
||||
"if optimization_start:\n",
|
||||
" for iters in range(num_betas):\n",
|
||||
" solver = nlopt.opt(algorithm, n)\n",
|
||||
" solver.set_lower_bounds(lb)\n",
|
||||
" solver.set_upper_bounds(ub)\n",
|
||||
" solver.set_min_objective(lambda v, g: f(v, g, cur_beta))\n",
|
||||
" solver.set_maxeval(update_factor) #number of iterations for each beta\n",
|
||||
" solver.set_ftol_rel(ftol)\n",
|
||||
" #observe that x is updated in place, so at\n",
|
||||
" # the end it will contain the optimized parameters\n",
|
||||
" x[...] = solver.optimize(x)\n",
|
||||
" cur_beta = cur_beta * beta_scale #update beta\n",
|
||||
" print(\"current beta: \", cur_beta)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######RESULTS######\n",
|
||||
"#plot the figure of merit over iterations\n",
|
||||
"plt.figure()\n",
|
||||
"plt.plot(evaluation_history)\n",
|
||||
"plt.xlabel(\"Iteration\")\n",
|
||||
"plt.ylabel(\"Figure of Merit\")\n",
|
||||
"plt.grid()\n",
|
||||
"plt.savefig(\"results/figure_of_merit.pdf\")\n",
|
||||
"\n",
|
||||
"#plot and save the final design\n",
|
||||
"np.savetxt(\"results/final_design.txt\", mapping(x, eta_i, cur_beta))\n",
|
||||
"opt.update_design([mapping(x, eta_i, cur_beta)])\n",
|
||||
"\n",
|
||||
"fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n",
|
||||
" \n",
|
||||
"opt.plot2D(\n",
|
||||
" False, \n",
|
||||
" ax=ax1,\n",
|
||||
" output_plane=output_plane,\n",
|
||||
" show_sources = False,\n",
|
||||
" show_monitors = False,\n",
|
||||
" show_boundary_layers = False,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"circ1 = Circle((2,2), minimum_length/2)\n",
|
||||
"ax1.add_patch(circ1)\n",
|
||||
"ax1.axis(\"off\")\n",
|
||||
"\n",
|
||||
"opt.plot2D(\n",
|
||||
" False, \n",
|
||||
" ax=ax2,\n",
|
||||
" output_plane=output_plane_xy,\n",
|
||||
" show_sources = False,\n",
|
||||
" show_monitors = False,\n",
|
||||
" show_boundary_layers = False,\n",
|
||||
")\n",
|
||||
"circ2 = Circle((2,2), minimum_length/2)\n",
|
||||
"ax2.add_patch(circ2)\n",
|
||||
"ax2.axis(\"off\")\n",
|
||||
" \n",
|
||||
"plt.savefig(\"results/final_design_xy.pdf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######FULL SIMULATION######\n",
|
||||
"#after completing the optimization we define the full domain and \n",
|
||||
"#verify that the intensity at the desired location matches the target.\n",
|
||||
"#Recall that the far field plane was defined at a distance d=10\n",
|
||||
"opt.update_design([mapping(x, eta_i, cur_beta)])\n",
|
||||
"\n",
|
||||
"full_Sz = 26\n",
|
||||
"\n",
|
||||
"opt.sim = mp.Simulation(\n",
|
||||
" cell_size = mp.Vector3(Sx,Sy,full_Sz),\n",
|
||||
" boundary_layers = pml_layers,\n",
|
||||
" k_point = mp.Vector3(),\n",
|
||||
" geometry = geometry,\n",
|
||||
" sources = source,\n",
|
||||
" default_material = Air,\n",
|
||||
" resolution = resolution,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"src = mp.ContinuousSource(frequency=fcen, fwidth=fwidth)\n",
|
||||
"source = [mp.Source(src, component=mp.Ex, size=source_size, center=source_center)]\n",
|
||||
"opt.sim.change_sources(source)\n",
|
||||
"\n",
|
||||
"full_output_plane = mp.Volume(\n",
|
||||
" center=mp.Vector3(0,0,0), \n",
|
||||
" size=mp.Vector3(Sx,0,full_Sz))\n",
|
||||
"\n",
|
||||
"opt.sim.plot2D(output_plane=full_output_plane,fields=mp.Ex)\n",
|
||||
"plt.savefig(\"results/full_simulation_geometry.pdf\")\n",
|
||||
"opt.sim.run(until=1500)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "mp",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.13.12"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
-526
@@ -1,526 +0,0 @@
|
||||
# %% [markdown]
|
||||
# This code implements an inverse designed metasurface that is used to recreate a given intenstity
|
||||
#
|
||||
# pattern. It is based on meep and tidy3d tutorials.
|
||||
|
||||
# %%
|
||||
import meep as mp
|
||||
import meep.adjoint as mpa
|
||||
import numpy as np
|
||||
from autograd import numpy as npa
|
||||
from autograd import tensor_jacobian_product, grad
|
||||
import nlopt
|
||||
from matplotlib import pyplot as plt
|
||||
from matplotlib.patches import Circle
|
||||
import scipy as sp
|
||||
import os
|
||||
|
||||
mp.verbosity(2)
|
||||
|
||||
#setup a results folder
|
||||
if not os.path.exists("results"):
|
||||
os.makedirs("results")
|
||||
|
||||
|
||||
# %%
|
||||
######PHYSICS SETUP######
|
||||
subs_index = 3.4
|
||||
subs_perm = subs_index**2
|
||||
|
||||
Si = mp.Medium(index = subs_index)
|
||||
Air = mp.Medium(index = 1.0)
|
||||
|
||||
wavelength = 1.55
|
||||
nf = 1 #number of frequencies to test
|
||||
frequencies = np.array([1 / 1.55])
|
||||
|
||||
#distance between PML and source/monitor
|
||||
buffer = 2.5
|
||||
|
||||
######PML######
|
||||
pml_size = 1.0
|
||||
pml_layers = [mp.PML(pml_size)]
|
||||
|
||||
|
||||
# %%
|
||||
######FILTER######
|
||||
minimum_length = 0.09 # minimum length scale (microns)
|
||||
eta_i = (
|
||||
0.5 # blueprint (or intermediate) design field thresholding point (between 0 and 1)
|
||||
)
|
||||
eta_e = 0.55 # erosion design field thresholding point (between 0 and 1)
|
||||
eta_d = 1 - eta_e # dilation design field thresholding point (between 0 and 1)
|
||||
filter_radius = mpa.get_conic_radius_from_eta_e(minimum_length, eta_e)
|
||||
|
||||
|
||||
# %%
|
||||
######DESIGN REGION######
|
||||
design_region_width = 4.0
|
||||
design_region_length = 4.0
|
||||
design_region_height = (wavelength/np.sqrt(subs_index))/2 #half-wavelength in the substrate
|
||||
|
||||
resolution = 5 # pixels/um
|
||||
design_region_resolution = int(resolution)
|
||||
|
||||
#number of pixels in the design region
|
||||
Nx = int(design_region_width * design_region_resolution) + 1
|
||||
Ny = int(design_region_length * design_region_resolution) + 1
|
||||
|
||||
design_variables = mp.MaterialGrid(mp.Vector3(Nx, Ny), Air, Si, grid_type="U_MEAN")
|
||||
|
||||
design_region = mpa.DesignRegion(
|
||||
design_variables,
|
||||
volume=mp.Volume(
|
||||
center = mp.Vector3(0,0,design_region_height / 2),
|
||||
size = mp.Vector3(
|
||||
design_region_width,
|
||||
design_region_length,
|
||||
design_region_height),
|
||||
),
|
||||
)
|
||||
|
||||
######CELL GEOMETRY######
|
||||
Sx = 2 * pml_size + design_region_width
|
||||
Sy = 2 * pml_size + design_region_length
|
||||
Sz = 2 * pml_size + design_region_height + 4*buffer
|
||||
cell_size = mp.Vector3(Sx, Sy, Sz)
|
||||
|
||||
substrate_thickness = Sz / 2
|
||||
substrate_size = mp.Vector3(design_region_width + 2 * pml_size,
|
||||
design_region_length + 2 * pml_size,
|
||||
substrate_thickness)
|
||||
|
||||
substrate_center = mp.Vector3(0, 0, - substrate_thickness / 2)
|
||||
|
||||
substrate_block = mp.Block(
|
||||
center = substrate_center,
|
||||
size = substrate_size,
|
||||
material = Si,
|
||||
)
|
||||
|
||||
design_region_block = mp.Block(
|
||||
center = design_region.center,
|
||||
size = design_region.size,
|
||||
material = design_variables,
|
||||
)
|
||||
|
||||
geometry = [#combine geometries
|
||||
substrate_block,
|
||||
design_region_block,
|
||||
]
|
||||
|
||||
# %%
|
||||
######SOURCE######
|
||||
fcen = 1 / wavelength
|
||||
width = 0.2
|
||||
fwidth = width * fcen
|
||||
source_center = [0, 0, -3]
|
||||
source_size = mp.Vector3(design_region_width, design_region_length, 0)
|
||||
src = mp.GaussianSource(frequency=fcen, fwidth=fwidth)
|
||||
source = [mp.Source(src, component=mp.Ex, size=source_size, center=source_center)]
|
||||
|
||||
# %%
|
||||
######OBSERVATION PLANES######
|
||||
#planes used for references in 2D plots
|
||||
output_plane = mp.Volume(
|
||||
center=mp.Vector3(0,0,0),
|
||||
size=mp.Vector3(Sx,0,Sz))
|
||||
|
||||
output_plane_xy = mp.Volume(
|
||||
center=mp.Vector3(0,0,design_region_height / 2),
|
||||
size=mp.Vector3(Sx,Sy,0)
|
||||
)
|
||||
|
||||
# %%
|
||||
######SIMULATION######
|
||||
sim_run_time = 100
|
||||
kpoint = mp.Vector3()
|
||||
sim = mp.Simulation(
|
||||
cell_size = cell_size,
|
||||
boundary_layers = pml_layers,
|
||||
geometry = geometry,
|
||||
sources = source,
|
||||
default_material = Air,
|
||||
resolution = resolution,
|
||||
)
|
||||
|
||||
sim.plot2D(output_plane=output_plane,show_sources=True, show_boundaries=True, show_geometry=True)
|
||||
plt.savefig("results/simulation_geometry.pdf")
|
||||
|
||||
# %%
|
||||
import xarray as xr
|
||||
from PIL import Image
|
||||
|
||||
logo_name = "misc/peg.png"
|
||||
|
||||
def get_logo() -> np.ndarray:
|
||||
logo = Image.open(logo_name).convert("L") #L mode stands for grayscale
|
||||
logo = np.array(logo).astype(float)
|
||||
#logo is normalized between 0 and 1
|
||||
logo -= np.min(logo)
|
||||
logo /= np.max(logo)
|
||||
#in general, logo has any shape. At a later stage it will
|
||||
#be rescaled to fit the design region
|
||||
print("Logo shape: ", logo.shape)
|
||||
return logo
|
||||
|
||||
def intensity_desired_fn_logo(xs: list, ys: list, rescale: float = 0.5) -> np.ndarray:
|
||||
'''return the value of the logo as a function of the x,y coordinates,
|
||||
with a rescale factor
|
||||
Parameters:
|
||||
xs: list of x coordinates
|
||||
ys: list of y coordinates
|
||||
Returns:
|
||||
logo_interp: 2D array of the same shape as the design region (nx,ny)
|
||||
'''
|
||||
logo_values = get_logo()
|
||||
logo_values = 1 - np.rot90(np.rot90(np.rot90(logo_values))) #rotate the logo by 90 degrees to match the orientation of the simulation
|
||||
|
||||
nx,ny = logo_values.shape
|
||||
xs_logo = np.linspace(rescale*min(xs), rescale*max(xs), nx)
|
||||
ys_logo = np.linspace(rescale*min(ys), rescale*max(ys), ny)
|
||||
logo_dataarray = xr.DataArray(logo_values, coords=dict(x=xs_logo, y=ys_logo))
|
||||
|
||||
logo_interp = logo_dataarray.interp(x=xs, y=ys)
|
||||
|
||||
return np.nan_to_num(logo_interp.values, nan=np.min(logo_interp))
|
||||
|
||||
# %%
|
||||
xs = ys = np.linspace(-design_region_width / 2, design_region_width / 2, Nx)
|
||||
intensity_desired = intensity_desired_fn_logo(xs,ys)
|
||||
|
||||
# %%
|
||||
plt.figure()
|
||||
plt.pcolormesh(xs, ys, intensity_desired.T, cmap="magma")
|
||||
plt.gca().set_aspect("equal")
|
||||
plt.xlabel("x")
|
||||
plt.ylabel("y")
|
||||
plt.title("desired intensity pattern")
|
||||
plt.colorbar()
|
||||
plt.show()
|
||||
|
||||
# %%
|
||||
######MONITOR######
|
||||
far_field_obs_point_z_coord = 10
|
||||
|
||||
plane_points_array = np.array([[x_p,y_p] for x_p in xs for y_p in ys])
|
||||
far_z_plane = [mp.Vector3(x_p,y_p,far_field_obs_point_z_coord) for x_p in xs for y_p in ys]
|
||||
plane_points_matrix = plane_points_array.reshape(Nx,Ny,2)
|
||||
#make a plot of the sampling coordinates
|
||||
plt.scatter(plane_points_array[:,0], plane_points_array[:,1], s=1)
|
||||
plt.xlabel("x")
|
||||
plt.ylabel("y")
|
||||
plt.title("sampling coordinates for near-to-far transformation")
|
||||
|
||||
#define the near-field observation plane (that spans the whole design region)
|
||||
NearRegions = [mp.Near2FarRegion(
|
||||
center=mp.Vector3(0,0, design_region_height + wavelength),
|
||||
size=mp.Vector3(design_region_width,design_region_length,0),
|
||||
weight=+1,
|
||||
)]
|
||||
|
||||
#mpa.Near2FarFields return a numpy array with shape (num_of_points, nfreq, 6) where the
|
||||
#third axis are the field components Ex (0),Ey (1),Ez (2),Hx (3),Hy (4),Hz (5)
|
||||
FarFields = mpa.Near2FarFields(sim, NearRegions, far_z_plane)
|
||||
ob_list = [FarFields]
|
||||
|
||||
# %%
|
||||
######INTENSITIES######
|
||||
def get_intensities(FF:np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||
'''Utility function to compute target and measured intensities given far field data.
|
||||
Input is a numpy array with shape (num_of_points, nfreq, 6) where the
|
||||
third axis are the field components Ex, Ey, Ez, Hx, Hy, Hz. num_of_points represents the number
|
||||
of far-field points that were passed in the list far_z_plane when defining the Near2FarRegions.
|
||||
|
||||
Parameters:
|
||||
FF: np.ndarray with shape (num_of_points, nfreq, 6)
|
||||
'''
|
||||
#first, retrieve the target intensities from the target
|
||||
target_intensities = intensity_desired_fn_logo(xs,ys)
|
||||
#second, compute the measured intensities from the far-field
|
||||
#TODO need to check if the measured matrix needs to be flipped up/down or
|
||||
#other transformations due to how it is constructed.
|
||||
measured_intensities = npa.reshape(npa.abs(FF[:,:,0])**2, (Nx,Ny))
|
||||
|
||||
return target_intensities, measured_intensities
|
||||
|
||||
# %%
|
||||
######NORMALIZATION######
|
||||
#compute the initial far-field intensity without
|
||||
#the presence of the metasurface to use as normalization
|
||||
norm_sim = mp.Simulation(
|
||||
cell_size = cell_size,
|
||||
boundary_layers = pml_layers,
|
||||
geometry = geometry,
|
||||
sources = source,
|
||||
default_material = Air,
|
||||
resolution = resolution,
|
||||
)
|
||||
|
||||
norm_near2far = norm_sim.add_near2far(fcen,0,1,*NearRegions)
|
||||
|
||||
norm_sim.plot2D(output_plane=output_plane,show_sources=True, show_boundaries=True, show_geometry=True)
|
||||
plt.savefig("results/normalization_simulation_geometry.pdf")
|
||||
|
||||
norm_sim.run(until=sim_run_time)
|
||||
ref_fields = np.array([norm_sim.get_farfield(norm_near2far, point) for point in far_z_plane])
|
||||
#TODO this todo is linked to the one above. Need to check if the far-field need to be flipped or something
|
||||
#at this time, the two definition are consistent with each other.
|
||||
#NOTE: read carefully the docs for get_farfield.
|
||||
#it returns a list of the type [Ex1,Ey1,Ez1,Hx1,Hy1,Hz1,Ex2,Ey2,Ez2,Hx2,Hy2,Hz2....]
|
||||
#for frequencies 1,2...
|
||||
intensity_norm_mean = npa.mean(npa.abs(ref_fields[:,0])**2) #get the Ex component for all elements in the the list
|
||||
print("intensity_norm_mean shape (should be 1)",intensity_norm_mean.shape)
|
||||
|
||||
# %%
|
||||
#this the objective to function
|
||||
#the objective is to minimize the distance between the measured intenstity
|
||||
# and the target.
|
||||
intensity_range = int_min, int_max = (0.0, 1.0)
|
||||
|
||||
def J1(FF:np.ndarray):
|
||||
'''Computes the objective function for the given input field
|
||||
Parameters:
|
||||
FF: np.ndarray with shape (num_of_points, nfreq, 6)
|
||||
Returns:
|
||||
the value of the objective function, a float
|
||||
'''
|
||||
print("FF shape", FF.shape)
|
||||
target_intensity, measured_intensity = get_intensities(FF)
|
||||
|
||||
measured_intensity = measured_intensity * npa.mean(target_intensity) / intensity_norm_mean
|
||||
|
||||
int_range_magnitude = npa.abs(int_max - int_min)
|
||||
measured_intensity = npa.clip(measured_intensity, int_min, int_max)
|
||||
target_intensity = int_range_magnitude * intensity_desired + int_min
|
||||
|
||||
difference = measured_intensity - target_intensity
|
||||
difference_denominator = npa.full(target_intensity.shape, int_range_magnitude)
|
||||
|
||||
#obj_to_return = npa.mean(npa.abs(FF[0,:,0]) ** 2)
|
||||
|
||||
return npa.linalg.norm(difference) / npa.linalg.norm(difference_denominator)
|
||||
#returns the first point of all frequencies of the Ex component
|
||||
|
||||
# %%
|
||||
######OPTIMIZATION PROBLEM######
|
||||
'''
|
||||
An OptimizationProblem class object knows how to do one basic thing:
|
||||
Given an input vector of design variables, compute the objective function value
|
||||
(forward calculation) and optionally its gradient (adjoint calculation)
|
||||
The gradient of the objective function is computed against its arguments, which are
|
||||
the FarFields.
|
||||
'''
|
||||
opt = mpa.OptimizationProblem(
|
||||
simulation=sim, #the optimizer uses this simulation object
|
||||
objective_functions=[J1], #the optimizer call this objective function
|
||||
objective_arguments=ob_list, #the optimizer passes this list of arguments to the objective function
|
||||
design_regions=[design_region], #the optimizer updates the design variables in this design region
|
||||
frequencies=frequencies, #the optimizer runs the simulation at these frequencies
|
||||
maximum_run_time=100, #maximum optimizer run time
|
||||
)
|
||||
######CHECK PLOT######
|
||||
figure = opt.plot2D(True, output_plane=output_plane)
|
||||
plt.savefig("results/simulation_setup.pdf")
|
||||
|
||||
# %%
|
||||
def mapping(x,eta,beta):
|
||||
'''
|
||||
Arguments:
|
||||
x: design variables (grey scale)
|
||||
eta: thresholding point for projection (between 0 and 1)
|
||||
beta: binarization strength for projection (positive)
|
||||
Returns:
|
||||
projected_field: the filtered and projected design variables
|
||||
'''
|
||||
#mapping is necessary to reach a physical design.
|
||||
#we start from the design variables (grey scale), which are values in the range (0,1).
|
||||
#typically, we set an initial value of 0.5.
|
||||
#filtering makes a local point behave more like its neighbourhood, avoiding checker patterns
|
||||
filtered_field = mpa.conic_filter(
|
||||
x,
|
||||
filter_radius,
|
||||
design_region_width,
|
||||
design_region_length,
|
||||
[design_region_resolution],
|
||||
)
|
||||
|
||||
#projection
|
||||
#this is an hyperbolic tangent projection
|
||||
#after filtering, the design is still blurry, so we project it
|
||||
#using an tanh projection method to push the values towards the upper/lower bound
|
||||
#If a filtered value is >0.5, then we push it to 1, otherwise, if <0.5, we push it to 0.
|
||||
#parameter beta represents the degree of binarization,
|
||||
#parameter eta is threshold point
|
||||
projected_field = mpa.tanh_projection(filtered_field,beta,eta)
|
||||
|
||||
#the following lines introduce symmetry. Need to understand if necessary
|
||||
#projected_field = (
|
||||
# npa.flipud(projected_field) + projected_field
|
||||
#)/2
|
||||
|
||||
#interpolate to actual materials
|
||||
return projected_field.flatten()
|
||||
|
||||
evaluation_history = []
|
||||
cur_iter = [0]
|
||||
|
||||
def f(v, gradient, cur_beta):
|
||||
print("current iteration: {}".format(cur_iter[0]+1))
|
||||
|
||||
#value and grad
|
||||
f0, dJ_du = opt([mapping(v,eta_i,cur_beta)])
|
||||
if gradient.size > 0:
|
||||
# NOTE: gradient is assigned in-place! No need to return.
|
||||
gradient[...] = tensor_jacobian_product(mapping, 0)(
|
||||
v, eta_i, cur_beta, dJ_du #NOTE: here we are using a single frequency!!
|
||||
)
|
||||
|
||||
evaluation_history.append(np.real(f0))
|
||||
|
||||
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(18, 5))
|
||||
|
||||
opt.plot2D(
|
||||
False,
|
||||
ax=ax1,
|
||||
output_plane=output_plane,
|
||||
show_sources = False,
|
||||
show_monitors = False,
|
||||
show_boundary_layers = False,
|
||||
)
|
||||
circ1 = Circle((2,2), minimum_length/2)
|
||||
ax1.add_patch(circ1)
|
||||
|
||||
opt.plot2D(
|
||||
False,
|
||||
ax=ax2,
|
||||
output_plane=output_plane_xy,
|
||||
show_sources = False,
|
||||
show_monitors = False,
|
||||
show_boundary_layers = False,
|
||||
)
|
||||
circ2 = Circle((2,2), minimum_length/2)
|
||||
ax2.add_patch(circ2)
|
||||
|
||||
ax3.plot(evaluation_history)
|
||||
ax3.set_xlabel("iteration")
|
||||
ax3.set_ylabel("objective function value")
|
||||
ax3.set_title("objective function history")
|
||||
|
||||
plt.savefig("results/iter_{}.pdf".format(cur_iter[0]+1))
|
||||
|
||||
cur_iter[0] += 1
|
||||
|
||||
return np.real(f0)
|
||||
|
||||
|
||||
algorithm = nlopt.LD_MMA
|
||||
n = Nx * Ny #number of parameters to optimize
|
||||
|
||||
x = np.ones((n,)) * 0.5 #initial guess
|
||||
#x = np.insert(x, 0, -1)
|
||||
#lower and upper bounds for design variables
|
||||
lb = np.zeros((Nx * Ny,))
|
||||
ub = np.ones((Nx * Ny,))
|
||||
|
||||
cur_beta = 4 #starting beta
|
||||
beta_scale = 2 #beta scaling (update) factor
|
||||
num_betas = 3 #number of betas to test
|
||||
update_factor = 1 #number of iterations per beta
|
||||
#total number of simulations = num_betas * update_factor * 2 (forward + adjoint)
|
||||
ftol = 1e-5
|
||||
|
||||
optimization_start = True
|
||||
if optimization_start:
|
||||
for iters in range(num_betas):
|
||||
solver = nlopt.opt(algorithm, n)
|
||||
solver.set_lower_bounds(lb)
|
||||
solver.set_upper_bounds(ub)
|
||||
solver.set_min_objective(lambda v, g: f(v, g, cur_beta))
|
||||
solver.set_maxeval(update_factor) #number of iterations for each beta
|
||||
solver.set_ftol_rel(ftol)
|
||||
#observe that x is updated in place, so at
|
||||
# the end it will contain the optimized parameters
|
||||
x[...] = solver.optimize(x)
|
||||
cur_beta = cur_beta * beta_scale #update beta
|
||||
print("current beta: ", cur_beta)
|
||||
|
||||
# %%
|
||||
######RESULTS######
|
||||
#plot the figure of merit over iterations
|
||||
plt.figure()
|
||||
plt.plot(evaluation_history)
|
||||
plt.xlabel("Iteration")
|
||||
plt.ylabel("Figure of Merit")
|
||||
plt.grid()
|
||||
plt.savefig("results/figure_of_merit.pdf")
|
||||
|
||||
#plot and save the final design
|
||||
np.savetxt("results/final_design.txt", mapping(x, eta_i, cur_beta))
|
||||
opt.update_design([mapping(x, eta_i, cur_beta)])
|
||||
|
||||
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
|
||||
|
||||
opt.plot2D(
|
||||
False,
|
||||
ax=ax1,
|
||||
output_plane=output_plane,
|
||||
show_sources = False,
|
||||
show_monitors = False,
|
||||
show_boundary_layers = False,
|
||||
)
|
||||
|
||||
circ1 = Circle((2,2), minimum_length/2)
|
||||
ax1.add_patch(circ1)
|
||||
ax1.axis("off")
|
||||
|
||||
opt.plot2D(
|
||||
False,
|
||||
ax=ax2,
|
||||
output_plane=output_plane_xy,
|
||||
show_sources = False,
|
||||
show_monitors = False,
|
||||
show_boundary_layers = False,
|
||||
)
|
||||
circ2 = Circle((2,2), minimum_length/2)
|
||||
ax2.add_patch(circ2)
|
||||
ax2.axis("off")
|
||||
|
||||
plt.savefig("results/final_design_xy.pdf")
|
||||
|
||||
# %%
|
||||
######FULL SIMULATION######
|
||||
#after completing the optimization we define the full domain and
|
||||
#verify that the intensity at the desired location matches the target.
|
||||
#Recall that the far field plane was defined at a distance d=10
|
||||
opt.update_design([mapping(x, eta_i, cur_beta)])
|
||||
|
||||
full_Sz = 26
|
||||
|
||||
opt.sim = mp.Simulation(
|
||||
cell_size = mp.Vector3(Sx,Sy,full_Sz),
|
||||
boundary_layers = pml_layers,
|
||||
k_point = mp.Vector3(),
|
||||
geometry = geometry,
|
||||
sources = source,
|
||||
default_material = Air,
|
||||
resolution = resolution,
|
||||
)
|
||||
|
||||
src = mp.ContinuousSource(frequency=fcen, fwidth=fwidth)
|
||||
source = [mp.Source(src, component=mp.Ex, size=source_size, center=source_center)]
|
||||
opt.sim.change_sources(source)
|
||||
|
||||
full_output_plane = mp.Volume(
|
||||
center=mp.Vector3(0,0,0),
|
||||
size=mp.Vector3(Sx,0,full_Sz))
|
||||
|
||||
design_output_plane = mp.Volume(
|
||||
center=mp.Vector3(0,0,10),
|
||||
size=mp.Vector3(design_region_width,design_region_length,0)
|
||||
)
|
||||
|
||||
#understand how to retrieve the far-field data at the design output plane
|
||||
opt.sim.plot2D(output_plane=full_output_plane,fields=mp.Ex, show_sources=True, show_boundaries=True, show_geometry=True)
|
||||
plt.savefig("results/full_simulation_geometry.pdf")
|
||||
opt.sim.run(until=sim_run_time)
|
||||
-823
@@ -1,823 +0,0 @@
|
||||
|
||||
import faulthandler
|
||||
faulthandler.enable()
|
||||
from typing import List, NamedTuple, Tuple
|
||||
from autograd import numpy as npa, tensor_jacobian_product, grad
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.animation as animation
|
||||
import meep as mp
|
||||
import meep.adjoint as mpa
|
||||
import nlopt
|
||||
import numpy as np
|
||||
import os
|
||||
import xarray as xr
|
||||
from PIL import Image
|
||||
import pickle
|
||||
mp.verbosity(3)
|
||||
|
||||
if mp.am_master():
|
||||
if not os.path.exists("results"):
|
||||
os.makedirs("results")
|
||||
|
||||
###### PHYSICS SETUP ######
|
||||
RESOLUTION = 20 #pixels per meep unit length (1um)
|
||||
MAX_RUN_TIME = 700 #meep units
|
||||
WAVELENGTH_MIN_UM = 1.50
|
||||
WAVELENGTH_MAX_UM = 1.60
|
||||
PML_UM = 1.0 #PML thickness
|
||||
BUFFER = 3.0
|
||||
DESIGN_WAVELENGTHS_UM = (1.50,1.55,1.60) #wavelengths at which to optimize the device
|
||||
DESIGN_REGION_UM = mp.Vector3(4,4,1)
|
||||
DESIGN_REGION_RESOLUTION = int(RESOLUTION)
|
||||
DESIGN_REGION_CENTER = mp.Vector3(0, 0, DESIGN_REGION_UM.z / 2)
|
||||
NX_DESIGN_GRID = int(DESIGN_REGION_UM.x * DESIGN_REGION_RESOLUTION) + 1
|
||||
NY_DESIGN_GRID = int(DESIGN_REGION_UM.y * DESIGN_REGION_RESOLUTION) + 1
|
||||
MIN_LENGTH_UM = 0.5 #500 nm minimum length scale
|
||||
SILICON = mp.Medium(index = 3.5)
|
||||
SILICON_DIOXIDE = mp.Medium(index = 1.5)
|
||||
AIR = mp.Medium(index = 1.0)
|
||||
|
||||
SIGMOID_THRESHOLD_INTRINSIC = 0.5
|
||||
SIGMOID_THRESHOLD_EROSION = 0.75
|
||||
SIGMOID_THRESHOLD_DILATION = 1 - SIGMOID_THRESHOLD_EROSION
|
||||
|
||||
cell_um = mp.Vector3(
|
||||
DESIGN_REGION_UM.x + 2*BUFFER + 2*PML_UM,
|
||||
DESIGN_REGION_UM.y + 2*BUFFER + 2*PML_UM,
|
||||
2*BUFFER + 2*PML_UM
|
||||
)
|
||||
|
||||
SUBSTRATE_THICKNESS = BUFFER + PML_UM
|
||||
SUBSTRATE_SIZE = mp.Vector3(cell_um.x, cell_um.y, SUBSTRATE_THICKNESS)
|
||||
SUBSTRATE_CENTER = mp.Vector3(0, 0, -SUBSTRATE_THICKNESS/2)
|
||||
|
||||
physical_domains_size = mp.Vector3(
|
||||
DESIGN_REGION_UM.x + 2*BUFFER,
|
||||
DESIGN_REGION_UM.y + 2*BUFFER,
|
||||
2*BUFFER
|
||||
)
|
||||
|
||||
filter_radius_um = mpa.get_conic_radius_from_eta_e(
|
||||
MIN_LENGTH_UM, SIGMOID_THRESHOLD_EROSION
|
||||
)
|
||||
|
||||
frequency_min = 1 / WAVELENGTH_MAX_UM
|
||||
frequency_max = 1 / WAVELENGTH_MIN_UM
|
||||
frequency_center = 0.5 * (frequency_min + frequency_max)
|
||||
wavelength_center = 1 / frequency_center #remember that c = 1 in meep units
|
||||
frequency_width = frequency_max - frequency_min
|
||||
frequencies = [1/wavelength for wavelength in DESIGN_WAVELENGTHS_UM]
|
||||
num_wavelengths = len(DESIGN_WAVELENGTHS_UM)
|
||||
|
||||
SOURCE_CENTER = mp.Vector3(0,0,-BUFFER/2)
|
||||
SOURCE_SIZE = mp.Vector3(physical_domains_size.x,physical_domains_size.y,0)
|
||||
|
||||
NEAR_REGION_MONITOR_CENTER = mp.Vector3(0,0, DESIGN_REGION_UM.z + BUFFER/2)
|
||||
NEAR_REGION_MONITOR_SIZE = mp.Vector3(physical_domains_size.x,physical_domains_size.y,0)
|
||||
|
||||
FF_MONITOR_CENTER = mp.Vector3(0,0,DESIGN_REGION_UM.z + BUFFER)
|
||||
FF_MONITOR_SIZE = mp.Vector3(DESIGN_REGION_UM.x, DESIGN_REGION_UM.y, 0)
|
||||
xs = ys = np.linspace(-FF_MONITOR_SIZE.x/2, FF_MONITOR_SIZE.x/2, NX_DESIGN_GRID)
|
||||
#collection of points in the far-field monitor
|
||||
ff_points = [mp.Vector3(x_p,y_p,FF_MONITOR_CENTER.z) for x_p in xs for y_p in ys]
|
||||
|
||||
stop_cond = mp.stop_when_fields_decayed(25, mp.Ex, NEAR_REGION_MONITOR_CENTER, 1e-6)
|
||||
|
||||
pml_layers = [mp.PML(PML_UM)]
|
||||
|
||||
view_2D_plane = mp.Volume(
|
||||
center=mp.Vector3(0,0,0),
|
||||
size=mp.Vector3(cell_um.x,0,cell_um.z)
|
||||
)
|
||||
|
||||
def str_from_list(list_: List[float]) -> str:
|
||||
return "[" + ", ".join(f"{val:.4f}" for val in list_) + "]"
|
||||
|
||||
def filter_and_project(
|
||||
weights: np.ndarray,
|
||||
sigmoid_threshold: float,
|
||||
sigmoid_bias: float
|
||||
) -> np.ndarray:
|
||||
'''Function to filter and project design variables. Differentiable by autograd
|
||||
|
||||
Args:
|
||||
weights: design weights as a flattened (1D) array
|
||||
sigmoid_threshold: erosion/dilation parameter for projection
|
||||
sigmoid_bias: bias parameter for the projection. 0 is no projection
|
||||
Returns:
|
||||
The flattened (1D) filtered/projected design weights
|
||||
'''
|
||||
|
||||
print("Inside filter_and_project function...")
|
||||
weights_filtered = mpa.conic_filter(
|
||||
weights.reshape(-1, NY_DESIGN_GRID), #row major mapping
|
||||
filter_radius_um,
|
||||
DESIGN_REGION_UM.x,
|
||||
DESIGN_REGION_UM.y,
|
||||
DESIGN_REGION_RESOLUTION,
|
||||
)
|
||||
|
||||
if sigmoid_bias == 0:
|
||||
print("No projection applied in filter_and_project function, exiting...")
|
||||
return weights_filtered.flatten()
|
||||
else:
|
||||
weights_projected = mpa.tanh_projection(
|
||||
weights_filtered.reshape(-1,NY_DESIGN_GRID),
|
||||
sigmoid_bias,
|
||||
sigmoid_threshold,
|
||||
)
|
||||
print("Projection applied in filter_and_project function, exiting...")
|
||||
return weights_projected.flatten()
|
||||
|
||||
def obj_fun(epigraph_and_weights: np.ndarray, grad: np.ndarray)-> float:
|
||||
'''Objective function for the epigraph formulation
|
||||
|
||||
Args:
|
||||
epigraph_and_weights: 1D array containing epigraph variable
|
||||
(at index 0) and design weigts (remaining elements)
|
||||
grad: the gradient modified in place of the objective function (flattened 1D array)
|
||||
Returns:
|
||||
The scalar epigraph variable (the objective function)
|
||||
'''
|
||||
|
||||
epigraph = epigraph_and_weights[0]
|
||||
|
||||
if grad.size > 0:
|
||||
grad[0] = 1
|
||||
grad[1:] = 0
|
||||
|
||||
return epigraph
|
||||
|
||||
def epigraph_constraint(
|
||||
result: np.ndarray,
|
||||
epigraph_and_weights: np.ndarray,
|
||||
gradient: np.ndarray,
|
||||
sigmoid_threshold: float,
|
||||
sigmoid_bias: float,
|
||||
use_epsavg: bool,
|
||||
) -> None:
|
||||
'''Constraint function for the epigraph formulation
|
||||
|
||||
Args:
|
||||
result: 1D array, modified in place, with the results
|
||||
of this constraint evaluation
|
||||
epigraph_and_weights: 1D array of epigraph (first element)
|
||||
and design weights (other elements)
|
||||
gradient: the jacobian matrix (backpropagated) with dimensions
|
||||
(1+NX_DESIGN_GRID*NY_DESIGN_GRID, 2 * num_wavelengths), modified in place.
|
||||
these gradients are used by the optimization algorithm,
|
||||
because, being backpropagated, they tell us how the constraint function behaves
|
||||
with respect to changes in the design parameters.
|
||||
sigmoid_threshold: erosion/dilation parameter for projection.
|
||||
sigmoid_bias: bias parameter for projection.
|
||||
use_epsavg: whether to use subpixel smoothing.
|
||||
'''
|
||||
print("Inside epigraph_constraint function...")
|
||||
epigraph = epigraph_and_weights[0]
|
||||
weights = epigraph_and_weights[1:]
|
||||
assert not np.isnan(weights).any(), "NaN values found in weights in epigraph_constraint function"
|
||||
print("===========")
|
||||
print("Shape of weights in epigraph_constraint:", weights.shape)
|
||||
print("===========")
|
||||
obj_val, grad = opt(
|
||||
[
|
||||
filter_and_project(
|
||||
weights, sigmoid_threshold, 0 if use_epsavg else sigmoid_bias
|
||||
)
|
||||
]
|
||||
)
|
||||
print("filter_and_project called")
|
||||
#in our case there is only one objective function,
|
||||
#which needs to be evaluated at multiple wavelengths.
|
||||
#for this reason, obj_val is a 1-element list (to be verified,
|
||||
# it might be that the list is directly replaced by the content)
|
||||
# that contains an array of objective function values at each wavelength.
|
||||
print("Backpropagating gradients...")
|
||||
#for each wavelength, backpropagate
|
||||
for k in range(num_wavelengths):
|
||||
grad[:,k] = tensor_jacobian_product(filter_and_project, 0)(
|
||||
weights,
|
||||
sigmoid_threshold,
|
||||
sigmoid_bias,
|
||||
grad[:,k],
|
||||
)
|
||||
|
||||
#modify in place the gradient result
|
||||
if gradient.size > 0:
|
||||
gradient[:,0] = -1 #gradient w.r.t epigraph
|
||||
gradient[:,1:] = grad.T
|
||||
|
||||
#modify in place the constraint result
|
||||
result[:] = np.real(obj_val) - epigraph
|
||||
|
||||
print("Updating history...")
|
||||
objfunc_history.append(np.real(obj_val))
|
||||
epivar_history.append(epigraph)
|
||||
|
||||
print(
|
||||
f"iteration:, {cur_iter[0]:3d}, sigmoid_bias: {sigmoid_bias:2d}, "
|
||||
f"epigraph: {epigraph:.5f}, obj. func.: {obj_val}, "
|
||||
f"epigraph constraint: {str_from_list(result)}"
|
||||
)
|
||||
print("Finished epigraph_constraint function")
|
||||
cur_iter[0] = cur_iter[0] + 1
|
||||
|
||||
def line_width_and_spacing_constraint(
|
||||
result: np.ndarray,
|
||||
epigraph_and_weights: np.ndarray,
|
||||
gradient: np.ndarray,
|
||||
sigmoid_bias: float,
|
||||
) -> float:
|
||||
'''Constraint function for minimum line width and spacing
|
||||
|
||||
Args:
|
||||
result: evaluation of this constraint function, modified in place
|
||||
epigraph_and_weights: 1D array of epigraph (first element)
|
||||
and design weights (other elements)
|
||||
gradient: the Jacobian matrix, modified in place
|
||||
sigmoid_bias: bias parameter for projection
|
||||
|
||||
Returns:
|
||||
The constraint value result
|
||||
'''
|
||||
|
||||
epigraph = epigraph_and_weights[0]
|
||||
weights = epigraph_and_weights[1:]
|
||||
|
||||
a1 = 1e-3
|
||||
b1 = 0
|
||||
gradient[:,0] = -a1
|
||||
|
||||
filter_func = lambda a: mpa.conic_filter(
|
||||
a.reshape(-1,NY_DESIGN_GRID),
|
||||
filter_radius_um,
|
||||
DESIGN_REGION_UM.x,
|
||||
DESIGN_REGION_UM.y,
|
||||
DESIGN_REGION_RESOLUTION,
|
||||
)
|
||||
|
||||
threshold_func = lambda a: mpa.tanh_projection(
|
||||
a.reshape(-1,NY_DESIGN_GRID),
|
||||
sigmoid_bias,
|
||||
SIGMOID_THRESHOLD_INTRINSIC,
|
||||
)
|
||||
|
||||
c0 = 1e7 * (filter_radius_um * 1 / RESOLUTION) ** 4
|
||||
|
||||
#constraints as lambda functions
|
||||
M1 = lambda a: mpa.constraint_solid(
|
||||
a,
|
||||
c0,
|
||||
SIGMOID_THRESHOLD_EROSION,
|
||||
filter_func,
|
||||
threshold_func,
|
||||
1
|
||||
)
|
||||
|
||||
M2 = lambda a: mpa.constraint_void(
|
||||
a,
|
||||
c0,
|
||||
SIGMOID_THRESHOLD_DILATION,
|
||||
filter_func,
|
||||
threshold_func,
|
||||
1
|
||||
)
|
||||
|
||||
#gradients of the constraints w.r.t design parameters
|
||||
g1 = grad(M1)(weights)
|
||||
g2 = grad(M2)(weights)
|
||||
|
||||
result[0] = M1(weights) - a1 * epigraph - b1
|
||||
result[1] = M2(weights) - a1 * epigraph - b1
|
||||
|
||||
gradient[0, 1:] = g1.flatten()
|
||||
gradient[1, 1:] = g2.flatten()
|
||||
|
||||
#these are the epigraph values for the constraints
|
||||
t1 = (M1(weights) - b1) / a1
|
||||
t2 = (M2(weights) - b1) / a1
|
||||
|
||||
print(f"line_width_and_spacing_constraint:, {result[0]}, {result[1]}, {t1}, {t2}")
|
||||
|
||||
return max(t1, t2)
|
||||
|
||||
def get_pattern() -> np.ndarray:
|
||||
'''Imports the intensity pattern from file'''
|
||||
pattern_name = 'peg.png'
|
||||
pattern = Image.open('misc/' + pattern_name).convert('L')
|
||||
pattern = np.array(pattern).astype(float)
|
||||
|
||||
pattern = pattern - np.min(pattern)
|
||||
pattern = pattern / np.max(pattern)
|
||||
|
||||
return pattern
|
||||
|
||||
def intensity_desired_fn_pattern(
|
||||
xs: list,
|
||||
ys: list,
|
||||
rescale: float = 0.5
|
||||
) -> np.ndarray:
|
||||
'''Returns the pattern intensity value at the specified
|
||||
(xs,ys) locations, with a scale factor
|
||||
|
||||
Args:
|
||||
xs: list of x coordinates
|
||||
ys: list of y coordinates
|
||||
rescale: scale factor for the pattern
|
||||
Returns:
|
||||
The flattened pattern intensity value at the specified (xs,ys) locations, with a scale factor
|
||||
'''
|
||||
pattern_values = get_pattern()
|
||||
pattern_values = 1 - np.rot90(np.rot90(np.rot90(pattern_values)))
|
||||
|
||||
nx,ny = pattern_values.shape
|
||||
xs_pattern = np.linspace(rescale*min(xs), rescale*max(xs), nx)
|
||||
ys_pattern = np.linspace(rescale*min(ys), rescale*max(ys), ny)
|
||||
pattern_dataArray = xr.DataArray(pattern_values, coords=dict(x=xs_pattern, y=ys_pattern))
|
||||
pattern_interp = pattern_dataArray.interp(x=xs, y=ys)
|
||||
|
||||
pattern_final = npa.nan_to_num(pattern_interp.values, nan=npa.min(pattern_interp))
|
||||
pattern_to_return = npa.nan_to_num(pattern_interp.values, nan=npa.min(pattern_interp)).flatten()
|
||||
|
||||
#make a plot before returning
|
||||
plt.figure()
|
||||
ax = plt.gca()
|
||||
im = ax.imshow(
|
||||
pattern_final,
|
||||
extent=(min(xs), max(xs), min(ys), max(ys)),
|
||||
origin='lower',
|
||||
cmap='inferno',
|
||||
)
|
||||
ax.set_xlabel('x (um)')
|
||||
ax.set_ylabel('y (um)')
|
||||
ax.set_title('Target Intensity Pattern')
|
||||
plt.colorbar(im, ax=ax, label='Intensity (a.u.)')
|
||||
plt.savefig("results/target_intensity_pattern.pdf")
|
||||
|
||||
return pattern_to_return
|
||||
|
||||
def normalization_sim() -> np.ndarray:
|
||||
''' Computes the Far-Field pattern at the monitor location and
|
||||
without the design region. Used for normalization purposes.
|
||||
|
||||
Returns:
|
||||
A 2D-array [Nx*Ny,num_wavelengths] of reference intensities, for each wavelength,
|
||||
at the monitor location.
|
||||
'''
|
||||
|
||||
sources = [
|
||||
mp.Source(
|
||||
src=mp.GaussianSource(
|
||||
frequency=frequency_center,
|
||||
fwidth=frequency_width),
|
||||
component=mp.Ex,
|
||||
center=SOURCE_CENTER,
|
||||
size=SOURCE_SIZE,
|
||||
)
|
||||
]
|
||||
|
||||
substrate_block = mp.Block(
|
||||
center = SUBSTRATE_CENTER,
|
||||
size = SUBSTRATE_SIZE,
|
||||
material = SILICON_DIOXIDE,
|
||||
)
|
||||
|
||||
geometry = [#combine geometries
|
||||
substrate_block,
|
||||
]
|
||||
|
||||
norm_sim = mp.Simulation(
|
||||
resolution=RESOLUTION,
|
||||
default_material=AIR,
|
||||
cell_size=cell_um,
|
||||
sources=sources,
|
||||
geometry=geometry,
|
||||
boundary_layers=pml_layers,
|
||||
k_point=mp.Vector3(),
|
||||
)
|
||||
|
||||
#plt.figure()
|
||||
#ax = plt.gca()
|
||||
#norm_sim.plot2D(output_plane=view_2D_plane, ax=ax)
|
||||
#plt.savefig("results/normalization_sim_setup.pdf")
|
||||
#plt.close()
|
||||
|
||||
NearRegions = [mp.Near2FarRegion(
|
||||
center=NEAR_REGION_MONITOR_CENTER,
|
||||
size=NEAR_REGION_MONITOR_SIZE,
|
||||
weight=+1,
|
||||
)]
|
||||
|
||||
norm_near2far = norm_sim.add_near2far(frequencies, *NearRegions)
|
||||
|
||||
norm_sim.run(
|
||||
#mp.at_every(1,mp.in_volume(
|
||||
# mp.Volume(center=mp.Vector3(), size=physical_domains_size),
|
||||
# mp.output_efield_x
|
||||
#)),
|
||||
#mp.at_every(20,record_fields),
|
||||
until_after_sources=stop_cond
|
||||
)
|
||||
#make a plot of the fields when simulation finished
|
||||
#plt.figure()
|
||||
#ax = plt.gca()
|
||||
#norm_sim.plot2D(output_plane=view_2D_plane, fields=mp.Ex, ax=ax)
|
||||
#plt.savefig("results/fields_after_norm_sim.pdf")
|
||||
#plt.close()
|
||||
|
||||
ref_fields = np.array(
|
||||
[norm_sim.get_farfield(norm_near2far, point) for point in ff_points]
|
||||
)
|
||||
|
||||
#select only the Ex component for all frequencies,
|
||||
# which is the one we will optimize for
|
||||
ref_fields_x_f1 = ref_fields[:,0]
|
||||
ref_fields_x_f2 = ref_fields[:,6]
|
||||
ref_fields_x_f3 = ref_fields[:,12]
|
||||
#concatenate the Ex component for all frequencies into a single array
|
||||
#the rows are the points, the columns are the frequencies
|
||||
ref_fields_x = npa.stack((ref_fields_x_f1, ref_fields_x_f2, ref_fields_x_f3), axis=-1)
|
||||
|
||||
ref_intensity_x = npa.power(npa.abs(ref_fields_x),2)
|
||||
return ref_intensity_x
|
||||
|
||||
def intensity_from_farfields(FarFields) -> np.ndarray:
|
||||
'''Computes the intensity pattern at the monitor location from the simulated far-fields
|
||||
|
||||
Args:
|
||||
FarFields: the simulated far-field patterns at the farfield
|
||||
monitor location, for each wavelength
|
||||
Returns:
|
||||
A 2D-array [Nx*Ny,num_wavelengths] of intensities, for each wavelength,
|
||||
at the monitor location.
|
||||
'''
|
||||
#select only the Ex component for all frequencies,
|
||||
# which is the one we will optimize for.
|
||||
#Reshape into matrix form (Nx,Ny)
|
||||
#print(FarFields.shape)
|
||||
#fields_x_f1 = FarFields[:,0,0].reshape(NX_DESIGN_GRID, NY_DESIGN_GRID)
|
||||
#fields_x_f2 = FarFields[:,1,0].reshape(NX_DESIGN_GRID, NY_DESIGN_GRID)
|
||||
#fields_x_f3 = FarFields[:,2,0].reshape(NX_DESIGN_GRID, NY_DESIGN_GRID)
|
||||
#concatenate the Ex component for all frequencies into a single array
|
||||
#the rows are the points, the columns are the frequencies
|
||||
#make a 3d arrray with dimensions (Nx,Ny,num_wavelengths)
|
||||
#fields_stack_3d = npa.stack((fields_x_f1, fields_x_f2, fields_x_f3), axis=-1)
|
||||
#fields_x = np.stack((fields_x_f1, fields_x_f2, fields_x_f3), axis=-1)
|
||||
|
||||
intensity_x = npa.abs(FarFields[:,:,0]) ** 2
|
||||
|
||||
return intensity_x
|
||||
|
||||
def intensity_optimization(
|
||||
ref_intensity: np.ndarray,
|
||||
use_damping:bool,
|
||||
use_epsavg: bool,
|
||||
sigmoid_bias: float,
|
||||
)-> mpa.OptimizationProblem:
|
||||
'''Sets up the optimization problem for the intensity pattern matching
|
||||
|
||||
Args:
|
||||
ref_intensity: 2D array of reference intensities at the monitor location, for each wavelength
|
||||
use_damping: whether to use damping in the optimization algorithm
|
||||
use_epsavg: whether to use subpixel smoothing in the optimization
|
||||
sigmoid_bias: bias parameter for projection
|
||||
Returns:
|
||||
The optimization problem class instance
|
||||
'''
|
||||
matgrid = mp.MaterialGrid(
|
||||
grid_size = mp.Vector3(NX_DESIGN_GRID, NY_DESIGN_GRID,0),
|
||||
medium1 = AIR,
|
||||
medium2 = SILICON,
|
||||
weights = np.ones((NX_DESIGN_GRID, NY_DESIGN_GRID)),
|
||||
beta = sigmoid_bias if use_epsavg else 0,
|
||||
do_averaging = True if use_epsavg else False,
|
||||
damping = 0.02*2*np.pi*frequency_center if use_damping else 0,
|
||||
)
|
||||
|
||||
matgrid_region = mpa.DesignRegion(
|
||||
matgrid,
|
||||
volume=mp.Volume(center=DESIGN_REGION_CENTER,
|
||||
size=DESIGN_REGION_UM
|
||||
)
|
||||
)
|
||||
|
||||
matgrid_block = mp.Block(
|
||||
center=matgrid_region.center,
|
||||
size=matgrid_region.size,
|
||||
material=matgrid,
|
||||
)
|
||||
|
||||
substrate_block = mp.Block(
|
||||
center = SUBSTRATE_CENTER,
|
||||
size = SUBSTRATE_SIZE,
|
||||
material = SILICON_DIOXIDE,
|
||||
)
|
||||
|
||||
geometry = [
|
||||
matgrid_block,
|
||||
substrate_block,
|
||||
]
|
||||
|
||||
sources = [
|
||||
mp.Source(
|
||||
src=mp.GaussianSource(frequency_center, fwidth=frequency_width),
|
||||
component=mp.Ex,
|
||||
size=SOURCE_SIZE,
|
||||
center=SOURCE_CENTER,
|
||||
)
|
||||
]
|
||||
|
||||
sim = mp.Simulation(
|
||||
resolution=RESOLUTION,
|
||||
default_material=AIR,
|
||||
cell_size=cell_um,
|
||||
sources=sources,
|
||||
geometry=geometry,
|
||||
boundary_layers=pml_layers,
|
||||
k_point=mp.Vector3(),
|
||||
)
|
||||
|
||||
NearRegions = [
|
||||
mp.Near2FarRegion(
|
||||
center=NEAR_REGION_MONITOR_CENTER,
|
||||
size=NEAR_REGION_MONITOR_SIZE,
|
||||
weight=+1,
|
||||
)
|
||||
]
|
||||
|
||||
FarFields = mpa.Near2FarFields(
|
||||
sim=sim,
|
||||
Near2FarRegions=NearRegions,
|
||||
far_pts=ff_points,
|
||||
)
|
||||
|
||||
obj_list = [FarFields]
|
||||
|
||||
#2D array of target intensity at the monitor location
|
||||
target_intensity = intensity_desired_fn_pattern(xs, ys)
|
||||
target_intensity_3d = npa.tile(target_intensity[:,npa.newaxis], (1,num_wavelengths))
|
||||
target_intensity_mean = npa.mean(target_intensity)
|
||||
|
||||
def J1(FarFields):
|
||||
'''Objective function. Computes the distance between simulated and target intensity patterns.
|
||||
Args:
|
||||
FarFields: the simulated far-field patterns at the monitor location, for each wavelength
|
||||
Returns:
|
||||
The distance between simulated and target intensity patterns
|
||||
'''
|
||||
|
||||
measured_intensity = intensity_from_farfields(FarFields)
|
||||
|
||||
#measured_intensity_mean = npa.mean(measured_intensity, axis=0)
|
||||
|
||||
translated_measured_intensity = measured_intensity / npa.mean(ref_intensity,axis=0) * target_intensity_mean
|
||||
|
||||
translated_measured_intensity = npa.clip(translated_measured_intensity, a_min=0, a_max=1)
|
||||
|
||||
distance = translated_measured_intensity - target_intensity_3d
|
||||
|
||||
return npa.power(npa.sum(distance**2, axis=0),0.5)
|
||||
|
||||
|
||||
opt = mpa.OptimizationProblem(
|
||||
simulation=sim,
|
||||
objective_functions=[J1],
|
||||
objective_arguments=obj_list,
|
||||
design_regions=[matgrid_region],
|
||||
frequencies=frequencies,
|
||||
maximum_run_time=MAX_RUN_TIME
|
||||
)
|
||||
|
||||
plt.figure()
|
||||
ax = plt.gca()
|
||||
opt.plot2D(True,output_plane=view_2D_plane, ax=ax)
|
||||
plt.savefig("results/optimization_problem_setup.pdf")
|
||||
|
||||
return opt
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Hello there! Starting normalization")
|
||||
ref_intensity = normalization_sim()
|
||||
|
||||
num_weights = NX_DESIGN_GRID * NY_DESIGN_GRID
|
||||
|
||||
epigraph_and_weights = np.ones((num_weights,)) * 0.5
|
||||
epigraph_and_weights = np.insert(epigraph_and_weights, 0, 1.2)
|
||||
|
||||
weights_lower_bound = np.zeros(num_weights)
|
||||
weights_upper_bound = np.ones(num_weights)
|
||||
epigraph_and_weights_lower_bound = np.insert(weights_lower_bound, 0, -np.inf)
|
||||
epigraph_and_weights_upper_bound = np.insert(weights_upper_bound, 0, +np.inf)
|
||||
|
||||
objfunc_history = []
|
||||
epivar_history = []
|
||||
cur_iter = [0]
|
||||
|
||||
sigmoid_bias_threshold = 64
|
||||
|
||||
#sigmoid_biases = [8, 16, 32, 64, 128, 256]
|
||||
sigmoid_biases = [8, 16, 32]
|
||||
#max_evals = [48, 48, 60, 60, 60, 60]
|
||||
max_evals = [48, 48, 60]
|
||||
|
||||
epigraph_tolerance = np.array([1e-4]*num_wavelengths)
|
||||
tolerance_width_and_spacing = np.array([1e-8]*2)
|
||||
|
||||
for sigmoid_bias, max_eval in zip(sigmoid_biases, max_evals):
|
||||
print(f"Starting optimization epoch with sigmoid bias {sigmoid_bias} and max evals {max_eval}")
|
||||
#the optimizer needs to work with the weights and epigraph (+1)
|
||||
solver = nlopt.opt(nlopt.LD_CCSAQ, num_weights + 1)
|
||||
solver.set_lower_bounds(epigraph_and_weights_lower_bound)
|
||||
solver.set_upper_bounds(epigraph_and_weights_upper_bound)
|
||||
solver.set_min_objective(obj_fun)
|
||||
solver.set_maxeval(max_eval)
|
||||
solver.set_param("dual_ftol_rel", 1e-7)
|
||||
solver.add_inequality_mconstraint(
|
||||
lambda result_, epigraph_and_weights_, grad_: epigraph_constraint(
|
||||
result_,
|
||||
epigraph_and_weights_,
|
||||
grad_,
|
||||
SIGMOID_THRESHOLD_INTRINSIC,
|
||||
sigmoid_bias,
|
||||
use_epsavg=False if sigmoid_bias < sigmoid_bias_threshold else True,
|
||||
),
|
||||
epigraph_tolerance,
|
||||
)
|
||||
solver.set_param("verbosity",1)
|
||||
if sigmoid_bias < sigmoid_bias_threshold:
|
||||
use_epsavg = False
|
||||
else:
|
||||
use_epsavg = True
|
||||
|
||||
opt = intensity_optimization(
|
||||
ref_intensity,
|
||||
use_damping=True,
|
||||
use_epsavg=use_epsavg,
|
||||
sigmoid_bias=sigmoid_bias,
|
||||
)
|
||||
|
||||
#the minimum linewidth and spacing constraint
|
||||
#is activated only in the last epoch. This is done
|
||||
#by adding the corresponding constraints to the same solver object.
|
||||
#constraints are added as lambda functions, which
|
||||
#are called directly by the nlopt solver.
|
||||
if sigmoid_bias == sigmoid_biases[-1]: #if we are at the last sigmoid bias...
|
||||
line_width_and_spacing = np.zeros(2)
|
||||
grad_line_width_and_spacing = np.zeros((2, num_weights + 1))
|
||||
#compute the value of the costraints at this time
|
||||
linewidth_constraint_val = line_width_and_spacing_constraint(
|
||||
line_width_and_spacing,
|
||||
epigraph_and_weights,
|
||||
grad_line_width_and_spacing,
|
||||
sigmoid_bias,
|
||||
)
|
||||
solver.add_inequality_mconstraint(
|
||||
lambda result_, epigraph_and_weights_, grad_: line_width_and_spacing_constraint(
|
||||
result_,
|
||||
epigraph_and_weights_,
|
||||
grad_,
|
||||
sigmoid_bias,
|
||||
),
|
||||
tolerance_width_and_spacing,
|
||||
)
|
||||
|
||||
#execute a SINGLE forward run before the start of each epoch and
|
||||
#manually set the initial epigraph variable to slightly larger
|
||||
#than the largest value of the objective function over the
|
||||
#wavelengths and the lengthscale constraint
|
||||
#making a call to opt like the following invokes
|
||||
#the __call__ method of the opt instance and starts the optimization
|
||||
print("Calibrating epigraph variable before starting optimization...")
|
||||
epigraph_initial, empty_grads = opt(
|
||||
[
|
||||
filter_and_project(
|
||||
epigraph_and_weights[1:],
|
||||
SIGMOID_THRESHOLD_INTRINSIC,
|
||||
sigmoid_bias if sigmoid_bias < sigmoid_bias_threshold else 0,
|
||||
),
|
||||
],
|
||||
need_gradient=False,
|
||||
)
|
||||
#epigraph_initial contains the objective function values
|
||||
#at each wavelength in the simulation. At each epoch,
|
||||
#we set the initial epigraph variable taking the max of the objective functions
|
||||
epigraph_and_weights[0] = np.max(epigraph_initial)
|
||||
fraction_max_epigraph = 0.05
|
||||
if sigmoid_bias == sigmoid_biases[-1]:#if last epoch
|
||||
epigraph_and_weights[0] = \
|
||||
(1 + fraction_max_epigraph) * max(
|
||||
epigraph_and_weights[0], linewidth_constraint_val)
|
||||
print(
|
||||
f"epigraph-calibration:, bias = {sigmoid_bias}, "
|
||||
f"{str_from_list(epigraph_initial)}, {epigraph_and_weights[0]}"
|
||||
)
|
||||
|
||||
print("Starting optimization...")
|
||||
epigraph_and_weights[:] = solver.optimize(epigraph_and_weights)
|
||||
print("Optimization completed")
|
||||
|
||||
optimal_design_weights = filter_and_project(
|
||||
epigraph_and_weights[1:],
|
||||
SIGMOID_THRESHOLD_INTRINSIC,
|
||||
sigmoid_bias
|
||||
).reshape(-1, NY_DESIGN_GRID)
|
||||
|
||||
fig, ax = plt.subplots()
|
||||
ax.imshow(
|
||||
optimal_design_weights,
|
||||
cmap="binary",
|
||||
interpolation="none",
|
||||
)
|
||||
ax.set_axis_off()
|
||||
if mp.am_master():
|
||||
fig.savefig(
|
||||
f"optimal_design_beta{sigmoid_bias}.png",
|
||||
dpi=150,
|
||||
bbox_inches="tight",
|
||||
)
|
||||
# Save the final (unmapped) design as a 2D array in CSV format
|
||||
np.savetxt(
|
||||
f"unmapped_design_weights_beta{sigmoid_bias}.csv",
|
||||
epigraph_and_weights[1:].reshape(NX_DESIGN_GRID, NY_DESIGN_GRID),
|
||||
fmt="%4.2f",
|
||||
delimiter=",",
|
||||
)
|
||||
|
||||
plt.figure()
|
||||
plt.subplot(1,2,1)
|
||||
line1,line2,line3, = plt.plot(objfunc_history)
|
||||
plt.xlabel("Iteration")
|
||||
plt.ylabel("Value")
|
||||
plt.legend([line1, line2, line3], ["Obj. func. - 1.50 um", "Obj. func. - 1.55 um", "Obj. func. - 1.60 um"])
|
||||
plt.subplot(1,2,2)
|
||||
plt.plot(epivar_history, label="Epigraph")
|
||||
plt.xlabel("Iteration")
|
||||
plt.ylabel("Value")
|
||||
plt.legend()
|
||||
plt.savefig(f"results/optimization_history_beta{sigmoid_bias}.pdf")
|
||||
plt.close()
|
||||
|
||||
saveResults = True
|
||||
if saveResults:
|
||||
with open('results/optimal_design.pkl', 'wb') as f:
|
||||
pickle.dump({
|
||||
'RESOLUTION': RESOLUTION,
|
||||
'MAX_RUN_TIME': MAX_RUN_TIME,
|
||||
'WAVELENGTH_MIN_UM': WAVELENGTH_MIN_UM,
|
||||
'WAVELENGTH_MAX_UM': WAVELENGTH_MAX_UM,
|
||||
'PML_UM': PML_UM,
|
||||
'BUFFER': BUFFER,
|
||||
'DESIGN_WAVELENGTHS_UM': DESIGN_WAVELENGTHS_UM,
|
||||
'DESIGN_REGION_UM': DESIGN_REGION_UM,
|
||||
'DESIGN_REGION_RESOLUTION': DESIGN_REGION_RESOLUTION,
|
||||
'DESIGN_REGION_CENTER': DESIGN_REGION_CENTER,
|
||||
'NX_DESIGN_GRID': NX_DESIGN_GRID,
|
||||
'NY_DESIGN_GRID': NY_DESIGN_GRID,
|
||||
'MIN_LENGTH_UM': MIN_LENGTH_UM,
|
||||
'SILICON': SILICON,
|
||||
'SILICON_DIOXIDE': SILICON_DIOXIDE,
|
||||
'AIR': AIR,
|
||||
|
||||
'SIGMOID_THRESHOLD_INTRINSIC': SIGMOID_THRESHOLD_INTRINSIC,
|
||||
'SIGMOID_THRESHOLD_EROSION': SIGMOID_THRESHOLD_EROSION,
|
||||
'SIGMOID_THRESHOLD_DILATION': SIGMOID_THRESHOLD_DILATION,
|
||||
|
||||
'cell_um': cell_um,
|
||||
|
||||
'SUBSTRATE_THICKNESS': SUBSTRATE_THICKNESS,
|
||||
'SUBSTRATE_SIZE': SUBSTRATE_SIZE,
|
||||
'SUBSTRATE_CENTER': SUBSTRATE_CENTER,
|
||||
|
||||
'physical_domains_size': physical_domains_size,
|
||||
'filter_radius_um': filter_radius_um,
|
||||
|
||||
'frequency_min': frequency_min,
|
||||
'frequency_max': frequency_max,
|
||||
'frequency_center': frequency_center,
|
||||
'wavelength_center': wavelength_center,
|
||||
'frequency_width': frequency_width,
|
||||
'frequencies': frequencies,
|
||||
'num_wavelengths': num_wavelengths,
|
||||
|
||||
'SOURCE_CENTER': SOURCE_CENTER,
|
||||
'SOURCE_SIZE': SOURCE_SIZE,
|
||||
|
||||
'NEAR_REGION_MONITOR_CENTER': NEAR_REGION_MONITOR_CENTER,
|
||||
'NEAR_REGION_MONITOR_SIZE': NEAR_REGION_MONITOR_SIZE,
|
||||
|
||||
'FF_MONITOR_CENTER': FF_MONITOR_CENTER,
|
||||
'FF_MONITOR_SIZE': FF_MONITOR_SIZE,
|
||||
'xs': xs,
|
||||
'ys': ys,
|
||||
'ff_points': ff_points,
|
||||
|
||||
'sigmoid_biases': sigmoid_biases,
|
||||
'max_eval': max_eval,
|
||||
'objfunc_history': objfunc_history,
|
||||
'epivar_history': epivar_history,
|
||||
'epigraph_variable': epigraph_and_weights[0],
|
||||
'unmapped_design_weights': epigraph_and_weights[1:],
|
||||
'optimal_design_weights': optimal_design_weights,
|
||||
}, f)
|
||||
|
||||
print("Simulation completed")
|
||||
@@ -1,827 +0,0 @@
|
||||
|
||||
import faulthandler
|
||||
faulthandler.enable()
|
||||
from typing import List, NamedTuple, Tuple
|
||||
from autograd import numpy as npa, tensor_jacobian_product, grad
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.animation as animation
|
||||
import meep as mp
|
||||
import meep.adjoint as mpa
|
||||
import nlopt
|
||||
import numpy as np
|
||||
import os
|
||||
import xarray as xr
|
||||
from PIL import Image
|
||||
import pickle
|
||||
mp.verbosity(3)
|
||||
|
||||
if mp.am_master():
|
||||
if not os.path.exists("results"):
|
||||
os.makedirs("results")
|
||||
|
||||
###### PHYSICS SETUP ######
|
||||
RESOLUTION = 20 #pixels per meep unit length (1um)
|
||||
MAX_RUN_TIME = 700 #meep units
|
||||
WAVELENGTH_MIN_UM = 1.50
|
||||
WAVELENGTH_MAX_UM = 1.60
|
||||
PML_UM = 1.0 #PML thickness
|
||||
BUFFER = 3.0
|
||||
DESIGN_WAVELENGTHS_UM = (1.50,1.55,1.60) #wavelengths at which to optimize the device
|
||||
DESIGN_REGION_UM = mp.Vector3(4,4,1)
|
||||
DESIGN_REGION_RESOLUTION = int(RESOLUTION)
|
||||
DESIGN_REGION_CENTER = mp.Vector3(0, 0, DESIGN_REGION_UM.z / 2)
|
||||
NX_DESIGN_GRID = int(DESIGN_REGION_UM.x * DESIGN_REGION_RESOLUTION) + 1
|
||||
NY_DESIGN_GRID = int(DESIGN_REGION_UM.y * DESIGN_REGION_RESOLUTION) + 1
|
||||
MIN_LENGTH_UM = 0.5 #500 nm minimum length scale
|
||||
SILICON = mp.Medium(index = 3.5)
|
||||
SILICON_DIOXIDE = mp.Medium(index = 1.5)
|
||||
AIR = mp.Medium(index = 1.0)
|
||||
|
||||
SIGMOID_THRESHOLD_INTRINSIC = 0.5
|
||||
SIGMOID_THRESHOLD_EROSION = 0.75
|
||||
SIGMOID_THRESHOLD_DILATION = 1 - SIGMOID_THRESHOLD_EROSION
|
||||
|
||||
cell_um = mp.Vector3(
|
||||
DESIGN_REGION_UM.x + 2*BUFFER + 2*PML_UM,
|
||||
DESIGN_REGION_UM.y + 2*BUFFER + 2*PML_UM,
|
||||
2*BUFFER + 2*PML_UM
|
||||
)
|
||||
|
||||
SUBSTRATE_THICKNESS = BUFFER + PML_UM
|
||||
SUBSTRATE_SIZE = mp.Vector3(cell_um.x, cell_um.y, SUBSTRATE_THICKNESS)
|
||||
SUBSTRATE_CENTER = mp.Vector3(0, 0, -SUBSTRATE_THICKNESS/2)
|
||||
|
||||
physical_domains_size = mp.Vector3(
|
||||
DESIGN_REGION_UM.x + 2*BUFFER,
|
||||
DESIGN_REGION_UM.y + 2*BUFFER,
|
||||
2*BUFFER
|
||||
)
|
||||
|
||||
filter_radius_um = mpa.get_conic_radius_from_eta_e(
|
||||
MIN_LENGTH_UM, SIGMOID_THRESHOLD_EROSION
|
||||
)
|
||||
|
||||
frequency_min = 1 / WAVELENGTH_MAX_UM
|
||||
frequency_max = 1 / WAVELENGTH_MIN_UM
|
||||
frequency_center = 0.5 * (frequency_min + frequency_max)
|
||||
wavelength_center = 1 / frequency_center #remember that c = 1 in meep units
|
||||
frequency_width = frequency_max - frequency_min
|
||||
frequencies = [1/wavelength for wavelength in DESIGN_WAVELENGTHS_UM]
|
||||
num_wavelengths = len(DESIGN_WAVELENGTHS_UM)
|
||||
|
||||
SOURCE_CENTER = mp.Vector3(0,0,-BUFFER/2)
|
||||
SOURCE_SIZE = mp.Vector3(physical_domains_size.x,physical_domains_size.y,0)
|
||||
|
||||
NEAR_REGION_MONITOR_CENTER = mp.Vector3(0,0, DESIGN_REGION_UM.z + BUFFER/2)
|
||||
NEAR_REGION_MONITOR_SIZE = mp.Vector3(physical_domains_size.x,physical_domains_size.y,0)
|
||||
|
||||
FF_MONITOR_CENTER = mp.Vector3(0,0,DESIGN_REGION_UM.z + BUFFER)
|
||||
FF_MONITOR_SIZE = mp.Vector3(DESIGN_REGION_UM.x, DESIGN_REGION_UM.y, 0)
|
||||
xs = ys = np.linspace(-FF_MONITOR_SIZE.x/2, FF_MONITOR_SIZE.x/2, NX_DESIGN_GRID)
|
||||
#collection of points in the far-field monitor
|
||||
ff_points = [mp.Vector3(x_p,y_p,FF_MONITOR_CENTER.z) for x_p in xs for y_p in ys]
|
||||
|
||||
stop_cond = mp.stop_when_fields_decayed(25, mp.Ex, NEAR_REGION_MONITOR_CENTER, 1e-6)
|
||||
|
||||
pml_layers = [mp.PML(PML_UM)]
|
||||
|
||||
view_2D_plane = mp.Volume(
|
||||
center=mp.Vector3(0,0,0),
|
||||
size=mp.Vector3(cell_um.x,0,cell_um.z)
|
||||
)
|
||||
|
||||
def str_from_list(list_: List[float]) -> str:
|
||||
return "[" + ", ".join(f"{val:.4f}" for val in list_) + "]"
|
||||
|
||||
def filter_and_project(
|
||||
weights: np.ndarray,
|
||||
sigmoid_threshold: float,
|
||||
sigmoid_bias: float
|
||||
) -> np.ndarray:
|
||||
'''Function to filter and project design variables. Differentiable by autograd
|
||||
|
||||
Args:
|
||||
weights: design weights as a flattened (1D) array
|
||||
sigmoid_threshold: erosion/dilation parameter for projection
|
||||
sigmoid_bias: bias parameter for the projection. 0 is no projection
|
||||
Returns:
|
||||
The flattened (1D) filtered/projected design weights
|
||||
'''
|
||||
|
||||
print("Inside filter_and_project function...")
|
||||
weights_filtered = mpa.conic_filter(
|
||||
weights.reshape(-1, NY_DESIGN_GRID), #row major mapping
|
||||
filter_radius_um,
|
||||
DESIGN_REGION_UM.x,
|
||||
DESIGN_REGION_UM.y,
|
||||
DESIGN_REGION_RESOLUTION,
|
||||
)
|
||||
|
||||
if sigmoid_bias == 0:
|
||||
print("No projection applied in filter_and_project function, exiting...")
|
||||
return weights_filtered.flatten()
|
||||
else:
|
||||
weights_projected = mpa.tanh_projection(
|
||||
weights_filtered.reshape(-1,NY_DESIGN_GRID),
|
||||
sigmoid_bias,
|
||||
sigmoid_threshold,
|
||||
)
|
||||
print("Projection applied in filter_and_project function, exiting...")
|
||||
return weights_projected.flatten()
|
||||
|
||||
def obj_fun(epigraph_and_weights: np.ndarray, grad: np.ndarray)-> float:
|
||||
'''Objective function for the epigraph formulation
|
||||
|
||||
Args:
|
||||
epigraph_and_weights: 1D array containing epigraph variable
|
||||
(at index 0) and design weigts (remaining elements)
|
||||
grad: the gradient modified in place of the objective function (flattened 1D array)
|
||||
Returns:
|
||||
The scalar epigraph variable (the objective function)
|
||||
'''
|
||||
|
||||
epigraph = epigraph_and_weights[0]
|
||||
|
||||
if grad.size > 0:
|
||||
grad[0] = 1
|
||||
grad[1:] = 0
|
||||
|
||||
return epigraph
|
||||
|
||||
def epigraph_constraint(
|
||||
result: np.ndarray,
|
||||
epigraph_and_weights: np.ndarray,
|
||||
gradient: np.ndarray,
|
||||
sigmoid_threshold: float,
|
||||
sigmoid_bias: float,
|
||||
use_epsavg: bool,
|
||||
) -> None:
|
||||
'''Constraint function for the epigraph formulation
|
||||
|
||||
Args:
|
||||
result: 1D array, modified in place, with the results
|
||||
of this constraint evaluation
|
||||
epigraph_and_weights: 1D array of epigraph (first element)
|
||||
and design weights (other elements)
|
||||
gradient: the jacobian matrix (backpropagated) with dimensions
|
||||
(1+NX_DESIGN_GRID*NY_DESIGN_GRID, 2 * num_wavelengths), modified in place.
|
||||
these gradients are used by the optimization algorithm,
|
||||
because, being backpropagated, they tell us how the constraint function behaves
|
||||
with respect to changes in the design parameters.
|
||||
sigmoid_threshold: erosion/dilation parameter for projection.
|
||||
sigmoid_bias: bias parameter for projection.
|
||||
use_epsavg: whether to use subpixel smoothing.
|
||||
'''
|
||||
print("Inside epigraph_constraint function...")
|
||||
epigraph = epigraph_and_weights[0]
|
||||
weights = epigraph_and_weights[1:]
|
||||
assert not np.isnan(weights).any(), "NaN values found in weights in epigraph_constraint function"
|
||||
print("===========")
|
||||
print("Shape of weights in epigraph_constraint:", weights.shape)
|
||||
print("===========")
|
||||
obj_val, grad = opt(
|
||||
[
|
||||
filter_and_project(
|
||||
weights, sigmoid_threshold, 0 if use_epsavg else sigmoid_bias
|
||||
)
|
||||
]
|
||||
)
|
||||
print("filter_and_project called")
|
||||
#in our case there is only one objective function,
|
||||
#which needs to be evaluated at multiple wavelengths.
|
||||
#for this reason, obj_val is a 1-element list (to be verified,
|
||||
# it might be that the list is directly replaced by the content)
|
||||
# that contains an array of objective function values at each wavelength.
|
||||
print("Backpropagating gradients...")
|
||||
#for each wavelength, backpropagate
|
||||
for k in range(num_wavelengths):
|
||||
grad[:,k] = tensor_jacobian_product(filter_and_project, 0)(
|
||||
weights,
|
||||
sigmoid_threshold,
|
||||
sigmoid_bias,
|
||||
grad[:,k],
|
||||
)
|
||||
|
||||
#modify in place the gradient result
|
||||
if gradient.size > 0:
|
||||
gradient[:,0] = -1 #gradient w.r.t epigraph
|
||||
gradient[:,1:] = grad.T
|
||||
|
||||
#modify in place the constraint result
|
||||
result[:] = np.real(obj_val) - epigraph
|
||||
|
||||
print("Updating history...")
|
||||
objfunc_history.append(np.real(obj_val))
|
||||
epivar_history.append(epigraph)
|
||||
|
||||
print(
|
||||
f"iteration:, {cur_iter[0]:3d}, sigmoid_bias: {sigmoid_bias:2d}, "
|
||||
f"epigraph: {epigraph:.5f}, obj. func.: {obj_val}, "
|
||||
f"epigraph constraint: {str_from_list(result)}"
|
||||
)
|
||||
print("Finished epigraph_constraint function")
|
||||
cur_iter[0] = cur_iter[0] + 1
|
||||
|
||||
def line_width_and_spacing_constraint(
|
||||
result: np.ndarray,
|
||||
epigraph_and_weights: np.ndarray,
|
||||
gradient: np.ndarray,
|
||||
sigmoid_bias: float,
|
||||
) -> float:
|
||||
'''Constraint function for minimum line width and spacing
|
||||
|
||||
Args:
|
||||
result: evaluation of this constraint function, modified in place
|
||||
epigraph_and_weights: 1D array of epigraph (first element)
|
||||
and design weights (other elements)
|
||||
gradient: the Jacobian matrix, modified in place
|
||||
sigmoid_bias: bias parameter for projection
|
||||
|
||||
Returns:
|
||||
The constraint value result
|
||||
'''
|
||||
|
||||
epigraph = epigraph_and_weights[0]
|
||||
weights = epigraph_and_weights[1:]
|
||||
|
||||
a1 = 1e-3
|
||||
b1 = 0
|
||||
gradient[:,0] = -a1
|
||||
|
||||
filter_func = lambda a: mpa.conic_filter(
|
||||
a.reshape(-1,NY_DESIGN_GRID),
|
||||
filter_radius_um,
|
||||
DESIGN_REGION_UM.x,
|
||||
DESIGN_REGION_UM.y,
|
||||
DESIGN_REGION_RESOLUTION,
|
||||
)
|
||||
|
||||
threshold_func = lambda a: mpa.tanh_projection(
|
||||
a.reshape(-1,NY_DESIGN_GRID),
|
||||
sigmoid_bias,
|
||||
SIGMOID_THRESHOLD_INTRINSIC,
|
||||
)
|
||||
|
||||
c0 = 1e7 * (filter_radius_um * 1 / RESOLUTION) ** 4
|
||||
|
||||
#constraints as lambda functions
|
||||
M1 = lambda a: mpa.constraint_solid(
|
||||
a,
|
||||
c0,
|
||||
SIGMOID_THRESHOLD_EROSION,
|
||||
filter_func,
|
||||
threshold_func,
|
||||
1
|
||||
)
|
||||
|
||||
M2 = lambda a: mpa.constraint_void(
|
||||
a,
|
||||
c0,
|
||||
SIGMOID_THRESHOLD_DILATION,
|
||||
filter_func,
|
||||
threshold_func,
|
||||
1
|
||||
)
|
||||
|
||||
#gradients of the constraints w.r.t design parameters
|
||||
g1 = grad(M1)(weights)
|
||||
g2 = grad(M2)(weights)
|
||||
|
||||
result[0] = M1(weights) - a1 * epigraph - b1
|
||||
result[1] = M2(weights) - a1 * epigraph - b1
|
||||
|
||||
gradient[0, 1:] = g1.flatten()
|
||||
gradient[1, 1:] = g2.flatten()
|
||||
|
||||
#these are the epigraph values for the constraints
|
||||
t1 = (M1(weights) - b1) / a1
|
||||
t2 = (M2(weights) - b1) / a1
|
||||
|
||||
print(f"line_width_and_spacing_constraint:, {result[0]}, {result[1]}, {t1}, {t2}")
|
||||
|
||||
return max(t1, t2)
|
||||
|
||||
def get_pattern() -> np.ndarray:
|
||||
'''Imports the intensity pattern from file'''
|
||||
pattern_name = 'peg.png'
|
||||
pattern = Image.open('misc/' + pattern_name).convert('L')
|
||||
pattern = np.array(pattern).astype(float)
|
||||
|
||||
pattern = pattern - np.min(pattern)
|
||||
pattern = pattern / np.max(pattern)
|
||||
|
||||
return pattern
|
||||
|
||||
def intensity_desired_fn_pattern(
|
||||
xs: list,
|
||||
ys: list,
|
||||
rescale: float = 0.5
|
||||
) -> np.ndarray:
|
||||
'''Returns the pattern intensity value at the specified
|
||||
(xs,ys) locations, with a scale factor
|
||||
|
||||
Args:
|
||||
xs: list of x coordinates
|
||||
ys: list of y coordinates
|
||||
rescale: scale factor for the pattern
|
||||
Returns:
|
||||
The flattened pattern intensity value at the specified (xs,ys) locations, with a scale factor
|
||||
'''
|
||||
pattern_values = get_pattern()
|
||||
pattern_values = 1 - np.rot90(np.rot90(np.rot90(pattern_values)))
|
||||
|
||||
nx,ny = pattern_values.shape
|
||||
xs_pattern = np.linspace(rescale*min(xs), rescale*max(xs), nx)
|
||||
ys_pattern = np.linspace(rescale*min(ys), rescale*max(ys), ny)
|
||||
pattern_dataArray = xr.DataArray(pattern_values, coords=dict(x=xs_pattern, y=ys_pattern))
|
||||
pattern_interp = pattern_dataArray.interp(x=xs, y=ys)
|
||||
|
||||
pattern_final = npa.nan_to_num(pattern_interp.values, nan=npa.min(pattern_interp))
|
||||
pattern_to_return = npa.nan_to_num(pattern_interp.values, nan=npa.min(pattern_interp)).flatten()
|
||||
|
||||
#make a plot before returning
|
||||
plt.figure()
|
||||
ax = plt.gca()
|
||||
im = ax.imshow(
|
||||
pattern_final,
|
||||
extent=(min(xs), max(xs), min(ys), max(ys)),
|
||||
origin='lower',
|
||||
cmap='inferno',
|
||||
)
|
||||
ax.set_xlabel('x (um)')
|
||||
ax.set_ylabel('y (um)')
|
||||
ax.set_title('Target Intensity Pattern')
|
||||
plt.colorbar(im, ax=ax, label='Intensity (a.u.)')
|
||||
plt.savefig("results/target_intensity_pattern.pdf")
|
||||
|
||||
return pattern_to_return
|
||||
|
||||
def normalization_sim() -> np.ndarray:
|
||||
''' Computes the Far-Field pattern at the monitor location and
|
||||
without the design region. Used for normalization purposes.
|
||||
|
||||
Returns:
|
||||
A 2D-array [Nx*Ny,num_wavelengths] of reference intensities, for each wavelength,
|
||||
at the monitor location.
|
||||
'''
|
||||
|
||||
sources = [
|
||||
mp.Source(
|
||||
src=mp.GaussianSource(
|
||||
frequency=frequency_center,
|
||||
fwidth=frequency_width),
|
||||
component=mp.Ex,
|
||||
center=SOURCE_CENTER,
|
||||
size=SOURCE_SIZE,
|
||||
)
|
||||
]
|
||||
|
||||
substrate_block = mp.Block(
|
||||
center = SUBSTRATE_CENTER,
|
||||
size = SUBSTRATE_SIZE,
|
||||
material = SILICON_DIOXIDE,
|
||||
)
|
||||
|
||||
geometry = [#combine geometries
|
||||
substrate_block,
|
||||
]
|
||||
|
||||
norm_sim = mp.Simulation(
|
||||
resolution=RESOLUTION,
|
||||
default_material=AIR,
|
||||
cell_size=cell_um,
|
||||
sources=sources,
|
||||
geometry=geometry,
|
||||
boundary_layers=pml_layers,
|
||||
k_point=mp.Vector3(),
|
||||
)
|
||||
|
||||
#plt.figure()
|
||||
#ax = plt.gca()
|
||||
#norm_sim.plot2D(output_plane=view_2D_plane, ax=ax)
|
||||
#plt.savefig("results/normalization_sim_setup.pdf")
|
||||
#plt.close()
|
||||
|
||||
NearRegions = [mp.Near2FarRegion(
|
||||
center=NEAR_REGION_MONITOR_CENTER,
|
||||
size=NEAR_REGION_MONITOR_SIZE,
|
||||
weight=+1,
|
||||
)]
|
||||
|
||||
norm_near2far = norm_sim.add_near2far(frequencies, *NearRegions)
|
||||
|
||||
norm_sim.run(
|
||||
#mp.at_every(1,mp.in_volume(
|
||||
# mp.Volume(center=mp.Vector3(), size=physical_domains_size),
|
||||
# mp.output_efield_x
|
||||
#)),
|
||||
#mp.at_every(20,record_fields),
|
||||
until_after_sources=stop_cond
|
||||
)
|
||||
#make a plot of the fields when simulation finished
|
||||
#plt.figure()
|
||||
#ax = plt.gca()
|
||||
#norm_sim.plot2D(output_plane=view_2D_plane, fields=mp.Ex, ax=ax)
|
||||
#plt.savefig("results/fields_after_norm_sim.pdf")
|
||||
#plt.close()
|
||||
|
||||
ref_fields = np.array(
|
||||
[norm_sim.get_farfield(norm_near2far, point) for point in ff_points]
|
||||
)
|
||||
|
||||
#select only the Ex component for all frequencies,
|
||||
# which is the one we will optimize for
|
||||
ref_fields_x_f1 = ref_fields[:,0]
|
||||
ref_fields_x_f2 = ref_fields[:,6]
|
||||
ref_fields_x_f3 = ref_fields[:,12]
|
||||
#concatenate the Ex component for all frequencies into a single array
|
||||
#the rows are the points, the columns are the frequencies
|
||||
ref_fields_x = npa.stack((ref_fields_x_f1, ref_fields_x_f2, ref_fields_x_f3), axis=-1)
|
||||
|
||||
ref_intensity_x = npa.power(npa.abs(ref_fields_x),2)
|
||||
return ref_intensity_x
|
||||
|
||||
def intensity_from_farfields(FarFields) -> np.ndarray:
|
||||
'''Computes the intensity pattern at the monitor location from the simulated far-fields
|
||||
|
||||
Args:
|
||||
FarFields: the simulated far-field patterns at the farfield
|
||||
monitor location, for each wavelength
|
||||
Returns:
|
||||
A 2D-array [Nx*Ny,num_wavelengths] of intensities, for each wavelength,
|
||||
at the monitor location.
|
||||
'''
|
||||
#select only the Ex component for all frequencies,
|
||||
# which is the one we will optimize for.
|
||||
#Reshape into matrix form (Nx,Ny)
|
||||
#print(FarFields.shape)
|
||||
#fields_x_f1 = FarFields[:,0,0].reshape(NX_DESIGN_GRID, NY_DESIGN_GRID)
|
||||
#fields_x_f2 = FarFields[:,1,0].reshape(NX_DESIGN_GRID, NY_DESIGN_GRID)
|
||||
#fields_x_f3 = FarFields[:,2,0].reshape(NX_DESIGN_GRID, NY_DESIGN_GRID)
|
||||
#concatenate the Ex component for all frequencies into a single array
|
||||
#the rows are the points, the columns are the frequencies
|
||||
#make a 3d arrray with dimensions (Nx,Ny,num_wavelengths)
|
||||
#fields_stack_3d = npa.stack((fields_x_f1, fields_x_f2, fields_x_f3), axis=-1)
|
||||
#fields_x = np.stack((fields_x_f1, fields_x_f2, fields_x_f3), axis=-1)
|
||||
|
||||
intensity_x = npa.abs(FarFields[:,:,0]) ** 2
|
||||
|
||||
return intensity_x
|
||||
|
||||
def intensity_optimization(
|
||||
ref_intensity: np.ndarray,
|
||||
use_damping:bool,
|
||||
use_epsavg: bool,
|
||||
sigmoid_bias: float,
|
||||
)-> mpa.OptimizationProblem:
|
||||
'''Sets up the optimization problem for the intensity pattern matching
|
||||
|
||||
Args:
|
||||
ref_intensity: 2D array of reference intensities at the monitor location, for each wavelength
|
||||
use_damping: whether to use damping in the optimization algorithm
|
||||
use_epsavg: whether to use subpixel smoothing in the optimization
|
||||
sigmoid_bias: bias parameter for projection
|
||||
Returns:
|
||||
The optimization problem class instance
|
||||
'''
|
||||
matgrid = mp.MaterialGrid(
|
||||
grid_size = mp.Vector3(NX_DESIGN_GRID, NY_DESIGN_GRID,0),
|
||||
medium1 = AIR,
|
||||
medium2 = SILICON,
|
||||
weights = np.ones((NX_DESIGN_GRID, NY_DESIGN_GRID)),
|
||||
beta = sigmoid_bias if use_epsavg else 0,
|
||||
do_averaging = True if use_epsavg else False,
|
||||
damping = 0.02*2*np.pi*frequency_center if use_damping else 0,
|
||||
)
|
||||
|
||||
matgrid_region = mpa.DesignRegion(
|
||||
matgrid,
|
||||
volume=mp.Volume(center=DESIGN_REGION_CENTER,
|
||||
size=DESIGN_REGION_UM
|
||||
)
|
||||
)
|
||||
|
||||
matgrid_block = mp.Block(
|
||||
center=matgrid_region.center,
|
||||
size=matgrid_region.size,
|
||||
material=matgrid,
|
||||
)
|
||||
|
||||
substrate_block = mp.Block(
|
||||
center = SUBSTRATE_CENTER,
|
||||
size = SUBSTRATE_SIZE,
|
||||
material = SILICON_DIOXIDE,
|
||||
)
|
||||
|
||||
geometry = [
|
||||
matgrid_block,
|
||||
substrate_block,
|
||||
]
|
||||
|
||||
sources = [
|
||||
mp.Source(
|
||||
src=mp.GaussianSource(frequency_center, fwidth=frequency_width),
|
||||
component=mp.Ex,
|
||||
size=SOURCE_SIZE,
|
||||
center=SOURCE_CENTER,
|
||||
)
|
||||
]
|
||||
|
||||
sim = mp.Simulation(
|
||||
resolution=RESOLUTION,
|
||||
default_material=AIR,
|
||||
cell_size=cell_um,
|
||||
sources=sources,
|
||||
geometry=geometry,
|
||||
boundary_layers=pml_layers,
|
||||
k_point=mp.Vector3(),
|
||||
)
|
||||
|
||||
NearRegions = [
|
||||
mp.Near2FarRegion(
|
||||
center=NEAR_REGION_MONITOR_CENTER,
|
||||
size=NEAR_REGION_MONITOR_SIZE,
|
||||
weight=+1,
|
||||
)
|
||||
]
|
||||
|
||||
FarFields = mpa.Near2FarFields(
|
||||
sim=sim,
|
||||
Near2FarRegions=NearRegions,
|
||||
far_pts=ff_points,
|
||||
)
|
||||
|
||||
obj_list = [FarFields]
|
||||
|
||||
#2D array of target intensity at the monitor location
|
||||
target_intensity = intensity_desired_fn_pattern(xs, ys)
|
||||
target_intensity_3d = npa.tile(target_intensity[:,npa.newaxis], (1,num_wavelengths))
|
||||
target_intensity_mean = npa.mean(target_intensity)
|
||||
|
||||
def J1(FarFields):
|
||||
'''Objective function. Computes the distance between simulated and target intensity patterns.
|
||||
Args:
|
||||
FarFields: the simulated far-field patterns at the monitor location, for each wavelength
|
||||
Returns:
|
||||
The distance between simulated and target intensity patterns
|
||||
'''
|
||||
|
||||
measured_intensity = intensity_from_farfields(FarFields)
|
||||
|
||||
#measured_intensity_mean = npa.mean(measured_intensity, axis=0)
|
||||
|
||||
translated_measured_intensity = measured_intensity / npa.mean(ref_intensity,axis=0) * target_intensity_mean
|
||||
|
||||
translated_measured_intensity = npa.clip(translated_measured_intensity, a_min=0, a_max=1)
|
||||
|
||||
distance = translated_measured_intensity - target_intensity_3d
|
||||
|
||||
distance_denominator = npa.full(target_intensity_3d.shape, 1)
|
||||
|
||||
norm_distance = npa.power(npa.sum(distance**2, axis=0),0.5) / npa.power(npa.sum(distance_denominator**2, axis=0),0.5)
|
||||
|
||||
return norm_distance
|
||||
|
||||
|
||||
opt = mpa.OptimizationProblem(
|
||||
simulation=sim,
|
||||
objective_functions=[J1],
|
||||
objective_arguments=obj_list,
|
||||
design_regions=[matgrid_region],
|
||||
frequencies=frequencies,
|
||||
maximum_run_time=MAX_RUN_TIME
|
||||
)
|
||||
|
||||
plt.figure()
|
||||
ax = plt.gca()
|
||||
opt.plot2D(True,output_plane=view_2D_plane, ax=ax)
|
||||
plt.savefig("results/optimization_problem_setup.pdf")
|
||||
|
||||
return opt
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Hello there! Starting normalization")
|
||||
ref_intensity = normalization_sim()
|
||||
|
||||
num_weights = NX_DESIGN_GRID * NY_DESIGN_GRID
|
||||
|
||||
epigraph_and_weights = np.ones((num_weights,)) * 0.5
|
||||
epigraph_and_weights = np.insert(epigraph_and_weights, 0, 1.2)
|
||||
|
||||
weights_lower_bound = np.zeros(num_weights)
|
||||
weights_upper_bound = np.ones(num_weights)
|
||||
epigraph_and_weights_lower_bound = np.insert(weights_lower_bound, 0, -np.inf)
|
||||
epigraph_and_weights_upper_bound = np.insert(weights_upper_bound, 0, +np.inf)
|
||||
|
||||
objfunc_history = []
|
||||
epivar_history = []
|
||||
cur_iter = [0]
|
||||
|
||||
sigmoid_bias_threshold = 16
|
||||
|
||||
#sigmoid_biases = [8, 16, 32, 64, 128, 256]
|
||||
sigmoid_biases = [8, 8, 8, 8, 8, 16]
|
||||
#max_evals = [48, 48, 60, 60, 60, 60]
|
||||
max_evals = [8, 8, 8, 8, 8, 24]
|
||||
|
||||
epigraph_tolerance = np.array([1e-4]*num_wavelengths)
|
||||
tolerance_width_and_spacing = np.array([1e-8]*2)
|
||||
|
||||
for sigmoid_bias, max_eval in zip(sigmoid_biases, max_evals):
|
||||
print(f"Starting optimization epoch with sigmoid bias {sigmoid_bias} and max evals {max_eval}")
|
||||
#the optimizer needs to work with the weights and epigraph (+1)
|
||||
solver = nlopt.opt(nlopt.LD_CCSAQ, num_weights + 1)
|
||||
solver.set_lower_bounds(epigraph_and_weights_lower_bound)
|
||||
solver.set_upper_bounds(epigraph_and_weights_upper_bound)
|
||||
solver.set_min_objective(obj_fun)
|
||||
solver.set_maxeval(max_eval)
|
||||
solver.set_param("dual_ftol_rel", 1e-7)
|
||||
solver.add_inequality_mconstraint(
|
||||
lambda result_, epigraph_and_weights_, grad_: epigraph_constraint(
|
||||
result_,
|
||||
epigraph_and_weights_,
|
||||
grad_,
|
||||
SIGMOID_THRESHOLD_INTRINSIC,
|
||||
sigmoid_bias,
|
||||
use_epsavg=False if sigmoid_bias < sigmoid_bias_threshold else True,
|
||||
),
|
||||
epigraph_tolerance,
|
||||
)
|
||||
solver.set_param("verbosity",1)
|
||||
if sigmoid_bias < sigmoid_bias_threshold:
|
||||
use_epsavg = False
|
||||
else:
|
||||
use_epsavg = True
|
||||
|
||||
opt = intensity_optimization(
|
||||
ref_intensity,
|
||||
use_damping=True,
|
||||
use_epsavg=use_epsavg,
|
||||
sigmoid_bias=sigmoid_bias,
|
||||
)
|
||||
|
||||
#the minimum linewidth and spacing constraint
|
||||
#is activated only in the last epoch. This is done
|
||||
#by adding the corresponding constraints to the same solver object.
|
||||
#constraints are added as lambda functions, which
|
||||
#are called directly by the nlopt solver.
|
||||
if sigmoid_bias == sigmoid_biases[-1]: #if we are at the last sigmoid bias...
|
||||
line_width_and_spacing = np.zeros(2)
|
||||
grad_line_width_and_spacing = np.zeros((2, num_weights + 1))
|
||||
#compute the value of the costraints at this time
|
||||
linewidth_constraint_val = line_width_and_spacing_constraint(
|
||||
line_width_and_spacing,
|
||||
epigraph_and_weights,
|
||||
grad_line_width_and_spacing,
|
||||
sigmoid_bias,
|
||||
)
|
||||
solver.add_inequality_mconstraint(
|
||||
lambda result_, epigraph_and_weights_, grad_: line_width_and_spacing_constraint(
|
||||
result_,
|
||||
epigraph_and_weights_,
|
||||
grad_,
|
||||
sigmoid_bias,
|
||||
),
|
||||
tolerance_width_and_spacing,
|
||||
)
|
||||
|
||||
#execute a SINGLE forward run before the start of each epoch and
|
||||
#manually set the initial epigraph variable to slightly larger
|
||||
#than the largest value of the objective function over the
|
||||
#wavelengths and the lengthscale constraint
|
||||
#making a call to opt like the following invokes
|
||||
#the __call__ method of the opt instance and starts the optimization
|
||||
print("Calibrating epigraph variable before starting optimization...")
|
||||
epigraph_initial, empty_grads = opt(
|
||||
[
|
||||
filter_and_project(
|
||||
epigraph_and_weights[1:],
|
||||
SIGMOID_THRESHOLD_INTRINSIC,
|
||||
sigmoid_bias if sigmoid_bias < sigmoid_bias_threshold else 0,
|
||||
),
|
||||
],
|
||||
need_gradient=False,
|
||||
)
|
||||
#epigraph_initial contains the objective function values
|
||||
#at each wavelength in the simulation. At each epoch,
|
||||
#we set the initial epigraph variable taking the max of the objective functions
|
||||
epigraph_and_weights[0] = np.max(epigraph_initial)
|
||||
fraction_max_epigraph = 0.05
|
||||
if sigmoid_bias == sigmoid_biases[-1]:#if last epoch
|
||||
epigraph_and_weights[0] = \
|
||||
(1 + fraction_max_epigraph) * max(
|
||||
epigraph_and_weights[0], linewidth_constraint_val)
|
||||
print(
|
||||
f"epigraph-calibration:, bias = {sigmoid_bias}, "
|
||||
f"{str_from_list(epigraph_initial)}, {epigraph_and_weights[0]}"
|
||||
)
|
||||
|
||||
print("Starting optimization...")
|
||||
epigraph_and_weights[:] = solver.optimize(epigraph_and_weights)
|
||||
print("Optimization completed")
|
||||
|
||||
optimal_design_weights = filter_and_project(
|
||||
epigraph_and_weights[1:],
|
||||
SIGMOID_THRESHOLD_INTRINSIC,
|
||||
sigmoid_bias
|
||||
).reshape(-1, NY_DESIGN_GRID)
|
||||
|
||||
fig, ax = plt.subplots()
|
||||
ax.imshow(
|
||||
optimal_design_weights,
|
||||
cmap="binary",
|
||||
interpolation="none",
|
||||
)
|
||||
ax.set_axis_off()
|
||||
if mp.am_master():
|
||||
fig.savefig(
|
||||
f"optimal_design_beta{sigmoid_bias}.png",
|
||||
dpi=150,
|
||||
bbox_inches="tight",
|
||||
)
|
||||
# Save the final (unmapped) design as a 2D array in CSV format
|
||||
np.savetxt(
|
||||
f"unmapped_design_weights_beta{sigmoid_bias}.csv",
|
||||
epigraph_and_weights[1:].reshape(NX_DESIGN_GRID, NY_DESIGN_GRID),
|
||||
fmt="%4.2f",
|
||||
delimiter=",",
|
||||
)
|
||||
|
||||
plt.figure()
|
||||
plt.subplot(1,2,1)
|
||||
line1,line2,line3, = plt.plot(objfunc_history)
|
||||
plt.xlabel("Iteration")
|
||||
plt.ylabel("Value")
|
||||
plt.legend([line1, line2, line3], ["Obj. func. - 1.50 um", "Obj. func. - 1.55 um", "Obj. func. - 1.60 um"])
|
||||
plt.subplot(1,2,2)
|
||||
plt.plot(epivar_history, label="Epigraph")
|
||||
plt.xlabel("Iteration")
|
||||
plt.ylabel("Value")
|
||||
plt.legend()
|
||||
plt.savefig(f"results/optimization_history_beta{sigmoid_bias}.pdf")
|
||||
plt.close()
|
||||
|
||||
saveResults = True
|
||||
if saveResults:
|
||||
with open('results/optimal_design.pkl', 'wb') as f:
|
||||
pickle.dump({
|
||||
'RESOLUTION': RESOLUTION,
|
||||
'MAX_RUN_TIME': MAX_RUN_TIME,
|
||||
'WAVELENGTH_MIN_UM': WAVELENGTH_MIN_UM,
|
||||
'WAVELENGTH_MAX_UM': WAVELENGTH_MAX_UM,
|
||||
'PML_UM': PML_UM,
|
||||
'BUFFER': BUFFER,
|
||||
'DESIGN_WAVELENGTHS_UM': DESIGN_WAVELENGTHS_UM,
|
||||
'DESIGN_REGION_UM': DESIGN_REGION_UM,
|
||||
'DESIGN_REGION_RESOLUTION': DESIGN_REGION_RESOLUTION,
|
||||
'DESIGN_REGION_CENTER': DESIGN_REGION_CENTER,
|
||||
'NX_DESIGN_GRID': NX_DESIGN_GRID,
|
||||
'NY_DESIGN_GRID': NY_DESIGN_GRID,
|
||||
'MIN_LENGTH_UM': MIN_LENGTH_UM,
|
||||
'SILICON': SILICON,
|
||||
'SILICON_DIOXIDE': SILICON_DIOXIDE,
|
||||
'AIR': AIR,
|
||||
|
||||
'SIGMOID_THRESHOLD_INTRINSIC': SIGMOID_THRESHOLD_INTRINSIC,
|
||||
'SIGMOID_THRESHOLD_EROSION': SIGMOID_THRESHOLD_EROSION,
|
||||
'SIGMOID_THRESHOLD_DILATION': SIGMOID_THRESHOLD_DILATION,
|
||||
|
||||
'cell_um': cell_um,
|
||||
|
||||
'SUBSTRATE_THICKNESS': SUBSTRATE_THICKNESS,
|
||||
'SUBSTRATE_SIZE': SUBSTRATE_SIZE,
|
||||
'SUBSTRATE_CENTER': SUBSTRATE_CENTER,
|
||||
|
||||
'physical_domains_size': physical_domains_size,
|
||||
'filter_radius_um': filter_radius_um,
|
||||
|
||||
'frequency_min': frequency_min,
|
||||
'frequency_max': frequency_max,
|
||||
'frequency_center': frequency_center,
|
||||
'wavelength_center': wavelength_center,
|
||||
'frequency_width': frequency_width,
|
||||
'frequencies': frequencies,
|
||||
'num_wavelengths': num_wavelengths,
|
||||
|
||||
'SOURCE_CENTER': SOURCE_CENTER,
|
||||
'SOURCE_SIZE': SOURCE_SIZE,
|
||||
|
||||
'NEAR_REGION_MONITOR_CENTER': NEAR_REGION_MONITOR_CENTER,
|
||||
'NEAR_REGION_MONITOR_SIZE': NEAR_REGION_MONITOR_SIZE,
|
||||
|
||||
'FF_MONITOR_CENTER': FF_MONITOR_CENTER,
|
||||
'FF_MONITOR_SIZE': FF_MONITOR_SIZE,
|
||||
'xs': xs,
|
||||
'ys': ys,
|
||||
'ff_points': ff_points,
|
||||
|
||||
'sigmoid_biases': sigmoid_biases,
|
||||
'max_eval': max_eval,
|
||||
'objfunc_history': objfunc_history,
|
||||
'epivar_history': epivar_history,
|
||||
'epigraph_variable': epigraph_and_weights[0],
|
||||
'unmapped_design_weights': epigraph_and_weights[1:],
|
||||
'optimal_design_weights': optimal_design_weights,
|
||||
}, f)
|
||||
|
||||
print("Simulation completed")
|
||||
-253
@@ -1,253 +0,0 @@
|
||||
from autograd import numpy as npa
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import xarray as xr
|
||||
from PIL import Image
|
||||
import nlopt
|
||||
|
||||
wavelength = 1
|
||||
k0 = 2 * np.pi / wavelength
|
||||
|
||||
size_x = 100 * wavelength
|
||||
size_y = 100 * wavelength
|
||||
res_x = 16 #number of pixels per wavelength
|
||||
res_y = res_x
|
||||
|
||||
Nx = int(res_x * size_x) + 1
|
||||
Ny = int(res_y * size_y) + 1
|
||||
S = Nx * Ny
|
||||
|
||||
norm_w_min = 0
|
||||
norm_w_max = 1
|
||||
w_norm = np.ones((S)) * 0.5 #normalized geometrical parameters in [0,1]
|
||||
w_min = 0.4
|
||||
w_max = 1
|
||||
|
||||
xs = np.linspace(-size_x/2, size_x/2, Nx)
|
||||
ys = np.linspace(-size_y/2, size_y/2, Ny)
|
||||
sampling_period = xs[1] - xs[0]
|
||||
|
||||
z_prop = 300 #propagation distance
|
||||
|
||||
input_field = np.ones((S))
|
||||
|
||||
opt_max_eval = 50
|
||||
|
||||
def geom_transform(w_norm):
|
||||
'''Applies a linear mapping to the input parameter field w_norm,
|
||||
to obtain true geometrical parameters.
|
||||
Args:
|
||||
w_norm: normalized geometrical parameters in [0,1] (flattened)
|
||||
Returns:
|
||||
The true geometrical parameters corresponding to the input normalized parameters (flattened)
|
||||
'''
|
||||
w = w_min + (w_max - w_min) * w_norm
|
||||
return w
|
||||
|
||||
def phase_given_w(w):
|
||||
'''Returns the phase shift given the geometrical parameters w.
|
||||
Args:
|
||||
w: geometrical parameters (flattened)
|
||||
Returns:
|
||||
The phase shift corresponding to the geometrical parameters w (flattened)'''
|
||||
#for simplicity, we assume that the phase shift is proportional to w
|
||||
return 2 * np.pi * (w - w_min) / (w_max - w_min)
|
||||
|
||||
def get_pattern() -> np.ndarray:
|
||||
'''Imports the intensity pattern from file'''
|
||||
pattern_name = 'peg.png'
|
||||
pattern = Image.open('misc/' + pattern_name).convert('L')
|
||||
pattern = np.array(pattern).astype(float)
|
||||
|
||||
pattern = pattern - np.min(pattern)
|
||||
pattern = pattern / np.max(pattern)
|
||||
|
||||
return pattern
|
||||
|
||||
def intensity_desired_fn_pattern(
|
||||
xs: list,
|
||||
ys: list,
|
||||
rescale: float = 0.5
|
||||
) -> np.ndarray:
|
||||
'''Returns the pattern intensity value at the specified (xs,ys) locations, with a scale factor.
|
||||
Args:
|
||||
xs: list of x coordinates
|
||||
ys: list of y coordinates
|
||||
rescale: scale factor for the pattern
|
||||
Returns:
|
||||
The flattened pattern intensity value at the specified (xs,ys) locations, with a scale factor
|
||||
'''
|
||||
pattern_values = get_pattern()
|
||||
pattern_values = 1 - np.rot90(np.rot90(np.rot90(pattern_values)))
|
||||
|
||||
nx,ny = pattern_values.shape
|
||||
xs_pattern = np.linspace(rescale*min(xs), rescale*max(xs), nx)
|
||||
ys_pattern = np.linspace(rescale*min(ys), rescale*max(ys), ny)
|
||||
pattern_dataArray = xr.DataArray(pattern_values, coords=dict(x=xs_pattern, y=ys_pattern))
|
||||
pattern_interp = pattern_dataArray.interp(x=xs, y=ys)
|
||||
|
||||
pattern_final = npa.nan_to_num(pattern_interp.values, nan=npa.min(pattern_interp))
|
||||
pattern_to_return = npa.nan_to_num(pattern_interp.values, nan=npa.min(pattern_interp)).flatten()
|
||||
|
||||
#make a plot before returning
|
||||
plt.figure()
|
||||
ax = plt.gca()
|
||||
im = ax.imshow(
|
||||
pattern_final,
|
||||
extent=(min(xs), max(xs), min(ys), max(ys)),
|
||||
origin='lower',
|
||||
cmap='inferno',
|
||||
)
|
||||
ax.set_xlabel('x (um)')
|
||||
ax.set_ylabel('y (um)')
|
||||
ax.set_title('Target Intensity Pattern')
|
||||
plt.colorbar(im, ax=ax, label='Intensity (a.u.)')
|
||||
plt.savefig("results/target_intensity_pattern.pdf")
|
||||
|
||||
return pattern_to_return
|
||||
|
||||
target_intensity = intensity_desired_fn_pattern(xs, ys)
|
||||
mean_target_intensity = np.mean(target_intensity)
|
||||
|
||||
def forward_propagate(phase_mask):
|
||||
'''Propagates the input field, after applying the phase mask
|
||||
|
||||
Args:
|
||||
phase_mask: the phase mask to be applied to the input field (flattened)
|
||||
Returns:
|
||||
A tuple with output_field (flattened) and corresponding propagation matrix P (2D)
|
||||
'''
|
||||
field_after_mask = input_field * np.exp(1j * phase_mask)
|
||||
field_after_mask_2d = field_after_mask.reshape((Nx, Ny))
|
||||
field_fft = np.fft.fftshift(np.fft.fft2(field_after_mask_2d))
|
||||
|
||||
ks = 2 * np.pi / sampling_period
|
||||
kappas = np.linspace(-field_fft.shape[0]//2, field_fft.shape[0]//2-1, field_fft.shape[0]) * ks / field_fft.shape[0]
|
||||
KX, KY = np.meshgrid(kappas, kappas)
|
||||
K_parallel = np.sqrt(KX**2 + KY**2)
|
||||
nu_parallel = K_parallel / (2 * np.pi)
|
||||
phase_factor = k0 * z_prop * np.emath.sqrt(1 - (wavelength * nu_parallel) ** 2)
|
||||
|
||||
P = np.exp(1j * phase_factor)
|
||||
fft_field_propagated = np.multiply(field_fft,P)
|
||||
|
||||
output_field_2d = np.fft.ifft2(np.fft.ifftshift(fft_field_propagated))
|
||||
output_field = output_field_2d.flatten()
|
||||
|
||||
return output_field, P
|
||||
|
||||
def adjoint_propagate(output_field, P, phase_mask):
|
||||
'''Backpropagates the output field using the adjoint of the propagation matrix P.
|
||||
Args:
|
||||
output_field: the output field to be backpropagated (flattened)
|
||||
P: the propagation matrix used in the forward propagation
|
||||
Returns:
|
||||
The backpropagated field (flattened)
|
||||
'''
|
||||
output_intensity = output_field * np.conj(output_field)
|
||||
adjoint_field = (output_intensity - target_intensity) * output_field
|
||||
adjoint_field_2d = adjoint_field.reshape((Nx, Ny))
|
||||
adjoint_field_fft = np.fft.fftshift(np.fft.fft2(adjoint_field_2d))
|
||||
P_dagger = P.T.conj()
|
||||
adjoint_field_propagated_fft = adjoint_field_fft * P_dagger
|
||||
adjoint_field_propagated_2d = np.fft.ifft2(np.fft.ifftshift(adjoint_field_propagated_fft))
|
||||
|
||||
#apply the conjugate of the phase mask to backpropagate through it
|
||||
adjoint_field_propagated = adjoint_field_propagated_2d.flatten() * np.exp(-1j * phase_mask)
|
||||
return adjoint_field_propagated
|
||||
|
||||
def cost_fun(x, grad):
|
||||
''' Cost function to be minimized.
|
||||
Args:
|
||||
x: input normalized parameter field (flattened)
|
||||
grad: gradient of the cost function with respect to x, modified in place (flattened)
|
||||
Returns:
|
||||
The cost function value for the input parameter field x.
|
||||
'''
|
||||
w_norm[:] = x
|
||||
true_geom = geom_transform(w_norm)
|
||||
phase_mask = phase_given_w(true_geom)
|
||||
|
||||
output_field, P = forward_propagate(phase_mask)
|
||||
|
||||
output_intensity = np.real(output_field * np.conj(output_field))
|
||||
C_s = (output_intensity - target_intensity) ** 2
|
||||
C = np.sum(C_s)
|
||||
|
||||
#make a plot of three items: 1 the target intensity, 2 the output intensity, and 3 the cost function value as a function of x and y
|
||||
plt.figure(figsize=(18, 5))
|
||||
plt.subplot(1, 3, 1)
|
||||
plt.imshow(target_intensity.reshape((Nx, Ny)), extent=(-size_x/2, size_x/2, -size_y/2, size_y/2), origin='lower', cmap='inferno')
|
||||
plt.colorbar()
|
||||
plt.title('Target Intensity')
|
||||
plt.xlabel('x (um)')
|
||||
plt.ylabel('y (um)')
|
||||
plt.subplot(1, 3, 2)
|
||||
plt.imshow(output_intensity.reshape((Nx, Ny)), extent=(-size_x/2, size_x/2, -size_y/2, size_y/2), origin='lower', cmap='inferno')
|
||||
plt.colorbar()
|
||||
plt.title('Output Intensity')
|
||||
plt.xlabel('x (um)')
|
||||
plt.ylabel('y (um)')
|
||||
plt.subplot(1, 3, 3)
|
||||
plt.imshow(C_s.reshape((Nx, Ny)), extent=(-size_x/2, size_x/2, -size_y/2, size_y/2), origin='lower', cmap='inferno')
|
||||
plt.colorbar()
|
||||
plt.title('Cost Function Value')
|
||||
plt.xlabel('x (um)')
|
||||
plt.ylabel('y (um)')
|
||||
plt.tight_layout()
|
||||
plt.savefig("results/cost_function_visualization.pdf")
|
||||
#plt.show()
|
||||
plt.close()
|
||||
|
||||
if grad.size > 0:
|
||||
adj_field_prop = adjoint_propagate(output_field, P, phase_mask)
|
||||
grad[:] = 4 * np.real(-1j * input_field.reshape((Nx,Ny)).T.conj().flatten() * adj_field_prop)
|
||||
|
||||
return C
|
||||
|
||||
if __name__ == "__main__":
|
||||
#initialize nlopt solver
|
||||
solver = nlopt.opt(nlopt.LD_CCSAQ, S)
|
||||
solver.set_lower_bounds(norm_w_min)
|
||||
solver.set_upper_bounds(norm_w_max)
|
||||
solver.set_min_objective(cost_fun)
|
||||
solver.set_maxeval(opt_max_eval)
|
||||
solver.set_param("dual_ftol_rel", 1e-7)
|
||||
solver.set_param("verbosity",1)
|
||||
|
||||
print("Starting optimization...")
|
||||
w_norm[:] = solver.optimize(w_norm)
|
||||
print("Optimization completed.")
|
||||
|
||||
#verify the results
|
||||
true_geom = geom_transform(w_norm)
|
||||
phase_mask = phase_given_w(true_geom)
|
||||
output_field, _ = forward_propagate(phase_mask)
|
||||
output_intensity = np.real(output_field * np.conj(output_field))
|
||||
#make a plot of the phase mask, with phase wrap between -pi and pi
|
||||
plt.figure()
|
||||
plt.imshow(phase_mask.reshape((Nx, Ny)), extent=(-size_x/2, size_x/2, -size_y/2, size_y/2), origin='lower')
|
||||
plt.colorbar()
|
||||
plt.title('Phase Mask')
|
||||
plt.xlabel('x (um)')
|
||||
plt.ylabel('y (um)')
|
||||
plt.savefig("results/optimized_phase_mask.pdf")
|
||||
plt.show()
|
||||
plt.close()
|
||||
#make a subplot to compare the output intensity with the target intensity
|
||||
plt.figure(figsize=(12, 5))
|
||||
plt.subplot(1, 2, 1)
|
||||
plt.imshow(output_intensity.reshape((Nx, Ny)), extent=(-size_x/2, size_x/2, -size_y/2, size_y/2), origin='lower', cmap='inferno')
|
||||
plt.colorbar()
|
||||
plt.title('Output Intensity')
|
||||
plt.xlabel('x (um)')
|
||||
plt.ylabel('y (um)')
|
||||
plt.subplot(1, 2, 2)
|
||||
plt.imshow(target_intensity.reshape((Nx, Ny)), extent=(-size_x/2, size_x/2, -size_y/2, size_y/2), origin='lower', cmap='inferno')
|
||||
plt.colorbar()
|
||||
plt.title('Target Intensity')
|
||||
plt.xlabel('x (um)')
|
||||
plt.ylabel('y (um)')
|
||||
plt.tight_layout()
|
||||
plt.savefig("results/comparison_output_target.pdf")
|
||||
plt.show()
|
||||
-432
@@ -1,432 +0,0 @@
|
||||
#from jax import numpy as npa, grad
|
||||
#from jax.numpy.fft import fft2, ifft2, fftshift, ifftshift
|
||||
from typing import List, Tuple, Union
|
||||
from autograd import numpy as npa, tensor_jacobian_product, grad as gd
|
||||
import meep.adjoint as mpa
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import xarray as xr
|
||||
from PIL import Image
|
||||
import nlopt
|
||||
ArrayLikeType = Union[List, Tuple, np.ndarray]
|
||||
|
||||
wavelength = 1.55
|
||||
k0 = 2 * np.pi / wavelength
|
||||
|
||||
size_x = 300 * wavelength
|
||||
size_y = 300 * wavelength
|
||||
res_x = 3 #number of pixels per unit-length
|
||||
res_y = res_x
|
||||
|
||||
Nx = int(res_x * size_x) + 1
|
||||
Ny = int(res_y * size_y) + 1
|
||||
S = Nx * Ny
|
||||
|
||||
norm_phase_min = 0
|
||||
norm_phase_max = 1
|
||||
w_norm = np.ones((S)) * 0.5 #normalized phase parameters in [0,1]
|
||||
phase_min = 0
|
||||
phase_max = 2 * np.pi
|
||||
|
||||
xs = np.linspace(-size_x/2, size_x/2, Nx)
|
||||
ys = np.linspace(-size_y/2, size_y/2, Ny)
|
||||
sampling_period = xs[1] - xs[0]
|
||||
|
||||
z_prop = 300 #propagation distance
|
||||
|
||||
input_field = np.ones((S))
|
||||
|
||||
opt_max_eval = 200
|
||||
|
||||
def phase_given_w(w):
|
||||
'''Returns the phase shift given the design parameters w.
|
||||
Args:
|
||||
w: design parameters (flattened)
|
||||
Returns:
|
||||
The phase shift corresponding to the geometrical parameters w (flattened)'''
|
||||
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
|
||||
return 2 * npa.pi * (w)
|
||||
|
||||
def get_pattern() -> np.ndarray:
|
||||
'''Imports the intensity pattern from file'''
|
||||
pattern_name = 'unipd.png'
|
||||
pattern = Image.open('misc/' + pattern_name).convert('L')
|
||||
pattern = np.array(pattern).astype(float)
|
||||
|
||||
pattern = pattern - np.min(pattern)
|
||||
pattern = pattern / np.max(pattern)
|
||||
|
||||
return pattern
|
||||
|
||||
def intensity_desired_fn_pattern(
|
||||
xs: list,
|
||||
ys: list,
|
||||
rescale: float = 0.5
|
||||
) -> np.ndarray:
|
||||
'''Returns the pattern intensity value at the specified (xs,ys) locations, with a scale factor.
|
||||
Args:
|
||||
xs: list of x coordinates
|
||||
ys: list of y coordinates
|
||||
rescale: scale factor for the pattern
|
||||
Returns:
|
||||
The flattened pattern intensity value at the specified (xs,ys) locations, with a scale factor
|
||||
'''
|
||||
pattern_values = get_pattern()
|
||||
pattern_values = np.rot90(np.rot90(np.rot90(pattern_values)))
|
||||
|
||||
nx,ny = pattern_values.shape
|
||||
xs_pattern = np.linspace(rescale*min(xs), rescale*max(xs), nx)
|
||||
ys_pattern = np.linspace(rescale*min(ys), rescale*max(ys), ny)
|
||||
pattern_dataArray = xr.DataArray(pattern_values, coords=dict(x=xs_pattern, y=ys_pattern))
|
||||
pattern_interp = pattern_dataArray.interp(x=xs, y=ys)
|
||||
|
||||
pattern_final = npa.nan_to_num(pattern_interp.values, nan=npa.min(pattern_interp))
|
||||
pattern_to_return = npa.nan_to_num(pattern_interp.values, nan=npa.min(pattern_interp)).flatten()
|
||||
|
||||
#make a plot before returning
|
||||
plt.figure()
|
||||
ax = plt.gca()
|
||||
im = ax.imshow(
|
||||
pattern_final,
|
||||
extent=(min(xs), max(xs), min(ys), max(ys)),
|
||||
origin='lower',
|
||||
cmap='inferno',
|
||||
)
|
||||
ax.set_xlabel('x (um)')
|
||||
ax.set_ylabel('y (um)')
|
||||
ax.set_title('Target Intensity Pattern')
|
||||
plt.colorbar(im, ax=ax, label='Intensity (a.u.)')
|
||||
plt.savefig("results/test1_target_intensity_pattern.pdf")
|
||||
plt.close()
|
||||
return pattern_to_return
|
||||
|
||||
target_intensity = intensity_desired_fn_pattern(xs, ys)
|
||||
mean_target_intensity = np.mean(target_intensity)
|
||||
|
||||
|
||||
filter_radius_um = 0.5
|
||||
def _centered(arr: np.ndarray, newshape: ArrayLikeType) -> np.ndarray:
|
||||
"""Formats the output of an FFT to center the zero-frequency component.
|
||||
|
||||
A helper function borrowed from SciPy:
|
||||
https://github.com/scipy/scipy/blob/v1.4.1/scipy/signal/signaltools.py#L263-L270
|
||||
|
||||
Args:
|
||||
arr: output array from an FFT operation.
|
||||
newshape: 1d array with two elements (integers) specifying the dimensions
|
||||
of the array to be returned.
|
||||
|
||||
Returns:
|
||||
The input array with the zero-frequency component as the central element.
|
||||
"""
|
||||
newshape = np.asarray(newshape)
|
||||
currshape = np.array(arr.shape)
|
||||
startind = (currshape - newshape) // 2
|
||||
endind = startind + newshape
|
||||
myslice = [slice(startind[k], endind[k]) for k in range(len(endind))]
|
||||
|
||||
return arr[tuple(myslice)]
|
||||
|
||||
def _edge_pad(arr: np.ndarray, pad: np.ndarray) -> np.ndarray:
|
||||
"""Border-pads the edges of an array.
|
||||
|
||||
Used to preprocess the design weights prior to convolution with the filter.
|
||||
Border padding an image will set the value of each padded pixel equal to
|
||||
the value of the nearest pixel in the image. Used to implement feature-
|
||||
preserving convolution filters that prevent unwanted edge effects.
|
||||
|
||||
Args:
|
||||
arr: 2d array whose borders contain the values to use for padding
|
||||
pad: 2x2 array of integers indicating the size
|
||||
of the borders to pad the array with.
|
||||
|
||||
Returns:
|
||||
A 2d array with border padding.
|
||||
"""
|
||||
# fill sides
|
||||
left = npa.tile(arr[0, :], (pad[0][0], 1))
|
||||
right = npa.tile(arr[-1, :], (pad[0][1], 1))
|
||||
top = npa.tile(arr[:, 0], (pad[1][0], 1)).transpose()
|
||||
bottom = npa.tile(arr[:, -1], (pad[1][1], 1)).transpose()
|
||||
|
||||
# fill corners
|
||||
top_left = npa.tile(arr[0, 0], (pad[0][0], pad[1][0]))
|
||||
top_right = npa.tile(arr[-1, 0], (pad[0][1], pad[1][0]))
|
||||
bottom_left = npa.tile(arr[0, -1], (pad[0][0], pad[1][1]))
|
||||
bottom_right = npa.tile(arr[-1, -1], (pad[0][1], pad[1][1]))
|
||||
|
||||
if pad[0][0] > 0 and pad[0][1] > 0 and pad[1][0] > 0 and pad[1][1] > 0:
|
||||
return npa.concatenate(
|
||||
(
|
||||
npa.concatenate((top_left, top, top_right)),
|
||||
npa.concatenate((left, arr, right)),
|
||||
npa.concatenate((bottom_left, bottom, bottom_right)),
|
||||
),
|
||||
axis=1,
|
||||
)
|
||||
elif pad[0][0] == 0 and pad[0][1] == 0 and pad[1][0] > 0 and pad[1][1] > 0:
|
||||
return npa.concatenate((top, arr, bottom), axis=1)
|
||||
elif pad[0][0] > 0 and pad[0][1] > 0 and pad[1][0] == 0 and pad[1][1] == 0:
|
||||
return npa.concatenate((left, arr, right), axis=0)
|
||||
elif pad[0][0] == 0 and pad[0][1] == 0 and pad[1][0] == 0 and pad[1][1] == 0:
|
||||
return arr
|
||||
else:
|
||||
raise ValueError("At least one of the padding numbers is invalid.")
|
||||
|
||||
def _quarter_to_full_kernel(arr: np.ndarray, pad_to: np.ndarray) -> np.ndarray:
|
||||
"""Constructs the full kernel from its nonnegative quadrant.
|
||||
|
||||
Args:
|
||||
arr: 2d input array representing the nonnegative quadrant of a
|
||||
filter kernel with C4v symmetry.
|
||||
pad_to: 1d array with two elements (integers) specifying the size
|
||||
of the zero padding.
|
||||
|
||||
Returns:
|
||||
The complete kernel.
|
||||
"""
|
||||
pad_size = pad_to - 2 * np.array(arr.shape) + 1
|
||||
|
||||
top = np.zeros((pad_size[0], arr.shape[1]))
|
||||
bottom = np.zeros((pad_size[0], arr.shape[1] - 1))
|
||||
middle = np.zeros((pad_to[0], pad_size[1]))
|
||||
|
||||
top_left = arr[:, :]
|
||||
top_right = npa.flipud(arr[1:, :])
|
||||
bottom_left = npa.fliplr(arr[:, 1:])
|
||||
bottom_right = npa.flipud(
|
||||
npa.fliplr(arr[1:, 1:])
|
||||
) # equivalent to flip, but flip is incompatible with autograd
|
||||
|
||||
return npa.concatenate(
|
||||
(
|
||||
npa.concatenate((top_left, top, top_right)),
|
||||
middle,
|
||||
npa.concatenate((bottom_left, bottom, bottom_right)),
|
||||
),
|
||||
axis=1,
|
||||
)
|
||||
|
||||
def conic_filter(phase_mask,radius,size_x,size_y,resolution):
|
||||
'''Applies a conic filter to the input phase mask. The filter is defined as a
|
||||
conic region of a specified radius, where the values are averaged.
|
||||
|
||||
Args:
|
||||
phase_mask: the input phase mask to be filtered (2D)
|
||||
radius: the radius of the conic filter in micrometers
|
||||
size_x: the size of the phase mask in x direction in micrometers
|
||||
size_y: the size of the phase mask in y direction in micrometers
|
||||
resolution: the resolution of the phase mask in pixels per micrometer
|
||||
Returns:
|
||||
The filtered phase mask (2D)
|
||||
'''
|
||||
Nx = int(round(size_x * resolution)) + 1
|
||||
Ny = int(round(size_y * resolution)) + 1
|
||||
|
||||
xv = np.arange(0, size_x / 2, 1 / resolution) if resolution > 0 else [0]
|
||||
yv = np.arange(0, size_y / 2, 1 / resolution) if resolution > 0 else [0]
|
||||
|
||||
X, Y = npa.meshgrid(xv, yv)
|
||||
|
||||
h = npa.where(X**2 + Y**2 < radius**2, (1 - np.sqrt(abs(X**2 + Y**2)) / radius),0)
|
||||
#h = npa.where(X**2 + Y**2 < radius**2, 1, 0)
|
||||
h = _quarter_to_full_kernel(h, 3 * np.array([Nx, Ny]))
|
||||
h = h / npa.sum(h)
|
||||
H = npa.fft.fft2(h)
|
||||
|
||||
phase_mask_2d = phase_mask.reshape(Nx,Ny)
|
||||
phase_mask_padded = _edge_pad(phase_mask_2d, ((Nx, Nx), (Ny, Ny)))
|
||||
phase_mask_fft = npa.fft.fft2(phase_mask_padded)
|
||||
|
||||
Y = phase_mask_fft * H
|
||||
|
||||
y = _centered(
|
||||
npa.real(npa.fft.ifft2(Y)),
|
||||
(Nx, Ny)
|
||||
)
|
||||
|
||||
return y.flatten()
|
||||
|
||||
def phase_filter(phase_mask):
|
||||
'''Applies a conic filter on the phase mask'''
|
||||
phase_mask_filtered = conic_filter(
|
||||
phase_mask.reshape(Nx,Ny), #row major mapping
|
||||
filter_radius_um,
|
||||
size_x,
|
||||
size_y,
|
||||
res_x,
|
||||
)
|
||||
return phase_mask_filtered.flatten()
|
||||
|
||||
def forward_propagate(phase_mask):
|
||||
'''Propagates the input field, after applying the phase mask
|
||||
|
||||
Args:
|
||||
phase_mask: the phase mask to be applied to the input field (flattened)
|
||||
Returns:
|
||||
A tuple with output_field (flattened) and corresponding propagation matrix P (2D)
|
||||
'''
|
||||
field_after_mask = input_field * npa.exp(1j * phase_mask)
|
||||
field_after_mask_2d = field_after_mask.reshape((Nx, Ny))
|
||||
field_fft = npa.fft.fftshift(npa.fft.fft2(field_after_mask_2d))
|
||||
|
||||
ks = 2 * npa.pi / sampling_period
|
||||
kappas = npa.arange(-field_fft.shape[0]//2, field_fft.shape[0]//2) * ks / field_fft.shape[0]
|
||||
KX, KY = npa.meshgrid(kappas, kappas)
|
||||
K_parallel = npa.sqrt(KX**2 + KY**2)
|
||||
nu_parallel = K_parallel / (2 * npa.pi)
|
||||
phase_factor = k0 * z_prop * npa.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
|
||||
|
||||
P = npa.exp(1j * phase_factor)
|
||||
fft_field_propagated = npa.multiply(field_fft,P) #element wise product
|
||||
|
||||
output_field_2d = npa.fft.ifft2(npa.fft.ifftshift(fft_field_propagated))
|
||||
output_field = output_field_2d.flatten()
|
||||
|
||||
return output_field, P
|
||||
|
||||
def adjoint_propagate(x):
|
||||
'''Backpropagates the output field using the adjoint of the propagation matrix P.
|
||||
Args:
|
||||
output_field: the output field to be backpropagated (flattened)
|
||||
P: the propagation matrix used in the forward propagation
|
||||
phase_mask: the phase mask used in the forward propagation (flattened)
|
||||
Returns:
|
||||
The backpropagated field (flattened)
|
||||
'''
|
||||
phase_mask = phase_given_w(x)
|
||||
field_after_mask = input_field * npa.exp(1j * phase_mask)
|
||||
field_after_mask_2d = field_after_mask.reshape((Nx, Ny))
|
||||
field_fft = npa.fft.fftshift(npa.fft.fft2(field_after_mask_2d))
|
||||
|
||||
ks = 2 * npa.pi / sampling_period
|
||||
kappas = npa.arange(-field_fft.shape[0]//2, field_fft.shape[0]//2) * ks / field_fft.shape[0]
|
||||
KX, KY = npa.meshgrid(kappas, kappas)
|
||||
K_parallel = npa.sqrt(KX**2 + KY**2)
|
||||
nu_parallel = K_parallel / (2 * npa.pi)
|
||||
phase_factor = k0 * z_prop * npa.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
|
||||
|
||||
P = npa.exp(1j * phase_factor)
|
||||
fft_field_propagated = npa.multiply(field_fft,P) #element wise product
|
||||
|
||||
output_field_2d = npa.fft.ifft2(npa.fft.ifftshift(fft_field_propagated))
|
||||
output_field = output_field_2d.flatten()
|
||||
|
||||
output_intensity = output_field * np.conj(output_field)
|
||||
adjoint_field = (output_intensity - target_intensity) * output_field
|
||||
adjoint_field_2d = adjoint_field.reshape((Nx, Ny))
|
||||
adjoint_field_fft = npa.fft.fftshift(npa.fft.fft2(adjoint_field_2d))
|
||||
P_dagger = P.T.conj()
|
||||
adjoint_field_propagated_fft = adjoint_field_fft * P_dagger #element wise
|
||||
adjoint_field_propagated_2d = npa.fft.ifft2(npa.fft.ifftshift(adjoint_field_propagated_fft))
|
||||
|
||||
#apply the conjugate of the phase mask to backpropagate through it
|
||||
adjoint_field_propagated = adjoint_field_propagated_2d.flatten() * np.exp(-1j * phase_mask)
|
||||
return adjoint_field_propagated
|
||||
|
||||
phase_mask_filtered = np.zeros((S))
|
||||
def _compute_cost(phase_mask):
|
||||
|
||||
output_field, P = forward_propagate(phase_mask)
|
||||
|
||||
output_intensity = npa.real(output_field * npa.conj(output_field))
|
||||
C_s = (output_intensity - target_intensity) ** 2
|
||||
|
||||
C = npa.sum(C_s)
|
||||
|
||||
return C
|
||||
|
||||
opt_history = []
|
||||
def cost_fun(x, grad):
|
||||
''' Cost function to be minimized.
|
||||
Args:
|
||||
x: input normalized parameter field (flattened)
|
||||
grad: gradient of the cost function with respect to x, modified in place (flattened)
|
||||
Returns:
|
||||
The cost function value for the input parameter field x.
|
||||
'''
|
||||
w_norm[:] = x
|
||||
|
||||
phase_mask = phase_given_w(w_norm)
|
||||
|
||||
phase_mask_filtered = phase_filter(phase_mask)
|
||||
|
||||
cost_function_denominator = np.sum(np.ones((S)))
|
||||
|
||||
C = _compute_cost(phase_mask_filtered) / cost_function_denominator
|
||||
opt_history.append(C)
|
||||
#make a plot of the optimization history
|
||||
plt.figure()
|
||||
plt.plot(opt_history)
|
||||
plt.xlabel('Iteration')
|
||||
plt.ylabel('Cost Function Value')
|
||||
plt.title('Optimization History')
|
||||
plt.savefig("results/test1_optimization_history.pdf")
|
||||
plt.close()
|
||||
#first round of backpropagation
|
||||
grad_test = gd(_compute_cost)(phase_mask_filtered)
|
||||
|
||||
#second round of backpropagation
|
||||
full_grad = tensor_jacobian_product(phase_filter)(phase_mask,grad_test)
|
||||
#print("grad from autograd",grad_test)
|
||||
|
||||
if grad.size > 0:
|
||||
#adj_field_prop = adjoint_propagate(x)
|
||||
#grad[:] = 4 * np.real(-1j * input_field.reshape((Nx,Ny)).T.conj().flatten() * adj_field_prop)
|
||||
grad[:] = full_grad * 2 * np.pi / cost_function_denominator
|
||||
#print("grad from theo",grad)
|
||||
#print("grad diff",grad - grad_test)
|
||||
return C
|
||||
|
||||
if __name__ == "__main__":
|
||||
#initialize nlopt solver
|
||||
solver = nlopt.opt(nlopt.LD_CCSAQ, S)
|
||||
solver.set_lower_bounds(norm_phase_min)
|
||||
solver.set_upper_bounds(norm_phase_max)
|
||||
solver.set_min_objective(cost_fun)
|
||||
solver.set_maxeval(opt_max_eval)
|
||||
solver.set_param("dual_ftol_rel", 1e-7)
|
||||
solver.set_param("verbosity",1)
|
||||
|
||||
print("Starting optimization...")
|
||||
w_norm[:] = solver.optimize(w_norm)
|
||||
print("Optimization completed.")
|
||||
|
||||
#verify the results
|
||||
phase_mask = phase_given_w(w_norm)
|
||||
filtered_mask = phase_filter(phase_mask)
|
||||
#save the phase_mask as raw 2D text
|
||||
np.savetxt("results/optimized_phase_mask.txt", filtered_mask.reshape((Nx, Ny)))
|
||||
|
||||
output_field, _ = forward_propagate(filtered_mask)
|
||||
output_intensity = np.real(output_field * np.conj(output_field))
|
||||
#make a plot of the phase mask, with phase wrap between -pi and pi
|
||||
plt.figure()
|
||||
plt.imshow(phase_mask.reshape((Nx, Ny)), extent=(-size_x/2, size_x/2, -size_y/2, size_y/2), origin='lower')
|
||||
plt.colorbar()
|
||||
plt.title('Phase Mask')
|
||||
plt.xlabel('x (um)')
|
||||
plt.ylabel('y (um)')
|
||||
plt.savefig("results/test1_optimized_phase_mask.pdf")
|
||||
plt.show()
|
||||
plt.close()
|
||||
#make a subplot to compare the output intensity with the target intensity
|
||||
plt.figure(figsize=(12, 5))
|
||||
plt.subplot(1, 2, 1)
|
||||
plt.imshow(output_intensity.reshape((Nx, Ny)), extent=(-size_x/2, size_x/2, -size_y/2, size_y/2), origin='lower', cmap='inferno')
|
||||
plt.colorbar()
|
||||
plt.clim(0,1)
|
||||
plt.title('Output Intensity')
|
||||
plt.xlabel('x (um)')
|
||||
plt.ylabel('y (um)')
|
||||
plt.subplot(1, 2, 2)
|
||||
plt.imshow(target_intensity.reshape((Nx, Ny)), extent=(-size_x/2, size_x/2, -size_y/2, size_y/2), origin='lower', cmap='inferno')
|
||||
plt.colorbar()
|
||||
plt.clim(0,1)
|
||||
plt.title('Target Intensity')
|
||||
plt.xlabel('x (um)')
|
||||
plt.ylabel('y (um)')
|
||||
plt.tight_layout()
|
||||
plt.savefig("results/test1_comparison_output_target.pdf")
|
||||
#plt.show()
|
||||
plt.close()
|
||||
+73
-8
@@ -1,6 +1,9 @@
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
import tidy3d as td
|
||||
from tidy3d import web
|
||||
|
||||
wavelength = 1.55e-6
|
||||
k0 = 2 * np.pi / wavelength
|
||||
|
||||
@@ -27,9 +30,10 @@ rho = np.sqrt(X**2 + Y**2)
|
||||
|
||||
sampling_period = xs[1] - xs[0]
|
||||
|
||||
z_prop = 1000e-6 #345e-6 #propagation distance
|
||||
#z_prop = 2.8e-6 #345e-6 #propagation distance
|
||||
z_prop = 10e-6
|
||||
|
||||
beam_waist = 6e-6
|
||||
beam_waist = 2e-6
|
||||
gaussian_field = np.exp(-rho**2 / (beam_waist)**2) #Gaussian input field, flattened
|
||||
input_field = gaussian_field.flatten() #not normalized input field
|
||||
|
||||
@@ -97,12 +101,10 @@ if __name__ == "__main__":
|
||||
gaussian_profile = (beam_waist / w_z) * np.exp(- xs**2 / w_z**2)
|
||||
radius_of_curvature = z * (1 + (rayleigh_range / z)**2)
|
||||
gaussian_phase = (k0 * z + k0 * xs**2 / (2 * radius_of_curvature) - np.arctan(z / rayleigh_range)) % (2*np.pi)
|
||||
#gaussian_profile = gaussian_profile / np.sqrt(np.sum(gaussian_profile**2)) #normalize the gaussian profile to have power = 1
|
||||
#make this subplot take two places in the figure
|
||||
|
||||
|
||||
plt.subplot(4, 2, 3)
|
||||
plt.plot(xs*1e6, np.abs(output_field.reshape((Nx,Ny))[Nx//2, :])**2 / np.max(np.abs(output_field.reshape((Nx,Ny))[Nx//2, :])**2), 'b-', label='output')
|
||||
plt.plot(xs*1e6, gaussian_profile**2 / np.max(gaussian_profile**2), 'r--', label='theory')
|
||||
plt.plot(xs*1e6, np.abs(output_field.reshape((Nx,Ny))[Nx//2, :])**2, 'b-', label='output')
|
||||
plt.plot(xs*1e6, gaussian_profile**2, 'r--', label='theory')
|
||||
plt.xlabel('x (um)')
|
||||
plt.ylabel('Intensity (a.u.)')
|
||||
plt.legend(loc='center left')
|
||||
@@ -132,4 +134,67 @@ if __name__ == "__main__":
|
||||
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig("propagation_verification.pdf")
|
||||
#plt.savefig("propagation_verification.pdf")
|
||||
|
||||
#load FDTD data from file
|
||||
lambda0 = 1.55
|
||||
freq0 = td.C_0 / lambda0
|
||||
# Load the job metadata from file.
|
||||
job_loaded = web.Job.from_file("tidy3d/data/job.json")
|
||||
sim_data = job_loaded.load(path="tidy3d/data/testing_gauss_prop.hdf5")
|
||||
|
||||
|
||||
nf_field = sim_data["monitor_nf"].Ex.sel(f=freq0, z=2, method="nearest")
|
||||
src_field = sim_data["monitor_src"].Ex.sel(f=freq0, z=-1+1e-9, method="nearest")
|
||||
|
||||
f = 10
|
||||
src_monitor_position = -0.8
|
||||
prj_field = sim_data["monitor_projection"].fields_cartesian.Ex.sel(f=freq0, z=f+src_monitor_position, method="nearest")
|
||||
|
||||
fig, ax = plt.subplots(1,2)
|
||||
|
||||
x_nf = nf_field.coords['x'].values
|
||||
x_src = src_field.coords['x'].values
|
||||
x_prj = prj_field.coords['x'].values
|
||||
N_nf_field = nf_field.shape[0]
|
||||
N_prj_field = prj_field.shape[0]
|
||||
|
||||
nf_field_phase = ((np.angle(nf_field) + 2 * np.pi) % (2 * np.pi))[N_nf_field//2,:]
|
||||
nf_field_mag = (np.abs(nf_field) / np.max(np.abs(nf_field)))[N_nf_field//2,:]
|
||||
src_field_phase = ((np.angle(src_field) + 2 * np.pi) % (2 * np.pi))[N_nf_field//2,:]
|
||||
src_field_mag = (np.abs(src_field) / np.max(np.abs(src_field)))[N_nf_field//2,:]
|
||||
|
||||
prj_field_phase = ((np.angle(prj_field) + 2 * np.pi) % (2 * np.pi))[N_prj_field//2,:]
|
||||
prj_field_mag = (np.abs(prj_field) / np.max(np.abs(prj_field)))[N_prj_field//2,:]
|
||||
|
||||
output_field_2d = output_field.reshape((Nx, Ny))
|
||||
output_field_mag = (np.abs(output_field_2d) / np.max(np.abs(output_field_2d)))[Nx//2,:]
|
||||
output_field_phase = ((np.angle(output_field_2d) + 2 * np.pi) % (2 * np.pi))[Nx//2,:]
|
||||
|
||||
#ax[0].plot(x_nf,nf_field_mag, '.b', label="FDTD magnitude")
|
||||
ax[0].plot(xs*1e6,output_field_mag, 'r', label="ASM magnitude")
|
||||
ax[0].plot(xs*1e6,gaussian_profile/np.max(gaussian_profile), '-.g', label="theory magnitude")
|
||||
ax[0].plot(x_prj,prj_field_mag, label="FDTD FF")
|
||||
ax[0].set_xlim(-5,5)
|
||||
ax[0].legend(loc='lower center')
|
||||
|
||||
true_phase = nf_field_phase - src_field_phase
|
||||
true_phase = (true_phase + 2 * np.pi) % (2 * np.pi)
|
||||
|
||||
#interpolate prj_field phase and src_field phase to the same x values of prj_field
|
||||
|
||||
src_field_phase_interp = np.interp(x_prj, x_src, src_field_phase)
|
||||
|
||||
true_phase_ff = prj_field_phase - src_field_phase_interp
|
||||
true_phase_ff = (true_phase_ff + 2 * np.pi) % (2 * np.pi)
|
||||
|
||||
#ax[1].plot(x_nf,true_phase, '.b', label="FDTD phase")
|
||||
ax[1].plot(xs*1e6,output_field_phase, 'r', label="ASM phase")
|
||||
ax[1].plot(xs*1e6,gaussian_phase, '-.g', label="theory phase")
|
||||
ax[1].plot(x_prj,true_phase_ff, label="FDTD FF phase")
|
||||
|
||||
ax[1].set_xlim(-5,5)
|
||||
ax[1].legend()
|
||||
|
||||
#plt.show()
|
||||
plt.savefig("asm_fdtd_comparison.pdf")
|
||||
+191
-159
@@ -12,13 +12,17 @@ import nlopt
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
#set the global font size for all plots
|
||||
plt.rcParams.update({'font.size': 14})
|
||||
|
||||
from scipy.special import jv, kv
|
||||
|
||||
##############################
|
||||
#physics and simulation domain
|
||||
wavelength = 1.55e-6
|
||||
k0 = 2 * np.pi / wavelength
|
||||
|
||||
opt_max_eval = 200
|
||||
opt_max_eval = 250
|
||||
|
||||
unit_cell_pitch = 400e-9 #equivalent to spatial sampling
|
||||
|
||||
@@ -29,7 +33,29 @@ S = Nx * Ny
|
||||
size_x = unit_cell_pitch * Nx #actual physical size
|
||||
size_y = unit_cell_pitch * Ny
|
||||
|
||||
######################
|
||||
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
|
||||
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
|
||||
|
||||
X, Y = np.meshgrid(xs, ys)
|
||||
rho = np.sqrt(X**2 + Y**2)
|
||||
|
||||
sampling_period = xs[1] - xs[0]
|
||||
|
||||
d0 = 345e-6 #propagation distance: source - MS1
|
||||
d1 = 500e-6 #propagation distance: MS1 - MS2
|
||||
d2 = 500e-6 #propagation distance: MS2 - target
|
||||
d = [d1, d2] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target
|
||||
|
||||
phase_min = 0
|
||||
phase_max = 2 * np.pi
|
||||
##############################
|
||||
|
||||
##############################
|
||||
#Source
|
||||
beam_waist = 9e-6
|
||||
##############################
|
||||
|
||||
##############################
|
||||
#plan pyfftw objects for forward and inverse FFTs
|
||||
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
|
||||
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
|
||||
@@ -38,22 +64,37 @@ ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
|
||||
|
||||
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
|
||||
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
|
||||
######################
|
||||
##############################
|
||||
|
||||
phase_min = 0
|
||||
phase_max = 2 * np.pi
|
||||
##############################
|
||||
#setting up spatial frequencies
|
||||
ks = 2 * np.pi / sampling_period
|
||||
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
|
||||
KX, KY = np.meshgrid(kappas, kappas)
|
||||
K_parallel = np.sqrt(KX**2 + KY**2)
|
||||
nu_parallel = K_parallel / (2 * np.pi)
|
||||
|
||||
xs = np.linspace(-size_x/2, size_x/2, Nx)
|
||||
ys = np.linspace(-size_y/2, size_y/2, Ny)
|
||||
phase_factor_0 = k0 * d0 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
|
||||
P_0 = np.exp(1j * phase_factor_0)
|
||||
P_0_nat = np.fft.ifftshift(P_0)
|
||||
phase_factor_1 = k0 * d1 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
|
||||
P_1 = np.exp(1j * phase_factor_1)
|
||||
P_1_nat = np.fft.ifftshift(P_1)
|
||||
phase_factor_2 = k0 * d2 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
|
||||
P_2 = np.exp(1j * phase_factor_2)
|
||||
P_2_nat = np.fft.ifftshift(P_2)
|
||||
|
||||
X, Y = np.meshgrid(xs, ys)
|
||||
rho = np.sqrt(X**2 + Y**2)
|
||||
P_1_dagger = P_1.T.conj()
|
||||
P_2_dagger = P_2.T.conj()
|
||||
P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj()
|
||||
P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj()
|
||||
##############################
|
||||
|
||||
sampling_period = xs[1] - xs[0]
|
||||
|
||||
d1 = 1000e-6 #propagation distance
|
||||
d2 = 1000e-6 #propagation distance
|
||||
d = [d1, d2] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target
|
||||
##############################
|
||||
#Plot zoom parameters [um]
|
||||
zoom_x = 60*3
|
||||
zoom_y = 60*3
|
||||
##############################
|
||||
|
||||
def phase_given_w(w):
|
||||
'''Returns the phase shift given the design parameters w.
|
||||
@@ -131,22 +172,98 @@ def get_fiber_mode_pattern():
|
||||
|
||||
return E_field
|
||||
|
||||
def get_source_field(beam_waist):
|
||||
'''Returns a normalized Gaussian source field for a given beam waist. Normalization consists in scaling
|
||||
the field such that its integrated intensity is 1.'''
|
||||
gaussian_field = np.exp(-rho**2 / (beam_waist)**2) #Gaussian input field, flattened
|
||||
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
|
||||
return gaussian_field_normalized
|
||||
|
||||
def propagate_source():
|
||||
'''Returns the propagated source field just before metasurface 1 (MS1). This involves propagating the
|
||||
source for a distance d0: from its plane of definition up to MS1.'''
|
||||
fft_input_array[:,:] = source_field_2d
|
||||
fft_operator()
|
||||
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
|
||||
ifft_operator()
|
||||
propagated_source_field = ifft_operator.output_array.flatten()
|
||||
return propagated_source_field
|
||||
|
||||
def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
|
||||
'''Makes a 1D plot of the given data and saves it to a PDF file.
|
||||
Args:
|
||||
x_axis: list of x-axis data (list of 1D arrays)
|
||||
y_axis: list of y-axis data (list of 1D arrays)
|
||||
plot_zoom_x: zoom level for the x-axis (in micrometers)
|
||||
save_name: name of the file to save the plot (without extension)
|
||||
Returns:
|
||||
None
|
||||
'''
|
||||
if len(x_axis) != len(y_axis):
|
||||
raise ValueError("Lists x_axis and y_axis must have the same length.")
|
||||
plt.figure()
|
||||
for i in range(len(x_axis)):
|
||||
plt.plot(x_axis[i], y_axis[i])
|
||||
plt.xlabel("x [um]")
|
||||
plt.tight_layout()
|
||||
plt.savefig("results/" + save_name + ".pdf")
|
||||
plt.xlim(-plot_zoom_x, plot_zoom_x)
|
||||
plt.close()
|
||||
|
||||
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
|
||||
'''Makes a plot of the given 2D field in a zoomed region.
|
||||
Saves the plot with the chosen filename to a PDF file.
|
||||
Args:
|
||||
given_field: the 2D field to be plotted (flattened)
|
||||
choose_quantity: either "amplitude" or "phase" to select quantity to plot
|
||||
save_name: name of the file to save the plot (without extension)
|
||||
plot_zoom_x: zoom level for the x-axis (in micrometers)
|
||||
plot_zoom_y: zoom level for the y-axis (in micrometers)
|
||||
Returns:
|
||||
None'''
|
||||
|
||||
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
|
||||
plt.figure()
|
||||
if choose_quantity == "amplitude":
|
||||
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
|
||||
elif choose_quantity == "phase":
|
||||
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
|
||||
elif choose_quantity == "phase_mask":
|
||||
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
|
||||
else:
|
||||
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
|
||||
|
||||
plt.xlim(-plot_zoom_x, plot_zoom_x)
|
||||
plt.ylim(-plot_zoom_y, plot_zoom_y)
|
||||
plt.xlabel("x [um]")
|
||||
plt.ylabel("y [um]")
|
||||
plt.colorbar()
|
||||
plt.tight_layout()
|
||||
plt.savefig("results/" + save_name + ".pdf")
|
||||
plt.close()
|
||||
|
||||
###########################
|
||||
#initialize input field
|
||||
beam_waist = 80e-6
|
||||
gaussian_field = np.exp(-rho**2 / (beam_waist)**2) #Gaussian input field, flattened
|
||||
input_field_2d = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
|
||||
input_field = input_field_2d.flatten() #normalize the input field to have power = 1
|
||||
#initialize source field
|
||||
source_field = get_source_field(beam_waist)
|
||||
source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting
|
||||
#source field integrated intensity
|
||||
source_power = np.sum(np.abs(source_field)**2)
|
||||
print("Source field integrated intensity: %.4e" % source_power)
|
||||
#make a plot and save the source field
|
||||
make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
|
||||
make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
|
||||
###########################
|
||||
|
||||
###########################
|
||||
#initialize input field (propagated source field)
|
||||
input_field = propagate_source()
|
||||
input_field_2d = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting
|
||||
#input field integrated intensity
|
||||
input_field_intensity = np.sum(np.abs(input_field)**2)
|
||||
print("Input field integrated intensity: %.4e" % input_field_intensity)
|
||||
#make a plot and save the input field
|
||||
plt.figure()
|
||||
plt.imshow(np.abs(input_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
|
||||
plt.title("Input Field Amplitude")
|
||||
plt.xlabel("x (um)")
|
||||
plt.ylabel("y (um)")
|
||||
plt.colorbar()
|
||||
plt.tight_layout()
|
||||
plt.savefig("results/input_field_amplitude.pdf")
|
||||
plt.close()
|
||||
make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x*2, plot_zoom_y=zoom_y*2)
|
||||
make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x*2, plot_zoom_y=zoom_y*2)
|
||||
###########################
|
||||
|
||||
###########################
|
||||
@@ -154,6 +271,10 @@ plt.close()
|
||||
fiber_mode_pattern = get_fiber_mode_pattern()
|
||||
target_Efield_2d = fiber_mode_pattern / np.sqrt(np.sum(np.abs(fiber_mode_pattern)**2))
|
||||
target_Efield = target_Efield_2d.flatten() #normalized target field (intensity = 1)
|
||||
|
||||
# Plot: Target field
|
||||
make_2Dplot_of(target_Efield_2d, choose_quantity="amplitude", save_name="target_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
|
||||
make_2Dplot_of(target_Efield_2d, choose_quantity="phase", save_name="target_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
|
||||
###########################
|
||||
|
||||
###########################
|
||||
@@ -177,41 +298,32 @@ mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0
|
||||
mask_lower_bounds = np.zeros_like(mask_upper_bounds)
|
||||
###########################
|
||||
|
||||
###########################
|
||||
#setting up spatial frequencies
|
||||
ks = 2 * np.pi / sampling_period
|
||||
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
|
||||
KX, KY = np.meshgrid(kappas, kappas)
|
||||
K_parallel = np.sqrt(KX**2 + KY**2)
|
||||
nu_parallel = K_parallel / (2 * np.pi)
|
||||
|
||||
phase_factor = k0 * d1 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
|
||||
P_1 = np.exp(1j * phase_factor)
|
||||
P_1_nat = np.fft.ifftshift(P_1)
|
||||
phase_factor_2 = k0 * d2 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
|
||||
P_2 = np.exp(1j * phase_factor_2)
|
||||
P_2_nat = np.fft.ifftshift(P_2)
|
||||
|
||||
P_1_dagger = P_1.T.conj()
|
||||
P_2_dagger = P_2.T.conj()
|
||||
P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj()
|
||||
P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj()
|
||||
###########################
|
||||
|
||||
intermediate_fields = [None, None]
|
||||
def forward_propagate() -> None:
|
||||
'''Propagates the input field through the system with the given phase mask.
|
||||
Args:
|
||||
phase_mask: the concatenated phase mask of the system. Masks are flattened and in order. Any mask has always S elements.
|
||||
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
|
||||
Returns:
|
||||
The output field after propagation (flattened) and the propagation matrices P used in the forward propagation.
|
||||
None
|
||||
'''
|
||||
print("Starting propagating...")
|
||||
start_time = time.time()
|
||||
|
||||
fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))
|
||||
fft_operator()
|
||||
print("BEFORE")
|
||||
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
|
||||
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
|
||||
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
|
||||
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
|
||||
print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
|
||||
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
|
||||
print("AFTER")
|
||||
|
||||
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
|
||||
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
|
||||
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
|
||||
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
|
||||
print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
|
||||
|
||||
ifft_operator()
|
||||
intermediate_fields[0] = ifft_operator.output_array.copy()
|
||||
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2))
|
||||
@@ -223,17 +335,18 @@ def forward_propagate() -> None:
|
||||
intermediate_fields[1] = ifft_operator.output_array.copy()
|
||||
|
||||
end_time = time.time()
|
||||
make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_after_first_phase_mask_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
|
||||
make_2Dplot_of(intermediate_fields[0], choose_quantity="phase", save_name="intermediate_field_after_first_phase_mask_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
|
||||
make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_after_second_phase_mask_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
|
||||
make_2Dplot_of(intermediate_fields[1], choose_quantity="phase", save_name="intermediate_field_after_second_phase_mask_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
|
||||
|
||||
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
|
||||
|
||||
|
||||
def adjoint_propagate():
|
||||
'''Backpropagates the output field using the adjoint of the propagation matrix P.
|
||||
Args:
|
||||
output_field: the output field to be backpropagated (flattened)
|
||||
P: the propagation matrix used in the forward propagation
|
||||
phase_mask: the phase mask used in the forward propagation (flattened)
|
||||
Returns:
|
||||
The backpropagated field (flattened)
|
||||
the gradient of the cost function with respect to the phase mask (flattened)
|
||||
'''
|
||||
print("Computing adjoint...")
|
||||
start_time = time.time()
|
||||
@@ -261,7 +374,6 @@ def adjoint_propagate():
|
||||
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
|
||||
|
||||
def _compute_cost():
|
||||
|
||||
forward_propagate()
|
||||
|
||||
output_field = intermediate_fields[-1].flatten()
|
||||
@@ -307,8 +419,7 @@ def cost_fun(x, grad):
|
||||
if __name__ == "__main__":
|
||||
#initialize nlopt solver
|
||||
solver = nlopt.opt(nlopt.LD_CCSAQ, 2*S)
|
||||
print("Type of bounds",type(mask_lower_bounds.flatten()), type(mask_upper_bounds.flatten()))
|
||||
print("Size of bounds",mask_lower_bounds.flatten().shape, mask_upper_bounds.flatten().shape)
|
||||
|
||||
solver.set_lower_bounds(mask_lower_bounds.flatten())
|
||||
solver.set_upper_bounds(mask_upper_bounds.flatten())
|
||||
solver.set_max_objective(cost_fun)
|
||||
@@ -338,106 +449,34 @@ if __name__ == "__main__":
|
||||
|
||||
# Reshape fields to 2D for visualization
|
||||
output_field_2d = output_field.reshape((Nx, Ny))
|
||||
target_field_2d = target_Efield.reshape((Nx, Ny))
|
||||
phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny))
|
||||
phase_mask_2 = phase_given_w(w_norm[S:]).reshape((Nx, Ny))
|
||||
|
||||
a = 6e-6
|
||||
d = 10 * a * 1e6
|
||||
# Plot 1: Output field amplitude
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
im = ax.imshow(np.abs(output_field_2d),extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), origin='lower', cmap='viridis')
|
||||
ax.set_title('Output Field Amplitude')
|
||||
ax.set_xlabel('x')
|
||||
ax.set_ylabel('y')
|
||||
#ax.set_xlim(-d, d)
|
||||
#ax.set_ylim(-d, d)
|
||||
ax.set_aspect('equal')
|
||||
plt.colorbar(im, ax=ax)
|
||||
plt.tight_layout()
|
||||
plt.savefig("results/output_field_amplitude.pdf")
|
||||
plt.close()
|
||||
|
||||
# Plot: Output field amplitude and phase
|
||||
make_2Dplot_of(output_field_2d, choose_quantity="amplitude", save_name="output_field_amplitude", plot_zoom_x=d, plot_zoom_y=d)
|
||||
make_2Dplot_of(output_field_2d, choose_quantity="phase", save_name="output_field_phase", plot_zoom_x=d, plot_zoom_y=d)
|
||||
|
||||
# Plot 2: Output field phase
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
im = ax.imshow((np.angle(output_field_2d)+2*np.pi)%(2*np.pi),extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), origin='lower', cmap='hsv')
|
||||
ax.set_title('Output Field Phase')
|
||||
ax.set_xlabel('x')
|
||||
ax.set_ylabel('y')
|
||||
ax.set_xlim(-d, d)
|
||||
ax.set_ylim(-d, d)
|
||||
ax.set_aspect('equal')
|
||||
plt.colorbar(im, ax=ax)
|
||||
plt.tight_layout()
|
||||
plt.savefig("results/output_field_phase.pdf")
|
||||
plt.close()
|
||||
phase_mask_zoom = 150
|
||||
# Plot: Phase mask 1
|
||||
make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
|
||||
# Plot: Phase mask 2
|
||||
make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
|
||||
|
||||
# Plot 3: Target field amplitude
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
im = ax.imshow(np.abs(target_field_2d),extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), origin='lower', cmap='viridis')
|
||||
ax.set_title('Target Field Amplitude')
|
||||
ax.set_xlabel('x')
|
||||
ax.set_ylabel('y')
|
||||
ax.set_xlim(-d, d)
|
||||
ax.set_ylim(-d, d)
|
||||
ax.set_aspect('equal')
|
||||
plt.colorbar(im, ax=ax)
|
||||
plt.tight_layout()
|
||||
plt.savefig("results/target_field_amplitude.pdf")
|
||||
plt.close()
|
||||
|
||||
# Plot 4: Target field phase
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
im = ax.imshow((np.angle(target_field_2d)+2*np.pi)%(2*np.pi),extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), origin='lower', cmap='hsv', vmin=0, vmax=2*np.pi)
|
||||
ax.set_title('Target Field Phase')
|
||||
ax.set_xlabel('x')
|
||||
ax.set_ylabel('y')
|
||||
ax.set_xlim(-d, d)
|
||||
ax.set_ylim(-d, d)
|
||||
ax.set_aspect('equal')
|
||||
plt.colorbar(im, ax=ax)
|
||||
plt.tight_layout()
|
||||
plt.savefig("results/target_field_phase.pdf")
|
||||
plt.close()
|
||||
|
||||
# Plot 5: Phase mask 1
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
im = ax.imshow((phase_mask_1+2*np.pi)%(2*np.pi),extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), origin='lower', cmap='hsv', vmin=0, vmax=2*np.pi)
|
||||
ax.set_title('Phase Mask 1')
|
||||
ax.set_xlabel('x')
|
||||
ax.set_ylabel('y')
|
||||
ax.set_aspect('equal')
|
||||
plt.colorbar(im, ax=ax)
|
||||
plt.tight_layout()
|
||||
plt.savefig("results/phase_mask_1.pdf")
|
||||
plt.show()
|
||||
#plt.close()
|
||||
|
||||
# Plot 6: Phase mask 2
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
im = ax.imshow((phase_mask_2+2*np.pi)%(2*np.pi),extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), origin='lower', cmap='hsv', vmin=0, vmax=2*np.pi)
|
||||
ax.set_title('Phase Mask 2')
|
||||
ax.set_xlabel('x')
|
||||
ax.set_ylabel('y')
|
||||
ax.set_aspect('equal')
|
||||
plt.colorbar(im, ax=ax)
|
||||
plt.tight_layout()
|
||||
plt.savefig("results/phase_mask_2.pdf")
|
||||
plt.show()
|
||||
#plt.close()
|
||||
|
||||
# Plot 7: Cuts along y=0 axis - Amplitude comparison
|
||||
# Plot: Cuts along y=0 axis - Amplitude comparison
|
||||
y_center_idx = Ny // 2
|
||||
output_amplitude_cut = np.abs(output_field_2d[y_center_idx, :])
|
||||
target_amplitude_cut = np.abs(target_field_2d[y_center_idx, :])
|
||||
output_amplitude_cut = np.abs(output_field_2d[y_center_idx, :]) / np.max(np.abs(output_field_2d[y_center_idx, :]))
|
||||
target_amplitude_cut = np.abs(target_Efield_2d[y_center_idx, :]) / np.max(np.abs(target_Efield_2d[y_center_idx, :]))
|
||||
|
||||
fig, ax = plt.subplots(figsize=(12, 6))
|
||||
ax.plot(xs*1e6, output_amplitude_cut, label='Output Field', linewidth=2)
|
||||
ax.plot(xs*1e6, target_amplitude_cut, label='Target Field', linewidth=2)
|
||||
ax.set_xlabel('x (m)')
|
||||
ax.set_xlabel('x [um]')
|
||||
ax.set_xlim(-d,d)
|
||||
ax.set_ylabel('Amplitude')
|
||||
ax.set_title('Field Amplitude along y=0 axis')
|
||||
ax.set_title('Norm. Field Amplitude along y=0 axis')
|
||||
ax.legend()
|
||||
ax.grid(True, alpha=0.3)
|
||||
plt.tight_layout()
|
||||
@@ -446,12 +485,13 @@ if __name__ == "__main__":
|
||||
|
||||
# Plot 8: Cuts along y=0 axis - Phase comparison
|
||||
output_phase_cut = (np.angle(output_field_2d[y_center_idx, :]) + 2*np.pi) % (2*np.pi)
|
||||
target_phase_cut = (np.angle(target_field_2d[y_center_idx, :]) + 2*np.pi) % (2*np.pi)
|
||||
|
||||
target_phase_cut = (np.angle(target_Efield_2d[y_center_idx, :]) + 2*np.pi) % (2*np.pi)
|
||||
x_ax = xs*1e6
|
||||
make_1Dplot_of([x_ax, x_ax], [output_phase_cut, target_phase_cut], save_name="phase_cut_comparison")
|
||||
fig, ax = plt.subplots(figsize=(12, 6))
|
||||
ax.plot(xs*1e6, output_phase_cut, label='Output Field', linewidth=2)
|
||||
ax.plot(xs*1e6, target_phase_cut, label='Target Field', linewidth=2)
|
||||
ax.set_xlabel('x (m)')
|
||||
ax.set_xlabel('x [um]')
|
||||
ax.set_ylabel('Phase (rad)')
|
||||
ax.set_xlim(-d,d)
|
||||
ax.set_title('Field Phase along y=0 axis')
|
||||
@@ -462,15 +502,7 @@ if __name__ == "__main__":
|
||||
plt.close()
|
||||
|
||||
# Plot 9: Intermediate field before second phase mask
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
im = ax.imshow(np.abs(intermediate_fields[0]),extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), origin='lower', cmap='viridis')
|
||||
ax.set_title('Intermediate Field Amplitude (Before Second Phase Mask)')
|
||||
ax.set_xlabel('x')
|
||||
ax.set_ylabel('y')
|
||||
ax.set_aspect('equal')
|
||||
plt.colorbar(im, ax=ax)
|
||||
plt.tight_layout()
|
||||
plt.savefig("results/intermediate_field_amplitude.pdf")
|
||||
make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_amplitude", plot_zoom_x=d, plot_zoom_y=d)
|
||||
|
||||
#save the phase masks for later use
|
||||
np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1)
|
||||
|
||||
@@ -51,8 +51,8 @@ rho = np.sqrt(X**2 + Y**2)
|
||||
|
||||
sampling_period = xs[1] - xs[0]
|
||||
|
||||
d0 = 345e-6 #propagation distance: source - MS1
|
||||
d1 = 100e-6 #propagation distance: MS1 - MS2
|
||||
d0 = 345-6 #propagation distance: source - MS1
|
||||
d1 = 500e-6 #propagation distance: MS1 - MS2
|
||||
d2 = 300e-6 #propagation distance: MS2 - target
|
||||
d = [d1, d2] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ phase_max = 2 * np.pi
|
||||
##############################
|
||||
#Source
|
||||
beam_waist = 4e-6
|
||||
source_spacing = 300e-6
|
||||
##############################
|
||||
|
||||
##############################
|
||||
@@ -93,8 +94,8 @@ P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj()
|
||||
|
||||
##############################
|
||||
#Plot zoom parameters [um]
|
||||
zoom_x = 160
|
||||
zoom_y = 160
|
||||
zoom_x = 200
|
||||
zoom_y = 200
|
||||
all_view_x = size_x * 1e6 / 2
|
||||
all_view_y = size_y * 1e6 / 2
|
||||
##############################
|
||||
@@ -275,7 +276,6 @@ def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot
|
||||
|
||||
###########################
|
||||
#initialize source field
|
||||
source_spacing = 120e-6
|
||||
source_field = get_source_field(beam_waist,center_x=source_spacing/2,center_y=0)
|
||||
source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting
|
||||
#source field integrated intensity
|
||||
|
||||
@@ -1,465 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f65ac12a",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This is a first attempt at creating an inverse designed fiber mode converter in meep. At first, we try to implement and combine tutorials from both tidy3d and meep."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "7b374471",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import meep as mp\n",
|
||||
"import meep.adjoint as mpa\n",
|
||||
"import numpy as np\n",
|
||||
"from autograd import numpy as npa\n",
|
||||
"from autograd import tensor_jacobian_product, grad\n",
|
||||
"import nlopt\n",
|
||||
"from matplotlib import pyplot as plt\n",
|
||||
"from matplotlib.patches import Circle\n",
|
||||
"import scipy as sp\n",
|
||||
"import os"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "3955252a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######PHYSICS SETUP######\n",
|
||||
"\n",
|
||||
"mp.verbosity(1)\n",
|
||||
"\n",
|
||||
"#setup a results folder\n",
|
||||
"if not os.path.exists(\"results\"):\n",
|
||||
" os.makedirs(\"results\")\n",
|
||||
"\n",
|
||||
"subs_index = 3.4\n",
|
||||
"subs_perm = subs_index**2\n",
|
||||
"\n",
|
||||
"Si = mp.Medium(index = subs_index)\n",
|
||||
"Air = mp.Medium(index = 1.0)\n",
|
||||
"\n",
|
||||
"wavelength = 1.55\n",
|
||||
"nf = 3 #number of frequencies to test\n",
|
||||
"frequencies = np.array([1/1.5, 1 / 1.55, 1 / 1.6])\n",
|
||||
"\n",
|
||||
"#distance between PML and source/monitor\n",
|
||||
"buffer = 2.5\n",
|
||||
"\n",
|
||||
"######PML######\n",
|
||||
"pml_size = 1.0\n",
|
||||
"pml_layers = [mp.PML(pml_size)]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "b39d1f98",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######FILTER######\n",
|
||||
"minimum_length = 0.09 # minimum length scale (microns)\n",
|
||||
"eta_i = (\n",
|
||||
" 0.5 # blueprint (or intermediate) design field thresholding point (between 0 and 1)\n",
|
||||
")\n",
|
||||
"eta_e = 0.55 # erosion design field thresholding point (between 0 and 1)\n",
|
||||
"eta_d = 1 - eta_e # dilation design field thresholding point (between 0 and 1)\n",
|
||||
"filter_radius = mpa.get_conic_radius_from_eta_e(minimum_length, eta_e)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "b1317dd3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######DESIGN REGION######\n",
|
||||
"design_region_width = 4.0\n",
|
||||
"design_region_length = 4.0\n",
|
||||
"design_region_height = 1.0\n",
|
||||
"\n",
|
||||
"resolution = 15 # pixels/um\n",
|
||||
"design_region_resolution = int(resolution)\n",
|
||||
"\n",
|
||||
"#number of pixels in the design region\n",
|
||||
"Nx = int(design_region_width * design_region_resolution) + 1\n",
|
||||
"Ny = int(design_region_length * design_region_resolution) + 1\n",
|
||||
"\n",
|
||||
"design_variables = mp.MaterialGrid(mp.Vector3(Nx, Ny), Air, Si, grid_type=\"U_MEAN\")\n",
|
||||
"\n",
|
||||
"design_region = mpa.DesignRegion(\n",
|
||||
" design_variables,\n",
|
||||
" volume=mp.Volume(\n",
|
||||
" center = mp.Vector3(0,0,design_region_height / 2),\n",
|
||||
" size = mp.Vector3(design_region_width, \n",
|
||||
" design_region_length, \n",
|
||||
" design_region_height),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"######CELL GEOMETRY######\n",
|
||||
"Sx = 2 * pml_size + design_region_width\n",
|
||||
"Sy = 2 * pml_size + design_region_length\n",
|
||||
"Sz = 2 * pml_size + design_region_height + 2*buffer\n",
|
||||
"print(\"Cell size: \", Sx, Sy, Sz)\n",
|
||||
"cell_size = mp.Vector3(Sx, Sy, Sz)\n",
|
||||
"\n",
|
||||
"substrate_thickness = Sz/2\n",
|
||||
"substrate_size = mp.Vector3(design_region_width, design_region_length, substrate_thickness)\n",
|
||||
"substrate_center = mp.Vector3(\n",
|
||||
" 0,\n",
|
||||
" 0,\n",
|
||||
" - substrate_thickness / 2)\n",
|
||||
"\n",
|
||||
"substrate_block = mp.Block(\n",
|
||||
" center = substrate_center,\n",
|
||||
" size = substrate_size,\n",
|
||||
" material = Si,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"design_region_block = mp.Block(\n",
|
||||
" center = design_region.center, \n",
|
||||
" size = design_region.size, \n",
|
||||
" material = design_variables,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"geometry = [\n",
|
||||
" substrate_block,\n",
|
||||
" design_region_block,\n",
|
||||
"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "607d05b7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######SOURCE######\n",
|
||||
"fcen = 1 / wavelength\n",
|
||||
"width = 0.2\n",
|
||||
"fwidth = width * fcen\n",
|
||||
"source_center = [0, 0, -3]\n",
|
||||
"source_size = mp.Vector3(design_region_width, design_region_length, 0)\n",
|
||||
"src = mp.GaussianSource(frequency=fcen, fwidth=fwidth)\n",
|
||||
"source = [mp.Source(src, component=mp.Ex, size=source_size, center=source_center)]\n",
|
||||
"#src = mp.ContinuousSource(frequency=fcen, fwidth=fwidth, is_integrated=True)\n",
|
||||
"#source = [mp.Source(src, component=mp.Ex, size=source_size, center=source_center)]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cdbe5c87",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######SIMULATION######\n",
|
||||
"kpoint = mp.Vector3()\n",
|
||||
"sim = mp.Simulation(\n",
|
||||
" cell_size = cell_size,\n",
|
||||
" boundary_layers = pml_layers,\n",
|
||||
" geometry = geometry,\n",
|
||||
" sources = source,\n",
|
||||
" default_material = Air,\n",
|
||||
" resolution = resolution,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "5f775457",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######MONITOR######\n",
|
||||
"far_field_obs_point_z_coord = 15\n",
|
||||
"far_z = [mp.Vector3(0,0,far_field_obs_point_z_coord)] #this is the far-field-focal-point\n",
|
||||
"NearRegions = [\n",
|
||||
" mp.Near2FarRegion(\n",
|
||||
" center=mp.Vector3(0,0, design_region_height + wavelength),\n",
|
||||
" size=mp.Vector3(design_region_width,design_region_length,0),\n",
|
||||
" weight=+1,\n",
|
||||
" )\n",
|
||||
"]\n",
|
||||
"#mpa.Near2FarFields return a numpy array with shape (num_of_points,nfreq,6) where the \n",
|
||||
"#third axis are the field components Ex (0),Ey (1),Ez (2),Hx (3),Hy (4),Hz (5)\n",
|
||||
"FarFields = mpa.Near2FarFields(sim, NearRegions, far_z)\n",
|
||||
"ob_list = [FarFields]\n",
|
||||
"\n",
|
||||
"#this the objective to function\n",
|
||||
"#the objective is to maximize the obj function,\n",
|
||||
"def J1(FF):\n",
|
||||
" #the far fields are passed directly by the opt object (which knows them through the ob_list)\n",
|
||||
" #in this case we want to maximize field intensity\n",
|
||||
" # at the position of the focal spot \n",
|
||||
" obj_to_return = npa.mean(npa.abs(FF[0,:,0]) ** 2)\n",
|
||||
" return obj_to_return\n",
|
||||
"#returns the first point of all frequencies of the Ey component"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "27fb5c57",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######CHECK PLOT######\n",
|
||||
"output_plane = mp.Volume(\n",
|
||||
" center=mp.Vector3(0,0,0), \n",
|
||||
" size=mp.Vector3(Sx-2*pml_size,0,Sz-2*pml_size))\n",
|
||||
"\n",
|
||||
"output_plane_xy = mp.Volume(\n",
|
||||
" center=mp.Vector3(0,0,design_region_height / 2),\n",
|
||||
" size=mp.Vector3(Sx-2*pml_size,Sy-2*pml_size,0)\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"fig = sim.plot2D(show_sources=True,show_monitors=True,show_epsilon=True, output_plane=output_plane)\n",
|
||||
"#sim.run(until=10)\n",
|
||||
"#sim.plot2D(fields=mp.Ex,output_plane=output_plane)\n",
|
||||
"#plt.savefig(\"fields.pdf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "9c576fdb",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######OPTIMIZATION PROBLEM######\n",
|
||||
"'''\n",
|
||||
"An OptimizationProblem class object knows how to do one basic thing: \n",
|
||||
"Given an input vector of design variables, compute the objective function value \n",
|
||||
"(forward calculation) and optionally its gradient (adjoint calculation)\n",
|
||||
"The gradient of the objective function is computed against its arguments, which are\n",
|
||||
"the FarFields.\n",
|
||||
"'''\n",
|
||||
"opt = mpa.OptimizationProblem(\n",
|
||||
" simulation=sim, #the optimizer uses this simulation object\n",
|
||||
" objective_functions=[J1], #the optimizer call this objective function\n",
|
||||
" objective_arguments=ob_list, #the optimizer passes this list of arguments to the objective function\n",
|
||||
" design_regions=[design_region], #the optimizer updates the design variables in this design region\n",
|
||||
" frequencies=frequencies, #the optimizer runs the simulation at these frequencies\n",
|
||||
" maximum_run_time=1500, #maximum optimizer run time\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"######CHECK PLOT######\n",
|
||||
"figure = opt.plot2D(True, output_plane=output_plane)\n",
|
||||
"plt.savefig(\"results/simulation_setup.pdf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "b315f21d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def mapping(x,eta,beta):\n",
|
||||
" '''\n",
|
||||
" Arguments:\n",
|
||||
" x: design variables (grey scale)\n",
|
||||
" eta: thresholding point for projection (between 0 and 1)\n",
|
||||
" beta: binarization strength for projection (positive)\n",
|
||||
" Returns:\n",
|
||||
" projected_field: the filtered and projected design variables\n",
|
||||
" '''\n",
|
||||
" #mapping is necessary to reach a physical design.\n",
|
||||
" #we start from the design variables (grey scale), which are values in the range (0,1).\n",
|
||||
" #typically, we set an initial value of 0.5.\n",
|
||||
" #filtering makes a local point behave more like its neighbourhood, avoiding checker patterns\n",
|
||||
" filtered_field = mpa.conic_filter(\n",
|
||||
" x,\n",
|
||||
" filter_radius,\n",
|
||||
" design_region_width,\n",
|
||||
" design_region_length,\n",
|
||||
" [design_region_resolution],\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" #projection\n",
|
||||
" #this is an hyperbolic tangent projection\n",
|
||||
" #after filtering, the design is still blurry, so we project it\n",
|
||||
" #using an tanh projection method to push the values towards the upper/lower bound\n",
|
||||
" #If a filtered value is >0.5, then we push it to 1, otherwise, if <0.5, we push it to 0.\n",
|
||||
" #parameter beta represents the degree of binarization,\n",
|
||||
" #parameter eta is threshold point\n",
|
||||
" projected_field = mpa.tanh_projection(filtered_field,beta,eta)\n",
|
||||
" \n",
|
||||
" #the following lines introduce symmetry. Need to understand if necessary \n",
|
||||
" #projected_field = (\n",
|
||||
" # npa.flipud(projected_field) + projected_field\n",
|
||||
" #)/2\n",
|
||||
"\n",
|
||||
" #interpolate to actual materials\n",
|
||||
" return projected_field.flatten()\n",
|
||||
"\n",
|
||||
"evaluation_history = []\n",
|
||||
"cur_iter = [0]\n",
|
||||
"\n",
|
||||
"def f(v, gradient, cur_beta):\n",
|
||||
" print(\"current iteration: {}\".format(cur_iter[0]+1))\n",
|
||||
"\n",
|
||||
" #value and grad\n",
|
||||
" f0, dJ_du = opt([mapping(v,eta_i,cur_beta)])\n",
|
||||
" if gradient.size > 0:\n",
|
||||
" # NOTE: gradient is assigned in-place! No need to return.\n",
|
||||
" gradient[...] = tensor_jacobian_product(mapping, 0)(\n",
|
||||
" v, eta_i, cur_beta, np.sum(dJ_du, axis=1)\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" evaluation_history.append(np.real(f0))\n",
|
||||
" \n",
|
||||
" fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n",
|
||||
" \n",
|
||||
" opt.plot2D(\n",
|
||||
" False, \n",
|
||||
" ax=ax1,\n",
|
||||
" output_plane=output_plane,\n",
|
||||
" show_sources = False,\n",
|
||||
" show_monitors = False,\n",
|
||||
" show_boundary_layers = False,\n",
|
||||
" )\n",
|
||||
" circ1 = Circle((2,2), minimum_length/2)\n",
|
||||
" ax1.add_patch(circ1)\n",
|
||||
" \n",
|
||||
" opt.plot2D(\n",
|
||||
" False, \n",
|
||||
" ax=ax2,\n",
|
||||
" output_plane=output_plane_xy,\n",
|
||||
" show_sources = False,\n",
|
||||
" show_monitors = False,\n",
|
||||
" show_boundary_layers = False,\n",
|
||||
" )\n",
|
||||
" circ2 = Circle((2,2), minimum_length/2)\n",
|
||||
" ax2.add_patch(circ2)\n",
|
||||
" \n",
|
||||
" if not os.path.exists(\"results\"):\n",
|
||||
" os.makedirs(\"results\")\n",
|
||||
"\n",
|
||||
" plt.savefig(\"results/iter_{}.pdf\".format(cur_iter[0]+1))\n",
|
||||
"\n",
|
||||
" cur_iter[0] += cur_iter[0] + 1\n",
|
||||
"\n",
|
||||
" return np.real(f0)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"algorithm = nlopt.LD_MMA\n",
|
||||
"n = Nx * Ny #number of parameters to optimize\n",
|
||||
"\n",
|
||||
"x = np.ones((n,)) * 0.5 #initial guess\n",
|
||||
"#x = np.insert(x, 0, -1)\n",
|
||||
"#lower and upper bounds for design variables\n",
|
||||
"lb = np.zeros((Nx * Ny,))\n",
|
||||
"ub = np.ones((Nx * Ny,))\n",
|
||||
"\n",
|
||||
"cur_beta = 4 #starting beta\n",
|
||||
"beta_scale = 2 #beta scaling (update) factor\n",
|
||||
"num_betas = 6 #number of betas to test\n",
|
||||
"update_factor = 12 #number of iterations per beta\n",
|
||||
"#total number of simulations = num_betas * update_factor * 2 (forward + adjoint)\n",
|
||||
"ftol = 1e-5\n",
|
||||
"\n",
|
||||
"optimization_start = True\n",
|
||||
"if optimization_start:\n",
|
||||
" for iters in range(num_betas):\n",
|
||||
" solver = nlopt.opt(algorithm, n)\n",
|
||||
" solver.set_lower_bounds(lb)\n",
|
||||
" solver.set_upper_bounds(ub)\n",
|
||||
" solver.set_max_objective(lambda v, g: f(v, g, cur_beta))\n",
|
||||
" solver.set_maxeval(update_factor) #number of iterations for each beta\n",
|
||||
" solver.set_ftol_rel(ftol)\n",
|
||||
" #observe that x is updated in place, so at\n",
|
||||
" # the end it will contain the optimized parameters\n",
|
||||
" x[...] = solver.optimize(x)\n",
|
||||
" cur_beta = cur_beta * beta_scale #update beta\n",
|
||||
" print(\"current beta: \", cur_beta)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "164e57d7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"######RESULTS######\n",
|
||||
"#plot the figure of merit over iterations\n",
|
||||
"plt.figure()\n",
|
||||
"plt.plot(evaluation_history)\n",
|
||||
"plt.xlabel(\"Iteration\")\n",
|
||||
"plt.ylabel(\"Figure of Merit\")\n",
|
||||
"plt.grid()\n",
|
||||
"plt.savefig(\"results/figure_of_merit.pdf\")\n",
|
||||
"\n",
|
||||
"#plot and save the final design\n",
|
||||
"np.savetxt(\"results/final_design.txt\", mapping(x, eta_i, cur_beta))\n",
|
||||
"opt.update_design([mapping(x, eta_i, cur_beta)])\n",
|
||||
"\n",
|
||||
"fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n",
|
||||
" \n",
|
||||
"opt.plot2D(\n",
|
||||
" False, \n",
|
||||
" ax=ax1,\n",
|
||||
" output_plane=output_plane,\n",
|
||||
" show_sources = False,\n",
|
||||
" show_monitors = False,\n",
|
||||
" show_boundary_layers = False,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"circ1 = Circle((2,2), minimum_length/2)\n",
|
||||
"ax1.add_patch(circ1)\n",
|
||||
"ax1.axis(\"off\")\n",
|
||||
"\n",
|
||||
"opt.plot2D(\n",
|
||||
" False, \n",
|
||||
" ax=ax2,\n",
|
||||
" output_plane=output_plane_xy,\n",
|
||||
" show_sources = False,\n",
|
||||
" show_monitors = False,\n",
|
||||
" show_boundary_layers = False,\n",
|
||||
")\n",
|
||||
"circ2 = Circle((2,2), minimum_length/2)\n",
|
||||
"ax2.add_patch(circ2)\n",
|
||||
"ax2.axis(\"off\")\n",
|
||||
" \n",
|
||||
"plt.savefig(\"results/final_design_xy.pdf\")\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "mp",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.13.12"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
# %% [markdown]
|
||||
# This is a first attempt at creating an inverse designed fiber mode converter in meep. At first, we try to implement and combine tutorials from both tidy3d and meep.
|
||||
|
||||
# %%
|
||||
import meep as mp
|
||||
import meep.adjoint as mpa
|
||||
import numpy as np
|
||||
from autograd import numpy as npa
|
||||
from autograd import tensor_jacobian_product, grad
|
||||
import nlopt
|
||||
from matplotlib import pyplot as plt
|
||||
from matplotlib.patches import Circle
|
||||
import scipy as sp
|
||||
import os
|
||||
|
||||
# %%
|
||||
######PHYSICS SETUP######
|
||||
|
||||
mp.verbosity(1)
|
||||
|
||||
#setup a results folder
|
||||
if not os.path.exists("results"):
|
||||
os.makedirs("results")
|
||||
|
||||
subs_index = 3.4
|
||||
subs_perm = subs_index**2
|
||||
|
||||
Si = mp.Medium(index = subs_index)
|
||||
Air = mp.Medium(index = 1.0)
|
||||
|
||||
wavelength = 1.55
|
||||
nf = 3 #number of frequencies to test
|
||||
frequencies = np.array([1/1.5, 1 / 1.55, 1 / 1.6])
|
||||
|
||||
#distance between PML and source/monitor
|
||||
buffer = 2.5
|
||||
|
||||
######PML######
|
||||
pml_size = 1.0
|
||||
pml_layers = [mp.PML(pml_size)]
|
||||
|
||||
# %%
|
||||
######FILTER######
|
||||
minimum_length = 0.09 # minimum length scale (microns)
|
||||
eta_i = (
|
||||
0.5 # blueprint (or intermediate) design field thresholding point (between 0 and 1)
|
||||
)
|
||||
eta_e = 0.55 # erosion design field thresholding point (between 0 and 1)
|
||||
eta_d = 1 - eta_e # dilation design field thresholding point (between 0 and 1)
|
||||
filter_radius = mpa.get_conic_radius_from_eta_e(minimum_length, eta_e)
|
||||
|
||||
# %%
|
||||
######DESIGN REGION######
|
||||
design_region_width = 4.0
|
||||
design_region_length = 4.0
|
||||
design_region_height = 1.0
|
||||
|
||||
resolution = 15 # pixels/um
|
||||
design_region_resolution = int(resolution)
|
||||
|
||||
#number of pixels in the design region
|
||||
Nx = int(design_region_width * design_region_resolution) + 1
|
||||
Ny = int(design_region_length * design_region_resolution) + 1
|
||||
|
||||
design_variables = mp.MaterialGrid(mp.Vector3(Nx, Ny), Air, Si, grid_type="U_MEAN")
|
||||
|
||||
design_region = mpa.DesignRegion(
|
||||
design_variables,
|
||||
volume=mp.Volume(
|
||||
center = mp.Vector3(0,0,design_region_height / 2),
|
||||
size = mp.Vector3(design_region_width,
|
||||
design_region_length,
|
||||
design_region_height),
|
||||
),
|
||||
)
|
||||
|
||||
######CELL GEOMETRY######
|
||||
Sx = 2 * pml_size + design_region_width
|
||||
Sy = 2 * pml_size + design_region_length
|
||||
Sz = 2 * pml_size + design_region_height + 2*buffer
|
||||
print("Cell size: ", Sx, Sy, Sz)
|
||||
cell_size = mp.Vector3(Sx, Sy, Sz)
|
||||
|
||||
substrate_thickness = Sz/2
|
||||
substrate_size = mp.Vector3(design_region_width, design_region_length, substrate_thickness)
|
||||
substrate_center = mp.Vector3(
|
||||
0,
|
||||
0,
|
||||
- substrate_thickness / 2)
|
||||
|
||||
substrate_block = mp.Block(
|
||||
center = substrate_center,
|
||||
size = substrate_size,
|
||||
material = Si,
|
||||
)
|
||||
|
||||
design_region_block = mp.Block(
|
||||
center = design_region.center,
|
||||
size = design_region.size,
|
||||
material = design_variables,
|
||||
)
|
||||
|
||||
geometry = [
|
||||
substrate_block,
|
||||
design_region_block,
|
||||
]
|
||||
|
||||
# %%
|
||||
######SOURCE######
|
||||
fcen = 1 / wavelength
|
||||
width = 0.2
|
||||
fwidth = width * fcen
|
||||
source_center = [0, 0, -3]
|
||||
source_size = mp.Vector3(design_region_width, design_region_length, 0)
|
||||
src = mp.GaussianSource(frequency=fcen, fwidth=fwidth)
|
||||
source = [mp.Source(src, component=mp.Ex, size=source_size, center=source_center)]
|
||||
#src = mp.ContinuousSource(frequency=fcen, fwidth=fwidth, is_integrated=True)
|
||||
#source = [mp.Source(src, component=mp.Ex, size=source_size, center=source_center)]
|
||||
|
||||
# %%
|
||||
######SIMULATION######
|
||||
kpoint = mp.Vector3()
|
||||
sim = mp.Simulation(
|
||||
cell_size = cell_size,
|
||||
boundary_layers = pml_layers,
|
||||
geometry = geometry,
|
||||
sources = source,
|
||||
default_material = Air,
|
||||
resolution = resolution,
|
||||
)
|
||||
|
||||
# %%
|
||||
######MONITOR######
|
||||
far_field_obs_point_z_coord = 15
|
||||
far_z = [mp.Vector3(0,0,far_field_obs_point_z_coord)] #this is the far-field-focal-point
|
||||
NearRegions = [
|
||||
mp.Near2FarRegion(
|
||||
center=mp.Vector3(0,0, design_region_height + wavelength),
|
||||
size=mp.Vector3(design_region_width,design_region_length,0),
|
||||
weight=+1,
|
||||
)
|
||||
]
|
||||
#mpa.Near2FarFields return a numpy array with shape (num_of_points,nfreq,6) where the
|
||||
#third axis are the field components Ex,Ey,Ez,Hx,Hy,Hz
|
||||
FarFields = mpa.Near2FarFields(sim, NearRegions, far_z)
|
||||
ob_list = [FarFields]
|
||||
|
||||
#this the objective to function
|
||||
#the objective is to maximize the obj function,
|
||||
def J1(FF):
|
||||
#in this case we want to maximize field intensity
|
||||
# at the position of the focal spot
|
||||
obj_to_return = npa.mean(npa.abs(FF[0,:,0]) ** 2)
|
||||
return obj_to_return
|
||||
#returns the first point of all frequencies of the Ey component
|
||||
|
||||
# %%
|
||||
######CHECK PLOT######
|
||||
output_plane = mp.Volume(
|
||||
center=mp.Vector3(0,0,0),
|
||||
size=mp.Vector3(Sx-2*pml_size,0,Sz-2*pml_size))
|
||||
|
||||
output_plane_xy = mp.Volume(
|
||||
center=mp.Vector3(0,0,design_region_height / 2),
|
||||
size=mp.Vector3(Sx-2*pml_size,Sy-2*pml_size,0)
|
||||
)
|
||||
|
||||
fig = sim.plot2D(show_sources=True,show_monitors=True,show_epsilon=True, output_plane=output_plane)
|
||||
#sim.run(until=10)
|
||||
#sim.plot2D(fields=mp.Ex,output_plane=output_plane)
|
||||
#plt.savefig("fields.pdf")
|
||||
|
||||
# %%
|
||||
######OPTIMIZATION PROBLEM######
|
||||
'''
|
||||
An OptimizationProblem class object knows how to do one basic thing:
|
||||
Given an input vector of design variables, compute the objective function value
|
||||
(forward calculation) and optionally its gradient (adjoint calculation)
|
||||
The gradient of the objective function is computed against its arguments, which are
|
||||
the FarFields.
|
||||
'''
|
||||
opt = mpa.OptimizationProblem(
|
||||
simulation=sim, #the optimizer uses this simulation object
|
||||
objective_functions=[J1], #the optimizer call this objective function
|
||||
objective_arguments=ob_list, #the optimizer passes this list of arguments to the objective function
|
||||
design_regions=[design_region], #the optimizer updates the design variables in this design region
|
||||
frequencies=frequencies, #the optimizer runs the simulation at these frequencies
|
||||
maximum_run_time=1500, #maximum optimizer run time
|
||||
)
|
||||
|
||||
######CHECK PLOT######
|
||||
figure = opt.plot2D(True, output_plane=output_plane)
|
||||
plt.savefig("results/simulation_setup.pdf")
|
||||
|
||||
# %%
|
||||
def mapping(x,eta,beta):
|
||||
'''
|
||||
Arguments:
|
||||
x: design variables (grey scale)
|
||||
eta: thresholding point for projection (between 0 and 1)
|
||||
beta: binarization strength for projection (positive)
|
||||
Returns:
|
||||
projected_field: the filtered and projected design variables
|
||||
'''
|
||||
#mapping is necessary to reach a physical design.
|
||||
#we start from the design variables (grey scale), which are values in the range (0,1).
|
||||
#typically, we set an initial value of 0.5.
|
||||
#filtering makes a local point behave more like its neighbourhood, avoiding checker patterns
|
||||
filtered_field = mpa.conic_filter(
|
||||
x,
|
||||
filter_radius,
|
||||
design_region_width,
|
||||
design_region_length,
|
||||
[design_region_resolution],
|
||||
)
|
||||
|
||||
#projection
|
||||
#this is an hyperbolic tangent projection
|
||||
#after filtering, the design is still blurry, so we project it
|
||||
#using an tanh projection method to push the values towards the upper/lower bound
|
||||
#If a filtered value is >0.5, then we push it to 1, otherwise, if <0.5, we push it to 0.
|
||||
#parameter beta represents the degree of binarization,
|
||||
#parameter eta is threshold point
|
||||
projected_field = mpa.tanh_projection(filtered_field,beta,eta)
|
||||
|
||||
#the following lines introduce symmetry. Need to understand if necessary
|
||||
#projected_field = (
|
||||
# npa.flipud(projected_field) + projected_field
|
||||
#)/2
|
||||
|
||||
#interpolate to actual materials
|
||||
return projected_field.flatten()
|
||||
|
||||
evaluation_history = []
|
||||
cur_iter = [0]
|
||||
|
||||
def f(v, gradient, cur_beta):
|
||||
print("current iteration: {}".format(cur_iter[0]+1))
|
||||
|
||||
#value and grad
|
||||
f0, dJ_du = opt([mapping(v,eta_i,cur_beta)])
|
||||
if gradient.size > 0:
|
||||
# NOTE: gradient is assigned in-place! No need to return.
|
||||
gradient[...] = tensor_jacobian_product(mapping, 0)(
|
||||
v, eta_i, cur_beta, np.sum(dJ_du, axis=1)
|
||||
)
|
||||
|
||||
evaluation_history.append(np.real(f0))
|
||||
|
||||
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
|
||||
|
||||
opt.plot2D(
|
||||
False,
|
||||
ax=ax1,
|
||||
output_plane=output_plane,
|
||||
show_sources = False,
|
||||
show_monitors = False,
|
||||
show_boundary_layers = False,
|
||||
)
|
||||
circ1 = Circle((2,2), minimum_length/2)
|
||||
ax1.add_patch(circ1)
|
||||
|
||||
opt.plot2D(
|
||||
False,
|
||||
ax=ax2,
|
||||
output_plane=output_plane_xy,
|
||||
show_sources = False,
|
||||
show_monitors = False,
|
||||
show_boundary_layers = False,
|
||||
)
|
||||
circ2 = Circle((2,2), minimum_length/2)
|
||||
ax2.add_patch(circ2)
|
||||
|
||||
if not os.path.exists("results"):
|
||||
os.makedirs("results")
|
||||
|
||||
plt.savefig("results/iter_{}.pdf".format(cur_iter[0]+1))
|
||||
|
||||
cur_iter[0] += cur_iter[0] + 1
|
||||
|
||||
return np.real(f0)
|
||||
|
||||
|
||||
algorithm = nlopt.LD_MMA
|
||||
n = Nx * Ny #number of parameters to optimize
|
||||
|
||||
x = np.ones((n,)) * 0.5 #initial guess
|
||||
#x = np.insert(x, 0, -1)
|
||||
#lower and upper bounds for design variables
|
||||
lb = np.zeros((Nx * Ny,))
|
||||
ub = np.ones((Nx * Ny,))
|
||||
|
||||
cur_beta = 4 #starting beta
|
||||
beta_scale = 2 #beta scaling (update) factor
|
||||
num_betas = 6 #number of betas to test
|
||||
update_factor = 12 #number of iterations per beta
|
||||
#total number of simulations = num_betas * update_factor * 2 (forward + adjoint)
|
||||
ftol = 1e-5
|
||||
|
||||
optimization_start = True
|
||||
if optimization_start:
|
||||
for iters in range(num_betas):
|
||||
solver = nlopt.opt(algorithm, n)
|
||||
solver.set_lower_bounds(lb)
|
||||
solver.set_upper_bounds(ub)
|
||||
solver.set_max_objective(lambda v, g: f(v, g, cur_beta))
|
||||
solver.set_maxeval(update_factor) #number of iterations for each beta
|
||||
solver.set_ftol_rel(ftol)
|
||||
#observe that x is updated in place, so at
|
||||
# the end it will contain the optimized parameters
|
||||
x[...] = solver.optimize(x)
|
||||
cur_beta = cur_beta * beta_scale #update beta
|
||||
print("current beta: ", cur_beta)
|
||||
|
||||
# %%
|
||||
######RESULTS######
|
||||
#plot the figure of merit over iterations
|
||||
plt.figure()
|
||||
plt.plot(evaluation_history)
|
||||
plt.xlabel("Iteration")
|
||||
plt.ylabel("Figure of Merit")
|
||||
plt.grid()
|
||||
plt.savefig("results/figure_of_merit.pdf")
|
||||
|
||||
#plot and save the final design
|
||||
np.savetxt("results/final_design.txt", mapping(x, eta_i, cur_beta))
|
||||
opt.update_design([mapping(x, eta_i, cur_beta)])
|
||||
|
||||
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
|
||||
|
||||
opt.plot2D(
|
||||
False,
|
||||
ax=ax1,
|
||||
output_plane=output_plane,
|
||||
show_sources = False,
|
||||
show_monitors = False,
|
||||
show_boundary_layers = False,
|
||||
)
|
||||
|
||||
circ1 = Circle((2,2), minimum_length/2)
|
||||
ax1.add_patch(circ1)
|
||||
ax1.axis("off")
|
||||
|
||||
opt.plot2D(
|
||||
False,
|
||||
ax=ax2,
|
||||
output_plane=output_plane_xy,
|
||||
show_sources = False,
|
||||
show_monitors = False,
|
||||
show_boundary_layers = False,
|
||||
)
|
||||
circ2 = Circle((2,2), minimum_length/2)
|
||||
ax2.add_patch(circ2)
|
||||
ax2.axis("off")
|
||||
|
||||
plt.savefig("results/final_design_xy.pdf")
|
||||
@@ -1,219 +0,0 @@
|
||||
#this file serves to simulate the optimized design parameters
|
||||
#it imports data from the optimization process and runs a meep simulation to verify the results
|
||||
import faulthandler
|
||||
faulthandler.enable()
|
||||
from typing import List, NamedTuple, Tuple
|
||||
from autograd import numpy as npa, tensor_jacobian_product, grad
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.animation as animation
|
||||
import meep as mp
|
||||
import meep.adjoint as mpa
|
||||
import nlopt
|
||||
import numpy as np
|
||||
import os
|
||||
import xarray as xr
|
||||
from PIL import Image
|
||||
import pickle
|
||||
|
||||
if not os.path.exists("sim_opt_design_results"):
|
||||
os.makedirs("sim_opt_design_results")
|
||||
#import data from previous savez file
|
||||
try:
|
||||
with open("results/optimal_design.pkl", "rb") as f:
|
||||
data = pickle.load(f)
|
||||
RESOLUTION=data['RESOLUTION']
|
||||
MAX_RUN_TIME = data['MAX_RUN_TIME']
|
||||
WAVELENGTH_MIN_UM = data['WAVELENGTH_MIN_UM']
|
||||
WAVELENGTH_MAX_UM = data['WAVELENGTH_MAX_UM']
|
||||
DESIGN_REGION_CENTER = data['DESIGN_REGION_CENTER']
|
||||
SILICON = data['SILICON']
|
||||
SILICON_DIOXIDE = data['SILICON_DIOXIDE']
|
||||
AIR = data['AIR']
|
||||
cell_um = data['cell_um']
|
||||
SUBSTRATE_THICKNESS = data['SUBSTRATE_THICKNESS']
|
||||
SUBSTRATE_SIZE = data['SUBSTRATE_SIZE']
|
||||
SUBSTRATE_CENTER = data['SUBSTRATE_CENTER']
|
||||
physical_domains_size = data['physical_domains_size']
|
||||
frequency_min = data['frequency_min']
|
||||
frequency_max = data['frequency_max']
|
||||
frequency_center = data['frequency_center']
|
||||
frequency_width = data['frequency_width']
|
||||
frequencies = data['frequencies']
|
||||
num_wavelength = data['num_wavelengths']
|
||||
SOURCE_CENTER = data['SOURCE_CENTER']
|
||||
SOURCE_SIZE = data['SOURCE_SIZE']
|
||||
NEAR_REGION_MONITOR_CENTER = data['NEAR_REGION_MONITOR_CENTER']
|
||||
NEAR_REGION_MONITOR_SIZE = data['NEAR_REGION_MONITOR_SIZE']
|
||||
FF_MONITOR_CENTER = data['FF_MONITOR_CENTER']
|
||||
FF_MONITOR_SIZE = data['FF_MONITOR_SIZE']
|
||||
xs = data['xs']
|
||||
ys = data['ys']
|
||||
ff_points = data['ff_points']
|
||||
DESIGN_WAVELENGTHS_UM=data['DESIGN_WAVELENGTHS_UM']
|
||||
BUFFER=data['BUFFER']
|
||||
PML_UM=data['PML_UM']
|
||||
DESIGN_REGION_UM=data['DESIGN_REGION_UM']
|
||||
DESIGN_REGION_RESOLUTION=data['DESIGN_REGION_RESOLUTION']
|
||||
NX_DESIGN_GRID=data['NX_DESIGN_GRID']
|
||||
NY_DESIGN_GRID=data['NY_DESIGN_GRID']
|
||||
MIN_LENGTH_UM=data['MIN_LENGTH_UM']
|
||||
sigmoid_biases=data['sigmoid_biases']
|
||||
max_eval=data['max_eval']
|
||||
objfunc_history=data['objfunc_history']
|
||||
epivar_history=data['epivar_history']
|
||||
epigraph_variable=data['epigraph_variable']
|
||||
unmapped_design_weights=data['unmapped_design_weights']
|
||||
optimal_design_weights=data['optimal_design_weights']
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError("Optimal design data not found.")
|
||||
|
||||
print(optimal_design_weights)
|
||||
pml_layers = [mp.PML(PML_UM)]
|
||||
|
||||
view_2D_plane_xz = mp.Volume(
|
||||
center=mp.Vector3(0,0,0),
|
||||
size=mp.Vector3(cell_um.x,0,cell_um.z)
|
||||
)
|
||||
|
||||
view_2D_plane_xy = mp.Volume(
|
||||
center=mp.Vector3(0,0,DESIGN_REGION_CENTER.z/2),
|
||||
size=mp.Vector3(cell_um.x,cell_um.y,0)
|
||||
)
|
||||
|
||||
matgrid = mp.MaterialGrid(
|
||||
grid_size = mp.Vector3(NX_DESIGN_GRID, NY_DESIGN_GRID,0),
|
||||
medium1 = AIR,
|
||||
medium2 = SILICON,
|
||||
weights = optimal_design_weights.reshape((NX_DESIGN_GRID, NY_DESIGN_GRID))
|
||||
)
|
||||
|
||||
matgrid_region = mpa.DesignRegion(
|
||||
matgrid,
|
||||
volume=mp.Volume(
|
||||
center=DESIGN_REGION_CENTER,
|
||||
size=DESIGN_REGION_UM
|
||||
)
|
||||
)
|
||||
|
||||
matgrid_block = mp.Block(
|
||||
center=matgrid_region.center,
|
||||
size=matgrid_region.size,
|
||||
material=matgrid,
|
||||
)
|
||||
|
||||
substrate_block = mp.Block(
|
||||
center = SUBSTRATE_CENTER,
|
||||
size = SUBSTRATE_SIZE,
|
||||
material = SILICON_DIOXIDE,
|
||||
)
|
||||
|
||||
geometry = [
|
||||
matgrid_block,
|
||||
substrate_block,
|
||||
]
|
||||
|
||||
sources = [
|
||||
mp.Source(
|
||||
src=mp.GaussianSource(frequency_center, fwidth=frequency_width),
|
||||
component=mp.Ex,
|
||||
size=SOURCE_SIZE,
|
||||
center=SOURCE_CENTER,
|
||||
)
|
||||
]
|
||||
|
||||
sim = mp.Simulation(
|
||||
resolution=RESOLUTION,
|
||||
default_material=AIR,
|
||||
cell_size=cell_um,
|
||||
sources=sources,
|
||||
geometry=geometry,
|
||||
boundary_layers=pml_layers,
|
||||
k_point=mp.Vector3(),
|
||||
)
|
||||
|
||||
plt.figure()
|
||||
ax = plt.gca()
|
||||
sim.plot2D(output_plane=view_2D_plane_xz, ax=ax)
|
||||
plt.savefig(f"sim_opt_design_results/simulation_setup.pdf")
|
||||
plt.close()
|
||||
|
||||
NearRegions = [
|
||||
mp.Near2FarRegion(
|
||||
center=NEAR_REGION_MONITOR_CENTER,
|
||||
size=NEAR_REGION_MONITOR_SIZE,
|
||||
weight=+1,
|
||||
)
|
||||
]
|
||||
|
||||
FarFields = mpa.Near2FarFields(
|
||||
sim=sim,
|
||||
Near2FarRegions=NearRegions,
|
||||
far_pts=ff_points,
|
||||
)
|
||||
obj_list = [FarFields]
|
||||
|
||||
def dummy_J1(ff):
|
||||
return [1]*num_wavelength
|
||||
|
||||
opt = mpa.OptimizationProblem(
|
||||
simulation=sim,
|
||||
objective_functions=[dummy_J1],
|
||||
objective_arguments=[FarFields],
|
||||
design_regions=[matgrid_region],
|
||||
frequencies=frequencies,
|
||||
maximum_run_time=MAX_RUN_TIME,
|
||||
)
|
||||
|
||||
#make a plot of the optimization setup
|
||||
plt.figure(figsize=(12,6))
|
||||
ax1 = plt.subplot(1,2,1)
|
||||
ax2 = plt.subplot(1,2,2)
|
||||
opt.plot2D(True, output_plane=view_2D_plane_xz, ax=ax1)
|
||||
|
||||
opt.plot2D(True, output_plane=view_2D_plane_xy, ax=ax2)
|
||||
|
||||
plt.savefig(f"sim_opt_design_results/optimization_setup.pdf")
|
||||
|
||||
plt.close()
|
||||
|
||||
|
||||
print(type(optimal_design_weights), optimal_design_weights.shape)
|
||||
dummy = opt(
|
||||
[
|
||||
optimal_design_weights.flatten()
|
||||
],
|
||||
need_gradient=False,
|
||||
)
|
||||
|
||||
#note that get_objective_arguments returns
|
||||
# a LIST of the arguments passed to the obj. fun.
|
||||
#we select the first list element, since we ony have one obj. fun.
|
||||
solved_ff = opt.get_objective_arguments()[0]
|
||||
|
||||
def intensity_from_farfields(FarFields) -> np.ndarray:
|
||||
'''Computes the intensity pattern at the monitor location from the simulated far-fields
|
||||
|
||||
Args:
|
||||
FarFields: the simulated far-field patterns at the farfield
|
||||
monitor location, for each wavelength
|
||||
Returns:
|
||||
A 2D-array [Nx*Ny,num_wavelengths] of intensities, for each wavelength,
|
||||
at the monitor location.
|
||||
'''
|
||||
intensity_x = npa.abs(FarFields[:,:,0]) ** 2
|
||||
|
||||
return intensity_x
|
||||
|
||||
solved_intensity = intensity_from_farfields(solved_ff)
|
||||
|
||||
#make a plot of the ff intensities (at the center frequency)
|
||||
array_to_plot = solved_intensity[:,1].reshape(NX_DESIGN_GRID, NY_DESIGN_GRID)
|
||||
plt.figure()
|
||||
plt.imshow(array_to_plot, origin='lower', extent=(-cell_um.x/2,cell_um.x/2,-cell_um.y/2,cell_um.y/2))
|
||||
plt.xlabel("x (um)")
|
||||
plt.ylabel("y (um)")
|
||||
plt.title("Simulated far-field intensity at center frequency")
|
||||
plt.colorbar(label="Intensity (a.u.)")
|
||||
plt.savefig(f"sim_opt_design_results/simulated_farfield_intensity.pdf")
|
||||
plt.close()
|
||||
|
||||
Reference in New Issue
Block a user