From a315e70576420662f9358f80ffd10b1ee35a2048 Mon Sep 17 00:00:00 2001 From: Edoardo Vergerio Date: Wed, 29 Jul 2026 22:11:49 +0200 Subject: [PATCH] repo cleanup --- fiber_modes.ipynb | 112 --- hdf5_vis.py | 49 -- metapeg.ipynb | 654 ----------------- metapeg.py | 526 -------------- metapeg_v2.py | 823 --------------------- metapeg_v2_unc.py | 827 ---------------------- metaprop.py | 253 ------- metaprop_ad.py | 432 ----------- metaprop_asm.py | 81 ++- metaprop_fibermode_doublet.py | 350 ++++----- metaprop_fibermode_doublet_multi.py | 4 +- metaprop_fibermode_doublet_shiftSource.py | 6 +- mode_converter.ipynb | 465 ------------ mode_converter.py | 356 ---------- sim_opt_design.py | 219 ------ 15 files changed, 269 insertions(+), 4888 deletions(-) delete mode 100644 fiber_modes.ipynb delete mode 100644 hdf5_vis.py delete mode 100644 metapeg.ipynb delete mode 100644 metapeg.py delete mode 100644 metapeg_v2.py delete mode 100644 metapeg_v2_unc.py delete mode 100644 metaprop.py delete mode 100644 metaprop_ad.py delete mode 100644 mode_converter.ipynb delete mode 100644 mode_converter.py delete mode 100644 sim_opt_design.py diff --git a/fiber_modes.ipynb b/fiber_modes.ipynb deleted file mode 100644 index 7e1e796..0000000 --- a/fiber_modes.ipynb +++ /dev/null @@ -1,112 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 17, - "id": "9047df66", - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "import scipy\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import ofiber" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "598ecadf", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA0QAAAGICAYAAABhrCDIAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAbpVJREFUeJzt3Qd4VFXaB/B/eiOFJCSkJxB66L1JFRQbqNgLdtaG665t9VNZXV37rr2tiCKKohQRFVRAem+hl4Q0UkhIIb3M97znZiYd0qb/f89zmZk7NzN3zgz33Peec97joNPpdCAiIiIiIrJDjubeASIiIiIiInNhQERERERERHaLAREREREREdktBkRERERERGS3GBAREREREZHdYkBERERERER2iwERERERERHZLQZERERERERktxgQERERERGR3WJAREREREREdosBkZl8/vnncHBwUMvatWsbPK/T6RAbG6ueHz9+fLu97/PPP69e09QWLVqEPn36wMPDQ73/nj17jP6eL7zwAjp16oRbb721XV7v3LlzeOSRRxAaGgp3d3cMGDAA33zzDYxF/12dOXMGlmDGjBnq+8vNzW1ym5tvvhkuLi7IyMho03vJ7+Oyyy5DZGSkek9/f3+MHDkSCxYsaNbf7969G9OnT1fflaenJ3r27Il//vOfKCoqavX3aurvn8gcWDdZV91UUFCAxx9/HFOmTFGvKXWG1B3GxLqJdZMtYkBkZt7e3vjf//7XYP26detw4sQJ9by1y8rKUgf+rl274pdffsHmzZvRvXt3o77nli1b8NJLL+Ef//iHut8err76asyfPx/PPfccfv75ZwwdOhQ33ngjFi5cCHtw1113oaSkpMnPm5eXhyVLluDyyy9HcHBwm95Lgq6IiAj1Ha5cuRJffPEFoqOj1e/oxRdfPO/fHjx4EKNGjUJiYiL+85//YMWKFbjhhhtUQCTfV2u/V3v//sm+sG6yjropOzsbH3/8MUpLS9VFIHvEuol1U7vQkVnMmzdPJ8V/99136zw8PHR5eXl1nr/lllt0I0eO1PXp00c3bty4dnvf5557Tr2vKW3YsEG956JFi9rtNQsLC8/7vJTrTTfdpHv11Vd1EydObPP7/fTTT+ozLFy4sM76iy++WBcaGqqrqKjQtTf9d5WVlaWzBPIZ5bMOHjy40ec/+OADtb8//vij0fZh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WQLF5ORk+PhwXheWy4XxN8NyaYm8vDxERkYajsPEuonHmNbj8Zflwt+MZddNFh0QOTpqWcElAuRJf+OkXFg2LJeW4G+G5dKa4zCxbuIxpu14/GW58DdjmXUTazoiIiIiIrJbDIiIiIiIiMhuWXRA5Obmhueee07dEsuGvxn+f+JxhsdfS8C6ieXC3wz/L/E4Y1vHXwcdc6oSEREREZGdsugWIiIiIiIiImNiQERERERERHaLAREREREREdktBkRERERERGS3zB4Qvf/++4iJiYG7uzsGDx6M9evXn3f7devWqe1k+y5duuDDDz+ELWpJufzwww+4+OKL0alTJzXp28iRI/Hrr7/CVrX0N6O3ceNGODs7Y8CAAbBFLS2X0tJSPP3004iKilLZWrp27YrPPvsMtqilZfPVV1+hf//+8PT0REhICO644w5kZ2fDlvz555+44oorEBoaCgcHByxduvSCf2Mvx1/Buqnt5cK6iXVTa/4vsW5qGusmI9ZNOjP65ptvdC4uLrpPPvlEd/DgQd2cOXN0Xl5eulOnTjW6/cmTJ3Wenp5qO9le/k7+fvHixTpb0tJykedfeeUV3bZt23RHjx7VPfXUU+rvd+3apbM1LS0bvdzcXF2XLl10U6ZM0fXv319na1pTLldeeaVu+PDhutWrV+sSEhJ0W7du1W3cuFFn72Wzfv16naOjo+6///2vOubI4z59+uimT5+usyUrV67UPf3007rvv/9eJ1XBkiVLzru9vRx/Beum9ikX1k2sm1g3td9xhnWTcesmswZEw4YN082ePbvOup49e+qefPLJRrd//PHH1fO13XfffboRI0bobElLy6UxvXv31s2dO1dna1pbNtdff73umWee0T333HM2GRC1tFx+/vlnna+vry47O1tn61paNq+99poKnmt7++23deHh4Tpb1ZyAyF6Ov4J1U/uUS2NYN9XFuqku1k2sm8xVN5mty1xZWRl27tyJKVOm1Fkvjzdt2tTo32zevLnB9lOnTsWOHTtQXl4OW9CacqmvqqoKBQUF8Pf3hy1pbdnMmzcPJ06cUBN52aLWlMvy5csxZMgQvPrqqwgLC0P37t3x97//HcXFxbD3shk1ahRSUlKwcuVKuWCEjIwMLF68GJdddhnsmT0cfwXrpvYrl/pYN9XFuqkh1k2sm8xVNznDTM6cOYPKykoEBwfXWS+P09PTG/0bWd/Y9hUVFer1pK+/tWtNudT3xhtvoLCwENdddx1sSWvK5tixY3jyySdVn2UZP2SLWlMuJ0+exIYNG1R/2yVLlqjXuP/++5GTk2NT44haUzYSEEk/7euvvx4lJSXq+HLllVfinXfegT2zh+OvYN3UfuVSH+umGqybWDexbrKsusnsSRVkMG9tckW2/roLbd/YemvX0nLR+/rrr/H8889j0aJFCAoKgi1qbtnIweamm27C3LlzVQuIrWvJb0au1MpzcuI/bNgwTJs2DW+++SY+//xzm2slamnZHDx4EA8//DCeffZZdUX8l19+QUJCAmbPng17Zy/HX8G6qX3KRY91Uw3WTaybWvP/iXWTcesms10yDwwMhJOTU4MrS5mZmQ0iPb3OnTs3ur1c+Q8ICIAtaE256EkQdNddd+G7777D5MmTYWtaWjbSbVCaTHfv3o0HH3zQEAjIfxT5zaxatQoTJ06EPf5m5IqJdJXz9fU1rOvVq5cqG+ku1q1bN9iC1pTNyy+/jNGjR+Oxxx5Tj/v16wcvLy+MHTsWL774os20hLSUPRx/Beum9isXPdZNdbFuYt3Eusny6iaztRC5urqqFHmrV6+us14eS5eVxkg66frby0mtjIVwcXGBLWhNueivvs2aNQsLFy602bEOLS0bSUG+f/9+7Nmzx7DIVf4ePXqo+8OHD4e9/mbkhD8tLQ3nzp0zrDt69CgcHR0RHh4OW9GasikqKlLlUJucCNa+6mSP7OH4K1g3tV+5CNZNrJtYN7XP/yfWTUaum3QWkHLwf//7n0qV98gjj6iUg4mJiep5yVxz6623Nkit99e//lVtL39ni2lfW1ouCxcu1Dk7O+vee+893enTpw2LpJq2NS0tm/psNctcS8uloKBAZU279tprdQcOHNCtW7dO161bN93dd9+ts/eymTdvnvr/9P777+tOnDih27Bhg27IkCEqw5Ytkd/A7t271SJVwZtvvqnu61O+2uvxV7Buap9yYd3Euol1U/sdZ1g3nTJq3WTWgEjISXxUVJTO1dVVN2jQIHVipnf77bfrxo0bV2f7tWvX6gYOHKi2j46O1n3wwQc6W9SScpH7ckJTf5HtbFFLfzP2EBC1plwOHTqkmzx5ss7Dw0MFR48++qiuqKhIZ4taWjaSZlvSA0vZhISE6G6++WZdSkqKzpasWbPmvMcNez7+CtZNbS8X1k2sm1rzf4l1U9Nlw7pJZ7S6yUH+aX57EhERERERke0we5Y5IiIiIiIic2FAREREREREdosBERERERER2S0GREREREREZLcYEBERERERkd1iQERERERERHaLAREREREREdktBkRERERERGS3GBARtbPx48fjkUceYbkSEZHFYN1E1DQGREREREREZLcYEBERERERkd1iQERkBBUVFXjwwQfh5+eHgIAAPPPMM9DpdCxrIiIyG9ZNRI1jQERkBPPnz4ezszO2bt2Kt99+G2+99RY+/fRTljUREZkN6yaixjnoeNmaqN0HrmZmZuLAgQNwcHBQ65588kksX74cBw8eZGkTEZHJsW4iahpbiIiMYMSIEYZgSIwcORLHjh1DZWUly5uIiMyCdRNR4xgQERERERGR3WJARGQEW7ZsafC4W7ducHJyYnkTEZFZsG4iahwDIiIjSE5OxqOPPoojR47g66+/xjvvvIM5c+awrImIyGxYNxE1zrmJ9UTUBrfddhuKi4sxbNgw1Sr00EMP4d5772WZEhGR2bBuImocs8wREREREZHdYpc5IiIiIiKyWwyIiIiIiIjIbjEgIiIiIiIiu8WAiIiIiIiI7BYDIiIiIiIislsMiIiIiIiIyG4xICIiIiIiIrvFgIiIiIiIiOwWAyIiIiIiIrJbDIiIiIiIiMhuMSAiIiIiIiLYq/8HAsDXRQuljWQAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "V = 3.8\n", - "ell = 0\n", - "plt.subplots(1, 2, figsize=(10, 4))\n", - "plt.subplot(1, 2, 1)\n", - "aplt = ofiber.plot_LP_modes(V, ell)\n", - "plt.subplot(1, 2, 2)\n", - "ell = 1\n", - "\n", - "aplt = ofiber.plot_LP_modes(V, ell)\n", - "aplt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "67132f9f", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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o+np6ReNotawsbFRt7pcXV1hauRNaApvRLp2fA8Q3wPE9wA5m1A8cLWR0hpX75XZQqytrdGjRw/8+eef9fbLdv/+/fV1WERERESkJ3qdypdpeSkX1bNnT/Tr1w9ffPEFkpOTcf/99+vzsIiIiIhIa4Hp9OnTkZmZif/7v/9TBfY7deqE33//HUFBQdAiWabwwgsvXLRcgbSD7wHie4D4HiAbDccDeu/8RERERESk1zWmRERERER1MTAlIiIiIoPAwJSIiIiIDAIDUyIiIiIyCAxMiYiIiMggMDDVo1deeUU1E7C3t29wByspovDiiy+qtl52dnYYOnQo4uLiWvxYqeVkZWWper7SFUNucj87O/uK33PnnXeqNrx1b3379uXLZCQ++eQThISEwNbWVjUa2bRp0xUfv2HDBvU4eXxoaCg+++yzVjtW0v97YP369Rf9e5fb4cOH+fIYqY0bN2LixInqb7m8losXL77q92zQyOcAA1M9Ki0txY033oh//OMfDf6eN998E++++y4++ugj7Nq1C76+vhg1apTqQUvGacaMGdi3bx/++OMPdZP7EpxezdixY1X935qb1AAmwzdv3jw88sgjeO6557B3714MGjQI48aNU81FLuXEiRMYP368epw8/tlnn8XDDz+MBQsWtPqxk37eAzWOHDlS7998REQEXxIjVVBQgC5duqi/5Q1xQkufA1LHlPTr22+/1bm4uFz1cZWVlTpfX1/d66+/XruvuLhYfe9nn33WwkdJLSE+Pl7qCOu2b99eu2/btm1q3+HDhy/7fTNnztRNnjyZL4oR6t27t+7++++vt69Dhw66p59++pKPf+qpp9TX67rvvvt0ffv2bdHjJMN5D6xbt059JmRlZfFlMUHy2i5atOiKj3lKQ58DHDE1InLFlJaWhtGjR9fuk64QQ4YMwdatW/V6bHRttm3bpqbv+/TpU7tPpuRl39VeU5ne8/b2Rrt27XDvvfciPT2dL4MRzJLs2bOn3r9hIduXe73lPXLh48eMGYPdu3ejrKysRY+XDOM9UKNbt27w8/PDiBEjsG7dOr48GrJNQ58DDEyNiASlwsfHp95+2a75GhkXed0kuLyQ7LvSayrTfj/99BPWrl2Ld955Ry3rGD58OEpKSlr4iKkpMjIyUFFR0ah/w7L/Uo8vLy9Xz0em/x6QYPSLL75Q07YLFy5E+/btVXAq6xRJG9I09Dlgqe8DMDWSmDRnzpwrPkaCiJ49e17zz5CF0nXJTMCF+8g43gfiUq/d1V7T6dOn197v1KmTej8FBQVh+fLluP7665t07NTyGvtv+FKPv9R+Ms33gASicqvRr18/nDp1Cm+//TYGDx7c4sdKhsFMI58DDEyb2axZs3DzzTdf8THBwcHX9NyS6FRz5SRX0DVkCvfCKykyjvfBgQMHcPbs2Yu+du7cuUa9pvJ+kMA0ISHhmo6XWoenpycsLCwuGhm70r9h+Xd/qcdbWlrCw8OjRY+XDOM9cCmy5OfHH39sgSMkQ+Sroc8BBqYt8KEjt5YgpUXkzfnnn3+qtUY165WkhMQbb7zRIj+TWvZ9ICMfOTk52LlzJ3r37q327dixQ+2TUmINlZmZqUZQ6l6wkOGxtrZW5V7k3/DUqVNr98v25MmTL/seWbp0ab19q1atUqPkVlZWLX7MpP/3wKVIZjb/vWtHPy19Dug7+0rLTp48qdu7d69uzpw5OkdHR3Vfbnl5ebWPad++vW7hwoW125KRL1n4su/gwYO6W265Refn56fLzc3V029BTTV27Fhd586dVTa+3KKjo3UTJkyo95i67wN5fzz++OO6rVu36k6cOKEydvv166cLCAjg+8AI/PzzzzorKyvd119/raoyPPLIIzoHBwddUlKS+rpkZt9+++21jz9+/LjO3t5e9+ijj6rHy/fJ9//66696/C2oNd8D7733nsraPnr0qC42NlZ9Xf58L1iwgC+EkZLP8Zq/+fJavvvuu+r+yZMndVr/HGBgqkdS8kfekBfeJNCofYEAVU6qbsmoF154QZWNsrGx0Q0ePFgFqGS8MjMzdbfeeqvOyclJ3eT+hWVh6r4PCgsLdaNHj9Z5eXmpD6a2bduq91JycrKefgNqrI8//lgXFBSks7a21nXv3l23YcOG2q/JazlkyJB6j1+/fr2uW7du6vHBwcG6Tz/9lCddQ++BN954QxcWFqaztbXVubm56QYOHKhbvny5no6cmkNNCbALbzNnztRp/XPATP6j71FbIiIiIiKWiyIiIiIig8DAlIiIiIgMAgNTIiIiIjIIDEyJiIiIyCAwMCUiIiIig8DAlIiIiIgMAgNTIiIiIjIIDEyJiIiIyCAwMCUiIiIig8DAlIioldx55514+umneb6JiC6DgSkRUTMpLS297NcqKyuxfPlyTJ48ucnPRURkqiz1fQBERMZq6NCh6NSpE6ytrfHf//4XUVFR2LBhwyUfu2XLFpibm6NPnz5Nfi4iIlPFwJSIqAm+//57/OMf/1CBp06nu+zjlixZgokTJ6rgtKnPRURkqsx0/PQjIromMsqZk5ODvXv3XvWx7du3x9tvv62C06Y+FxGRqeIaUyKiJujZs+dVH3Po0CGcPn0aI0eObPJzNVRFRUWzPRcRUWthYEpE1AQODg5XfYxM448aNQp2dnaNfq6TJ09iwoQJ6Nq1q1p3eubMGRw8eBD9+vVTa1JvuOGG2kSpcePG4amnnsLgwYPVOtUjR45g/Pjx6NGjhxqRzcjIaMJvSkTU8hiYEhG1sN9++w2TJk1q9PdJwHndddepYHPfvn3YtGkTXFxccMstt6j1qLGxsfD09MTPP/+sHi/bAQEB2LhxI2bMmIEHH3wQX3zxBfbs2YNp06bhq6++aoHfjoio+TD5iYioBaWnp2PXrl1YvHhxo7930aJF6Nu3rxoBFe7u7ioIHTt2LNq1a6f2dejQAefOnVPrU83MzDB79my1X35efHy8Gm0VJSUluOeee5r1dyMiam4MTImIWtDSpUtViShvb+9Gf69M2ffq1eui9aodO3as3Y6Li1PT+TJa2r9//3rf+84776jRVSIiY8HAlIjoGq1fv77ZpvEv9Vw+Pj4q4KxJZpJRUT8/Pxw+fFjti4mJwc6dO/H555+rafro6Oja7/X19cXKlStrA1MJVOt+nYjIEHGNKRFRCxo4cOA1j1pKC9Njx46pJCfJ2E9MTMTtt9+upuglyHzkkUcwf/58WFhYqJHTuoHnXXfdhezsbDXV36VLF8ydO7cZfysiopbBOqZEREREZBA4YkpEREREBoGBKREREREZBAamRERERGQQGJgSERERkUFgYEpEREREBoGBKREREREZBAamRERERGQQGJgSERERkUFgYEpEREREBoGBKREREREZBAamRERERARD8P9HeozS+9liewAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.subplots(figsize=(8,4.5))\n", - "r_over_a = np.linspace(-1.2, 1.2, 100)\n", - "\n", - "for ell in range(3):\n", - " for em in range(1,2):\n", - " b = ofiber.LP_mode_value(V, ell, em)\n", - " if b is None:\n", - " continue\n", - " irradiance = ofiber.LP_radial_irradiance(V, b, ell, r_over_a)\n", - " i = np.argmax(irradiance)\n", - "\n", - " plt.plot(r_over_a, irradiance)\n", - " plt.annotate(r\" LP$_{%d%d}$\" % (ell,em), xy=(r_over_a[i],irradiance[i]), va=\"bottom\", ha=\"center\")\n", - "\n", - "plt.xlabel(\"r / r$_{core}$\")\n", - "plt.ylabel(\"Normalized Irradiance\")\n", - "plt.plot([-1, -1], [0, 5], \":k\")\n", - "plt.plot([1, 1], [0, 5], \":k\")\n", - "plt.axvspan(-1, 1, color=\"cyan\", alpha=0.5)\n", - "plt.title(\"Step-index fiber, V=%.2f\" % V)\n", - "plt.ylim(0, 5)\n", - "plt.xlim(-1.2, 1.2)\n", - "plt.show()" - ] - } - ], - "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 -} diff --git a/hdf5_vis.py b/hdf5_vis.py deleted file mode 100644 index 3dd45ef..0000000 --- a/hdf5_vis.py +++ /dev/null @@ -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") - diff --git a/metapeg.ipynb b/metapeg.ipynb deleted file mode 100644 index cfb30c4..0000000 --- a/metapeg.ipynb +++ /dev/null @@ -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 -} diff --git a/metapeg.py b/metapeg.py deleted file mode 100644 index 4d4c51c..0000000 --- a/metapeg.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/metapeg_v2.py b/metapeg_v2.py deleted file mode 100644 index a6de29c..0000000 --- a/metapeg_v2.py +++ /dev/null @@ -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") diff --git a/metapeg_v2_unc.py b/metapeg_v2_unc.py deleted file mode 100644 index bc36cc0..0000000 --- a/metapeg_v2_unc.py +++ /dev/null @@ -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") diff --git a/metaprop.py b/metaprop.py deleted file mode 100644 index feb0773..0000000 --- a/metaprop.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/metaprop_ad.py b/metaprop_ad.py deleted file mode 100644 index 3773596..0000000 --- a/metaprop_ad.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/metaprop_asm.py b/metaprop_asm.py index ee1557f..d2b740c 100644 --- a/metaprop_asm.py +++ b/metaprop_asm.py @@ -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") \ No newline at end of file + #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") \ No newline at end of file diff --git a/metaprop_fibermode_doublet.py b/metaprop_fibermode_doublet.py index 04b80b4..79035fd 100644 --- a/metaprop_fibermode_doublet.py +++ b/metaprop_fibermode_doublet.py @@ -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) diff --git a/metaprop_fibermode_doublet_multi.py b/metaprop_fibermode_doublet_multi.py index 8ca4730..4bac07e 100644 --- a/metaprop_fibermode_doublet_multi.py +++ b/metaprop_fibermode_doublet_multi.py @@ -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 diff --git a/metaprop_fibermode_doublet_shiftSource.py b/metaprop_fibermode_doublet_shiftSource.py index 823802e..1577e16 100644 --- a/metaprop_fibermode_doublet_shiftSource.py +++ b/metaprop_fibermode_doublet_shiftSource.py @@ -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 diff --git a/mode_converter.ipynb b/mode_converter.ipynb deleted file mode 100644 index 327fc7f..0000000 --- a/mode_converter.ipynb +++ /dev/null @@ -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 -} diff --git a/mode_converter.py b/mode_converter.py deleted file mode 100644 index 11090c5..0000000 --- a/mode_converter.py +++ /dev/null @@ -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") \ No newline at end of file diff --git a/sim_opt_design.py b/sim_opt_design.py deleted file mode 100644 index b743a48..0000000 --- a/sim_opt_design.py +++ /dev/null @@ -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() -