repo cleanup

This commit is contained in:
2026-07-29 22:11:49 +02:00
parent 7d827aa5ff
commit a315e70576
15 changed files with 269 additions and 4888 deletions
-112
View File
File diff suppressed because one or more lines are too long
-49
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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")
-827
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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
-465
View File
@@ -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
}
-356
View File
@@ -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")
-219
View File
@@ -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()