diff --git a/_doc_mathimg/gladtex.cache b/_doc_mathimg/gladtex.cache new file mode 100644 index 0000000..c4d4ee6 --- /dev/null +++ b/_doc_mathimg/gladtex.cache @@ -0,0 +1 @@ +{"GladTeX__cache__version": "2.0", "\\begin{aligned}\n\\beta^2 E_x - \\tilde{\\partial}_x (\n \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x)\n + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y)\n ) &= \\omega^2 \\mu_{yy} \\epsilon_{xx} E_x\n +\\mu_{yy} \\hat{\\partial}_y \\frac{1}{\\mu_{zz}}\n(\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\\n-\\beta^2 E_y + \\tilde{\\partial}_y (\n \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x)\n + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y)\n ) &= -\\omega^2 \\mu_{xx} \\epsilon_{yy} E_y\n -\\mu_{xx} \\hat{\\partial}_x \\frac{1}{\\mu_{zz}}\n(\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 111.7813692054657}, "path": "_doc_mathimg/eqn037.svg"}}, "\\begin{bmatrix}\n -\\imath \\omega \\epsilon & \\nabla \\times \\\\\n \\nabla \\times & \\imath \\omega \\mu\n \\end{bmatrix}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 77.3383153998754}, "path": "_doc_mathimg/eqn011.svg"}}, "\\beta^2 E_{xy} = A_E E_{xy}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn053.svg"}}, "\\mu_{yy}": {"false": {"pos": {"depth": 5.1432292047526005, "width": 23.4001260816635, "height": 12.701293015801}, "path": "_doc_mathimg/eqn050.svg"}}, "\\imath \\omega \\epsilon_{zz} E_z = \\hat{\\partial}_x H_y -\n\\hat{\\partial}_y H_x \\\\": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn043.svg"}}, "\\vec{E}": {"false": {"pos": {"depth": 0.6691693166041001, "width": 13.873412986498002, "height": 16.787190246986903}, "path": "_doc_mathimg/eqn018.svg"}}, "\\imath \\beta H_y": {"false": {"pos": {"depth": 5.1432292047526005, "width": 35.7014351074641, "height": 16.923482243579603}, "path": "_doc_mathimg/eqn024.svg"}}, "\\imath \\beta \\tilde{\\partial}_x": {"false": {"pos": {"depth": 3.780253238827, "width": 31.386291215342702, "height": 19.290690184399402}, "path": "_doc_mathimg/eqn022.svg"}}, "A_E": {"false": {"pos": {"depth": 3.0691532566045, "width": 22.653136767004902, "height": 14.6716502998754}, "path": "_doc_mathimg/eqn054.svg"}}, "(\\nabla \\times (\\frac{1}{\\epsilon} \\nabla \\times) - \\omega^2 \\mu) E =\n\\imath \\omega M": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 75.14021678816121}, "path": "_doc_mathimg/eqn014.svg"}}, "\\begin{aligned}\nE_z &= \\frac{1}{- \\omega \\beta \\epsilon_{zz}} ((\n \\hat{\\partial}_y \\hat{\\partial}_x H_z\n -\\hat{\\partial}_x \\hat{\\partial}_y H_z)\n + \\imath \\omega (\\hat{\\partial}_x \\epsilon_{xx} E_x +\n\\hat{\\partial}_y \\epsilon{yy} E_y))\n &= \\frac{1}{\\imath \\beta \\epsilon_{zz}} (\\hat{\\partial}_x\n\\epsilon_{xx} E_x + \\hat{\\partial}_y \\epsilon{yy} E_y)\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 75.61576210960591}, "path": "_doc_mathimg/eqn045.svg"}}, "\\begin{bmatrix}\n -\\imath \\omega \\epsilon & \\nabla \\times \\\\\n \\nabla \\times & \\imath \\omega \\mu\n \\end{bmatrix}\n \\begin{bmatrix} E \\\\\n H\n \\end{bmatrix}\n = \\begin{bmatrix} J \\\\\n -M\n \\end{bmatrix}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 77.3383153998754}, "path": "_doc_mathimg/eqn012.svg"}}, "\\hat{\\partial}_x": {"false": {"pos": {"depth": 3.0691532566045, "width": 16.6549675836258, "height": 19.0715941898768}, "path": "_doc_mathimg/eqn048.svg"}}, "\\begin{aligned}\n\\imath \\beta H_y &= \\imath \\omega \\epsilon_{xx} E_x -\n\\hat{\\partial}_y H_z \\\\\n\\imath \\beta H_x &= -\\imath \\omega \\epsilon_{yy} E_y -\n\\hat{\\partial}_x H_z \\\\\n\\imath \\omega E_z &= \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x H_y -\n\\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y H_x \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 123.44872758044839}, "path": "_doc_mathimg/eqn021.svg"}}, "\\begin{aligned}\n\\nabla \\times \\vec{E}(x, y, z) &= -\\imath \\omega \\mu \\vec{H} \\\\\n\\nabla \\times \\vec{H}(x, y, z) &= \\imath \\omega \\epsilon \\vec{E} \\\\\n\\vec{E}(x,y,z) &= (\\vec{E}_t(x, y) + E_z(x, y)\\vec{z}) e^{-\\imath\n\\beta z} \\\\\n\\vec{H}(x,y,z) &= (\\vec{H}_t(x, y) + H_z(x, y)\\vec{z}) e^{-\\imath\n\\beta z} \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 137.0006072416514}, "path": "_doc_mathimg/eqn016.svg"}}, "\\begin{aligned}\n \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}} &=\n \\imath \\Omega e^{-\\imath \\omega \\Delta_t / 2} \\hat{B}_{\\vec{r}\n+ \\frac{1}{2}}\n - \\hat{M}_{\\vec{r}\n+ \\frac{1}{2}} \\\\\n \\hat{\\nabla} \\times \\hat{H}_{\\vec{r} + \\frac{1}{2}} &=\n -\\imath \\Omega e^{ \\imath \\omega \\Delta_t / 2}\n\\tilde{D}_{\\vec{r}}\n +\n\\tilde{J}_{\\vec{r}} \\\\\n \\tilde{\\nabla} \\cdot \\hat{B}_{\\vec{r} + \\frac{1}{2}} &= 0 \\\\\n \\hat{\\nabla} \\cdot \\tilde{D}_{\\vec{r}} &= \\rho_{\\vec{r}}\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 142.99240709185642}, "path": "_doc_mathimg/eqn002.svg"}}, "S = E \\times H": {"false": {"pos": {"depth": 2.0025026166041, "width": 79.28586601785331, "height": 13.605000993208302}, "path": "_doc_mathimg/eqn008.svg"}}, "H_x": {"false": {"pos": {"depth": 3.0691532566045, "width": 21.365172799204, "height": 14.6716502998754}, "path": "_doc_mathimg/eqn040.svg"}}, "\\begin{aligned}\n-\\imath \\omega \\mu_{xx} \\imath \\beta H_x &= -\\beta^2 E_y + \\imath\n\\beta \\tilde{\\partial}_y E_z \\\\\n-\\imath \\omega \\mu_{xx} \\imath \\beta H_x &= -\\beta^2 E_y +\n\\tilde{\\partial}_y (\n \\frac{1}{\\epsilon_{zz}}\n\\hat{\\partial}_x (\\epsilon_{xx} E_x)\n + \\frac{1}{\\epsilon_{zz}}\n\\hat{\\partial}_y (\\epsilon_{yy} E_y)\n )\\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 96.4900695877482}, "path": "_doc_mathimg/eqn031.svg"}}, "\\tilde{\\partial}_x": {"false": {"pos": {"depth": 3.0691532566045, "width": 16.6549675836258, "height": 18.5795888688436}, "path": "_doc_mathimg/eqn047.svg"}}, "E \\times H": {"false": {"pos": {"depth": 2.0025026166041, "width": 47.6454868088628, "height": 13.605000993208302}, "path": "_doc_mathimg/eqn007.svg"}}, "\\begin{aligned}\n \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}} &=\n \\imath \\omega \\hat{B}_{\\vec{r} + \\frac{1}{2}}\n - \\hat{M}_{\\vec{r} + \\frac{1}{2}} \\\\\n \\hat{\\nabla} \\times \\hat{H}_{\\vec{r} + \\frac{1}{2}} &=\n -\\imath \\omega \\tilde{D}_{\\vec{r}}\n + \\tilde{J}_{\\vec{r}} \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 88.0262431326772}, "path": "_doc_mathimg/eqn005.svg"}}, "\\Delta_t \\to 0": {"false": {"pos": {"depth": 3.0691532566045, "width": 51.8751213697886, "height": 14.6716502998754}, "path": "_doc_mathimg/eqn003.svg"}}, "H_{yx}^\\star =\n\\begin{bmatrix}H_y^\\star \\\\ -H_x^\\star \\end{bmatrix}": {"false": {"pos": {"depth": 16.1172275970693, "width": 98.3070655423233, "height": 40.2344549941386}, "path": "_doc_mathimg/eqn058.svg"}}, "\\imath \\beta\n\\tilde{\\partial}_y": {"false": {"pos": {"depth": 5.1432292047526005, "width": 30.9912298918859, "height": 20.653666150325}, "path": "_doc_mathimg/eqn026.svg"}}, "\\beta^2 \\begin{bmatrix} E_x \\\\\n E_y \\end{bmatrix} =\n (\\omega^2 \\begin{bmatrix} \\mu_{yy} \\epsilon_{xx} & 0 \\\\\n 0 & \\mu_{xx}\n\\epsilon_{yy} \\end{bmatrix} +\n \\begin{bmatrix} -\\mu_{yy} \\hat{\\partial}_y \\\\\n \\mu_{xx} \\hat{\\partial}_x \\end{bmatrix}\n\\mu_{zz}^{-1}\n \\begin{bmatrix} -\\tilde{\\partial}_y &\n\\tilde{\\partial}_x \\end{bmatrix} +\n \\begin{bmatrix} \\tilde{\\partial}_x \\\\\n \\tilde{\\partial}_y \\end{bmatrix}\n\\epsilon_{zz}^{-1}\n \\begin{bmatrix} \\hat{\\partial}_x \\epsilon_{xx} &\n\\hat{\\partial}_y \\epsilon_{yy} \\end{bmatrix})\n \\begin{bmatrix} E_x \\\\\n E_y \\end{bmatrix}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 79.13832468820851}, "path": "_doc_mathimg/eqn038.svg"}}, "\\imath \\beta\nH_x": {"false": {"pos": {"depth": 3.780253238827, "width": 36.096495097587606, "height": 15.560506277654}, "path": "_doc_mathimg/eqn023.svg"}}, "\\imath\n\\omega \\mu_{zz} H_z": {"false": {"pos": {"depth": 3.780253238827, "width": 57.574298560642504, "height": 15.382751615431202}, "path": "_doc_mathimg/eqn034.svg"}}, "\\begin{aligned}\n\\tilde{E}_{l, \\vec{r}} &\\to \\tilde{E}_{\\vec{r}} \\\\\n\\tilde{H}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &\\to\n\\tilde{H}_{\\vec{r} + \\frac{1}{2}} \\\\\n\\tilde{J}_{l, \\vec{r}} &\\to \\tilde{J}_{\\vec{r}} \\\\\n\\tilde{M}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &\\to\n\\tilde{M}_{\\vec{r} + \\frac{1}{2}} \\\\\n\\Omega &\\to \\omega \\\\\n\\tilde{\\partial}_t &\\to -\\imath \\omega \\\\\n \\hat{\\partial}_t &\\to -\\imath \\omega \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 215.7439719397339}, "path": "_doc_mathimg/eqn004.svg"}}, "(2 \\beta) \\partial_{\\epsilon_i}(\\beta) E_{xy} + \\beta^2\n\\partial_{\\epsilon_i} E_{xy}\n = \\partial_{\\epsilon_i}(A_E) E_{xy} + A_E \\partial_{\\epsilon_i}\nE_{xy}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn057.svg"}}, "\\begin{aligned}\n-\\imath \\omega \\mu_{yy} \\imath \\beta H_y &= \\beta^2 E_x - \\imath\n\\beta \\tilde{\\partial}_x E_z \\\\\n-\\imath \\omega \\mu_{yy} \\imath \\beta H_y &= \\beta^2 E_x -\n\\tilde{\\partial}_x (\n \\frac{1}{\\epsilon_{zz}}\n\\hat{\\partial}_x (\\epsilon_{xx} E_x)\n + \\frac{1}{\\epsilon_{zz}}\n\\hat{\\partial}_y (\\epsilon_{yy} E_y)\n )\\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 96.4900695877482}, "path": "_doc_mathimg/eqn032.svg"}}, "\\omega^2 \\begin{bmatrix} \\mu_{yy} \\epsilon_{xx} & 0 \\\\\n 0 & \\mu_{xx}\n\\epsilon_{yy} \\end{bmatrix} +\n \\begin{bmatrix} -\\mu_{yy} \\hat{\\partial}_y \\\\\n \\mu_{xx} \\hat{\\partial}_x \\end{bmatrix}\n\\mu_{zz}^{-1}\n \\begin{bmatrix} -\\tilde{\\partial}_y &\n\\tilde{\\partial}_x \\end{bmatrix} +\n \\begin{bmatrix} \\tilde{\\partial}_x \\\\\n \\tilde{\\partial}_y \\end{bmatrix} \\epsilon_{zz}^{-1}\n \\begin{bmatrix} \\hat{\\partial}_x \\epsilon_{xx} &\n\\hat{\\partial}_y \\epsilon_{yy} \\end{bmatrix}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 79.13832468820851}, "path": "_doc_mathimg/eqn046.svg"}}, "\\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot\n\\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}})\n -\\omega^2 \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} = -\\imath\n\\omega \\tilde{J}_{\\vec{r}} \\\\": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn006.svg"}}, "\\beta": {"false": {"pos": {"depth": 3.780253238827, "width": 11.0697223899236, "height": 15.560506277654}, "path": "_doc_mathimg/eqn039.svg"}}, "(\\nabla \\times (\\frac{1}{\\mu} \\nabla \\times) - \\Omega^2 \\epsilon) E =\n-\\imath \\omega J": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 75.14021678816121}, "path": "_doc_mathimg/eqn010.svg"}}, "E_{xy} = \\begin{bmatrix} E_x \\\\\n E_y\n\\end{bmatrix}": {"false": {"pos": {"depth": 16.0024822666046, "width": 82.572691269016, "height": 40.0049629998759}, "path": "_doc_mathimg/eqn055.svg"}}, "\\begin{aligned}\n-\\imath \\omega \\mu_{yy} (\\imath \\beta H_y) &= \\omega^2 \\mu_{yy}\n\\epsilon_{xx} E_x\n +\\mu_{yy} \\hat{\\partial}_y\n\\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 75.61576210960591}, "path": "_doc_mathimg/eqn036.svg"}}, "\\epsilon_{xx}": {"false": {"pos": {"depth": 3.0691532566045, "width": 21.0902701394099, "height": 10.6272170676529}, "path": "_doc_mathimg/eqn049.svg"}}, "\\begin{aligned}\n\\imath \\beta \\tilde{\\partial}_x \\imath \\omega E_z &= \\imath \\beta\n\\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x H_y\n - \\imath \\beta\n\\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y H_x \\\\\n &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}}\n\\hat{\\partial}_x ( \\imath \\omega \\epsilon_{xx} E_x - \\hat{\\partial}_y\nH_z)\n - \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y\n(-\\imath \\omega \\epsilon_{yy} E_y - \\hat{\\partial}_x H_z) \\\\\n &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}}\n\\hat{\\partial}_x ( \\imath \\omega \\epsilon_{xx} E_x)\n - \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y\n(-\\imath \\omega \\epsilon_{yy} E_y) \\\\\n\\imath \\beta \\tilde{\\partial}_x E_z &= \\tilde{\\partial}_x\n\\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x)\n + \\tilde{\\partial}_x\n\\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 190.0466672488332}, "path": "_doc_mathimg/eqn025.svg"}}, "\\epsilon_i": {"false": {"pos": {"depth": 3.0691532566045, "width": 12.1894730285965, "height": 10.6272170676529}, "path": "_doc_mathimg/eqn056.svg"}}, "sens_{i} =\n\\frac{\\partial\\beta}{\\partial\\epsilon_i}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 75.9402034348282}, "path": "_doc_mathimg/eqn052.svg"}}, "\\begin{aligned}\n-\\imath \\omega \\mu_{xx} H_x &= \\tilde{\\partial}_y E_z + \\imath \\beta\nE_y \\\\\n-\\imath \\omega \\mu_{yy} H_y &= -\\imath \\beta E_x -\n\\tilde{\\partial}_x E_z \\\\\n-\\imath \\omega \\mu_{zz} H_z &= \\tilde{\\partial}_x E_y -\n\\tilde{\\partial}_y E_x \\\\\n\\imath \\omega \\epsilon_{xx} E_x &= \\hat{\\partial}_y H_z + \\imath\n\\beta H_y \\\\\n\\imath \\omega \\epsilon_{yy} E_y &= -\\imath \\beta H_x -\n\\hat{\\partial}_x H_z \\\\\n\\imath \\omega \\epsilon_{zz} E_z &= \\hat{\\partial}_x H_y -\n\\hat{\\partial}_y H_x \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 187.99558063344372}, "path": "_doc_mathimg/eqn020.svg"}}, "\\imath \\omega \\mu_{yy} H_y": {"false": {"pos": {"depth": 5.1432292047526005, "width": 58.3613465409663, "height": 16.7457275813568}, "path": "_doc_mathimg/eqn030.svg"}}, "\\begin{aligned}\n\\imath \\beta \\tilde{\\partial}_y E_z &= \\tilde{\\partial}_y\n\\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x)\n + \\tilde{\\partial}_y\n\\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 75.260211451828}, "path": "_doc_mathimg/eqn027.svg"}}, "\\begin{aligned}\n-\\imath \\omega \\mu_{xx} (\\imath \\beta H_x) &= -\\imath \\omega\n\\mu_{xx} (-\\imath \\omega \\epsilon_{yy} E_y - \\hat{\\partial}_x H_z) \\\\\n &= -\\omega^2 \\mu_{xx} \\epsilon_{yy} E_y + \\imath\n\\omega \\mu_{xx} \\hat{\\partial}_x (\n \\frac{1}{-\\imath \\omega \\mu_{zz}}\n(\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x)) \\\\\n &= -\\omega^2 \\mu_{xx} \\epsilon_{yy} E_y\n -\\mu_{xx} \\hat{\\partial}_x \\frac{1}{\\mu_{zz}}\n(\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 138.24802321046602}, "path": "_doc_mathimg/eqn035.svg"}}, "\\nabla \\times\n(\\frac{1}{\\epsilon} \\nabla \\times) - \\omega^2 \\mu": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 75.14021678816121}, "path": "_doc_mathimg/eqn013.svg"}}, "H_y": {"false": {"pos": {"depth": 5.1432292047526005, "width": 20.9701128090805, "height": 16.7457275813568}, "path": "_doc_mathimg/eqn041.svg"}}, "\\imath \\beta\n\\tilde{\\partial}_y E_z": {"false": {"pos": {"depth": 5.1432292047526005, "width": 48.9772934422343, "height": 20.653666150325}, "path": "_doc_mathimg/eqn028.svg"}}, "\\begin{aligned}\n\\tilde{\\partial}_t &\\Rightarrow -\\imath \\Omega e^{-\\imath \\omega\n\\Delta_t / 2}\\\\\n \\hat{\\partial}_t &\\Rightarrow -\\imath \\Omega e^{ \\imath \\omega\n\\Delta_t / 2}\\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 83.11297392217561}, "path": "_doc_mathimg/eqn001.svg"}}, "E_z": {"false": {"pos": {"depth": 3.0691532566045, "width": 19.324400850223302, "height": 14.6716502998754}, "path": "_doc_mathimg/eqn042.svg"}}, "\\vec{H}": {"false": {"pos": {"depth": 0.6691693166041001, "width": 15.554931611126701, "height": 16.787190246986903}, "path": "_doc_mathimg/eqn019.svg"}}, "\\begin{aligned}\n\\imath \\beta H_y &= \\imath \\omega \\epsilon_{xx} E_x -\n\\hat{\\partial}_y H_z \\\\\n\\imath \\beta H_x &= -\\imath \\omega \\epsilon_{yy} E_y -\n\\hat{\\partial}_x H_z \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 83.6049779098755}, "path": "_doc_mathimg/eqn044.svg"}}, "\\imath \\omega\n\\mu_{xx} H_x": {"false": {"pos": {"depth": 3.780253238827, "width": 59.5465278446701, "height": 15.382751615431202}, "path": "_doc_mathimg/eqn029.svg"}}, "\\nabla \\times (\\frac{1}{\\mu}\n\\nabla \\times) - \\Omega^2 \\epsilon": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 75.14021678816121}, "path": "_doc_mathimg/eqn009.svg"}}, "\\epsilon": {"false": {"pos": {"depth": 0.6691693166041001, "width": 7.664205141728201, "height": 8.2272331276525}, "path": "_doc_mathimg/eqn015.svg"}}, "\\omega^2 \\begin{bmatrix} \\epsilon_{yy} \\mu_{xx} & 0 \\\\\n 0 & \\epsilon_{xx}\n\\mu_{yy} \\end{bmatrix} +\n \\begin{bmatrix} -\\epsilon_{yy} \\tilde{\\partial}_y \\\\\n \\epsilon_{xx} \\tilde{\\partial}_x\n\\end{bmatrix} \\epsilon_{zz}^{-1}\n \\begin{bmatrix} -\\hat{\\partial}_y & \\hat{\\partial}_x\n\\end{bmatrix} +\n \\begin{bmatrix} \\hat{\\partial}_x \\\\\n \\hat{\\partial}_y \\end{bmatrix} \\mu_{zz}^{-1}\n \\begin{bmatrix} \\tilde{\\partial}_x \\mu_{xx} &\n\\tilde{\\partial}_y \\mu_{yy} \\end{bmatrix}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 79.13832468820851}, "path": "_doc_mathimg/eqn051.svg"}}, "\\imath\n\\beta H_x": {"false": {"pos": {"depth": 3.780253238827, "width": 36.096495097587606, "height": 15.560506277654}, "path": "_doc_mathimg/eqn033.svg"}}, "(2 \\beta) \\partial_{\\epsilon_i}(\\beta) H_{yx}^\\star E_{xy} + \\beta^2\nH_{yx}^\\star \\partial_{\\epsilon_i} E_{xy}\n = H_{yx}^\\star \\partial_{\\epsilon_i}(A_E) E_{xy} + H_{yx}^\\star A_E\n\\partial_{\\epsilon_i} E_{xy}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn059.svg"}}, "\\begin{aligned}\n\\tilde{E}_{l, \\vec{r}} &= \\tilde{E}_{\\vec{r}} e^{-\\imath \\omega l\n\\Delta_t} \\\\\n\\tilde{H}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &=\n\\tilde{H}_{\\vec{r} + \\frac{1}{2}} e^{-\\imath \\omega (l - \\frac{1}{2})\n\\Delta_t} \\\\\n\\tilde{J}_{l, \\vec{r}} &= \\tilde{J}_{\\vec{r}} e^{-\\imath \\omega (l -\n\\frac{1}{2}) \\Delta_t} \\\\\n\\tilde{M}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &=\n\\tilde{M}_{\\vec{r} + \\frac{1}{2}} e^{-\\imath \\omega l \\Delta_t} \\\\\n\\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot\n\\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}})\n -\\Omega^2 \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} &=\n-\\imath \\Omega \\tilde{J}_{\\vec{r}} e^{\\imath \\omega \\Delta_t / 2} \\\\\n\\Omega &= 2 \\sin(\\omega \\Delta_t / 2) / \\Delta_t\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 197.45816573021241}, "path": "_doc_mathimg/eqn000.svg"}}, "\\begin{aligned}\n-\\imath \\omega \\mu_{xx} H_x &= \\partial_y E_z - \\partial_z E_y \\\\\n-\\imath \\omega \\mu_{yy} H_y &= \\partial_z E_x - \\partial_x E_z \\\\\n-\\imath \\omega \\mu_{zz} H_z &= \\partial_x E_y - \\partial_y E_x \\\\\n\\imath \\omega \\epsilon_{xx} E_x &= \\partial_y H_z - \\partial_z H_y\n\\\\\n\\imath \\omega \\epsilon_{yy} E_y &= \\partial_z H_x - \\partial_x H_z\n\\\\\n\\imath \\omega \\epsilon_{zz} E_z &= \\partial_x H_y - \\partial_y H_x\n\\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 172.0049796998754}, "path": "_doc_mathimg/eqn017.svg"}}, "H_{yx}^\\star \\cdot\nE_{xy} = H^\\star \\times E": {"false": {"pos": {"depth": 6.6987104991989, "width": 137.2477592354726, "height": 18.3012075424698}, "path": "_doc_mathimg/eqn062.svg"}}, "A_H": {"false": {"pos": {"depth": 3.0691532566045, "width": 23.880919402977, "height": 14.6716502998754}, "path": "_doc_mathimg/eqn061.svg"}}, "\\partial_{\\epsilon_i} A_E": {"false": {"pos": {"depth": 4.69533321595, "width": 40.4030949899226, "height": 16.4755849214437}, "path": "_doc_mathimg/eqn066.svg"}}, "i": {"false": {"pos": {"depth": 0.6691693166041001, "width": 6.6828811662613, "height": 11.8277783709722}, "path": "_doc_mathimg/eqn065.svg"}}, "f": {"false": {"pos": {"depth": 3.780253238827, "width": 10.7688183974462, "height": 15.560506277654}, "path": "_doc_mathimg/eqn070.svg"}}, "H_{yx}^\\star": {"false": {"pos": {"depth": 6.6987104991989, "width": 27.349812649588003, "height": 18.3012075424698}, "path": "_doc_mathimg/eqn060.svg"}}, "\\vec{sens} =\n\\vec{v}_{left} \\star \\vec{v}_{right}": {"false": {"pos": {"depth": 5.1432292047526005, "width": 130.3767460739146, "height": 17.2168169029129}, "path": "_doc_mathimg/eqn068.svg"}}, "[\\tilde{\\partial}f]_{1 + \\frac{1}{2}}": {"false": {"pos": {"depth": 8.8576051118932, "width": 51.8502187037445, "height": 24.3680407241323}, "path": "_doc_mathimg/eqn078.svg"}}, "[\\tilde{\\partial}_x f]_{m + \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m\n+ 1} - f_m)": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 75.14021678816121}, "path": "_doc_mathimg/eqn069.svg"}}, "\\partial_{\\epsilon_i}(\\beta)\n = \\frac{1}{2 \\beta} \\frac{H_{yx}^\\star \\partial_{\\epsilon_i}(A_E)\nE_{xy} }{H_{yx}^\\star E_{xy}}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 78.2744527098053}, "path": "_doc_mathimg/eqn064.svg"}}, "m +\n\\frac{1}{2}": {"false": {"pos": {"depth": 6.1866251786677, "width": 43.20469091988271, "height": 20.0295808325938}, "path": "_doc_mathimg/eqn082.svg"}}, "(m \\pm \\frac{1}{2},n,p)": {"false": {"pos": {"depth": 6.1866251786677, "width": 86.9006738274831, "height": 20.0295808325938}, "path": "_doc_mathimg/eqn090.svg"}}, "sens_i =\n\\vec{v}_{left} \\partial_{\\epsilon_i} (\\epsilon_{xyz})\n\\vec{v}_{right}": {"false": {"pos": {"depth": 5.1432292047526005, "width": 174.97193962570142, "height": 17.812398221356702}, "path": "_doc_mathimg/eqn067.svg"}}, "[\\hat{\\partial}_x f ]_{m - \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m}\n- f_{m - 1})": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 75.14021678816121}, "path": "_doc_mathimg/eqn074.svg"}}, "(m,n,p)": {"false": {"pos": {"depth": 4.6691692166041, "width": 58.4784532047053, "height": 17.3383382332082}, "path": "_doc_mathimg/eqn089.svg"}}, "m": {"false": {"pos": {"depth": 0.6691693166041001, "width": 15.041890290619401, "height": 8.2272331276525}, "path": "_doc_mathimg/eqn071.svg"}}, "[\\hat{\\nabla} f]_{m,n,p} =\n\\vec{x} [\\hat{\\partial}_x f]_{m + \\frac{1}{2},n,p} +\n \\vec{y} [\\hat{\\partial}_y f]_{m,n +\n\\frac{1}{2},p} +\n \\vec{z} [\\hat{\\partial}_z f]_{m,n,p +\n\\frac{1}{2}}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn084.svg"}}, "\\Delta_{x, m}": {"false": {"pos": {"depth": 5.1432292047526005, "width": 34.613137801338205, "height": 16.7457275813568}, "path": "_doc_mathimg/eqn072.svg"}}, "\\Delta_{x, m + \\frac{1}{2}} = \\frac{1}{2} * (\\Delta_{x, m} + \\Delta_{x,\nm + 1})": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 75.14021678816121}, "path": "_doc_mathimg/eqn081.svg"}}, "m + \\frac{1}{2}": {"false": {"pos": {"depth": 6.1866251786677, "width": 43.20469091988271, "height": 20.0295808325938}, "path": "_doc_mathimg/eqn080.svg"}}, "[\\tilde{\\nabla} f]_{m,n,p} = \\vec{x}\n[\\tilde{\\partial}_x f]_{m + \\frac{1}{2},n,p} +\n \\vec{y} [\\tilde{\\partial}_y f]_{m,n +\n\\frac{1}{2},p} +\n \\vec{z} [\\tilde{\\partial}_z f]_{m,n,p\n+ \\frac{1}{2}}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn083.svg"}}, "f_1": {"false": {"pos": {"depth": 3.780253238827, "width": 15.3939209484853, "height": 15.560506277654}, "path": "_doc_mathimg/eqn076.svg"}}, "\\vec{r} +\n\\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2})": {"false": {"pos": {"depth": 6.1866251786677, "width": 199.6987656741974, "height": 20.0295808325938}, "path": "_doc_mathimg/eqn102.svg"}}, "[\\tilde{\\partial}f]_{0 + \\frac{1}{2}}": {"false": {"pos": {"depth": 8.8576051118932, "width": 51.8502187037445, "height": 24.3680407241323}, "path": "_doc_mathimg/eqn077.svg"}}, "\\hat{g}": {"false": {"pos": {"depth": 3.780253238827, "width": 9.4141837646454, "height": 15.560506277654}, "path": "_doc_mathimg/eqn093.svg"}}, "\\begin{aligned}\n \\hat{h}_{m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}} &=\n\\\\\n [\\tilde{\\nabla} \\times \\tilde{g}]_{m + \\frac{1}{2}, n +\n\\frac{1}{2}, p + \\frac{1}{2}} &=\n \\vec{x} (\\tilde{\\partial}_y g^z_{m,n,p + \\frac{1}{2}} -\n\\tilde{\\partial}_z g^y_{m,n + \\frac{1}{2},p}) \\\\\n &+ \\vec{y} (\\tilde{\\partial}_z g^x_{m + \\frac{1}{2},n,p} -\n\\tilde{\\partial}_x g^z_{m,n,p + \\frac{1}{2}}) \\\\\n &+ \\vec{z} (\\tilde{\\partial}_x g^y_{m,n + \\frac{1}{2},p} -\n\\tilde{\\partial}_y g^z_{m + \\frac{1}{2},n,p})\n \\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 151.20033355332492}, "path": "_doc_mathimg/eqn091.svg"}}, "\\Delta_{x, m},\n\\Delta_{x, m+1}, ...": {"false": {"pos": {"depth": 5.1432292047526005, "width": 109.4619119301188, "height": 16.7457275813568}, "path": "_doc_mathimg/eqn073.svg"}}, "\\hat{h}": {"false": {"pos": {"depth": 0.6691693166041001, "width": 10.356770407747401, "height": 16.6716102498764}, "path": "_doc_mathimg/eqn095.svg"}}, "\\hat{\\nabla} \\times \\hat{H}": {"false": {"pos": {"depth": 2.0025026166041, "width": 48.443785455572, "height": 17.8271888876536}, "path": "_doc_mathimg/eqn110.svg"}}, "H_{yx}^\\star A_E \\partial_{\\epsilon_i} E_{xy} = \\beta^2 H_{yx}^\\star\n\\partial_{\\epsilon_i} E_{xy}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn063.svg"}}, "(m, n \\pm\n\\frac{1}{2}, p \\pm \\frac{1}{2})": {"false": {"pos": {"depth": 6.1866251786677, "width": 115.3228931169276, "height": 20.0295808325938}, "path": "_doc_mathimg/eqn098.svg"}}, "\\hat{g}_{m,n,p} = \\vec{x} g^x_{m - \\frac{1}{2},n,p} +\n \\vec{y} g^y_{m,n - \\frac{1}{2},p} +\n \\vec{z} g^z_{m,n,p - \\frac{1}{2}}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn086.svg"}}, "\\vec{r} = (m, n, p)": {"false": {"pos": {"depth": 4.6691692166041, "width": 87.0475644904775, "height": 17.3383382332082}, "path": "_doc_mathimg/eqn101.svg"}}, "f_0": {"false": {"pos": {"depth": 3.780253238827, "width": 15.3939209484853, "height": 15.560506277654}, "path": "_doc_mathimg/eqn075.svg"}}, "\\tilde{h}_{m - \\frac{1}{2}, n - \\frac{1}{2}, p\n- \\frac{1}{2}} =\n [\\hat{\\nabla} \\times \\hat{g}]_{m - \\frac{1}{2}, n - \\frac{1}{2}, p\n- \\frac{1}{2}}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn092.svg"}}, "\\hat{M}": {"false": {"pos": {"depth": 0.6691693166041001, "width": 18.1660155458496, "height": 16.493855587653602}, "path": "_doc_mathimg/eqn106.svg"}}, "\\hat{\\nabla} \\cdot \\tilde{J} + \\hat{\\partial}_t \\rho\n= 0": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn108.svg"}}, "\\begin{aligned}\n\\hat{\\nabla} \\times \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}}\n &= \\tilde{\\nabla}(\\hat{\\nabla} \\cdot \\tilde{E}_{\\vec{r}}) -\n\\hat{\\nabla} \\cdot \\tilde{\\nabla} \\tilde{E}_{\\vec{r}} \\\\\n &= - \\hat{\\nabla} \\cdot \\tilde{\\nabla} \\tilde{E}_{\\vec{r}} \\\\\n &= - \\tilde{\\nabla}^2 \\tilde{E}_{\\vec{r}}\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 109.04632260717521}, "path": "_doc_mathimg/eqn118.svg"}}, "d_{n,m,p} = [\\hat{\\nabla} \\cdot\n\\tilde{g}]_{n,m,p}\n = [\\hat{\\partial}_x g^x]_{m,n,p} +\n [\\hat{\\partial}_y g^y]_{m,n,p} +\n [\\hat{\\partial}_z g^z]_{m,n,p}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn088.svg"}}, "d_{n,m,p} = [\\tilde{\\nabla} \\cdot\n\\hat{g}]_{n,m,p}\n = [\\tilde{\\partial}_x g^x]_{m,n,p} +\n [\\tilde{\\partial}_y g^y]_{m,n,p} +\n [\\tilde{\\partial}_z g^z]_{m,n,p}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn087.svg"}}, "\\tilde{h}": {"false": {"pos": {"depth": 0.6691693166041001, "width": 10.356770407747401, "height": 16.179606262176502}, "path": "_doc_mathimg/eqn096.svg"}}, "\\begin{aligned}\n \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &=\n -\\tilde{\\partial}_t \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}}\n - \\hat{M}_{l-1, \\vec{r} + \\frac{1}{2}} \\\\\n \\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times\n\\tilde{E}_{l,\\vec{r}} &=\n -\\tilde{\\partial}_t \\hat{H}_{l-\\frac{1}{2}, \\vec{r} +\n\\frac{1}{2}} \\\\\n \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot\n\\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) &=\n \\hat{\\nabla} \\times (-\\tilde{\\partial}_t \\hat{H}_{l-\\frac{1}{2},\n\\vec{r} + \\frac{1}{2}}) \\\\\n \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot\n\\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) &=\n -\\tilde{\\partial}_t \\hat{\\nabla} \\times \\hat{H}_{l-\\frac{1}{2},\n\\vec{r} + \\frac{1}{2}} \\\\\n \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot\n\\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) &=\n -\\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_{\\vec{r}} \\tilde{E}_{l,\n\\vec{r}} + \\hat{\\partial}_t \\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\\n \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot\n\\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}})\n + \\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_{\\vec{r}}\n\\cdot \\tilde{E}_{l, \\vec{r}}\n &= \\tilde{\\partial}_t \\tilde{J}_{l - \\frac{1}{2},\n\\vec{r}}\n \\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 215.8903759360738}, "path": "_doc_mathimg/eqn111.svg"}}, "[\\hat{\\partial}f]_{0 -\n\\frac{1}{2}}": {"false": {"pos": {"depth": 8.8576051118932, "width": 51.8502187037445, "height": 24.8600460451655}, "path": "_doc_mathimg/eqn079.svg"}}, "\\tilde{g}_{m,n,p} = \\vec{x} g^x_{m + \\frac{1}{2},n,p}\n+\n \\vec{y} g^y_{m,n + \\frac{1}{2},p} +\n \\vec{z} g^z_{m,n,p + \\frac{1}{2}}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn085.svg"}}, "\\hat{\\nabla} \\cdot \\tilde{E}_{\\vec{r}} = 0": {"false": {"pos": {"depth": 3.0691532566045, "width": 72.8270861793228, "height": 18.8938181943212}, "path": "_doc_mathimg/eqn117.svg"}}, "(m \\pm \\frac{1}{2}, n \\pm \\frac{1}{2}, p \\pm\n\\frac{1}{2})": {"false": {"pos": {"depth": 6.1866251786677, "width": 143.7451137397054, "height": 20.0295808325938}, "path": "_doc_mathimg/eqn097.svg"}}, "\\tilde{E}": {"false": {"pos": {"depth": 0.6691693166041001, "width": 13.873412986498002, "height": 16.0018515999537}, "path": "_doc_mathimg/eqn103.svg"}}, "\\begin{aligned}\n\\tilde{\\partial}_t &\\Rightarrow (e^{ \\imath \\omega \\Delta_t} - 1) /\n\\Delta_t = \\frac{-2 \\imath}{\\Delta_t} \\sin(\\omega \\Delta_t / 2)\ne^{-\\imath \\omega \\Delta_t / 2} = -\\imath \\Omega e^{-\\imath \\omega\n\\Delta_t / 2}\\\\\n \\hat{\\partial}_t &\\Rightarrow (1 - e^{-\\imath \\omega \\Delta_t}) /\n\\Delta_t = \\frac{-2 \\imath}{\\Delta_t} \\sin(\\omega \\Delta_t / 2) e^{\n\\imath \\omega \\Delta_t / 2} = -\\imath \\Omega e^{ \\imath \\omega \\Delta_t\n/ 2}\\\\\n\\Omega &= 2 \\sin(\\omega \\Delta_t / 2) / \\Delta_t\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 135.02581395768792}, "path": "_doc_mathimg/eqn113.svg"}}, "\\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot\n\\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}})\n -\\Omega^2 \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} = -\\imath\n\\Omega \\tilde{J}_{\\vec{r}} e^{\\imath \\omega \\Delta_t / 2} \\\\": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn114.svg"}}, "\\hat{H}": {"false": {"pos": {"depth": 0.6691693166041001, "width": 15.554931611126701, "height": 16.493855587653602}, "path": "_doc_mathimg/eqn104.svg"}}, "\\tilde{g}": {"false": {"pos": {"depth": 3.780253238827, "width": 9.4141837646454, "height": 15.0685009566208}, "path": "_doc_mathimg/eqn094.svg"}}, "\\begin{aligned}\n \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &= -\\tilde{\\partial}_t\n\\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}}\n -\n\\hat{M}_{l, \\vec{r} + \\frac{1}{2}} \\\\\n \\hat{\\nabla} \\times \\hat{H}_{l-\\frac{1}{2},\\vec{r} + \\frac{1}{2}}\n&= \\hat{\\partial}_t \\tilde{D}_{l, \\vec{r}}\n +\n\\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\\n \\tilde{\\nabla} \\cdot \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}}\n&= 0 \\\\\n \\hat{\\nabla} \\cdot \\tilde{D}_{l,\\vec{r}} &= \\rho_{l,\\vec{r}}\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 143.1701630874125}, "path": "_doc_mathimg/eqn099.svg"}}, "\\tilde{J}": {"false": {"pos": {"depth": 0.6691693166041001, "width": 11.4790437130239, "height": 16.0018515999537}, "path": "_doc_mathimg/eqn105.svg"}}, "\\begin{aligned}\n\\tilde{E}_{l, \\vec{r}} &= \\tilde{E}_{\\vec{r}} e^{-\\imath \\omega l\n\\Delta_t} \\\\\n\\tilde{J}_{l, \\vec{r}} &= \\tilde{J}_{\\vec{r}} e^{-\\imath \\omega (l -\n\\frac{1}{2}) \\Delta_t}\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 83.7246272402176}, "path": "_doc_mathimg/eqn112.svg"}}, "\\begin{aligned}\n\\mu_{\\vec{r} + \\frac{1}{2}} &= \\mu \\\\\n\\epsilon_{\\vec{r}} &= \\epsilon \\\\\n\\tilde{J}_{\\vec{r}} &= 0 \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 108.19024396191051}, "path": "_doc_mathimg/eqn115.svg"}}, "\\tilde{\\nabla}^2 \\tilde{E}_{\\vec{r}} +\n\\Omega^2 \\epsilon \\mu \\tilde{E}_{\\vec{r}} = 0": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn119.svg"}}, "\\tilde{\\nabla} \\times\n\\tilde{E}": {"false": {"pos": {"depth": 2.0025026166041, "width": 46.7622668309433, "height": 17.3351848999537}, "path": "_doc_mathimg/eqn109.svg"}}, "c = \\sqrt{\\mu \\epsilon}": {"false": {"pos": {"depth": 6.0602825151596, "width": 58.239723877340204, "height": 17.9782915505427}, "path": "_doc_mathimg/eqn126.svg"}}, "\\mu": {"false": {"pos": {"depth": 3.780253238827, "width": 10.764179730895501, "height": 11.338317049875402}, "path": "_doc_mathimg/eqn107.svg"}}, "\\begin{aligned}\n \\hat{B}_{\\vec{r}} &= \\mu_{\\vec{r} + \\frac{1}{2}} \\cdot\n\\hat{H}_{\\vec{r} + \\frac{1}{2}} \\\\\n \\tilde{D}_{\\vec{r}} &= \\epsilon_{\\vec{r}} \\cdot\n\\tilde{E}_{\\vec{r}}\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 85.145851204687}, "path": "_doc_mathimg/eqn100.svg"}}, "(\\tilde{\\nabla}^2 + K^2) \\phi_{\\vec{r}} = 0": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn120.svg"}}, "K^2 = \\Omega^2 \\mu \\epsilon": {"false": {"pos": {"depth": 3.780253238827, "width": 76.6230447510905, "height": 17.1297809050888}, "path": "_doc_mathimg/eqn121.svg"}}, "\\hat{\\nabla} \\times \\tilde{\\nabla} \\times\n\\tilde{E}_{\\vec{r}} - \\Omega^2 \\epsilon \\mu \\tilde{E}_{\\vec{r}} = 0": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn116.svg"}}, "= \\nabla_f\n\\times E": {"false": {"pos": {"depth": 5.1432292047526005, "width": 70.6787902330302, "height": 16.7457275813568}, "path": "_doc_mathimg/eqn138.svg"}}, "\\phi_{\\vec{r}} = A e^{\\imath (k_x m \\Delta_x\n+ k_y n \\Delta_y + k_z p \\Delta_z)}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn122.svg"}}, "c \\Delta_t < \\Delta_x / \\sqrt{3}": {"false": {"pos": {"depth": 4.6691692166041, "width": 96.07104293155722, "height": 19.8538208369878}, "path": "_doc_mathimg/eqn130.svg"}}, "(k_x, k_y, k_z), \\omega": {"false": {"pos": {"depth": 5.1432292047526005, "width": 89.4390444306905, "height": 17.812398221356702}, "path": "_doc_mathimg/eqn127.svg"}}, "K_y, K_z": {"false": {"pos": {"depth": 5.1432292047526005, "width": 48.0542587986435, "height": 16.7457275813568}, "path": "_doc_mathimg/eqn124.svg"}}, "c^2 \\Delta_t^2 = \\frac{\\Delta_t^2}{\\mu\n\\epsilon} < 1/(\\frac{1}{\\Delta_x^2} + \\frac{1}{\\Delta_y^2} +\n\\frac{1}{\\Delta_z^2})": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 77.5094793955963}, "path": "_doc_mathimg/eqn128.svg"}}, "\\begin{aligned}\n\\tilde{\\partial}_x &\\Rightarrow (e^{ \\imath k_x \\Delta_x} - 1) /\n\\Delta_t = \\frac{-2 \\imath}{\\Delta_x} \\sin(k_x \\Delta_x / 2) e^{ \\imath\nk_x \\Delta_x / 2} = \\imath K_x e^{ \\imath k_x \\Delta_x / 2}\\\\\n \\hat{\\partial}_x &\\Rightarrow (1 - e^{-\\imath k_x \\Delta_x}) /\n\\Delta_t = \\frac{-2 \\imath}{\\Delta_x} \\sin(k_x \\Delta_x / 2) e^{-\\imath\nk_x \\Delta_x / 2} = \\imath K_x e^{-\\imath k_x \\Delta_x / 2}\\\\\nK_x &= 2 \\sin(k_x \\Delta_x / 2) / \\Delta_x \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 135.02581395768792}, "path": "_doc_mathimg/eqn123.svg"}}, "\\tilde{\\nabla}^2 = -(K_x^2 + K_y^2 + K_z^2) \\phi_{\\vec{r}} \\\\\n K_x^2 + K_y^2 + K_z^2 = \\Omega^2 \\mu \\epsilon = \\Omega^2 / c^2": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 72.00500353320821}, "path": "_doc_mathimg/eqn125.svg"}}, "\\Delta_x / \\sqrt{3}": {"false": {"pos": {"depth": 4.6691692166041, "width": 50.4400014056666, "height": 19.8538208369878}, "path": "_doc_mathimg/eqn132.svg"}}, "r +\n\\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2},\np + \\frac{1}{2})": {"false": {"pos": {"depth": 6.1866251786677, "width": 199.6987656741974, "height": 20.0295808325938}, "path": "_doc_mathimg/eqn134.svg"}}, "r = (m, n,\np)": {"false": {"pos": {"depth": 4.6691692166041, "width": 87.0475644904775, "height": 17.3383382332082}, "path": "_doc_mathimg/eqn133.svg"}}, "c\n\\Delta_t": {"false": {"pos": {"depth": 3.0691532566045, "width": 25.8955686859441, "height": 14.6716502998754}, "path": "_doc_mathimg/eqn131.svg"}}, "\\Delta_x = \\Delta_y = \\Delta_z": {"false": {"pos": {"depth": 5.1432292047526005, "width": 102.73884809819539, "height": 16.7457275813568}, "path": "_doc_mathimg/eqn129.svg"}}, "\\mu = \\begin{bmatrix} \\mu_{xx} & 0 & 0 \\\\\n 0 & \\mu_{yy} & 0 \\\\\n 0 & 0 & \\mu_{zz} \\end{bmatrix}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 94.00494164987641}, "path": "_doc_mathimg/eqn136.svg"}}, "\\begin{aligned}\nU_l &= \\epsilon \\tilde{E}^2_l + \\mu \\hat{H}_{l + \\frac{1}{2}} \\cdot\n\\hat{H}_{l - \\frac{1}{2}} \\\\\nU_{l + \\frac{1}{2}} &= \\epsilon \\tilde{E}_l \\cdot \\tilde{E}_{l + 1}\n+ \\mu \\hat{H}^2_{l + \\frac{1}{2}} \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 89.49096442939249}, "path": "_doc_mathimg/eqn149.svg"}}, "= \\nabla_b\n\\times H": {"false": {"pos": {"depth": 3.0691532566045, "width": 70.5881129019638, "height": 14.6716502998754}, "path": "_doc_mathimg/eqn137.svg"}}, "\\otimes": {"false": {"pos": {"depth": 2.0025026166041, "width": 13.7828156554296, "height": 12.005005033208201}, "path": "_doc_mathimg/eqn141.svg"}}, "\\begin{aligned}\n \\tilde{S}_{l, l', \\vec{r}} &=& &\\tilde{E}_{l, \\vec{r}}\n\\otimes \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\\\\n &=& &\\vec{x} (\\tilde{E}^y_{l,m+1,n,p}\n\\hat{H}^z_{l',\\vec{r} + \\frac{1}{2}} - \\tilde{E}^z_{l,m+1,n,p}\n\\hat{H}^y_{l', \\vec{r} + \\frac{1}{2}}) \\\\\n & &+ &\\vec{y} (\\tilde{E}^z_{l,m,n+1,p}\n\\hat{H}^x_{l',\\vec{r} + \\frac{1}{2}} - \\tilde{E}^x_{l,m,n+1,p}\n\\hat{H}^z_{l', \\vec{r} + \\frac{1}{2}}) \\\\\n & &+ &\\vec{z} (\\tilde{E}^x_{l,m,n,p+1}\n\\hat{H}^y_{l',\\vec{r} + \\frac{1}{2}} - \\tilde{E}^y_{l,m,n,p+1}\n\\hat{H}^z_{l', \\vec{r} + \\frac{1}{2}})\n \\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 151.9653282008667}, "path": "_doc_mathimg/eqn140.svg"}}, "S": {"false": {"pos": {"depth": 0.6691693166041001, "width": 11.904906369044001, "height": 12.2716676932083}, "path": "_doc_mathimg/eqn158.svg"}}, "\\begin{aligned}\n \\tilde{E}_l &= \\tilde{E}_{l, \\vec{r}} \\\\\n \\hat{H}_l &= \\tilde{H}_{l, \\vec{r} + \\frac{1}{2}} \\\\\n \\tilde{\\epsilon} &= \\tilde{\\epsilon}_{\\vec{r}} \\\\\n \\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 109.8124972546875}, "path": "_doc_mathimg/eqn144.svg"}}, "l, l'": {"false": {"pos": {"depth": 3.780253238827, "width": 22.1345007799708, "height": 16.181639595459}, "path": "_doc_mathimg/eqn143.svg"}}, "l' = l - \\frac{1}{2}": {"false": {"pos": {"depth": 6.1866251786677, "width": 64.6120997180308, "height": 20.0295808325938}, "path": "_doc_mathimg/eqn147.svg"}}, "c \\Delta_t < \\frac{\\Delta_x}{\\sqrt{3}}": {"false": {"pos": {"depth": 8.7757971139384, "width": 65.8639303534017, "height": 23.033548757494604}, "path": "_doc_mathimg/eqn139.svg"}}, "J": {"false": {"pos": {"depth": 0.6691693166041001, "width": 11.4790437130239, "height": 12.2716676932083}, "path": "_doc_mathimg/eqn151.svg"}}, "H": {"false": {"pos": {"depth": 0.6691693166041001, "width": 15.554931611126701, "height": 12.2716676932083}, "path": "_doc_mathimg/eqn154.svg"}}, "\\begin{aligned}\n \\hat{\\nabla} \\cdot \\tilde{S}_{l, l - \\frac{1}{2}}\n &= (\\mu \\hat{H}^2_{l - \\frac{1}{2}}\n +\\epsilon \\tilde{E}_{l-1} \\cdot \\tilde{E}_l) / \\Delta_t \\\\\n -(\\mu \\hat{H}_{l + \\frac{1}{2}} \\cdot \\hat{H}_{l - \\frac{1}{2}}\n +\\epsilon \\tilde{E}^2_l) / \\Delta_t \\\\\n - \\hat{H}_{l-\\frac{1}{2}} \\cdot \\hat{M}_l \\\\\n - \\tilde{E}_l \\cdot \\tilde{J}_{l-\\frac{1}{2}} \\\\\n \\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 146.3535323411616}, "path": "_doc_mathimg/eqn148.svg"}}, "\\epsilon = \\begin{bmatrix} \\epsilon_{xx} & 0 & 0 \\\\\n 0 & \\epsilon_{yy} & 0 \\\\\n 0 & 0 & \\epsilon_{zz} \\end{bmatrix}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 94.00494164987641}, "path": "_doc_mathimg/eqn135.svg"}}, "M": {"false": {"pos": {"depth": 0.6691693166041001, "width": 18.1660155458496, "height": 12.2716676932083}, "path": "_doc_mathimg/eqn153.svg"}}, "l' = l + \\frac{1}{2}": {"false": {"pos": {"depth": 6.1866251786677, "width": 64.3526797245163, "height": 20.0295808325938}, "path": "_doc_mathimg/eqn145.svg"}}, "E": {"false": {"pos": {"depth": 0.6691693166041001, "width": 13.873412986498002, "height": 12.2716676932083}, "path": "_doc_mathimg/eqn152.svg"}}, "\\begin{aligned}\n (U_{l+\\frac{1}{2}} - U_l) / \\Delta_t\n &= -\\hat{\\nabla} \\cdot \\tilde{S}_{l, l + \\frac{1}{2}} \\\\\n - \\hat{H}_{l+\\frac{1}{2}} \\cdot \\hat{M}_l \\\\\n - \\tilde{E}_l \\cdot \\tilde{J}_{l+\\frac{1}{2}} \\\\\n (U_l - U_{l-\\frac{1}{2}}) / \\Delta_t\n &= -\\hat{\\nabla} \\cdot \\tilde{S}_{l, l - \\frac{1}{2}} \\\\\n - \\hat{H}_{l-\\frac{1}{2}} \\cdot \\hat{M}_l \\\\\n - \\tilde{E}_l \\cdot \\tilde{J}_{l-\\frac{1}{2}} \\\\\n\\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 201.75133762288309}, "path": "_doc_mathimg/eqn150.svg"}}, "\\sim 10^{-3}": {"false": {"pos": {"depth": 0.6691693166041001, "width": 49.042052107282004, "height": 14.018696982865901}, "path": "_doc_mathimg/eqn157.svg"}}, "\\begin{aligned}\n \\hat{\\nabla} \\cdot \\tilde{S}_{l, l + \\frac{1}{2}}\n &= \\hat{H}_{l + \\frac{1}{2}} \\cdot\n (-\\mu / \\Delta_t)(\\hat{H}_{l + \\frac{1}{2}} - \\hat{H}_{l -\n\\frac{1}{2}}) -\n \\tilde{E}_l \\cdot (\\epsilon / \\Delta_t)(\\tilde{E}_{l+1} -\n\\tilde{E}_l)\n - \\hat{H}_{l'} \\cdot \\hat{M}_l - \\tilde{E}_l \\cdot \\tilde{J}_{l +\n\\frac{1}{2}} \\\\\n &= (-\\mu / \\Delta_t)(\\hat{H}^2_{l + \\frac{1}{2}} - \\hat{H}_{l +\n\\frac{1}{2}} \\cdot \\hat{H}_{l - \\frac{1}{2}}) -\n (\\epsilon / \\Delta_t)(\\tilde{E}_{l+1} \\cdot \\tilde{E}_l -\n\\tilde{E}^2_l)\n - \\hat{H}_{l'} \\cdot \\hat{M}_l - \\tilde{E}_l \\cdot \\tilde{J}_{l +\n\\frac{1}{2}} \\\\\n &= -(\\mu \\hat{H}^2_{l + \\frac{1}{2}}\n +\\epsilon \\tilde{E}_{l+1} \\cdot \\tilde{E}_l) / \\Delta_t \\\\\n +(\\mu \\hat{H}_{l + \\frac{1}{2}} \\cdot \\hat{H}_{l - \\frac{1}{2}}\n +\\epsilon \\tilde{E}^2_l) / \\Delta_t \\\\\n - \\hat{H}_{l+\\frac{1}{2}} \\cdot \\hat{M}_l \\\\\n - \\tilde{E}_l \\cdot \\tilde{J}_{l+\\frac{1}{2}} \\\\\n \\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 205.17282553734591}, "path": "_doc_mathimg/eqn146.svg"}}, "\\tau > \\frac{2 * \\pi}{\\omega}": {"false": {"pos": {"depth": 6.1866251786677, "width": 52.4271426893214, "height": 20.0295808325938}, "path": "_doc_mathimg/eqn156.svg"}}, "f_r(t) = (1 - \\frac{1}{2} (\\omega (t -\n\\tau))^2) e^{-(\\frac{\\omega (t - \\tau)}{2})^2}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 75.14021678816121}, "path": "_doc_mathimg/eqn155.svg"}}, "\\begin{aligned}\n \\hat{\\nabla} \\cdot \\tilde{S}_{l, l', \\vec{r}}\n &= \\hat{\\nabla} \\cdot (\\tilde{E}_{l, \\vec{r}} \\otimes\n\\hat{H}_{l', \\vec{r} + \\frac{1}{2}}) \\\\\n &= \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla}\n\\times \\tilde{E}_{l, \\vec{r}} -\n \\tilde{E}_{l, \\vec{r}} \\cdot \\hat{\\nabla} \\times \\hat{H}_{l',\n\\vec{r} + \\frac{1}{2}} \\\\\n &= \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\cdot\n (-\\tilde{\\partial}_t \\mu_{\\vec{r} + \\frac{1}{2}} \\hat{H}_{l -\n\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} -\n \\hat{M}_{l, \\vec{r} + \\frac{1}{2}}) -\n \\tilde{E}_{l, \\vec{r}} \\cdot (\\hat{\\partial}_t\n\\tilde{\\epsilon}_{\\vec{r}} \\tilde{E}_{l'+\\frac{1}{2}, \\vec{r}} +\n \\tilde{J}_{l', \\vec{r}}) \\\\\n &= \\hat{H}_{l'} \\cdot (-\\mu / \\Delta_t)(\\hat{H}_{l + \\frac{1}{2}}\n- \\hat{H}_{l - \\frac{1}{2}}) -\n \\tilde{E}_l \\cdot (\\epsilon / \\Delta_t\n)(\\tilde{E}_{l'+\\frac{1}{2}} - \\tilde{E}_{l'-\\frac{1}{2}})\n - \\hat{H}_{l'} \\cdot \\hat{M}_{l} - \\tilde{E}_l \\cdot\n\\tilde{J}_{l'} \\\\\n \\end{aligned}": {"true": {"pos": {"depth": 0.6691693166041001, "width": 598.8462583621765, "height": 145.558569694369}, "path": "_doc_mathimg/eqn142.svg"}}} \ No newline at end of file diff --git a/doc.htex b/doc.htex new file mode 100644 index 0000000..a9e1411 --- /dev/null +++ b/doc.htex @@ -0,0 +1,4607 @@ + + + + + + + + meanas + + + + +
+

meanas

+
+
+

Module meanas

+

meanas

+

meanas is a python package for electromagnetic +simulations

+

** UNSTABLE / WORK IN PROGRESS **

+

Formerly known as fdfd_tools.

+

This package is intended for building simulation inputs, analyzing +simulation outputs, and running short simulations on unspecialized +hardware. It is designed to provide tooling and a baseline for other, +high-performance purpose- and hardware-specific solvers.

+

Contents

+ +

This package does not provide a fast matrix solver, though +by default generic()(…) will call +scipy.sparse.linalg.qmr(…) to perform a solve. For 2D FDFD +problems this should be fine; likewise, the waveguide mode solver uses +scipy’s eigenvalue solver, with reasonable results.

+

For solving large (or 3D) FDFD problems, I recommend a GPU-based +iterative solver, such as opencl_fdfd or those +included in MAGMA. +Your solver will need the ability to solve complex symmetric +(non-Hermitian) linear systems, ideally with double precision.

+ +

Installation

+

Requirements:

+ +

Install from PyPI with pip:

+
pip3 install 'meanas[dev]'
+

Development install

+

Install python3 and git:

+
# This is for Debian/Ubuntu/other-apt-based systems; you may need an alternative command
+sudo apt install python3 build-essential python3-dev git
+

In-place development install:

+
# Download using git
+git clone https://mpxd.net/code/jan/meanas.git
+
+# If you'd like to create a virtualenv, do so:
+python3 -m venv my_venv
+
+# If you are using a virtualenv, activate it
+source my_venv/bin/activate
+
+# Install in-place (-e, editable) from ./meanas, including development dependencies ([dev])
+pip3 install --user -e './meanas[dev]'
+
+# Run tests
+cd meanas
+python3 -m pytest -rsxX | tee test_results.txt
+

See also:

+ +

Use

+

See examples/ for some simple examples; you may need +additional packages such as gridlock to run the +examples.

+

Sub-modules

+ +
+

Module +meanas.eigensolvers

+

Solvers for eigenvalue / eigenvector problems

+

Functions

+

Function +power_iteration

+
+

def power_iteration(operator: scipy.sparse._matrix.spmatrix, guess_vector: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]] | None = None, iterations: int = 20) -> tuple[complex, numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]

+
+

Use power iteration to estimate the dominant eigenvector of a +matrix.

+

Args —–= operator : Matrix to +analyze.

+
+
guess_vector
+
+Starting point for the eigenvector. Default is a randomly chosen vector. +
+
iterations
+
+Number of iterations to perform. Default 20. +
+
+

Returns —–= (Largest-magnitude eigenvalue, Corresponding eigenvector +estimate)

+

Function +rayleigh_quotient_iteration

+
+

def rayleigh_quotient_iteration(operator: scipy.sparse._matrix.spmatrix | scipy.sparse.linalg._interface.LinearOperator, guess_vector: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], iterations: int = 40, tolerance: float = 1e-13, solver: collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]] | None = None) -> tuple[complex, numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]

+
+

Use Rayleigh quotient iteration to refine an eigenvector guess.

+

Args —–= operator : Matrix to +analyze.

+
+
guess_vector
+
+Eigenvector to refine. +
+
iterations
+
+Maximum number of iterations to perform. Default 40. +
+
tolerance
+
+Stop iteration if +(A - I*eigenvalue) @ v < num_vectors * tolerance, +Default 1e-13. +
+
solver
+
+Solver function of the form x = solver(A, b). By default, +use scipy.sparse.spsolve for sparse matrices and scipy.sparse.bicgstab +for general LinearOperator instances. +
+
+

Returns —–= (eigenvalues, eigenvectors)

+

Function +signed_eigensolve

+
+

def signed_eigensolve(operator: scipy.sparse._matrix.spmatrix | scipy.sparse.linalg._interface.LinearOperator, how_many: int, negative: bool = False) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]

+
+

Find the largest-magnitude positive-only (or negative-only) +eigenvalues and eigenvectors of the provided matrix.

+

Args —–= operator : Matrix to +analyze.

+
+
how_many
+
+How many eigenvalues to find. +
+
negative
+
+Whether to find negative-only eigenvalues. Default False (positive +only). +
+
+

Returns —–= (sorted list of eigenvalues, 2D ndarray of corresponding +eigenvectors) eigenvectors[:, k] corresponds to the k-th +eigenvalue

+
+

Module meanas.fdfd

+

Tools for finite difference frequency-domain (FDFD) simulations and +calculations.

+

These mostly involve picking a single frequency, then setting up and +solving a matrix equation (Ax=b) or eigenvalue problem.

+

Submodules:

+ +

================================================================

+

From the “Frequency domain” section of meanas.fdmath, we have

+

+\begin{aligned} +\tilde{E}_{l, \vec{r}} &= \tilde{E}_{\vec{r}} e^{-\imath \omega l +\Delta_t} \\ +\tilde{H}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &= +\tilde{H}_{\vec{r} + \frac{1}{2}} e^{-\imath \omega (l - \frac{1}{2}) +\Delta_t} \\ +\tilde{J}_{l, \vec{r}} &= \tilde{J}_{\vec{r}} e^{-\imath \omega (l - +\frac{1}{2}) \Delta_t} \\ +\tilde{M}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &= +\tilde{M}_{\vec{r} + \frac{1}{2}} e^{-\imath \omega l \Delta_t} \\ +\hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot +\tilde{\nabla} \times \tilde{E}_{\vec{r}}) + -\Omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} &= +-\imath \Omega \tilde{J}_{\vec{r}} e^{\imath \omega \Delta_t / 2} \\ +\Omega &= 2 \sin(\omega \Delta_t / 2) / \Delta_t +\end{aligned} +

+

resulting in

+

+\begin{aligned} +\tilde{\partial}_t &\Rightarrow -\imath \Omega e^{-\imath \omega +\Delta_t / 2}\\ + \hat{\partial}_t &\Rightarrow -\imath \Omega e^{ \imath \omega +\Delta_t / 2}\\ +\end{aligned} +

+

Maxwell’s equations are then

+

+ \begin{aligned} + \tilde{\nabla} \times \tilde{E}_{\vec{r}} &= + \imath \Omega e^{-\imath \omega \Delta_t / 2} \hat{B}_{\vec{r} ++ \frac{1}{2}} + - \hat{M}_{\vec{r} ++ \frac{1}{2}} \\ + \hat{\nabla} \times \hat{H}_{\vec{r} + \frac{1}{2}} &= + -\imath \Omega e^{ \imath \omega \Delta_t / 2} +\tilde{D}_{\vec{r}} + + +\tilde{J}_{\vec{r}} \\ + \tilde{\nabla} \cdot \hat{B}_{\vec{r} + \frac{1}{2}} &= 0 \\ + \hat{\nabla} \cdot \tilde{D}_{\vec{r}} &= \rho_{\vec{r}} +\end{aligned} +

+

With \Delta_t \to 0, this simplifies to

+

+\begin{aligned} +\tilde{E}_{l, \vec{r}} &\to \tilde{E}_{\vec{r}} \\ +\tilde{H}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &\to +\tilde{H}_{\vec{r} + \frac{1}{2}} \\ +\tilde{J}_{l, \vec{r}} &\to \tilde{J}_{\vec{r}} \\ +\tilde{M}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &\to +\tilde{M}_{\vec{r} + \frac{1}{2}} \\ +\Omega &\to \omega \\ +\tilde{\partial}_t &\to -\imath \omega \\ + \hat{\partial}_t &\to -\imath \omega \\ +\end{aligned} +

+

and then

+

+ \begin{aligned} + \tilde{\nabla} \times \tilde{E}_{\vec{r}} &= + \imath \omega \hat{B}_{\vec{r} + \frac{1}{2}} + - \hat{M}_{\vec{r} + \frac{1}{2}} \\ + \hat{\nabla} \times \hat{H}_{\vec{r} + \frac{1}{2}} &= + -\imath \omega \tilde{D}_{\vec{r}} + + \tilde{J}_{\vec{r}} \\ +\end{aligned} +

+

+\hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot +\tilde{\nabla} \times \tilde{E}_{\vec{r}}) + -\omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} = -\imath +\omega \tilde{J}_{\vec{r}} \\ +

+

TODO FDFD?

+

TODO PML

+

Sub-modules

+ +
+

Module meanas.fdfd.bloch

+

Bloch eigenmode solver/operators

+

This module contains functions for generating and solving the 3D +Bloch eigenproblem. The approach is to transform the problem into the +(spatial) fourier domain, transforming the equation

+
1/mu * curl(1/eps * curl(H_eigenmode)) = (w/c)^2 H_eigenmode
+

into

+
conv(1/mu_k, ik x conv(1/eps_k, ik x H_k)) = (w/c)^2 H_k
+

where:

+ +

Since k and H are orthogonal for each plane +wave, we can use each k to create an orthogonal basis (k, +m, n), with k x m = n, and |m| = |n| = 1. The +cross products are then simplified as follows:

+ +

We know

+
k @ h = kx hx + ky hy + kz hz = 0 = hk
+h = hk + hm + hn = hm + hn
+k = kk + km + kn = kk = |k|
+

We can write

+
k x h = (ky hz - kz hy,
+         kz hx - kx hz,
+         kx hy - ky hx)_xyz
+      = ((k x h) @ k, (k x h) @ m, (k x h) @ n)_kmn
+      = (0, (m x k) @ h, (n x k) @ h)_kmn         # triple product ordering
+      = (0, kk (-n @ h), kk (m @ h))_kmn          # (m x k) = -|k| n, etc.
+      = |k| (0, -h @ n, h @ m)_kmn
+

which gives us a straightforward way to perform the cross product +while simultaneously transforming into the _kmn basis. We +can also write

+
k x h = (km hn - kn hm,
+         kn hk - kk hn,
+         kk hm - km hk)_kmn
+      = (0, -kk hn, kk hm)_kmn
+      = (-kk hn)(mx, my, mz)_xyz + (kk hm)(nx, ny, nz)_xyz
+      = |k| (hm * (nx, ny, nz)_xyz
+           - hn * (mx, my, mz)_xyz)
+

which gives us a way to perform the cross product while +simultaneously trasnforming back into the _xyz basis.

+

We can also simplify conv(X_k, Y_k) as +fftn(X * ifftn(Y_k)).

+

Using these results and storing H_k as +h = (hm, hn), we have

+
e_xyz = fftn(1/eps * ifftn(|k| (hm * n - hn * m)))
+b_mn = |k| (-e_xyz @ n, e_xyz @ m)
+h_mn = fftn(1/mu * ifftn(b_m * m + b_n * n))
+

which forms the operator from the left side of the equation.

+

We can then use a preconditioned block Rayleigh iteration algorithm, +as in SG Johnson and JD Joannopoulos, Block-iterative frequency-domain +methods for Maxwell’s equations in a planewave basis, Optics Express 8, +3, 173-190 (2001) (similar to that used in MPB) to find the eigenvectors +for this operator.

+

===

+

Typically you will want to do something like

+
recip_lattice = numpy.diag(1/numpy.array(epsilon[0].shape * dx))
+n, v = bloch.eigsolve(5, k0, recip_lattice, epsilon)
+f = numpy.sqrt(-numpy.real(n[0]))
+n_eff = norm(recip_lattice @ k0) / f
+
+v2e = bloch.hmn_2_exyz(k0, recip_lattice, epsilon)
+e_field = v2e(v[0])
+
+k, f = find_k(frequency=1/1550,
+              tolerance=(1/1550 - 1/1551),
+              direction=[1, 0, 0],
+              G_matrix=recip_lattice,
+              epsilon=epsilon,
+              band=0)
+

Functions

+

Function eigsolve

+
+

def eigsolve(num_modes: int, k0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, tolerance: float = 1e-07, max_iters: int = 10000, reset_iters: int = 100, y0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]], ForwardRef(None)] = None, callback: collections.abc.Callable[..., None] | None = None) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]

+
+

Find the first (lowest-frequency) num_modes eigenmodes with Bloch +wavevector k0 of the specified structure.

+

Args —–= k0 : Bloch wavevector, +[k0x, k0y, k0z].

+
+
G_matrix
+
+3x3 matrix, with reciprocal lattice vectors as columns. +
+
epsilon
+
+Dielectric constant distribution for the simulation. All fields are +sampled at cell centers (i.e., NOT Yee-gridded) +
+
mu
+
+Magnetic permability distribution for the simulation. Default +None (1 everywhere). +
+
tolerance
+
+Solver stops when fractional change in the objective +trace(Z.H @ A @ Z @ inv(Z Z.H)) is smaller than the +tolerance +
+
max_iters
+
+TODO +
+
reset_iters
+
+TODO +
+
callback
+
+TODO +
+
y0
+
+TODO, initial guess +
+
+

Returns —–= (eigenvalues, eigenvectors) where +eigenvalues[i] corresponds to the vector +eigenvectors[i, :]

+

Function fftn

+
+

def fftn(*args: Any, **kwargs: Any) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]

+
+

Function find_k

+
+

def find_k(frequency: float, tolerance: float, direction: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, band: int = 0, k_bounds: tuple[float, float] = (0, 0.5), k_guess: float | None = None, solve_callback: collections.abc.Callable[..., None] | None = None, iter_callback: collections.abc.Callable[..., None] | None = None, v0: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]] | None = None) -> tuple[float, float, numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]

+
+

Search for a bloch vector that has a given frequency.

+

Args —–= frequency : Target +frequency.

+
+
tolerance
+
+Target frequency tolerance. +
+
direction
+
+k-vector direction to search along. +
+
G_matrix
+
+3x3 matrix, with reciprocal lattice vectors as columns. +
+
epsilon
+
+Dielectric constant distribution for the simulation. All fields are +sampled at cell centers (i.e., NOT Yee-gridded) +
+
mu
+
+Magnetic permability distribution for the simulation. Default None (1 +everywhere). +
+
band
+
+Which band to search in. Default 0 (lowest frequency). +
+
k_bounds
+
+Minimum and maximum values for k. Default (0, 0.5). +
+
k_guess
+
+Initial value for k. +
+
solve_callback
+
+TODO +
+
iter_callback
+
+TODO +
+
+

Returns —–= (k, actual_frequency, eigenvalues, +eigenvectors) The found k-vector and its frequency, along with +all eigenvalues and eigenvectors.

+

Function +generate_kmn

+
+

def generate_kmn(k0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], shape: collections.abc.Sequence[int]) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]

+
+

Generate a (k, m, n) orthogonal basis for each k-vector in the +simulation grid.

+

Args —–= k0 : [k0x, k0y, k0z], Bloch +wavevector, in G basis.

+
+
G_matrix
+
+3x3 matrix, with reciprocal lattice vectors as columns. +
+
shape
+
+[nx, ny, nz] shape of the simulation grid. +
+
+

Returns —–= (|k|, m, n) where |k| has shape +tuple(shape) + (1,) and m, n have +shape tuple(shape) + (3,). All are given in the xyz basis +(e.g. |k|[0,0,0] = norm(G_matrix @ k0)).

+

Function +hmn_2_exyz

+
+

def hmn_2_exyz(k0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]

+
+

Generate an operator which converts a vectorized +spatial-frequency-space h_mn into an E-field distribution, +i.e.

+
ifft(conv(1/eps_k, ik x h_mn))
+

The operator is a function that acts on a vector h_mn of +size 2 * epsilon[0].size.

+

See the meanas.fdfd.bloch docstring for +more information.

+

Args —–= k0 : Bloch wavevector, +[k0x, k0y, k0z].

+
+
G_matrix
+
+3x3 matrix, with reciprocal lattice vectors as columns. +
+
epsilon
+
+Dielectric constant distribution for the simulation. All fields are +sampled at cell centers (i.e., NOT Yee-gridded) +
+
+

Returns —–= Function for converting h_mn into +E_xyz

+

Function +hmn_2_hxyz

+
+

def hmn_2_hxyz(k0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]

+
+

Generate an operator which converts a vectorized +spatial-frequency-space h_mn into an H-field distribution, +i.e.

+
ifft(h_mn)
+

The operator is a function that acts on a vector h_mn of +size 2 * epsilon[0].size.

+

See the meanas.fdfd.bloch docstring for +more information.

+

Args —–= k0 : Bloch wavevector, +[k0x, k0y, k0z].

+
+
G_matrix
+
+3x3 matrix, with reciprocal lattice vectors as columns. +
+
epsilon
+
+Dielectric constant distribution for the simulation. Only +epsilon[0].shape is used. +
+
+

Returns —–= Function for converting h_mn into +H_xyz

+

Function ifftn

+
+

def ifftn(*args: Any, **kwargs: Any) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]

+
+

Function +inner_product

+
+

def inner_product(eL, hL, eR, hR) -> complex

+
+

Function +inverse_maxwell_operator_approx

+
+

def inverse_maxwell_operator_approx(k0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]

+
+

Generate an approximate inverse of the Maxwell operator,

+
ik x conv(eps_k, ik x conv(mu_k, ___))
+

which can be used to improve the speed of ARPACK in shift-invert +mode.

+

See the meanas.fdfd.bloch docstring for +more information.

+

Args —–= k0 : Bloch wavevector, +[k0x, k0y, k0z].

+
+
G_matrix
+
+3x3 matrix, with reciprocal lattice vectors as columns. +
+
epsilon
+
+Dielectric constant distribution for the simulation. All fields are +sampled at cell centers (i.e., NOT Yee-gridded) +
+
mu
+
+Magnetic permability distribution for the simulation. Default None (1 +everywhere). +
+
+

Returns —–= Function which applies the approximate inverse of the +maxwell operator to h_mn.

+

Function +maxwell_operator

+
+

def maxwell_operator(k0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]

+
+

Generate the Maxwell operator

+
conv(1/mu_k, ik x conv(1/eps_k, ik x ___))
+

which is the spatial-frequency-space representation of

+
1/mu * curl(1/eps * curl(___))
+

The operator is a function that acts on a vector h_mn of size +2 * epsilon[0].size

+

See the meanas.fdfd.bloch docstring for +more information.

+

Args —–= k0 : Bloch wavevector, +[k0x, k0y, k0z].

+
+
G_matrix
+
+3x3 matrix, with reciprocal lattice vectors as columns. +
+
epsilon
+
+Dielectric constant distribution for the simulation. All fields are +sampled at cell centers (i.e., NOT Yee-gridded) +
+
mu
+
+Magnetic permability distribution for the simulation. Default None (1 +everywhere). +
+
+

Returns —–= Function which applies the maxwell operator to h_mn.

+

Function trq

+
+

def trq(eI, hI, eO, hO) -> tuple[complex, complex]

+
+
+

Module +meanas.fdfd.farfield

+

Functions for performing near-to-farfield transformation (and the +reverse).

+

Functions

+

Function +far_to_nearfield

+
+

def far_to_nearfield(E_far: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], H_far: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], dkx: float, dky: float, padded_size: list[int] | int | None = None) -> dict[str, typing.Any]

+
+

Compute the farfield, i.e. the distribution of the fields after +propagation through several wavelengths of uniform medium.

+

The input fields should be complex phasors.

+

Args —–= E_far : List of 2 ndarrays +containing the 2D phasor field slices for the transverse E fields +(e.g. [Ex, Ey] for calculating the nearfield toward the z-direction). +Fields should be normalized so that E_far = E_far_actual / (i k exp(-i k +r) / (4 pi r))

+
+
H_far
+
+List of 2 ndarrays containing the 2D phasor field slices for the +transverse H fields (e.g. [Hx, hy] for calculating the nearfield toward +the z-direction). Fields should be normalized so that H_far = +H_far_actual / (i k exp(-i k r) / (4 pi r)) +
+
dkx
+
+kx discretization, in units of wavelength. +
+
dky
+
+ky discretization, in units of wavelength. +
+
padded_size
+
+Shape of the output. A single integer n will be expanded to +(n, n). Powers of 2 are most efficient for FFT computation. +Default is the smallest power of 2 larger than the input, for each axis. +
+
+

Returns —–= Dict with keys

+ +

Function +near_to_farfield

+
+

def near_to_farfield(E_near: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], H_near: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], dx: float, dy: float, padded_size: list[int] | int | None = None) -> dict[str, typing.Any]

+
+

Compute the farfield, i.e. the distribution of the fields after +propagation through several wavelengths of uniform medium.

+

The input fields should be complex phasors.

+

Args —–= E_near : List of 2 ndarrays +containing the 2D phasor field slices for the transverse E fields +(e.g. [Ex, Ey] for calculating the farfield toward the z-direction).

+
+
H_near
+
+List of 2 ndarrays containing the 2D phasor field slices for the +transverse H fields (e.g. [Hx, hy] for calculating the farfield towrad +the z-direction). +
+
dx
+
+Cell size along x-dimension, in units of wavelength. +
+
dy
+
+Cell size along y-dimension, in units of wavelength. +
+
padded_size
+
+Shape of the output. A single integer n will be expanded to +(n, n). Powers of 2 are most efficient for FFT computation. +Default is the smallest power of 2 larger than the input, for each axis. +
+
+

Returns —–= Dict with keys

+ +
+

Module +meanas.fdfd.functional

+

Functional versions of many FDFD operators. These can be useful for +performing FDFD calculations without needing to construct large matrices +in memory.

+

The functions generated here expect cfdfield_t inputs +with shape (3, X, Y, Z), e.g. E = [E_x, E_y, E_z] where each (complex) +component has shape (X, Y, Z)

+

Functions

+

Function e2h

+
+

def e2h(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]

+
+

Utility operator for converting the E field into the +H field. For use with e_full() – assumes that +there is no magnetic current M.

+

Args —–= omega : Angular frequency of +the simulation

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
mu
+
+Magnetic permeability (default 1 everywhere) +
+
+

Returns —–= Function f for converting E to +H, f(E) -> H

+

Function e_full

+
+

def e_full(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]

+
+

Wave operator for use with E-field. See operators.e_full +for details.

+

Args —–= omega : Angular frequency of +the simulation

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
epsilon
+
+Dielectric constant +
+
mu
+
+Magnetic permeability (default 1 everywhere) +
+
+

Returns —–= Function f implementing the wave operator +f(E) -> -i * omega * J

+

Function +e_tfsf_source

+
+

def e_tfsf_source(TF_region: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]

+
+

Operator that turns an E-field distribution into a +total-field/scattered-field (TFSF) source.

+

Args —–= TF_region : mask which is set +to 1 in the total-field region, and 0 elsewhere (i.e. in the +scattered-field region). Should have the same shape as the simulation +grid, e.g. epsilon[0].shape.

+
+
omega
+
+Angular frequency of the simulation +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
epsilon
+
+Dielectric constant distribution +
+
mu
+
+Magnetic permeability (default 1 everywhere) +
+
+

Returns —–= Function f which takes an E field and +returns a current distribution, f(E) -> +J

+

Function +eh_full

+
+

def eh_full(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]], tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]]

+
+

Wave operator for full (both E and H) field representation. See +operators.eh_full.

+

Args —–= omega : Angular frequency of +the simulation

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
epsilon
+
+Dielectric constant +
+
mu
+
+Magnetic permeability (default 1 everywhere) +
+
+

Returns —–= Function f implementing the wave operator +f(E, H) -> (J, -M)

+

Function m2j

+
+

def m2j(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]

+
+

Utility operator for converting magnetic current M +distribution into equivalent electric current distribution +J. For use with e.g. e_full().

+

Args —–= omega : Angular frequency of +the simulation

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
mu
+
+Magnetic permeability (default 1 everywhere) +
+
+

Returns —–= Function f for converting M to +J, f(M) -> J

+

Function +poynting_e_cross_h

+
+

def poynting_e_cross_h(dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]]) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]

+
+

Generates a function that takes the single-frequency E +and H fields and calculates the cross product +E x H = E \times H as +required for the Poynting vector, S = E \times H

+

Note —–= This function also shifts the input E field by +one cell as required for computing the Poynting cross product (see +meanas.fdfd module docs).

+

Note —–= If E and H are peak amplitudes as +assumed elsewhere in this code, the time-average of the poynting vector +is <S> = Re(S)/2 = Re(E x H*) / 2. The factor of +1/2 can be omitted if root-mean-square quantities are used +instead.

+

Args —–= dxes : Grid parameters +[dx_e, dx_h] as described in meanas.fdmath.types

+

Returns —–= Function f that returns E x H as required +for the poynting vector.

+
+

Module +meanas.fdfd.operators

+

Sparse matrix operators for use with electromagnetic wave +equations.

+

These functions return sparse-matrix +(scipy.sparse.spmatrix) representations of a variety of +operators, intended for use with E and H fields vectorized using the +vec() and +unvec() +functions.

+

E- and H-field values are defined on a Yee cell; epsilon +values should be calculated for cells centered at each E component +(mu at each H component).

+

Many of these functions require a dxes parameter, of +type dx_lists_t; see the meanas.fdmath.types submodule for +details.

+

The following operators are included:

+ +

Functions

+

Function e2h

+
+

def e2h(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Utility operator for converting the E field into the H field. For use +with e_full() – +assumes that there is no magnetic current M.

+

Args —–= omega : Angular frequency of +the simulation

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
mu
+
+Vectorized magnetic permeability (default 1 everywhere) +
+
pmc
+
+Vectorized mask specifying PMC cells. Any cells where +pmc != 0 are interpreted as containing a perfect magnetic +conductor (PMC). The PMC is applied per-field-component +(i.e. pmc.size == epsilon.size) +
+
+

Returns —–= Sparse matrix for converting E to H.

+

Function +e_boundary_source

+
+

def e_boundary_source(mask: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, periodic_mask_edges: bool = False) -> scipy.sparse._matrix.spmatrix

+
+

Operator that turns an E-field distrubtion into a current (J) +distribution along the edges (external and internal) of the provided +mask. This is just an e_tfsf_source() +with an additional masking step.

+

Args —–= mask : The current +distribution is generated at the edges of the mask, i.e. any points +where shifting the mask by one cell in any direction would change its +value.

+
+
omega
+
+Angular frequency of the simulation +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
epsilon
+
+Vectorized dielectric constant +
+
mu
+
+Vectorized magnetic permeability (default 1 everywhere). +
+
+

Returns —–= Sparse matrix that turns an E-field into a current (J) +distribution.

+

Function e_full

+
+

def e_full(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Wave operator \nabla \times (\frac{1}{\mu} +\nabla \times) - \Omega^2 \epsilon

+
del x (1/mu * del x) - omega**2 * epsilon
+

for use with the E-field, with wave equation +(\nabla \times (\frac{1}{\mu} \nabla \times) - \Omega^2 \epsilon) E = +-\imath \omega J

+
(del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J
+

To make this matrix symmetric, use the preconditioners from e_full_preconditioners().

+

Args —–= omega : Angular frequency of +the simulation

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
epsilon
+
+Vectorized dielectric constant +
+
mu
+
+Vectorized magnetic permeability (default 1 everywhere). +
+
pec
+
+Vectorized mask specifying PEC cells. Any cells where +pec != 0 are interpreted as containing a perfect electrical +conductor (PEC). The PEC is applied per-field-component +(i.e. pec.size == epsilon.size) +
+
pmc
+
+Vectorized mask specifying PMC cells. Any cells where +pmc != 0 are interpreted as containing a perfect magnetic +conductor (PMC). The PMC is applied per-field-component +(i.e. pmc.size == epsilon.size) +
+
+

Returns —–= Sparse matrix containing the wave operator.

+

Function +e_full_preconditioners

+
+

def e_full_preconditioners(dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]]) -> tuple[scipy.sparse._matrix.spmatrix, scipy.sparse._matrix.spmatrix]

+
+

Left and right preconditioners (Pl, Pr) for symmetrizing +the e_full() +wave operator.

+

The preconditioned matrix A_symm = (Pl @ A @ Pr) is +complex-symmetric (non-Hermitian unless there is no loss or PMLs).

+

The preconditioner matrices are diagonal and complex, with +Pr = 1 / Pl

+

Args —–= dxes : Grid parameters +[dx_e, dx_h] as described in meanas.fdmath.types

+

Returns —–= Preconditioner matrices (Pl, Pr).

+

Function +e_tfsf_source

+
+

def e_tfsf_source(TF_region: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Operator that turns a desired E-field distribution into a +total-field/scattered-field (TFSF) source.

+

TODO: Reference Rumpf paper

+

Args —–= TF_region : Mask, which is set +to 1 inside the total-field region and 0 in the scattered-field +region

+
+
omega
+
+Angular frequency of the simulation +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
epsilon
+
+Vectorized dielectric constant +
+
mu
+
+Vectorized magnetic permeability (default 1 everywhere). +
+
+

Returns —–= Sparse matrix that turns an E-field into a current (J) +distribution.

+

Function +eh_full

+
+

def eh_full(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Wave operator for [E, H] field representation. This +operator implements Maxwell’s equations without cancelling out either E +or H. The operator is \begin{bmatrix} + -\imath \omega \epsilon & \nabla \times \\ + \nabla \times & \imath \omega \mu + \end{bmatrix}

+
[[-i * omega * epsilon,  del x         ],
+ [del x,                 i * omega * mu]]
+

for use with a field vector of the form cat(vec(E), +vec(H)): \begin{bmatrix} + -\imath \omega \epsilon & \nabla \times \\ + \nabla \times & \imath \omega \mu + \end{bmatrix} + \begin{bmatrix} E \\ + H + \end{bmatrix} + = \begin{bmatrix} J \\ + -M + \end{bmatrix}

+

Args —–= omega : Angular frequency of +the simulation

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
epsilon
+
+Vectorized dielectric constant +
+
mu
+
+Vectorized magnetic permeability (default 1 everywhere) +
+
pec
+
+Vectorized mask specifying PEC cells. Any cells where +pec != 0 are interpreted as containing a perfect electrical +conductor (PEC). The PEC is applied per-field-component +(i.e. pec.size == epsilon.size) +
+
pmc
+
+Vectorized mask specifying PMC cells. Any cells where +pmc != 0 are interpreted as containing a perfect magnetic +conductor (PMC). The PMC is applied per-field-component +(i.e. pmc.size == epsilon.size) +
+
+

Returns —–= Sparse matrix containing the wave operator.

+

Function h_full

+
+

def h_full(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Wave operator \nabla \times +(\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu

+
del x (1/epsilon * del x) - omega**2 * mu
+

for use with the H-field, with wave equation +(\nabla \times (\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu) E = +\imath \omega M

+
(del x (1/epsilon * del x) - omega**2 * mu) E = i * omega * M
+

Args —–= omega : Angular frequency of +the simulation

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
epsilon
+
+Vectorized dielectric constant +
+
mu
+
+Vectorized magnetic permeability (default 1 everywhere) +
+
pec
+
+Vectorized mask specifying PEC cells. Any cells where +pec != 0 are interpreted as containing a perfect electrical +conductor (PEC). The PEC is applied per-field-component +(i.e. pec.size == epsilon.size) +
+
pmc
+
+Vectorized mask specifying PMC cells. Any cells where +pmc != 0 are interpreted as containing a perfect magnetic +conductor (PMC). The PMC is applied per-field-component +(i.e. pmc.size == epsilon.size) +
+
+

Returns —–= Sparse matrix containing the wave operator.

+

Function m2j

+
+

def m2j(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Operator for converting a magnetic current M into an electric current +J. For use with eg. e_full().

+

Args —–= omega : Angular frequency of +the simulation

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
mu
+
+Vectorized magnetic permeability (default 1 everywhere) +
+
+

Returns —–= Sparse matrix for converting M to J.

+

Function +poynting_e_cross

+
+

def poynting_e_cross(e: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]]) -> scipy.sparse._matrix.spmatrix

+
+

Operator for computing the Poynting vector, containing the (E x) +portion of the Poynting vector.

+

Args —–= e : Vectorized E-field for the +ExH cross product

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
+

Returns —–= Sparse matrix containing (E x) portion of Poynting cross +product.

+

Function +poynting_h_cross

+
+

def poynting_h_cross(h: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]]) -> scipy.sparse._matrix.spmatrix

+
+

Operator for computing the Poynting vector, containing the (H x) +portion of the Poynting vector.

+

Args —–= h : Vectorized H-field for the +HxE cross product

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
+

Returns —–= Sparse matrix containing (H x) portion of Poynting cross +product.

+
+

Module meanas.fdfd.scpml

+

Functions for creating stretched coordinate perfectly matched layer +(PML) absorbers.

+

Variables

+

Variable +s_function_t

+

Typedef for s-functions, see prepare_s_function()

+

Functions

+

Function +prepare_s_function

+
+

def prepare_s_function(ln_R: float = -16, m: float = 4) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]

+
+

Create an s_function to pass to the SCPML functions. This is used +when you would like to customize the PML parameters.

+

Args —–= ln_R : Natural logarithm of +the desired reflectance

+
+
m
+
+Polynomial order for the PML (imaginary part increases as distance ** m) +
+
+

Returns —–= An s_function, which takes an ndarray (distances) and +returns an ndarray (complex part of the cell width; needs to be divided +by sqrt(epilon_effective) * real(omega)) before use.

+

Function +stretch_with_scpml

+
+

def stretch_with_scpml(dxes: list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], axis: int, polarity: int, omega: float, epsilon_effective: float = 1.0, thickness: int = 10, s_function: collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] | None = None) -> list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]]

+
+

Stretch dxes to contain a stretched-coordinate PML (SCPML) in one +direction along one axis.

+

Args —–= dxes : Grid parameters +[dx_e, dx_h] as described in meanas.fdmath.types

+
+
axis
+
+axis to stretch (0=x, 1=y, 2=z) +
+
polarity
+
+direction to stretch (-1 for -ve, +1 for +ve) +
+
omega
+
+Angular frequency for the simulation +
+
epsilon_effective
+
+Effective epsilon of the PML. Match this to the material at the edge of +your grid. Default 1. +
+
thickness
+
+number of cells to use for pml (default 10) +
+
s_function
+
+Created by prepare_s_function()(…), +allowing customization of pml parameters. Default uses prepare_s_function() +with no parameters. +
+
+

Returns —–= Complex cell widths (dx_lists_mut) as discussed in +meanas.fdmath.types. +Multiple calls to this function may be necessary if multiple absorpbing +boundaries are needed.

+

Function +uniform_grid_scpml

+
+

def uniform_grid_scpml(shape: collections.abc.Sequence[int], thicknesses: collections.abc.Sequence[int], omega: float, epsilon_effective: float = 1.0, s_function: collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] | None = None) -> list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]]

+
+

Create dx arrays for a uniform grid with a cell width of 1 and a +pml.

+

If you want something more fine-grained, check out stretch_with_scpml()(…).

+

Args —–= shape : Shape of the grid, +including the PMLs (which are 2*thicknesses thick)

+
+
thicknesses
+
+[th_x, th_y, th_z] Thickness of the PML in each direction. +Both polarities are added. Each th_ of pml is applied twice, once on +each edge of the grid along the given axis. th_* may be +zero, in which case no pml is added. +
+
omega
+
+Angular frequency for the simulation +
+
epsilon_effective
+
+Effective epsilon of the PML. Match this to the material at the edge of +your grid. Default 1. +
+
s_function
+
+created by prepare_s_function()(…), +allowing customization of pml parameters. Default uses prepare_s_function() +with no parameters. +
+
+

Returns —–= Complex cell widths (dx_lists_mut) as discussed in +meanas.fdmath.types.

+
+

Module +meanas.fdfd.solvers

+

Solvers and solver interface for FDFD problems.

+

Functions

+

Function generic

+
+

def generic(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], J: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, adjoint: bool = False, matrix_solver: collections.abc.Callable[..., typing.Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[typing.Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[typing.Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[typing.Union[bool, int, float, complex, str, bytes]]]] = <function _scipy_qmr>, matrix_solver_opts: dict[str, typing.Any] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]

+
+

Conjugate gradient FDFD solver using CSR sparse matrices.

+

All ndarray arguments should be 1D arrays, as returned by vec().

+

Args —–= omega : Complex frequency to +solve at.

+
+
dxes
+
+[[dx_e, dy_e, dz_e], [dx_h, dy_h, dz_h]] (complex cell +sizes) as discussed in meanas.fdmath.types +
+
J
+
+Electric current distribution (at E-field locations) +
+
epsilon
+
+Dielectric constant distribution (at E-field locations) +
+
mu
+
+Magnetic permeability distribution (at H-field locations) +
+
pec
+
+Perfect electric conductor distribution (at E-field locations; non-zero +value indicates PEC is present) +
+
pmc
+
+Perfect magnetic conductor distribution (at H-field locations; non-zero +value indicates PMC is present) +
+
adjoint
+
+If true, solves the adjoint problem. +
+
matrix_solver
+
+Called as +matrix_solver(A, b, **matrix_solver_opts) -> x, where +A: scipy.sparse.csr_matrix; b: +ArrayLike; x: ArrayLike; Default +is a wrapped version of scipy.sparse.linalg.qmr() which +doesn’t return convergence info and logs the residual every 100 +iterations. +
+
matrix_solver_opts
+
+Passed as kwargs to matrix_solver(…) +
+
+

Returns —–= E-field which solves the system.

+
+

Module +meanas.fdfd.waveguide_2d

+

Operators and helper functions for waveguides with unchanging +cross-section.

+

The propagation direction is chosen to be along the z axis, and all +fields are given an implicit z-dependence of the form +exp(-1 * wavenumber * z).

+

As the z-dependence is known, all the functions in this file assume a +2D grid +(i.e. dxes = [[[dx_e[0], dx_e[1], ...], [dy_e[0], ...]], [[dx_h[0], ...], [dy_h[0], ...]]]).

+

===============

+

Consider Maxwell’s equations in continuous space, in the frequency +domain. Assuming a structure with some (x, y) cross-section extending +uniformly into the z dimension, with a diagonal \epsilon tensor, we have

+

+\begin{aligned} +\nabla \times \vec{E}(x, y, z) &= -\imath \omega \mu \vec{H} \\ +\nabla \times \vec{H}(x, y, z) &= \imath \omega \epsilon \vec{E} \\ +\vec{E}(x,y,z) &= (\vec{E}_t(x, y) + E_z(x, y)\vec{z}) e^{-\imath +\beta z} \\ +\vec{H}(x,y,z) &= (\vec{H}_t(x, y) + H_z(x, y)\vec{z}) e^{-\imath +\beta z} \\ +\end{aligned} +

+

Expanding the first two equations into vector components, we get

+

+\begin{aligned} +-\imath \omega \mu_{xx} H_x &= \partial_y E_z - \partial_z E_y \\ +-\imath \omega \mu_{yy} H_y &= \partial_z E_x - \partial_x E_z \\ +-\imath \omega \mu_{zz} H_z &= \partial_x E_y - \partial_y E_x \\ +\imath \omega \epsilon_{xx} E_x &= \partial_y H_z - \partial_z H_y +\\ +\imath \omega \epsilon_{yy} E_y &= \partial_z H_x - \partial_x H_z +\\ +\imath \omega \epsilon_{zz} E_z &= \partial_x H_y - \partial_y H_x +\\ +\end{aligned} +

+

Substituting in our expressions for \vec{E}, \vec{H} and discretizing:

+

+\begin{aligned} +-\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \imath \beta +E_y \\ +-\imath \omega \mu_{yy} H_y &= -\imath \beta E_x - +\tilde{\partial}_x E_z \\ +-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_x E_y - +\tilde{\partial}_y E_x \\ +\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath +\beta H_y \\ +\imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - +\hat{\partial}_x H_z \\ +\imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - +\hat{\partial}_y H_x \\ +\end{aligned} +

+

Rewrite the last three equations as

+

+\begin{aligned} +\imath \beta H_y &= \imath \omega \epsilon_{xx} E_x - +\hat{\partial}_y H_z \\ +\imath \beta H_x &= -\imath \omega \epsilon_{yy} E_y - +\hat{\partial}_x H_z \\ +\imath \omega E_z &= \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y - +\frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\ +\end{aligned} +

+

Now apply \imath \beta \tilde{\partial}_x to the +last equation, then substitute in for \imath \beta +H_x and \imath \beta H_y:

+

+\begin{aligned} +\imath \beta \tilde{\partial}_x \imath \omega E_z &= \imath \beta +\tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y + - \imath \beta +\tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\ + &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} +\hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y +H_z) + - \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y +(-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\ + &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} +\hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x) + - \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y +(-\imath \omega \epsilon_{yy} E_y) \\ +\imath \beta \tilde{\partial}_x E_z &= \tilde{\partial}_x +\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \tilde{\partial}_x +\frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\ +\end{aligned} +

+

With a similar approach (but using \imath \beta +\tilde{\partial}_y instead), we can get

+

+\begin{aligned} +\imath \beta \tilde{\partial}_y E_z &= \tilde{\partial}_y +\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \tilde{\partial}_y +\frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\ +\end{aligned} +

+

We can combine this equation for \imath \beta +\tilde{\partial}_y E_z with the unused \imath \omega +\mu_{xx} H_x and \imath \omega \mu_{yy} H_y +equations to get

+

+\begin{aligned} +-\imath \omega \mu_{xx} \imath \beta H_x &= -\beta^2 E_y + \imath +\beta \tilde{\partial}_y E_z \\ +-\imath \omega \mu_{xx} \imath \beta H_x &= -\beta^2 E_y + +\tilde{\partial}_y ( + \frac{1}{\epsilon_{zz}} +\hat{\partial}_x (\epsilon_{xx} E_x) + + \frac{1}{\epsilon_{zz}} +\hat{\partial}_y (\epsilon_{yy} E_y) + )\\ +\end{aligned} +

+

and

+

+\begin{aligned} +-\imath \omega \mu_{yy} \imath \beta H_y &= \beta^2 E_x - \imath +\beta \tilde{\partial}_x E_z \\ +-\imath \omega \mu_{yy} \imath \beta H_y &= \beta^2 E_x - +\tilde{\partial}_x ( + \frac{1}{\epsilon_{zz}} +\hat{\partial}_x (\epsilon_{xx} E_x) + + \frac{1}{\epsilon_{zz}} +\hat{\partial}_y (\epsilon_{yy} E_y) + )\\ +\end{aligned} +

+

However, based on our rewritten equation for \imath +\beta H_x and the so-far unused equation for \imath +\omega \mu_{zz} H_z we can also write

+

+\begin{aligned} +-\imath \omega \mu_{xx} (\imath \beta H_x) &= -\imath \omega +\mu_{xx} (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\ + &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y + \imath +\omega \mu_{xx} \hat{\partial}_x ( + \frac{1}{-\imath \omega \mu_{zz}} +(\tilde{\partial}_x E_y - \tilde{\partial}_y E_x)) \\ + &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y + -\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} +(\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +\end{aligned} +

+

and, similarly,

+

+\begin{aligned} +-\imath \omega \mu_{yy} (\imath \beta H_y) &= \omega^2 \mu_{yy} +\epsilon_{xx} E_x + +\mu_{yy} \hat{\partial}_y +\frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +\end{aligned} +

+

By combining both pairs of expressions, we get

+

+\begin{aligned} +\beta^2 E_x - \tilde{\partial}_x ( + \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + ) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x + +\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} +(\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +-\beta^2 E_y + \tilde{\partial}_y ( + \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + ) &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y + -\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} +(\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +\end{aligned} +

+

Using these, we can construct the eigenvalue problem

+

+\beta^2 \begin{bmatrix} E_x \\ + E_y \end{bmatrix} = + (\omega^2 \begin{bmatrix} \mu_{yy} \epsilon_{xx} & 0 \\ + 0 & \mu_{xx} +\epsilon_{yy} \end{bmatrix} + + \begin{bmatrix} -\mu_{yy} \hat{\partial}_y \\ + \mu_{xx} \hat{\partial}_x \end{bmatrix} +\mu_{zz}^{-1} + \begin{bmatrix} -\tilde{\partial}_y & +\tilde{\partial}_x \end{bmatrix} + + \begin{bmatrix} \tilde{\partial}_x \\ + \tilde{\partial}_y \end{bmatrix} +\epsilon_{zz}^{-1} + \begin{bmatrix} \hat{\partial}_x \epsilon_{xx} & +\hat{\partial}_y \epsilon_{yy} \end{bmatrix}) + \begin{bmatrix} E_x \\ + E_y \end{bmatrix} +

+

In the literature, \beta is usually used to +denote the lossless/real part of the propagation constant, but in +meanas it is allowed to be +complex.

+

An equivalent eigenvalue problem can be formed using the H_x and H_y fields, if those are +more convenient.

+

Note that E_z was never discretized, so \beta will need adjustment to account for numerical +dispersion if the result is introduced into a space with a discretized +z-axis.

+

Functions

+

Function +curl_e

+
+

def curl_e(wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]]) -> scipy.sparse._matrix.spmatrix

+
+

Discretized curl operator for use with the waveguide E field.

+

Args —–= wavenumber : Wavenumber +assuming fields have z-dependence of +exp(-i * wavenumber * z)

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
+

Returns —–= Sparse matrix representation of the operator.

+

Function +curl_h

+
+

def curl_h(wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]]) -> scipy.sparse._matrix.spmatrix

+
+

Discretized curl operator for use with the waveguide H field.

+

Args —–= wavenumber : Wavenumber +assuming fields have z-dependence of +exp(-i * wavenumber * z)

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
+

Returns —–= Sparse matrix representation of the operator.

+

Function e2h

+
+

def e2h(wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Returns an operator which, when applied to a vectorized E eigenfield, +produces the vectorized H eigenfield.

+

Args —–= wavenumber : Wavenumber +assuming fields have z-dependence of +exp(-i * wavenumber * z)

+
+
omega
+
+The angular frequency of the system +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
mu
+
+Vectorized magnetic permeability grid (default 1 everywhere) +
+
+

Returns —–= Sparse matrix representation of the operator.

+

Function e_err

+
+

def e_err(e: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> float

+
+

Calculates the relative error in the E field

+

Args —–= e : Vectorized E field

+
+
wavenumber
+
+Wavenumber assuming fields have z-dependence of +exp(-i * wavenumber * z) +
+
omega
+
+The angular frequency of the system +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
mu
+
+Vectorized magnetic permeability grid (default 1 everywhere) +
+
+

Returns —–= Relative error norm(A_e @ e) / norm(e).

+

Function exy2e

+
+

def exy2e(wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]) -> scipy.sparse._matrix.spmatrix

+
+

Operator which transforms the vector e_xy containing the +vectorized E_x and E_y fields, into a vectorized E containing all three +E components

+

From the operator derivation (see module docs), we have

+

+\imath \omega \epsilon_{zz} E_z = \hat{\partial}_x H_y - +\hat{\partial}_y H_x \\ +

+

as well as the intermediate equations

+

+\begin{aligned} +\imath \beta H_y &= \imath \omega \epsilon_{xx} E_x - +\hat{\partial}_y H_z \\ +\imath \beta H_x &= -\imath \omega \epsilon_{yy} E_y - +\hat{\partial}_x H_z \\ +\end{aligned} +

+

Combining these, we get

+

+\begin{aligned} +E_z &= \frac{1}{- \omega \beta \epsilon_{zz}} (( + \hat{\partial}_y \hat{\partial}_x H_z + -\hat{\partial}_x \hat{\partial}_y H_z) + + \imath \omega (\hat{\partial}_x \epsilon_{xx} E_x + +\hat{\partial}_y \epsilon{yy} E_y)) + &= \frac{1}{\imath \beta \epsilon_{zz}} (\hat{\partial}_x +\epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y) +\end{aligned} +

+

Args —–= wavenumber : Wavenumber +assuming fields have z-dependence of +exp(-i * wavenumber * z) It should satisfy +operator_e() @ e_xy == wavenumber**2 * e_xy

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
+

Returns —–= Sparse matrix representing the operator.

+

Function exy2h

+
+

def exy2h(wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Operator which transforms the vector e_xy containing the +vectorized E_x and E_y fields, into a vectorized H containing all three +H components

+

Args —–= wavenumber : Wavenumber +assuming fields have z-dependence of +exp(-i * wavenumber * z). It should satisfy +operator_e() @ e_xy == wavenumber**2 * e_xy

+
+
omega
+
+The angular frequency of the system +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
mu
+
+Vectorized magnetic permeability grid (default 1 everywhere) +
+
+

Returns —–= Sparse matrix representing the operator.

+

Function +get_abcd

+
+

def get_abcd(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, **kwargs)

+
+

Function get_s

+
+

def get_s(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, force_nogain: bool = False, force_reciprocal: bool = False, **kwargs)

+
+

Function +get_tr

+
+

def get_tr(ehL, wavenumbers_L, ehR, wavenumbers_R, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]])

+
+

Function h2e

+
+

def h2e(wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]) -> scipy.sparse._matrix.spmatrix

+
+

Returns an operator which, when applied to a vectorized H eigenfield, +produces the vectorized E eigenfield.

+

Args —–= wavenumber : Wavenumber +assuming fields have z-dependence of +exp(-i * wavenumber * z)

+
+
omega
+
+The angular frequency of the system +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
+

Returns —–= Sparse matrix representation of the operator.

+

Function h_err

+
+

def h_err(h: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> float

+
+

Calculates the relative error in the H field

+

Args —–= h : Vectorized H field

+
+
wavenumber
+
+Wavenumber assuming fields have z-dependence of +exp(-i * wavenumber * z) +
+
omega
+
+The angular frequency of the system +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
mu
+
+Vectorized magnetic permeability grid (default 1 everywhere) +
+
+

Returns —–= Relative error norm(A_h @ h) / norm(h).

+

Function hxy2e

+
+

def hxy2e(wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Operator which transforms the vector h_xy containing the +vectorized H_x and H_y fields, into a vectorized E containing all three +E components

+

Args —–= wavenumber : Wavenumber +assuming fields have z-dependence of +exp(-i * wavenumber * z). It should satisfy +operator_h() @ h_xy == wavenumber**2 * h_xy

+
+
omega
+
+The angular frequency of the system +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
mu
+
+Vectorized magnetic permeability grid (default 1 everywhere) +
+
+

Returns —–= Sparse matrix representing the operator.

+

Function hxy2h

+
+

def hxy2h(wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Operator which transforms the vector h_xy containing the +vectorized H_x and H_y fields, into a vectorized H containing all three +H components

+

Args —–= wavenumber : Wavenumber +assuming fields have z-dependence of +exp(-i * wavenumber * z). It should satisfy +operator_h() @ h_xy == wavenumber**2 * h_xy

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
mu
+
+Vectorized magnetic permeability grid (default 1 everywhere) +
+
+

Returns —–= Sparse matrix representing the operator.

+

Function +inner_product

+
+

def inner_product(e1: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], h2: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], prop_phase: float = 0, conj_h: bool = False, trapezoid: bool = False) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]

+
+

Function +normalized_fields_e

+
+

def normalized_fields_e(e_xy: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, prop_phase: float = 0) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]

+
+

Given a vector e_xy containing the vectorized E_x and +E_y fields, returns normalized, vectorized E and H fields for the +system.

+

Args —–= e_xy : Vector containing E_x +and E_y fields

+
+
wavenumber
+
+Wavenumber assuming fields have z-dependence of +exp(-i * wavenumber * z). It should satisfy +operator_e() @ e_xy == wavenumber**2 * e_xy +
+
omega
+
+The angular frequency of the system +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
mu
+
+Vectorized magnetic permeability grid (default 1 everywhere) +
+
prop_phase
+
+Phase shift (dz * corrected_wavenumber) over 1 cell in +propagation direction. Default 0 (continuous propagation direction, +i.e. dz->0). +
+
+

Returns —–= (e, h), where each field is vectorized, +normalized, and contains all three vector components.

+

Function +normalized_fields_h

+
+

def normalized_fields_h(h_xy: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, prop_phase: float = 0) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]

+
+

Given a vector h_xy containing the vectorized H_x and +H_y fields, returns normalized, vectorized E and H fields for the +system.

+

Args —–= h_xy : Vector containing H_x +and H_y fields

+
+
wavenumber
+
+Wavenumber assuming fields have z-dependence of +exp(-i * wavenumber * z). It should satisfy +operator_h() @ h_xy == wavenumber**2 * h_xy +
+
omega
+
+The angular frequency of the system +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
mu
+
+Vectorized magnetic permeability grid (default 1 everywhere) +
+
prop_phase
+
+Phase shift (dz * corrected_wavenumber) over 1 cell in +propagation direction. Default 0 (continuous propagation direction, +i.e. dz->0). +
+
+

Returns —–= (e, h), where each field is vectorized, +normalized, and contains all three vector components.

+

Function +operator_e

+
+

def operator_e(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Waveguide operator of the form

+
omega**2 * mu * epsilon +
+mu * [[-Dy], [Dx]] / mu * [-Dy, Dx] +
+[[Dx], [Dy]] / epsilon * [Dx, Dy] * epsilon
+

for use with a field vector of the form cat([E_x, +E_y]).

+

More precisely, the operator is

+

+\omega^2 \begin{bmatrix} \mu_{yy} \epsilon_{xx} & 0 \\ + 0 & \mu_{xx} +\epsilon_{yy} \end{bmatrix} + + \begin{bmatrix} -\mu_{yy} \hat{\partial}_y \\ + \mu_{xx} \hat{\partial}_x \end{bmatrix} +\mu_{zz}^{-1} + \begin{bmatrix} -\tilde{\partial}_y & +\tilde{\partial}_x \end{bmatrix} + + \begin{bmatrix} \tilde{\partial}_x \\ + \tilde{\partial}_y \end{bmatrix} \epsilon_{zz}^{-1} + \begin{bmatrix} \hat{\partial}_x \epsilon_{xx} & +\hat{\partial}_y \epsilon_{yy} \end{bmatrix} +

+

\tilde{\partial}_x and \hat{\partial}_x are the forward and backward +derivatives along x, and each \epsilon_{xx}, \mu_{yy}, etc. is a diagonal matrix containing the +vectorized material property distribution.

+

This operator can be used to form an eigenvalue problem of the form +operator_e(...) @ [E_x, E_y] = wavenumber**2 * [E_x, E_y]

+

which can then be solved for the eigenmodes of the system (an +exp(-i * wavenumber * z) z-dependence is assumed for the +fields).

+

Args —–= omega : The angular frequency +of the system.

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
mu
+
+Vectorized magnetic permeability grid (default 1 everywhere) +
+
+

Returns —–= Sparse matrix representation of the operator.

+

Function +operator_h

+
+

def operator_h(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Waveguide operator of the form

+
omega**2 * epsilon * mu +
+epsilon * [[-Dy], [Dx]] / epsilon * [-Dy, Dx] +
+[[Dx], [Dy]] / mu * [Dx, Dy] * mu
+

for use with a field vector of the form cat([H_x, +H_y]).

+

More precisely, the operator is

+

+\omega^2 \begin{bmatrix} \epsilon_{yy} \mu_{xx} & 0 \\ + 0 & \epsilon_{xx} +\mu_{yy} \end{bmatrix} + + \begin{bmatrix} -\epsilon_{yy} \tilde{\partial}_y \\ + \epsilon_{xx} \tilde{\partial}_x +\end{bmatrix} \epsilon_{zz}^{-1} + \begin{bmatrix} -\hat{\partial}_y & \hat{\partial}_x +\end{bmatrix} + + \begin{bmatrix} \hat{\partial}_x \\ + \hat{\partial}_y \end{bmatrix} \mu_{zz}^{-1} + \begin{bmatrix} \tilde{\partial}_x \mu_{xx} & +\tilde{\partial}_y \mu_{yy} \end{bmatrix} +

+

\tilde{\partial}_x and \hat{\partial}_x are the forward and backward +derivatives along x, and each \epsilon_{xx}, \mu_{yy}, etc. is a diagonal matrix containing the +vectorized material property distribution.

+

This operator can be used to form an eigenvalue problem of the form +operator_h(...) @ [H_x, H_y] = wavenumber**2 * [H_x, H_y]

+

which can then be solved for the eigenmodes of the system (an +exp(-i * wavenumber * z) z-dependence is assumed for the +fields).

+

Args —–= omega : The angular frequency +of the system.

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
mu
+
+Vectorized magnetic permeability grid (default 1 everywhere) +
+
+

Returns —–= Sparse matrix representation of the operator.

+

Function +sensitivity

+
+

def sensitivity(e_norm: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], h_norm: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]

+
+

Given a waveguide structure (dxes, epsilon, +mu) and mode fields (e_norm, +h_norm, wavenumber, omega), +calculates the sensitivity of the wavenumber \beta +to changes in the dielectric structure \epsilon.

+

The output is a vector of the same size as vec(epsilon), +with each element specifying the sensitivity of wavenumber +to changes in the corresponding element in vec(epsilon), +i.e.

+

sens_{i} = +\frac{\partial\beta}{\partial\epsilon_i}

+

An adjoint approach is used to calculate the sensitivity; the +derivation is provided here:

+

Starting with the eigenvalue equation

+

\beta^2 E_{xy} = A_E E_{xy}

+

where A_E is the waveguide operator from operator_e(), and +E_{xy} = \begin{bmatrix} E_x \\ + E_y +\end{bmatrix}, we can differentiate with respect to one of the \epsilon elements (i.e. at one Yee grid point), \epsilon_i:

+

+(2 \beta) \partial_{\epsilon_i}(\beta) E_{xy} + \beta^2 +\partial_{\epsilon_i} E_{xy} + = \partial_{\epsilon_i}(A_E) E_{xy} + A_E \partial_{\epsilon_i} +E_{xy} +

+

We then multiply by H_{yx}^\star = +\begin{bmatrix}H_y^\star \\ -H_x^\star \end{bmatrix} from the +left:

+

+(2 \beta) \partial_{\epsilon_i}(\beta) H_{yx}^\star E_{xy} + \beta^2 +H_{yx}^\star \partial_{\epsilon_i} E_{xy} + = H_{yx}^\star \partial_{\epsilon_i}(A_E) E_{xy} + H_{yx}^\star A_E +\partial_{\epsilon_i} E_{xy} +

+

However, H_{yx}^\star is actually a +left-eigenvector of A_E. This can be verified by +inspecting the form of operator_h() (A_H) and comparing its conjugate transpose to operator_e() (A_E). Also, note H_{yx}^\star \cdot +E_{xy} = H^\star \times E recalls the mode orthogonality relation. +See doi:10.5194/ars-9-85-201 for a similar approach. Therefore,

+

+H_{yx}^\star A_E \partial_{\epsilon_i} E_{xy} = \beta^2 H_{yx}^\star +\partial_{\epsilon_i} E_{xy} +

+

and we can simplify to

+

+\partial_{\epsilon_i}(\beta) + = \frac{1}{2 \beta} \frac{H_{yx}^\star \partial_{\epsilon_i}(A_E) +E_{xy} }{H_{yx}^\star E_{xy}} +

+

This expression can be quickly calculated for all i by writing out the various terms of \partial_{\epsilon_i} A_E and recognizing that the +vector-matrix-vector products (i.e. scalars) sens_i = +\vec{v}_{left} \partial_{\epsilon_i} (\epsilon_{xyz}) +\vec{v}_{right}, indexed by i, can be expressed +as elementwise multiplications \vec{sens} = +\vec{v}_{left} \star \vec{v}_{right}

+

Args —–= e_norm : Normalized, +vectorized E_xyz field for the mode. E.g. as returned by normalized_fields_e().

+
+
h_norm
+
+Normalized, vectorized H_xyz field for the mode. E.g. as returned by +normalized_fields_e(). +
+
wavenumber
+
+Propagation constant for the mode. The z-axis is assumed to be +continuous (i.e. without numerical dispersion). +
+
omega
+
+The angular frequency of the system. +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
mu
+
+Vectorized magnetic permeability grid (default 1 everywhere) +
+
+

Returns —–= Sparse matrix representation of the operator.

+

Function +solve_mode

+
+

def solve_mode(mode_number: int, *args: Any, **kwargs: Any) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], complex]

+
+

Wrapper around solve_modes() +that solves for a single mode.

+

Args —–= mode_number : 0-indexed mode +number to solve for

+
+
*args
+
+passed to solve_modes() +
+
**kwargs
+
+passed to solve_modes() +
+
+

Returns —–= (e_xy, wavenumber)

+

Function +solve_modes

+
+

def solve_modes(mode_numbers: collections.abc.Sequence[int], omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, mode_margin: int = 2) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]

+
+

Given a 2D region, attempts to solve for the eigenmode with the +specified mode numbers.

+

Args —–= mode_numbers : List of +0-indexed mode numbers to solve for

+
+
omega
+
+Angular frequency of the simulation +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
epsilon
+
+Dielectric constant +
+
mu
+
+Magnetic permeability (default 1 everywhere) +
+
mode_margin
+
+The eigensolver will actually solve for +(max(mode_number) + mode_margin) modes, but only return the +target mode. Increasing this value can improve the solver’s ability to +find the correct mode. Default 2. +
+
+

Returns —–= e_xys : NDArray of vfdfield_t specifying +fields. First dimension is mode number.

+
+
wavenumbers
+
+list of wavenumbers +
+
+
+

Module +meanas.fdfd.waveguide_3d

+

Tools for working with waveguide modes in 3D domains.

+

This module relies heavily on waveguide_2d and mostly +just transforms its parameters into 2D equivalents and expands the +results back into 3D.

+

Functions

+

Function +compute_overlap_e

+
+

def compute_overlap_e(E: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], axis: int, polarity: int, slices: collections.abc.Sequence[slice]) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]

+
+

Given an eigenmode obtained by solve_mode(), +calculates an overlap_e for the mode orthogonality relation +Integrate(((E x H_mode) + (E_mode x H)) dot dn) [assumes reflection +symmetry].

+

TODO: add reference

+

Args —–= E : E-field of the mode

+
+
H
+
+H-field of the mode (advanced by half of a Yee cell from E) +
+
wavenumber
+
+Wavenumber of the mode +
+
omega
+
+Angular frequency of the simulation +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
axis
+
+Propagation axis (0=x, 1=y, 2=z) +
+
polarity
+
+Propagation direction (+1 for +ve, -1 for -ve) +
+
slices
+
+epsilon[tuple(slices)] is used to select the portion of the +grid to use as the waveguide cross-section. slices[axis] should select +only one item. +
+
mu
+
+Magnetic permeability (default 1 everywhere) +
+
+

Returns —–= overlap_e such that +numpy.sum(overlap_e * other_e.conj()) computes the overlap +integral

+

Function +compute_source

+
+

def compute_source(E: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], axis: int, polarity: int, slices: collections.abc.Sequence[slice], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]

+
+

Given an eigenmode obtained by solve_mode(), +returns the current source distribution necessary to position a +unidirectional source at the slice location.

+

Args —–= E : E-field of the mode

+
+
wavenumber
+
+Wavenumber of the mode +
+
omega
+
+Angular frequency of the simulation +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
axis
+
+Propagation axis (0=x, 1=y, 2=z) +
+
polarity
+
+Propagation direction (+1 for +ve, -1 for -ve) +
+
slices
+
+epsilon[tuple(slices)] is used to select the portion of the +grid to use as the waveguide cross-section. slices[axis] +should select only one item. +
+
mu
+
+Magnetic permeability (default 1 everywhere) +
+
+

Returns —–= J distribution for the unidirectional source

+

Function +expand_e

+
+

def expand_e(E: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], axis: int, polarity: int, slices: collections.abc.Sequence[slice]) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]

+
+

Given an eigenmode obtained by solve_mode(), +expands the E-field from the 2D slice where the mode was calculated to +the entire domain (along the propagation axis). This assumes the epsilon +cross-section remains constant throughout the entire domain; it is up to +the caller to truncate the expansion to any regions where it is +valid.

+

Args —–= E : E-field of the mode

+
+
wavenumber
+
+Wavenumber of the mode +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
axis
+
+Propagation axis (0=x, 1=y, 2=z) +
+
polarity
+
+Propagation direction (+1 for +ve, -1 for -ve) +
+
slices
+
+epsilon[tuple(slices)] is used to select the portion of the +grid to use as the waveguide cross-section. slices[axis] should select +only one item. +
+
+

Returns —–= E, with the original field expanded along +the specified axis.

+

Function +solve_mode

+
+

def solve_mode(mode_number: int, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], axis: int, polarity: int, slices: collections.abc.Sequence[slice], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> dict[str, complex | numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]

+
+

Given a 3D grid, selects a slice from the grid and attempts to solve +for an eigenmode propagating through that slice.

+

Args —–= mode_number : Number of the +mode, 0-indexed

+
+
omega
+
+Angular frequency of the simulation +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types +
+
axis
+
+Propagation axis (0=x, 1=y, 2=z) +
+
polarity
+
+Propagation direction (+1 for +ve, -1 for -ve) +
+
slices
+
+epsilon[tuple(slices)] is used to select the portion of the +grid to use as the waveguide cross-section. slices[axis] +should select only one item. +
+
epsilon
+
+Dielectric constant +
+
mu
+
+Magnetic permeability (default 1 everywhere) +
+
+

Returns —–=

+
{
+    'E': NDArray[complexfloating],
+    'H': NDArray[complexfloating],
+    'wavenumber': complex,
+}
+
+

Module +meanas.fdfd.waveguide_cyl

+

Operators and helper functions for cylindrical waveguides with +unchanging cross-section.

+

WORK IN PROGRESS, CURRENTLY BROKEN

+

As the z-dependence is known, all the functions in this file assume a +2D grid +(i.e. dxes = [[[dr_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dr_h_0, ...], [dy_h_0, ...]]]).

+

Functions

+

Function +cylindrical_operator

+
+

def cylindrical_operator(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], rmin: float) -> scipy.sparse._matrix.spmatrix

+
+

Cylindrical coordinate waveguide operator of the form

+

(NOTE: See 10.1364/OL.33.001848) TODO: consider +10.1364/OE.20.021583

+

TODO

+

for use with a field vector of the form [E_r, E_y].

+

This operator can be used to form an eigenvalue problem of the form A +@ [E_r, E_y] = wavenumber**2 * [E_r, E_y]

+

which can then be solved for the eigenmodes of the system (an +exp(-i * wavenumber * theta) theta-dependence is assumed +for the fields).

+

Args —–= omega : The angular frequency +of the system

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
rmin
+
+Radius at the left edge of the simulation domain (minimum ‘x’) +
+
+

Returns —–= Sparse matrix representation of the operator

+

Function +dxes2T

+
+

def dxes2T(dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], rmin=builtins.float) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]

+
+

Function e2h

+
+

def e2h(wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Returns an operator which, when applied to a vectorized E eigenfield, +produces the vectorized H eigenfield.

+

Args —–= wavenumber : Wavenumber +assuming fields have z-dependence of +exp(-i * wavenumber * z)

+
+
omega
+
+The angular frequency of the system +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
mu
+
+Vectorized magnetic permeability grid (default 1 everywhere) +
+
+

Returns —–= Sparse matrix representation of the operator.

+

Function +exy2e

+
+

def exy2e(wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]) -> scipy.sparse._matrix.spmatrix

+
+

Operator which transforms the vector e_xy containing the +vectorized E_x and E_y fields, into a vectorized E containing all three +E components

+

Args —–= wavenumber : Wavenumber +assuming fields have z-dependence of +exp(-i * wavenumber * z) It should satisfy +operator_e() @ e_xy == wavenumber**2 * e_xy

+
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
+

Returns —–= Sparse matrix representing the operator.

+

Function +exy2h

+
+

def exy2h(wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix

+
+

Operator which transforms the vector e_xy containing the +vectorized E_x and E_y fields, into a vectorized H containing all three +H components

+

Args —–= wavenumber : Wavenumber +assuming fields have z-dependence of +exp(-i * wavenumber * z). It should satisfy +operator_e() @ e_xy == wavenumber**2 * e_xy

+
+
omega
+
+The angular frequency of the system +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
epsilon
+
+Vectorized dielectric constant grid +
+
mu
+
+Vectorized magnetic permeability grid (default 1 everywhere) +
+
+

Returns —–= Sparse matrix representing the operator.

+

Function +linear_wavenumbers

+
+

def linear_wavenumbers(e_xys: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], angular_wavenumbers: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], rmin: float) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]

+
+

Calculate linear wavenumbers (1/distance) based on angular +wavenumbers (1/rad) and the mode’s energy distribution.

+

Args —–= e_xys : Vectorized mode fields +with shape [num_modes, 2 * x *y)

+
+
angular_wavenumbers
+
+Angular wavenumbers corresponding to the fields in e_xys +
+
epsilon
+
+Vectorized dielectric constant grid with shape (3, x, y) +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D) +
+
rmin
+
+Radius at the left edge of the simulation domain (minimum ‘x’) +
+
+

Returns —–= NDArray containing the calculated linear (1/distance) +wavenumbers

+

Function +solve_mode

+
+

def solve_mode(mode_number: int, *args: Any, **kwargs: Any) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], complex]

+
+

Wrapper around solve_modes() +that solves for a single mode.

+

Args —–= mode_number : 0-indexed mode +number to solve for

+
+
*args
+
+passed to solve_modes() +
+
**kwargs
+
+passed to solve_modes() +
+
+

Returns —–= (e_xy, angular_wavenumber)

+

Function +solve_modes

+
+

def solve_modes(mode_numbers: collections.abc.Sequence[int], omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], rmin: float, mode_margin: int = 2) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]

+
+

TODO: fixup Given a 2d (r, y) slice of epsilon, attempts to solve for +the eigenmode of the bent waveguide with the specified mode number.

+

Args —–= mode_number : Number of the +mode, 0-indexed

+
+
omega
+
+Angular frequency of the simulation +
+
dxes
+
+Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types. The +first coordinate is assumed to be r, the second is y. +
+
epsilon
+
+Dielectric constant +
+
rmin
+
+Radius of curvature for the simulation. This should be the minimum value +of r within the simulation domain. +
+
+

Returns —–= e_xys : NDArray of vfdfield_t specifying +fields. First dimension is mode number.

+
+
angular_wavenumbers
+
+list of wavenumbers in 1/rad units. +
+
+
+

Module meanas.fdmath

+

Basic discrete calculus for finite difference (fd) simulations.

+

Fields, Functions, and +Operators

+

Discrete fields are stored in one of two forms:

+ +

Operators which act on fields also come in two forms: + Python +functions, created by the functions in meanas.fdmath.functional. +The generated functions act on fields in the fdfield_t +form. + Linear operators, usually 2D sparse matrices using +scipy.sparse, created by meanas.fdmath.operators. +These operators act on vectorized fields in the vfdfield_t +form.

+

The operations performed should be equivalent: +functional.op(*args)(E) should be equivalent to +unvec(operators.op(*args) @ vec(E), E.shape[1:]).

+

Generally speaking the field_t form is easier to work +with, but can be harder or less efficient to compose (e.g. it is easy to +generate a single matrix by multiplying a series of other matrices).

+

Discrete calculus

+

This documentation and approach is roughly based on W.C. Chew’s +excellent “Electromagnetic Theory on a Lattice” (doi:10.1063/1.355770), +which covers a superset of this material with similar notation and more +detail.

+

Scalar Derivatives And Cell +Shifts

+

Define the discrete forward derivative as +[\tilde{\partial}_x f]_{m + \frac{1}{2}} = \frac{1}{\Delta_{x, m}} (f_{m ++ 1} - f_m) where f is a function defined at +discrete locations on the x-axis (labeled using m). +The value at m occupies a length \Delta_{x, m} along the x-axis. Note that m is an index along the x-axis, not necessarily +an x-coordinate, since each length \Delta_{x, m}, +\Delta_{x, m+1}, ... is independently chosen.

+

If we treat f as a 1D array of values, with the +i-th value f[i] taking up a length +dx[i] along the x-axis, the forward derivative is

+
deriv_forward(f)[i] = (f[i + 1] - f[i]) / dx[i]
+

Likewise, discrete reverse derivative is +[\hat{\partial}_x f ]_{m - \frac{1}{2}} = \frac{1}{\Delta_{x, m}} (f_{m} +- f_{m - 1}) or

+
deriv_back(f)[i] = (f[i] - f[i - 1]) / dx[i]
+

The derivatives’ values are shifted by a half-cell relative to the +original function, and will have different cell widths if all the +dx[i] ( \Delta_{x, m} ) are not +identical:

+
[figure: derivatives and cell sizes]
+    dx0   dx1      dx2      dx3      cell sizes for function
+   ----- ----- ----------- -----
+   ______________________________
+        |     |           |     |
+     f0 |  f1 |     f2    |  f3 |    function
+   _____|_____|___________|_____|
+     |     |        |        |
+     | Df0 |   Df1  |   Df2  | Df3   forward derivative (periodic boundary)
+   __|_____|________|________|___
+
+ dx'3] dx'0   dx'1     dx'2  [dx'3   cell sizes for forward derivative
+   -- ----- -------- -------- ---
+ dx'0] dx'1   dx'2     dx'3  [dx'0   cell sizes for reverse derivative
+   ______________________________
+     |     |        |        |
+     | df1 |  df2   |   df3  | df0   reverse derivative (periodic boundary)
+   __|_____|________|________|___
+
+Periodic boundaries are used here and elsewhere unless otherwise noted.
+

In the above figure, f0 = f_0, +f1 = f_1 Df0 = [\tilde{\partial}f]_{0 + \frac{1}{2}} Df1 = +[\tilde{\partial}f]_{1 + \frac{1}{2}} +df0 = [\hat{\partial}f]_{0 - +\frac{1}{2}} etc.

+

The fractional subscript m + \frac{1}{2} is used +to indicate values defined at shifted locations relative to the original +m, with corresponding lengths +\Delta_{x, m + \frac{1}{2}} = \frac{1}{2} * (\Delta_{x, m} + \Delta_{x, +m + 1})

+

Just as m is not itself an x-coordinate, neither +is m + \frac{1}{2}; carefully note the positions of +the various cells in the above figure vs their labels. If the positions +labeled with m are considered the “base” or +“original” grid, the positions labeled with m + +\frac{1}{2} are said to lie on a “dual” or “derived” grid.

+

For the remainder of the Discrete calculus section, all +figures will show constant-length cells in order to focus on the vector +derivatives themselves. See the Grid description section +below for additional information on this topic and generalization to +three dimensions.

+

Gradients and fore-vectors

+

Expanding to three dimensions, we can define two gradients [\tilde{\nabla} f]_{m,n,p} = \vec{x} +[\tilde{\partial}_x f]_{m + \frac{1}{2},n,p} + + \vec{y} [\tilde{\partial}_y f]_{m,n + +\frac{1}{2},p} + + \vec{z} [\tilde{\partial}_z f]_{m,n,p ++ \frac{1}{2}} [\hat{\nabla} f]_{m,n,p} = +\vec{x} [\hat{\partial}_x f]_{m + \frac{1}{2},n,p} + + \vec{y} [\hat{\partial}_y f]_{m,n + +\frac{1}{2},p} + + \vec{z} [\hat{\partial}_z f]_{m,n,p + +\frac{1}{2}}

+

or

+
[code: gradients]
+grad_forward(f)[i,j,k] = [Dx_forward(f)[i, j, k],
+                          Dy_forward(f)[i, j, k],
+                          Dz_forward(f)[i, j, k]]
+                       = [(f[i + 1, j, k] - f[i, j, k]) / dx[i],
+                          (f[i, j + 1, k] - f[i, j, k]) / dy[i],
+                          (f[i, j, k + 1] - f[i, j, k]) / dz[i]]
+
+grad_back(f)[i,j,k] = [Dx_back(f)[i, j, k],
+                       Dy_back(f)[i, j, k],
+                       Dz_back(f)[i, j, k]]
+                    = [(f[i, j, k] - f[i - 1, j, k]) / dx[i],
+                       (f[i, j, k] - f[i, j - 1, k]) / dy[i],
+                       (f[i, j, k] - f[i, j, k - 1]) / dz[i]]
+

The three derivatives in the gradient cause shifts in different +directions, so the x/y/z components of the resulting “vector” are +defined at different points: the x-component is shifted in the +x-direction, y in y, and z in z.

+

We call the resulting object a “fore-vector” or “back-vector”, +depending on the direction of the shift. We write it as \tilde{g}_{m,n,p} = \vec{x} g^x_{m + \frac{1}{2},n,p} ++ + \vec{y} g^y_{m,n + \frac{1}{2},p} + + \vec{z} g^z_{m,n,p + \frac{1}{2}} \hat{g}_{m,n,p} = \vec{x} g^x_{m - \frac{1}{2},n,p} + + \vec{y} g^y_{m,n - \frac{1}{2},p} + + \vec{z} g^z_{m,n,p - \frac{1}{2}}

+
[figure: gradient / fore-vector]
+   (m, n+1, p+1) ______________ (m+1, n+1, p+1)
+                /:            /|
+               / :           / |
+              /  :          /  |
+  (m, n, p+1)/_____________/   |     The forward derivatives are defined
+             |   :         |   |     at the Dx, Dy, Dz points,
+             |   :.........|...|     but the forward-gradient fore-vector
+ z y        Dz  /          |  /      is the set of all three
+ |/_x        | Dy          | /       and is said to be "located" at (m,n,p)
+             |/            |/
+    (m, n, p)|_____Dx______| (m+1, n, p)
+

Divergences

+

There are also two divergences,

+

d_{n,m,p} = [\tilde{\nabla} \cdot +\hat{g}]_{n,m,p} + = [\tilde{\partial}_x g^x]_{m,n,p} + + [\tilde{\partial}_y g^y]_{m,n,p} + + [\tilde{\partial}_z g^z]_{m,n,p}

+

d_{n,m,p} = [\hat{\nabla} \cdot +\tilde{g}]_{n,m,p} + = [\hat{\partial}_x g^x]_{m,n,p} + + [\hat{\partial}_y g^y]_{m,n,p} + + [\hat{\partial}_z g^z]_{m,n,p}

+

or

+
[code: divergences]
+div_forward(g)[i,j,k] = Dx_forward(gx)[i, j, k] +
+                        Dy_forward(gy)[i, j, k] +
+                        Dz_forward(gz)[i, j, k]
+                      = (gx[i + 1, j, k] - gx[i, j, k]) / dx[i] +
+                        (gy[i, j + 1, k] - gy[i, j, k]) / dy[i] +
+                        (gz[i, j, k + 1] - gz[i, j, k]) / dz[i]
+
+div_back(g)[i,j,k] = Dx_back(gx)[i, j, k] +
+                     Dy_back(gy)[i, j, k] +
+                     Dz_back(gz)[i, j, k]
+                   = (gx[i, j, k] - gx[i - 1, j, k]) / dx[i] +
+                     (gy[i, j, k] - gy[i, j - 1, k]) / dy[i] +
+                     (gz[i, j, k] - gz[i, j, k - 1]) / dz[i]
+

where g = [gx, gy, gz] is a fore- or back-vector +field.

+

Since we applied the forward divergence to the back-vector (and +vice-versa), the resulting scalar value is defined at the back-vector’s +(fore-vector’s) location (m,n,p) and not at the +locations of its components (m \pm \frac{1}{2},n,p) +etc.

+
[figure: divergence]
+                                ^^
+     (m-1/2, n+1/2, p+1/2) _____||_______ (m+1/2, n+1/2, p+1/2)
+                          /:    ||  ,,  /|
+                         / :    || //  / |      The divergence at (m, n, p) (the center
+                        /  :      //  /  |      of this cube) of a fore-vector field
+  (m-1/2, n-1/2, p+1/2)/_____________/   |      is the sum of the outward-pointing
+                       |   :         |   |      fore-vector components, which are
+     z y            <==|== :.........|.====>    located at the face centers.
+     |/_x              |  /          |  /
+                       | /    //     | /       Note that in a nonuniform grid, each
+                       |/    // ||   |/        dimension is normalized by the cell width.
+  (m-1/2, n-1/2, p-1/2)|____//_______| (m+1/2, n-1/2, p-1/2)
+                           ''   ||
+                                VV
+

Curls

+

The two curls are then

+

\begin{aligned} + \hat{h}_{m + \frac{1}{2}, n + \frac{1}{2}, p + \frac{1}{2}} &= +\\ + [\tilde{\nabla} \times \tilde{g}]_{m + \frac{1}{2}, n + +\frac{1}{2}, p + \frac{1}{2}} &= + \vec{x} (\tilde{\partial}_y g^z_{m,n,p + \frac{1}{2}} - +\tilde{\partial}_z g^y_{m,n + \frac{1}{2},p}) \\ + &+ \vec{y} (\tilde{\partial}_z g^x_{m + \frac{1}{2},n,p} - +\tilde{\partial}_x g^z_{m,n,p + \frac{1}{2}}) \\ + &+ \vec{z} (\tilde{\partial}_x g^y_{m,n + \frac{1}{2},p} - +\tilde{\partial}_y g^z_{m + \frac{1}{2},n,p}) + \end{aligned}

+

and

+

\tilde{h}_{m - \frac{1}{2}, n - \frac{1}{2}, p +- \frac{1}{2}} = + [\hat{\nabla} \times \hat{g}]_{m - \frac{1}{2}, n - \frac{1}{2}, p +- \frac{1}{2}}

+

where \hat{g} and \tilde{g} +are located at (m,n,p) with components at (m \pm \frac{1}{2},n,p) etc., while \hat{h} and \tilde{h} are located at +(m \pm \frac{1}{2}, n \pm \frac{1}{2}, p \pm +\frac{1}{2}) with components at (m, n \pm +\frac{1}{2}, p \pm \frac{1}{2}) etc.

+
[code: curls]
+curl_forward(g)[i,j,k] = [Dy_forward(gz)[i, j, k] - Dz_forward(gy)[i, j, k],
+                          Dz_forward(gx)[i, j, k] - Dx_forward(gz)[i, j, k],
+                          Dx_forward(gy)[i, j, k] - Dy_forward(gx)[i, j, k]]
+
+curl_back(g)[i,j,k] = [Dy_back(gz)[i, j, k] - Dz_back(gy)[i, j, k],
+                       Dz_back(gx)[i, j, k] - Dx_back(gz)[i, j, k],
+                       Dx_back(gy)[i, j, k] - Dy_back(gx)[i, j, k]]
+

For example, consider the forward curl, at (m, n, p), of a +back-vector field g, defined on a grid containing (m + 1/2, +n + 1/2, p + 1/2). The curl will be a fore-vector, so its z-component +will be defined at (m, n, p + 1/2). Take the nearest x- and y-components +of g in the xy plane where the curl’s z-component is +located; these are

+
[curl components]
+(m,       n + 1/2, p + 1/2) : x-component of back-vector at (m + 1/2, n + 1/2, p + 1/2)
+(m + 1,   n + 1/2, p + 1/2) : x-component of back-vector at (m + 3/2, n + 1/2, p + 1/2)
+(m + 1/2, n      , p + 1/2) : y-component of back-vector at (m + 1/2, n + 1/2, p + 1/2)
+(m + 1/2, n + 1  , p + 1/2) : y-component of back-vector at (m + 1/2, n + 3/2, p + 1/2)
+

These four xy-components can be used to form a loop around the curl’s +z-component; its magnitude and sign is set by their loop-oriented sum +(i.e. two have their signs flipped to complete the loop).

+
[figure: z-component of curl]
+                          :             |
+    z y                   :    ^^       |
+    |/_x                  :....||.<.....|  (m+1, n+1, p+1/2)
+                          /    ||      /
+                       | v     ||   | ^
+                       |/           |/
+         (m, n, p+1/2) |_____>______|  (m+1, n, p+1/2)
+

Maxwell’s Equations

+

If we discretize both space (m,n,p) and time (l), Maxwell’s equations +become

+

\begin{aligned} + \tilde{\nabla} \times \tilde{E}_{l,\vec{r}} &= -\tilde{\partial}_t +\hat{B}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}} + - +\hat{M}_{l, \vec{r} + \frac{1}{2}} \\ + \hat{\nabla} \times \hat{H}_{l-\frac{1}{2},\vec{r} + \frac{1}{2}} +&= \hat{\partial}_t \tilde{D}_{l, \vec{r}} + + +\tilde{J}_{l-\frac{1}{2},\vec{r}} \\ + \tilde{\nabla} \cdot \hat{B}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}} +&= 0 \\ + \hat{\nabla} \cdot \tilde{D}_{l,\vec{r}} &= \rho_{l,\vec{r}} +\end{aligned}

+

with

+

\begin{aligned} + \hat{B}_{\vec{r}} &= \mu_{\vec{r} + \frac{1}{2}} \cdot +\hat{H}_{\vec{r} + \frac{1}{2}} \\ + \tilde{D}_{\vec{r}} &= \epsilon_{\vec{r}} \cdot +\tilde{E}_{\vec{r}} +\end{aligned}

+

where the spatial subscripts are abbreviated as \vec{r} = (m, n, p) and \vec{r} + +\frac{1}{2} = (m + \frac{1}{2}, n + \frac{1}{2}, p + \frac{1}{2}), +\tilde{E} and \hat{H} are the +electric and magnetic fields, \tilde{J} and \hat{M} are the electric and magnetic current +distributions, and \epsilon and \mu are the dielectric permittivity and magnetic +permeability.

+

The above is Yee’s algorithm, written in a form analogous to +Maxwell’s equations. The time derivatives can be expanded to form the +update equations:

+
[code: Maxwell's equations updates]
+H[i, j, k] -= dt * (curl_forward(E)[i, j, k] + M[t, i, j, k]) /      mu[i, j, k]
+E[i, j, k] += dt * (curl_back(   H)[i, j, k] + J[t, i, j, k]) / epsilon[i, j, k]
+

Note that the E-field fore-vector and H-field back-vector are offset +by a half-cell, resulting in distinct locations for all six E- and +H-field components:

+
[figure: Field components]
+
+        (m - 1/2,=> ____________Hx__________[H] <= r + 1/2 = (m + 1/2,
+         n + 1/2,  /:           /:          /|                n + 1/2,
+   z y   p + 1/2) / :          / :         / |                p + 1/2)
+   |/_x          /  :         /  :        /  |
+                /   :       Ez__________Hy   |      Locations of the E- and
+               /    :        :   :      /|   |      H-field components for the
+ (m - 1/2,    /     :        :  Ey...../.|..Hz      [E] fore-vector at r = (m,n,p)
+  n - 1/2, =>/________________________/  |  /|      (the large cube's center)
+  p + 1/2)   |      :        : /      |  | / |      and [H] back-vector at r + 1/2
+             |      :        :/       |  |/  |      (the top right corner)
+             |      :       [E].......|.Ex   |
+             |      :.................|......| <= (m + 1/2, n + 1/2, p + 1/2)
+             |     /                  |     /
+             |    /                   |    /
+             |   /                    |   /         This is the Yee discretization
+             |  /                     |  /          scheme ("Yee cell").
+r - 1/2 =    | /                      | /
+ (m - 1/2,   |/                       |/
+  n - 1/2,=> |________________________| <= (m + 1/2, n - 1/2, p - 1/2)
+  p - 1/2)
+

Each component forms its own grid, offset from the others:

+
[figure: E-fields for adjacent cells]
+
+              H1__________Hx0_________H0
+  z y        /:                       /|
+  |/_x      / :                      / |    This figure shows H back-vector locations
+           /  :                     /  |    H0, H1, etc. and their associated components
+         Hy1  :                   Hy0  |    H0 = (Hx0, Hy0, Hz0) etc.
+         /    :                   /    |
+        /    Hz1                 /     Hz0
+       H2___________Hx3_________H3     |    The equivalent drawing for E would have
+       |      :                 |      |    fore-vectors located at the cube's
+       |      :                 |      |    center (and the centers of adjacent cubes),
+       |      :                 |      |    with components on the cube's faces.
+       |      H5..........Hx4...|......H4
+       |     /                  |     /
+      Hz2   /                  Hz2   /
+       |   /                    |   /
+       | Hy6                    | Hy4
+       | /                      | /
+       |/                       |/
+       H6__________Hx7__________H7
+

The divergence equations can be derived by taking the divergence of +the curl equations and combining them with charge continuity, \hat{\nabla} \cdot \tilde{J} + \hat{\partial}_t \rho += 0 implying that the discrete Maxwell’s equations do not produce +spurious charges.

+

Wave Equation

+

Taking the backward curl of the \tilde{\nabla} \times +\tilde{E} equation and replacing the resulting \hat{\nabla} \times \hat{H} term using its respective +equation, and setting \hat{M} to zero, we can form +the discrete wave equation:

+

+ \begin{aligned} + \tilde{\nabla} \times \tilde{E}_{l,\vec{r}} &= + -\tilde{\partial}_t \hat{B}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}} + - \hat{M}_{l-1, \vec{r} + \frac{1}{2}} \\ + \mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times +\tilde{E}_{l,\vec{r}} &= + -\tilde{\partial}_t \hat{H}_{l-\frac{1}{2}, \vec{r} + +\frac{1}{2}} \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot +\tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) &= + \hat{\nabla} \times (-\tilde{\partial}_t \hat{H}_{l-\frac{1}{2}, +\vec{r} + \frac{1}{2}}) \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot +\tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) &= + -\tilde{\partial}_t \hat{\nabla} \times \hat{H}_{l-\frac{1}{2}, +\vec{r} + \frac{1}{2}} \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot +\tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) &= + -\tilde{\partial}_t \hat{\partial}_t \epsilon_{\vec{r}} \tilde{E}_{l, +\vec{r}} + \hat{\partial}_t \tilde{J}_{l-\frac{1}{2},\vec{r}} \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot +\tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) + + \tilde{\partial}_t \hat{\partial}_t \epsilon_{\vec{r}} +\cdot \tilde{E}_{l, \vec{r}} + &= \tilde{\partial}_t \tilde{J}_{l - \frac{1}{2}, +\vec{r}} + \end{aligned} +

+

Frequency Domain

+

We can substitute in a time-harmonic fields

+

+\begin{aligned} +\tilde{E}_{l, \vec{r}} &= \tilde{E}_{\vec{r}} e^{-\imath \omega l +\Delta_t} \\ +\tilde{J}_{l, \vec{r}} &= \tilde{J}_{\vec{r}} e^{-\imath \omega (l - +\frac{1}{2}) \Delta_t} +\end{aligned} +

+

resulting in

+

+\begin{aligned} +\tilde{\partial}_t &\Rightarrow (e^{ \imath \omega \Delta_t} - 1) / +\Delta_t = \frac{-2 \imath}{\Delta_t} \sin(\omega \Delta_t / 2) +e^{-\imath \omega \Delta_t / 2} = -\imath \Omega e^{-\imath \omega +\Delta_t / 2}\\ + \hat{\partial}_t &\Rightarrow (1 - e^{-\imath \omega \Delta_t}) / +\Delta_t = \frac{-2 \imath}{\Delta_t} \sin(\omega \Delta_t / 2) e^{ +\imath \omega \Delta_t / 2} = -\imath \Omega e^{ \imath \omega \Delta_t +/ 2}\\ +\Omega &= 2 \sin(\omega \Delta_t / 2) / \Delta_t +\end{aligned} +

+

This gives the frequency-domain wave equation,

+

+\hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot +\tilde{\nabla} \times \tilde{E}_{\vec{r}}) + -\Omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} = -\imath +\Omega \tilde{J}_{\vec{r}} e^{\imath \omega \Delta_t / 2} \\ +

+

Plane Waves And Dispersion +Relation

+

With uniform material distribution and no sources

+

+\begin{aligned} +\mu_{\vec{r} + \frac{1}{2}} &= \mu \\ +\epsilon_{\vec{r}} &= \epsilon \\ +\tilde{J}_{\vec{r}} &= 0 \\ +\end{aligned} +

+

the frequency domain wave equation simplifies to

+

\hat{\nabla} \times \tilde{\nabla} \times +\tilde{E}_{\vec{r}} - \Omega^2 \epsilon \mu \tilde{E}_{\vec{r}} = 0 +

+

Since \hat{\nabla} \cdot \tilde{E}_{\vec{r}} = 0, +we can simplify

+

+\begin{aligned} +\hat{\nabla} \times \tilde{\nabla} \times \tilde{E}_{\vec{r}} + &= \tilde{\nabla}(\hat{\nabla} \cdot \tilde{E}_{\vec{r}}) - +\hat{\nabla} \cdot \tilde{\nabla} \tilde{E}_{\vec{r}} \\ + &= - \hat{\nabla} \cdot \tilde{\nabla} \tilde{E}_{\vec{r}} \\ + &= - \tilde{\nabla}^2 \tilde{E}_{\vec{r}} +\end{aligned} +

+

and we get

+

\tilde{\nabla}^2 \tilde{E}_{\vec{r}} + +\Omega^2 \epsilon \mu \tilde{E}_{\vec{r}} = 0

+

We can convert this to three scalar-wave equations of the form

+

(\tilde{\nabla}^2 + K^2) \phi_{\vec{r}} = 0 +

+

with K^2 = \Omega^2 \mu \epsilon. Now we let

+

\phi_{\vec{r}} = A e^{\imath (k_x m \Delta_x ++ k_y n \Delta_y + k_z p \Delta_z)}

+

resulting in

+

+\begin{aligned} +\tilde{\partial}_x &\Rightarrow (e^{ \imath k_x \Delta_x} - 1) / +\Delta_t = \frac{-2 \imath}{\Delta_x} \sin(k_x \Delta_x / 2) e^{ \imath +k_x \Delta_x / 2} = \imath K_x e^{ \imath k_x \Delta_x / 2}\\ + \hat{\partial}_x &\Rightarrow (1 - e^{-\imath k_x \Delta_x}) / +\Delta_t = \frac{-2 \imath}{\Delta_x} \sin(k_x \Delta_x / 2) e^{-\imath +k_x \Delta_x / 2} = \imath K_x e^{-\imath k_x \Delta_x / 2}\\ +K_x &= 2 \sin(k_x \Delta_x / 2) / \Delta_x \\ +\end{aligned} +

+

with similar expressions for the y and z dimnsions (and K_y, K_z).

+

This implies

+

+ \tilde{\nabla}^2 = -(K_x^2 + K_y^2 + K_z^2) \phi_{\vec{r}} \\ + K_x^2 + K_y^2 + K_z^2 = \Omega^2 \mu \epsilon = \Omega^2 / c^2 +

+

where c = \sqrt{\mu \epsilon}.

+

Assuming real (k_x, k_y, k_z), \omega will be +real only if

+

c^2 \Delta_t^2 = \frac{\Delta_t^2}{\mu +\epsilon} < 1/(\frac{1}{\Delta_x^2} + \frac{1}{\Delta_y^2} + +\frac{1}{\Delta_z^2})

+

If \Delta_x = \Delta_y = \Delta_z, this +simplifies to c \Delta_t < \Delta_x / \sqrt{3}. +This last form can be interpreted as enforcing causality; the distance +that light travels in one timestep (i.e., c +\Delta_t) must be less than the diagonal of the smallest cell ( \Delta_x / \sqrt{3} when on a uniform cubic grid).

+

Grid description

+

As described in the section on scalar discrete derivatives above, +cell widths (dx[i], dy[j], dz[k]) +along each axis can be arbitrary and independently defined. Moreover, +all field components are actually defined at “derived” or “dual” +positions, in-between the “base” grid points on one or more axes.

+

To get a better sense of how this works, let’s start by drawing a +grid with uniform dy and dz and nonuniform +dx. We will only draw one cell in the y and z dimensions to +make the illustration simpler; we need at least two cells in the x +dimension to demonstrate how nonuniform dx affects the +various components.

+

Place the E fore-vectors at integer indices r = (m, n, +p) and the H back-vectors at fractional indices r + +\frac{1}{2} = (m + \frac{1}{2}, n + \frac{1}{2}, +p + \frac{1}{2}). Remember that these are indices and not +coordinates; they can correspond to arbitrary (monotonically increasing) +coordinates depending on the cell widths.

+

Draw lines to denote the planes on which the H components and +back-vectors are defined. For simplicity, don’t draw the equivalent +planes for the E components and fore-vectors, except as necessary to +show their locations – it’s easiest to just connect them to their +associated H-equivalents.

+

The result looks something like this:

+
[figure: Component centers]
+                                                             p=
+          [H]__________Hx___________[H]_____Hx______[H]   __ +1/2
+  z y     /:           /:           /:      /:      /|     |      |
+  |/_x   / :          / :          / :     / :     / |     |      |
+        /  :         /  :         /  :    /  :    /  |     |      |
+      Hy   :       Ez...........Hy   :  Ez......Hy   |     |      |
+      /:   :        :   :       /:   :   :   :  /|   |     |      |
+     / :  Hz        :  Ey....../.:..Hz   :  Ey./.|..Hz    __ 0    | dz[0]
+    /  :  /:        :  /      /  :  /:   :  / /  |  /|     |      |
+   /_________________________/_______________/   | / |     |      |
+   |   :/  :        :/       |   :/  :   :/  |   |/  |     |      |
+   |  Ex   :       [E].......|..Ex   :  [E]..|..Ex   |     |      |
+   |       :                 |       :       |       |     |      |
+   |      [H]..........Hx....|......[H].....H|x.....[H]   __ --------- (n=+1/2, p=-1/2)
+   |      /                  |      /        |      /     /       /
+  Hz     /                  Hz     /        Hz     /     /       /
+   |    /                    |    /          |    /     /       /
+   |  Hy                     |  Hy           |  Hy    __ 0     / dy[0]
+   |  /                      |  /            |  /     /       /
+   | /                       | /             | /     /       /
+   |/                        |/              |/     /       /
+  [H]__________Hx___________[H]_____Hx______[H]   __ -1/2  /
+                                                       =n
+   |------------|------------|-------|-------|
+ -1/2           0          +1/2     +1     +3/2 = m
+
+    ------------------------- ----------------
+              dx[0]                  dx[1]
+
+  Part of a nonuniform "base grid", with labels specifying
+  positions of the various field components. [E] fore-vectors
+  are at the cell centers, and [H] back-vectors are at the
+  vertices. H components along the near (-y) top (+z) edge
+  have been omitted to make the insides of the cubes easier
+  to visualize.
+

The above figure shows where all the components are located; however, +it is also useful to show what volumes those components correspond to. +Consider the Ex component at m = +1/2: it is shifted in the +x-direction by a half-cell from the E fore-vector at m = 0 +(labeled [E] in the figure). It corresponds to a volume +between m = 0 and m = +1 (the other dimensions +are not shifted, i.e. they are still bounded by +n, p = +-1/2). (See figure below). Since m is +an index and not an x-coordinate, the Ex component is not necessarily at +the center of the volume it represents, and the x-length of its volume +is the derived quantity dx'[0] = (dx[0] + dx[1]) / 2 rather +than the base dx. (See also Scalar derivatives and +cell shifts).

+
[figure: Ex volumes]
+                                                             p=
+           <_________________________________________>   __ +1/2
+  z y     <<           /:           /       /:      >>    |      |
+  |/_x   < <          / :          /       / :     > >    |      |
+        <  <         /  :         /       /  :    >  >    |      |
+       <   <        /   :        /       /   :   >   >    |      |
+      <:   <       /    :        :      /    :  >:   >    |      |
+     < :   <      /     :        :     /     : > :   >   __ 0    | dz[0]
+    <  :   <     /      :        :    /      :>  :   >    |      |
+   <____________/____________________/_______>   :   >    |      |
+   <   :   <    |       :        :   |       >   :   >    |      |
+   <  Ex   <    |       :       Ex   |       >  Ex   >    |      |
+   <   :   <    |       :        :   |       >   :   >    |      |
+   <   :   <....|.......:........:...|.......>...:...>   __ --------- (n=+1/2, p=-1/2)
+   <   :  <     |      /         :  /|      />   :  >    /       /
+   <   : <      |     /          : / |     / >   : >    /       /
+   <   :<       |    /           :/  |    /  >   :>    /       /
+   <   <        |   /            :   |   /   >   >    _ 0     / dy[0]
+   <  <         |  /                 |  /    >  >    /       /
+   < <          | /                  | /     > >    /       /
+   <<           |/                   |/      >>    /       /
+   <____________|____________________|_______>   __ -1/2  /
+                                                     =n
+   |------------|------------|-------|-------|
+ -1/2           0          +1/2      +1    +3/2 = m
+
+   ~------------ -------------------- -------~
+     dx'[-1]          dx'[0]           dx'[1]
+
+ The Ex values are positioned on the x-faces of the base
+ grid. They represent the Ex field in volumes shifted by
+ a half-cell in the x-dimension, as shown here. Only the
+ center cell (with width dx'[0]) is fully shown; the
+ other two are truncated (shown using >< markers).
+
+ Note that the Ex positions are the in the same positions
+ as the previous figure; only the cell boundaries have moved.
+ Also note that the points at which Ex is defined are not
+ necessarily centered in the volumes they represent; non-
+ uniform cell sizes result in off-center volumes like the
+ center cell here.
+

The next figure shows the volumes corresponding to the Hy components, +which are shifted in two dimensions (x and z) compared to the base +grid.

+
[figure: Hy volumes]
+                                                             p=
+  z y     mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm   __ +1/2  s
+  |/_x   <<           m:                    m:      >>    |       |
+        < <          m :                   m :     > >    |       | dz'[1]
+       <  <         m  :                  m  :    >  >    |       |
+     Hy........... m........Hy...........m......Hy   >    |       |
+     <    <       m    :                m    :  >    >    |       |
+    <     ______ m_____:_______________m_____:_>______   __ 0
+   <      <     m     /:              m     / >      >    |       |
+  mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm       >    |       |
+  <       <    |    /  :             |    /  >       >    |       | dz'[0]
+  <       <    |   /   :             |   /   >       >    |       |
+  <       <    |  /    :             |  /    >       >    |       |
+  <       wwwww|w/wwwwwwwwwwwwwwwwwww|w/wwwww>wwwwwwww   __       s
+  <      <     |/     w              |/     w>      >    /         /
+  _____________|_____________________|________     >    /         /
+  <    <       |    w                |    w  >    >    /         /
+  <  Hy........|...w........Hy.......|...w...>..Hy    _ 0       / dy[0]
+  < <          |  w                  |  w    >  >    /         /
+  <<           | w                   | w     > >    /         /
+  <            |w                    |w      >>    /         /
+  wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww   __ -1/2    /
+
+  |------------|------------|--------|-------|
+-1/2           0          +1/2      +1     +3/2 = m
+
+  ~------------ --------------------- -------~
+     dx'[-1]            dx'[0]         dx'[1]
+
+ The Hy values are positioned on the y-edges of the base
+ grid. Again here, the 'Hy' labels represent the same points
+ as in the basic grid figure above; the edges have shifted
+ by a half-cell along the x- and z-axes.
+
+ The grid lines _|:/ are edges of the area represented by
+ each Hy value, and the lines drawn using <m>.w represent
+ edges where a cell's faces extend beyond the drawn area
+ (i.e. where the drawing is truncated in the x- or z-
+ directions).
+

Datastructure: dx_lists_t

+

In this documentation, the E fore-vectors are placed on the base +grid. An equivalent formulation could place the H back-vectors on the +base grid instead. However, in the case of a non-uniform grid, the +operation to get from the “base” cell widths to “derived” ones is not +its own inverse.

+

The base grid’s cell sizes could be fully described by a list of +three 1D arrays, specifying the cell widths along all three axes:

+
[dx, dy, dz] = [[dx[0], dx[1], ...], [dy[0], ...], [dz[0], ...]]
+

Note that this is a list-of-arrays rather than a 2D array, as the +simulation domain may have a different number of cells along each +axis.

+

Knowing the base grid’s cell widths and the boundary conditions +(periodic unless otherwise noted) is enough information to calculate the +cell widths dx', dy', and dz' for +the derived grids.

+

However, since most operations are trivially generalized to allow +either E or H to be defined on the base grid, they are written to take +the a full set of base and derived cell widths, distinguished by which +field they apply to rather than their “base” or “derived” status. This +removes the need for each function to generate the derived widths, and +makes the “base” vs “derived” distinction unnecessary in the code.

+

The resulting data structure containing all the cell widths takes the +form of a list-of-lists-of-arrays. The first list-of-arrays provides the +cell widths for the E-field fore-vectors, while the second +list-of-arrays does the same for the H-field back-vectors:

+
 [[[dx_e[0], dx_e[1], ...], [dy_e[0], ...], [dz_e[0], ...]],
+  [[dx_h[0], dx_h[1], ...], [dy_h[0], ...], [dz_h[0], ...]]]
+

where dx_e[0] is the x-width of the m=0 +cells, as used when calculating dE/dx, and dy_h[0] is the +y-width of the n=0 cells, as used when calculating dH/dy, +etc.

+

Permittivity and +Permeability

+

Since each vector component of E and H is defined in a different +location and represents a different volume, the value of the +spatially-discrete epsilon and mu can also be +different for all three field components, even when representing a +simple planar interface between two isotropic materials.

+

As a result, epsilon and mu are taken to +have the same dimensions as the field, and composed of the three +diagonal tensor components:

+
[equations: epsilon_and_mu]
+epsilon = [epsilon_xx, epsilon_yy, epsilon_zz]
+mu = [mu_xx, mu_yy, mu_zz]
+

or

+

+\epsilon = \begin{bmatrix} \epsilon_{xx} & 0 & 0 \\ + 0 & \epsilon_{yy} & 0 \\ + 0 & 0 & \epsilon_{zz} \end{bmatrix} + +\mu = \begin{bmatrix} \mu_{xx} & 0 & 0 \\ + 0 & \mu_{yy} & 0 \\ + 0 & 0 & \mu_{zz} \end{bmatrix} +

+

where the off-diagonal terms (e.g. epsilon_xy) are +assumed to be zero.

+

High-accuracy volumetric integration of shapes on multiple grids can +be performed by the gridlock module.

+

The values of the vacuum permittivity and permability effectively +become scaling factors that appear in several locations (e.g. between +the E and H fields). In order to limit floating-point inaccuracy and +simplify calculations, they are often set to 1 and relative +permittivities and permeabilities are used in their places; the true +values can be multiplied back in after the simulation is complete if +non- normalized results are needed.

+

Sub-modules

+ +
+

Module +meanas.fdmath.functional

+

Math functions for finite difference simulations

+

Basic discrete calculus etc.

+

Functions

+

Function +curl_back

+
+

def curl_back(dx_h: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]] | None = None) -> collections.abc.Callable[[~TT], ~TT]

+
+

Create a function which takes the backward curl of a field.

+

Args —–= dx_h : Lists of cell sizes for +all axes [[dx_0, dx_1, …], [dy_0, dy_1, …], …].

+

Returns —–= Function f for taking the discrete backward +curl of a field, f(H) -> curlH = \nabla_b +\times H

+

Function +curl_back_parts

+
+

def curl_back_parts(dx_h: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]] | None = None) -> collections.abc.Callable

+
+

Function +curl_forward

+
+

def curl_forward(dx_e: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]] | None = None) -> collections.abc.Callable[[~TT], ~TT]

+
+

Curl operator for use with the E field.

+

Args —–= dx_e : Lists of cell sizes for +all axes [[dx_0, dx_1, …], [dy_0, dy_1, …], …].

+

Returns —–= Function f for taking the discrete forward +curl of a field, f(E) -> curlE = \nabla_f +\times E

+

Function +curl_forward_parts

+
+

def curl_forward_parts(dx_e: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]] | None = None) -> collections.abc.Callable

+
+

Function +deriv_back

+
+

def deriv_back(dx_h: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]] | None = None) -> tuple[collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]]

+
+

Utility operators for taking discretized derivatives (forward +variant).

+

Args —–= dx_h : Lists of cell sizes for +all axes [[dx_0, dx_1, …], [dy_0, dy_1, …], …].

+

Returns —–= List of functions for taking forward derivatives along +each axis.

+

Function +deriv_forward

+
+

def deriv_forward(dx_e: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]] | None = None) -> tuple[collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]]

+
+

Utility operators for taking discretized derivatives (backward +variant).

+

Args —–= dx_e : Lists of cell sizes for +all axes [[dx_0, dx_1, …], [dy_0, dy_1, …], …].

+

Returns —–= List of functions for taking forward derivatives along +each axis.

+
+

Module +meanas.fdmath.operators

+

Matrix operators for finite difference simulations

+

Basic discrete calculus etc.

+

Functions

+

Function +avg_back

+
+

def avg_back(axis: int, shape: collections.abc.Sequence[int]) -> scipy.sparse._matrix.spmatrix

+
+

Backward average operator (x4 = (x4 + x3) / 2)

+

Args —–= axis : Axis to average along +(x=0, y=1, z=2)

+
+
shape
+
+Shape of the grid to average +
+
+

Returns —–= Sparse matrix for backward average operation.

+

Function +avg_forward

+
+

def avg_forward(axis: int, shape: collections.abc.Sequence[int]) -> scipy.sparse._matrix.spmatrix

+
+

Forward average operator (x4 = (x4 + x5) / 2)

+

Args —–= axis : Axis to average along +(x=0, y=1, z=2)

+
+
shape
+
+Shape of the grid to average +
+
+

Returns —–= Sparse matrix for forward average operation.

+

Function cross

+
+

def cross(B: collections.abc.Sequence[scipy.sparse._matrix.spmatrix]) -> scipy.sparse._matrix.spmatrix

+
+

Cross product operator

+

Args —–= B : List [Bx, By, +Bz] of sparse matrices corresponding to the x, y, z portions of +the operator on the left side of the cross product.

+

Returns —–= Sparse matrix corresponding to (B x), where x is the +cross product.

+

Function +curl_back

+
+

def curl_back(dx_h: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]) -> scipy.sparse._matrix.spmatrix

+
+

Curl operator for use with the H field.

+

Args —–= dx_h : Lists of cell sizes for +all axes [[dx_0, dx_1, …], [dy_0, dy_1, …], …].

+

Returns —–= Sparse matrix for taking the discretized curl of the +H-field

+

Function +curl_forward

+
+

def curl_forward(dx_e: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]) -> scipy.sparse._matrix.spmatrix

+
+

Curl operator for use with the E field.

+

Args —–= dx_e : Lists of cell sizes for +all axes [[dx_0, dx_1, …], [dy_0, dy_1, …], …].

+

Returns —–= Sparse matrix for taking the discretized curl of the +E-field

+

Function +deriv_back

+
+

def deriv_back(dx_h: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]) -> list[scipy.sparse._matrix.spmatrix]

+
+

Utility operators for taking discretized derivatives (backward +variant).

+

Args —–= dx_h : Lists of cell sizes for +all axes [[dx_0, dx_1, …], [dy_0, dy_1, …], …].

+

Returns —–= List of operators for taking forward derivatives along +each axis.

+

Function +deriv_forward

+
+

def deriv_forward(dx_e: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]) -> list[scipy.sparse._matrix.spmatrix]

+
+

Utility operators for taking discretized derivatives (forward +variant).

+

Args —–= dx_e : Lists of cell sizes for +all axes [[dx_0, dx_1, …], [dy_0, dy_1, …], …].

+

Returns —–= List of operators for taking forward derivatives along +each axis.

+

Function +shift_circ

+
+

def shift_circ(axis: int, shape: collections.abc.Sequence[int], shift_distance: int = 1) -> scipy.sparse._matrix.spmatrix

+
+

Utility operator for performing a circular shift along a specified +axis by a specified number of elements.

+

Args —–= axis : Axis to shift along. +x=0, y=1, z=2

+
+
shape
+
+Shape of the grid being shifted +
+
shift_distance
+
+Number of cells to shift by. May be negative. Default 1. +
+
+

Returns —–= Sparse matrix for performing the circular shift.

+

Function +shift_with_mirror

+
+

def shift_with_mirror(axis: int, shape: collections.abc.Sequence[int], shift_distance: int = 1) -> scipy.sparse._matrix.spmatrix

+
+

Utility operator for performing an n-element shift along a specified +axis, with mirror boundary conditions applied to the cells beyond the +receding edge.

+

Args —–= axis : Axis to shift along. +x=0, y=1, z=2

+
+
shape
+
+Shape of the grid being shifted +
+
shift_distance
+
+Number of cells to shift by. May be negative. Default 1. +
+
+

Returns —–= Sparse matrix for performing the shift-with-mirror.

+

Function +vec_cross

+
+

def vec_cross(b: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]) -> scipy.sparse._matrix.spmatrix

+
+

Vector cross product operator

+

Args —–= b : Vector on the left side of +the cross product.

+

Returns —–= Sparse matrix corresponding to (b x), where x is the +cross product.

+
+

Module +meanas.fdmath.types

+

Types shared across multiple submodules

+

Variables

+

Variable +cfdfield_t

+

Complex vector field with shape (3, X, Y, Z) (e.g. [E_x, E_y, +E_z])

+

Variable +cfdfield_updater_t

+

Convenience type for functions which take and return an +cfdfield_t

+

Variable +dx_lists_mut

+

Mutable version of dx_lists_t

+

Variable +dx_lists_t

+

‘dxes’ datastructure which contains grid cell width information in +the following format:

+
[[[dx_e[0], dx_e[1], ...], [dy_e[0], ...], [dz_e[0], ...]],
+ [[dx_h[0], dx_h[1], ...], [dy_h[0], ...], [dz_h[0], ...]]]
+

where dx_e[0] is the x-width of the x=0 +cells, as used when calculating dE/dx, and dy_h[0] is the +y-width of the y=0 cells, as used when calculating dH/dy, +etc.

+

Variable +fdfield_t

+

Vector field with shape (3, X, Y, Z) (e.g. [E_x, E_y, +E_z])

+

Variable +fdfield_updater_t

+

Convenience type for functions which take and return an fdfield_t

+

Variable +vcfdfield_t

+

Linearized complex vector field (single vector of length +3XY*Z)

+

Variable +vfdfield_t

+

Linearized vector field (single vector of length 3XY*Z)

+
+

Module +meanas.fdmath.vectorization

+

Functions for moving between a vector field (list of 3 ndarrays, +[f_x, f_y, f_z]) and a 1D array representation of that +field [f_x0, f_x1, f_x2,… f_y0,… f_z0,…]. Vectorized +versions of the field use row-major (ie., C-style) ordering.

+

Functions

+

Function +unvec

+
+

def unvec(v: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]] | None, shape: collections.abc.Sequence[int], nvdim: int = 3) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]] | None

+
+

Perform the inverse of vec(): take a 1D ndarray and output an +nvdim-component field of form e.g. [f_x, f_y, +f_z] (nvdim=3) where each of f_* is a +len(shape)-dimensional ndarray.

+

Returns None if called with v=None.

+

Args —–= v : 1D ndarray representing a +vector field of shape shape (or None)

+
+
shape
+
+shape of the vector field +
+
nvdim
+
+Number of components in each vector +
+
+

Returns —–= [f_x, f_y, f_z] where each f_ +is a len(shape) dimensional ndarray (or +None)

+

Function vec

+
+

def vec(f: Union[numpy.ndarray[Any, numpy.dtype[numpy.floating]], numpy.ndarray[Any, numpy.dtype[numpy.complexfloating]], collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]], ForwardRef(None)]) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]] | None

+
+

Create a 1D ndarray from a vector field which spans a 1-3D +region.

+

Returns None if called with f=None.

+

Args —–= f : A vector field, +e.g. [f_x, f_y, f_z] where each f_ component +is a 1- to 3-D ndarray (f_* should all be the same size). +Doesn’t fail with f=None.

+

Returns —–= 1D ndarray containing the linearized field (or +None)

+
+

Module meanas.fdtd

+

Utilities for running finite-difference time-domain (FDTD) +simulations

+

See the discussion of Maxwell's Equations in meanas.fdmath for basic mathematical +background.

+

Timestep

+

From the discussion of “Plane waves and the Dispersion relation” in +meanas.fdmath, we have

+

c^2 \Delta_t^2 = \frac{\Delta_t^2}{\mu +\epsilon} < 1/(\frac{1}{\Delta_x^2} + \frac{1}{\Delta_y^2} + +\frac{1}{\Delta_z^2})

+

or, if \Delta_x = \Delta_y = \Delta_z, then c \Delta_t < \frac{\Delta_x}{\sqrt{3}}.

+

Based on this, we can set

+
dt = sqrt(mu.min() * epsilon.min()) / sqrt(1/dx_min**2 + 1/dy_min**2 + 1/dz_min**2)
+

The dx_min, dy_min, dz_min +should be the minimum value across both the base and derived grids.

+

Poynting Vector and +Energy Conservation

+

Let

+

\begin{aligned} + \tilde{S}_{l, l', \vec{r}} &=& &\tilde{E}_{l, \vec{r}} +\otimes \hat{H}_{l', \vec{r} + \frac{1}{2}} \\ + &=& &\vec{x} (\tilde{E}^y_{l,m+1,n,p} +\hat{H}^z_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^z_{l,m+1,n,p} +\hat{H}^y_{l', \vec{r} + \frac{1}{2}}) \\ + & &+ &\vec{y} (\tilde{E}^z_{l,m,n+1,p} +\hat{H}^x_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^x_{l,m,n+1,p} +\hat{H}^z_{l', \vec{r} + \frac{1}{2}}) \\ + & &+ &\vec{z} (\tilde{E}^x_{l,m,n,p+1} +\hat{H}^y_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^y_{l,m,n,p+1} +\hat{H}^z_{l', \vec{r} + \frac{1}{2}}) + \end{aligned} +

+

where \vec{r} = (m, n, p) and \otimes is a modified cross product in which the \tilde{E} terms are shifted as indicated.

+

By taking the divergence and rearranging terms, we can show that

+

+ \begin{aligned} + \hat{\nabla} \cdot \tilde{S}_{l, l', \vec{r}} + &= \hat{\nabla} \cdot (\tilde{E}_{l, \vec{r}} \otimes +\hat{H}_{l', \vec{r} + \frac{1}{2}}) \\ + &= \hat{H}_{l', \vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} +\times \tilde{E}_{l, \vec{r}} - + \tilde{E}_{l, \vec{r}} \cdot \hat{\nabla} \times \hat{H}_{l', +\vec{r} + \frac{1}{2}} \\ + &= \hat{H}_{l', \vec{r} + \frac{1}{2}} \cdot + (-\tilde{\partial}_t \mu_{\vec{r} + \frac{1}{2}} \hat{H}_{l - +\frac{1}{2}, \vec{r} + \frac{1}{2}} - + \hat{M}_{l, \vec{r} + \frac{1}{2}}) - + \tilde{E}_{l, \vec{r}} \cdot (\hat{\partial}_t +\tilde{\epsilon}_{\vec{r}} \tilde{E}_{l'+\frac{1}{2}, \vec{r}} + + \tilde{J}_{l', \vec{r}}) \\ + &= \hat{H}_{l'} \cdot (-\mu / \Delta_t)(\hat{H}_{l + \frac{1}{2}} +- \hat{H}_{l - \frac{1}{2}}) - + \tilde{E}_l \cdot (\epsilon / \Delta_t +)(\tilde{E}_{l'+\frac{1}{2}} - \tilde{E}_{l'-\frac{1}{2}}) + - \hat{H}_{l'} \cdot \hat{M}_{l} - \tilde{E}_l \cdot +\tilde{J}_{l'} \\ + \end{aligned} +

+

where in the last line the spatial subscripts have been dropped to +emphasize the time subscripts l, l', i.e.

+

+ \begin{aligned} + \tilde{E}_l &= \tilde{E}_{l, \vec{r}} \\ + \hat{H}_l &= \tilde{H}_{l, \vec{r} + \frac{1}{2}} \\ + \tilde{\epsilon} &= \tilde{\epsilon}_{\vec{r}} \\ + \end{aligned} +

+

etc. For l' = l + \frac{1}{2} we get

+

+ \begin{aligned} + \hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}} + &= \hat{H}_{l + \frac{1}{2}} \cdot + (-\mu / \Delta_t)(\hat{H}_{l + \frac{1}{2}} - \hat{H}_{l - +\frac{1}{2}}) - + \tilde{E}_l \cdot (\epsilon / \Delta_t)(\tilde{E}_{l+1} - +\tilde{E}_l) + - \hat{H}_{l'} \cdot \hat{M}_l - \tilde{E}_l \cdot \tilde{J}_{l + +\frac{1}{2}} \\ + &= (-\mu / \Delta_t)(\hat{H}^2_{l + \frac{1}{2}} - \hat{H}_{l + +\frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}}) - + (\epsilon / \Delta_t)(\tilde{E}_{l+1} \cdot \tilde{E}_l - +\tilde{E}^2_l) + - \hat{H}_{l'} \cdot \hat{M}_l - \tilde{E}_l \cdot \tilde{J}_{l + +\frac{1}{2}} \\ + &= -(\mu \hat{H}^2_{l + \frac{1}{2}} + +\epsilon \tilde{E}_{l+1} \cdot \tilde{E}_l) / \Delta_t \\ + +(\mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}} + +\epsilon \tilde{E}^2_l) / \Delta_t \\ + - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\ + \end{aligned} +

+

and for l' = l - \frac{1}{2},

+

+ \begin{aligned} + \hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}} + &= (\mu \hat{H}^2_{l - \frac{1}{2}} + +\epsilon \tilde{E}_{l-1} \cdot \tilde{E}_l) / \Delta_t \\ + -(\mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}} + +\epsilon \tilde{E}^2_l) / \Delta_t \\ + - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\ + \end{aligned} +

+

These two results form the discrete time-domain analogue to +Poynting’s theorem. They hint at the expressions for the energy, which +can be calculated at the same time-index as either the E or H field:

+

+\begin{aligned} +U_l &= \epsilon \tilde{E}^2_l + \mu \hat{H}_{l + \frac{1}{2}} \cdot +\hat{H}_{l - \frac{1}{2}} \\ +U_{l + \frac{1}{2}} &= \epsilon \tilde{E}_l \cdot \tilde{E}_{l + 1} ++ \mu \hat{H}^2_{l + \frac{1}{2}} \\ +\end{aligned} +

+

Rewriting the Poynting theorem in terms of the energy +expressions,

+

+ \begin{aligned} + (U_{l+\frac{1}{2}} - U_l) / \Delta_t + &= -\hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}} \\ + - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\ + (U_l - U_{l-\frac{1}{2}}) / \Delta_t + &= -\hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}} \\ + - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\ +\end{aligned} +

+

This result is exact and should practically hold to within numerical +precision. No time- or spatial-averaging is necessary.

+

Note that each value of J contributes to the +energy twice (i.e. once per field update) despite only causing the value +of E to change once (same for M +and H).

+

Sources

+

It is often useful to excite the simulation with an arbitrary +broadband pulse and then extract the frequency-domain response by +performing an on-the-fly Fourier transform of the time-domain +fields.

+

The Ricker wavelet (normalized second derivative of a Gaussian) is +commonly used for the pulse shape. It can be written

+

f_r(t) = (1 - \frac{1}{2} (\omega (t - +\tau))^2) e^{-(\frac{\omega (t - \tau)}{2})^2}

+

with \tau > \frac{2 * \pi}{\omega} as a +minimum delay to avoid a discontinuity at t=0 (assuming the source is +off for t<0 this gives \sim 10^{-3} error at +t=0).

+

Boundary conditions

+

TODO notes about boundaries / +PMLs

+

Sub-modules

+ +
+

Module meanas.fdtd.base

+

Basic FDTD field updates

+

Functions

+

Function maxwell_e

+
+

def maxwell_e(dt: float, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]

+
+

Build a function which performs a portion the time-domain E-field +update,

+
E += curl_back(H[t]) / epsilon
+

The full update should be

+
E += (curl_back(H[t]) + J) / epsilon
+

which requires an additional step of E += J / epsilon +which is not performed by the generated function.

+

See meanas.fdmath for +descriptions of

+ +

Also see the “Timestep” section of meanas.fdtd for a discussion of the +dt parameter.

+

Args —–= dt : Timestep. See meanas.fdtd for details.

+
+
dxes
+
+Grid description; see meanas.fdmath. +
+
+

Returns —–= Function +f(E_old, H_old, epsilon) -> E_new.

+

Function maxwell_h

+
+

def maxwell_h(dt: float, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]

+
+

Build a function which performs part of the time-domain H-field +update,

+
H -= curl_forward(E[t]) / mu
+

The full update should be

+
H -= (curl_forward(E[t]) + M) / mu
+

which requires an additional step of H -= M / mu which +is not performed by the generated function; this step can be omitted if +there is no magnetic current M.

+

See meanas.fdmath for +descriptions of

+ +

Also see the “Timestep” section of meanas.fdtd for a discussion of the +dt parameter.

+

Args —–= dt : Timestep. See meanas.fdtd for details.

+
+
dxes
+
+Grid description; see meanas.fdmath. +
+
+

Returns —–= Function +f(E_old, H_old, epsilon) -> E_new.

+
+

Module +meanas.fdtd.boundaries

+

Boundary conditions

+

#TODO conducting boundary documentation

+

Functions

+

Function +conducting_boundary

+
+

def conducting_boundary(direction: int, polarity: int) -> tuple[collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]]

+
+
+

Module meanas.fdtd.energy

+

Functions

+

Function +delta_energy_e2h

+
+

def delta_energy_e2h(dt: float, h0: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], e1: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], h2: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], e3: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]

+
+

Change in energy during the half-step from e1 to +h2.

+

This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2)

+

Args —–= h0 : E-field one half-timestep +before the start of the energy delta.

+
+
e1
+
+H-field at the start of the energy delta. +
+
h2
+
+E-field at the end of the energy delta (one half-timestep after +e1). +
+
e3
+
+H-field one half-timestep after the end of the energy delta. +
+
epsilon
+
+Dielectric constant distribution. +
+
mu
+
+Magnetic permeability distribution. +
+
dxes
+
+Grid description; see meanas.fdmath. +
+
+

Returns —–= Change in energy from the time of e1 to the +time of h2.

+

Function +delta_energy_h2e

+
+

def delta_energy_h2e(dt: float, e0: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], h1: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], e2: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], h3: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]

+
+

Change in energy during the half-step from h1 to +e2.

+

This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2)

+

Args —–= e0 : E-field one half-timestep +before the start of the energy delta.

+
+
h1
+
+H-field at the start of the energy delta. +
+
e2
+
+E-field at the end of the energy delta (one half-timestep after +h1). +
+
h3
+
+H-field one half-timestep after the end of the energy delta. +
+
epsilon
+
+Dielectric constant distribution. +
+
mu
+
+Magnetic permeability distribution. +
+
dxes
+
+Grid description; see meanas.fdmath. +
+
+

Returns —–= Change in energy from the time of h1 to the +time of e2.

+

Function +delta_energy_j

+
+

def delta_energy_j(j0: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], e1: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]

+
+

Calculate

+

Note that each value of J contributes to the +energy twice (i.e. once per field update) despite only causing the value +of E to change once (same for M +and H).

+

Function dxmul

+
+

def dxmul(ee: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], hh: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | float | None = None, mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | float | None = None, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]

+
+

Function +energy_estep

+
+

def energy_estep(h0: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], e1: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], h2: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]

+
+

Calculate energy U at the time of the provided E-field +e1.

+

TODO: Figure out what this means spatially.

+

Args —–= h0 : H-field one half-timestep +before the energy.

+
+
e1
+
+E-field (at the same timestep as the energy). +
+
h2
+
+H-field one half-timestep after the energy. +
+
epsilon
+
+Dielectric constant distribution. +
+
mu
+
+Magnetic permeability distribution. +
+
dxes
+
+Grid description; see meanas.fdmath. +
+
+

Returns —–= Energy, at the time of the E-field e1.

+

Function +energy_hstep

+
+

def energy_hstep(e0: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], h1: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], e2: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]

+
+

Calculate energy U at the time of the provided H-field +h1.

+

TODO: Figure out what this means spatially.

+

Args —–= e0 : E-field one half-timestep +before the energy.

+
+
h1
+
+H-field (at the same timestep as the energy). +
+
e2
+
+E-field one half-timestep after the energy. +
+
epsilon
+
+Dielectric constant distribution. +
+
mu
+
+Magnetic permeability distribution. +
+
dxes
+
+Grid description; see meanas.fdmath. +
+
+

Returns —–= Energy, at the time of the H-field h1.

+

Function poynting

+
+

def poynting(e: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], h: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]

+
+

Calculate the poynting vector S (S).

+

This is the energy transfer rate (amount of energy U per +dt transferred between adjacent cells) in each direction +that happens during the half-step bounded by the two provided +fields.

+

The returned vector field S is the energy flow across ++x, +y, and +z boundaries of the corresponding U cell. For +example,

+
    mx = numpy.roll(mask, -1, axis=0)
+    my = numpy.roll(mask, -1, axis=1)
+    mz = numpy.roll(mask, -1, axis=2)
+
+    u_hstep = fdtd.energy_hstep(e0=es[ii - 1], h1=hs[ii], e2=es[ii],     **args)
+    u_estep = fdtd.energy_estep(h0=hs[ii],     e1=es[ii], h2=hs[ii + 1], **args)
+    delta_j_B = fdtd.delta_energy_j(j0=js[ii], e1=es[ii], dxes=dxes)
+    du_half_h2e = u_estep - u_hstep - delta_j_B
+
+    s_h2e = -fdtd.poynting(e=es[ii], h=hs[ii], dxes=dxes) * dt
+    planes = [s_h2e[0, mask].sum(), -s_h2e[0, mx].sum(),
+              s_h2e[1, mask].sum(), -s_h2e[1, my].sum(),
+              s_h2e[2, mask].sum(), -s_h2e[2, mz].sum()]
+
+    assert_close(sum(planes), du_half_h2e[mask])
+

(see meanas.tests.test_fdtd.test_poynting_planes)

+

The full relationship is + \begin{aligned} + (U_{l+\frac{1}{2}} - U_l) / \Delta_t + &= -\hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}} \\ + - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\ + (U_l - U_{l-\frac{1}{2}}) / \Delta_t + &= -\hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}} \\ + - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\ +\end{aligned} +

+

These equalities are exact and should practically hold to within +numerical precision. No time- or spatial-averaging is necessary. (See +meanas.fdtd docs for +derivation.)

+

Args —–= e : E-field

+
+
h
+
+H-field (one half-timestep before or after e) +
+
dxes
+
+Grid description; see meanas.fdmath. +
+
+

Returns —–= s : Vector field. Components indicate the +energy transfer rate from the corresponding energy cell into its +x, +y, +and +z neighbors during the half-step from the time of the earlier input +field until the time of later input field.

+

Function +poynting_divergence

+
+

def poynting_divergence(s: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, *, e: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, h: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]

+
+

Calculate the divergence of the poynting vector.

+

This is the net energy flow for each cell, i.e. the change in energy +U per dt caused by transfer of energy to +nearby cells (rather than absorption/emission by currents J +or M).

+

See poynting() and meanas.fdtd for more details.

+

Args —–= s : Poynting vector, as +calculated with poynting(). Optional; +caller can provide e and h instead.

+
+
e
+
+E-field (optional; need either s or both e and +h) +
+
h
+
+H-field (optional; need either s or both e and +h) +
+
dxes
+
+Grid description; see meanas.fdmath. +
+
+

Returns —–= ds : Divergence of the poynting vector. +Entries indicate the net energy flow out of the corresponding energy +cell.

+
+

Module meanas.fdtd.pml

+

PML implementations

+

#TODO discussion of PMLs #TODO cpml documentation

+

Functions

+

Function +cpml_params

+
+

def cpml_params(axis: int, polarity: int, dt: float, thickness: int = 8, ln_R_per_layer: float = -1.6, epsilon_eff: float = 1, mu_eff: float = 1, m: float = 3.5, ma: float = 1, cfs_alpha: float = 0) -> dict[str, typing.Any]

+
+

Function +updates_with_cpml

+
+

def updates_with_cpml(cpml_params: collections.abc.Sequence[collections.abc.Sequence[dict[str, typing.Any] | None]], dt: float, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], *, dtype: Union[numpy.dtype[Any], ForwardRef(None), type[Any], numpy._typing._dtype_like._SupportsDType[numpy.dtype[Any]], str, tuple[Any, int], tuple[Any, Union[SupportsIndex, collections.abc.Sequence[SupportsIndex]]], list[Any], numpy._typing._dtype_like._DTypeDict, tuple[Any, Any]] = numpy.float32) -> tuple[collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], None], collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], None]]

+
+
+

Module meanas.test

+

Tests (run with +python3 -m pytest -rxPXs | tee results.txt)

+

Sub-modules

+ +
+

Module +meanas.test.conftest

+

Test fixtures

+

Functions

+

Function dx

+
+

def dx(request: Any) -> float

+
+

Function dxes

+
+

def dxes(request: Any, shape: tuple[int, ...], dx: float) -> list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]]

+
+

Function epsilon

+
+

def epsilon(request: Any, shape: tuple[int, ...], epsilon_bg: float, epsilon_fg: float) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]

+
+

Function +epsilon_bg

+
+

def epsilon_bg(request: Any) -> float

+
+

Function +epsilon_fg

+
+

def epsilon_fg(request: Any) -> float

+
+

Function j_mag

+
+

def j_mag(request: Any) -> float

+
+

Function shape

+
+

def shape(request: Any) -> tuple[int, ...]

+
+
+

Module +meanas.test.test_fdfd

+

Functions

+

Function +j_distribution

+
+

def j_distribution(request: Any, shape: tuple[int, ...], j_mag: float) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]

+
+

Function omega

+
+

def omega(request: Any) -> float

+
+

Function pec

+
+

def pec(request: Any) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None

+
+

Function pmc

+
+

def pmc(request: Any) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None

+
+

Function sim

+
+

def sim(request: Any, shape: tuple[int, ...], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], dxes: list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], j_distribution: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], omega: float, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None) -> meanas.test.test_fdfd.FDResult

+
+

Build simulation from parts

+

Function +test_poynting_planes

+
+

def test_poynting_planes(sim: FDResult) -> None

+
+

Function +test_residual

+
+

def test_residual(sim: FDResult) -> None

+
+

Classes

+

Class FDResult

+

[view +code]

+
+

class FDResult(shape: tuple[int, ...], dxes: list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], omega: complex, j: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], e: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None)

+
+

FDResult(shape: tuple[int, …], dxes: +list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], +epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], omega: +complex, j: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], e: +numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], pmc: +numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None, pec: +numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None)

+

Class variables

+
Variable +dxes
+
Variable e
+
Variable +epsilon
+
Variable j
+
Variable +omega
+
Variable +pec
+
Variable +pmc
+
Variable +shape
+
+

Module +meanas.test.test_fdfd_pml

+

Functions

+

Function dxes

+
+

def dxes(request: Any, shape: tuple[int, ...], dx: float, omega: float, epsilon_fg: float) -> list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]]

+
+

Function +epsilon

+
+

def epsilon(request: Any, shape: tuple[int, ...], epsilon_bg: float, epsilon_fg: float) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]

+
+

Function +j_distribution

+
+

def j_distribution(request: Any, shape: tuple[int, ...], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], dxes: collections.abc.MutableSequence[collections.abc.MutableSequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], omega: float, src_polarity: int) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]

+
+

Function +omega

+
+

def omega(request: Any) -> float

+
+

Function pec

+
+

def pec(request: Any) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None

+
+

Function pmc

+
+

def pmc(request: Any) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None

+
+

Function +shape

+
+

def shape(request: Any) -> tuple[int, int, int]

+
+

Function sim

+
+

def sim(request: Any, shape: tuple[int, ...], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], dxes: collections.abc.MutableSequence[collections.abc.MutableSequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], j_distribution: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], omega: float, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None) -> meanas.test.test_fdfd.FDResult

+
+

Function +src_polarity

+
+

def src_polarity(request: Any) -> int

+
+

Function +test_pml

+
+

def test_pml(sim: meanas.test.test_fdfd.FDResult, src_polarity: int) -> None

+
+
+

Module +meanas.test.test_fdtd

+

Functions

+

Function dt

+
+

def dt(request: Any) -> float

+
+

Function +j_distribution

+
+

def j_distribution(request: Any, shape: tuple[int, ...], j_mag: float) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]

+
+

Function +j_steps

+
+

def j_steps(request: Any) -> tuple[int, ...]

+
+

Function sim

+
+

def sim(request: Any, shape: tuple[int, ...], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], dxes: list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], dt: float, j_distribution: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], j_steps: tuple[int, ...]) -> meanas.test.test_fdtd.TDResult

+
+

Function +test_energy_conservation

+
+

def test_energy_conservation(sim: TDResult) -> None

+
+

Assumes fields start at 0 before J0 is added

+

Function +test_initial_energy

+
+

def test_initial_energy(sim: TDResult) -> None

+
+

Assumes fields start at 0 before J0 is added

+

Function +test_initial_fields

+
+

def test_initial_fields(sim: TDResult) -> None

+
+

Function +test_poynting_divergence

+
+

def test_poynting_divergence(sim: TDResult) -> None

+
+

Function +test_poynting_planes

+
+

def test_poynting_planes(sim: TDResult) -> None

+
+

Classes

+

Class TDResult

+

[view +code]

+
+

class TDResult(shape: tuple[int, ...], dt: float, dxes: list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], j_distribution: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], j_steps: tuple[int, ...], es: list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] = <factory>, hs: list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] = <factory>, js: list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] = <factory>)

+
+

TDResult(shape: tuple[int, …], dt: float, dxes: +list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], +epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], +j_distribution: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], +j_steps: tuple[int, …], es: list[numpy.ndarray[typing.Any, +numpy.dtype[numpy.float64]]] = , hs: +list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] = , +js: list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] = +)

+

Class variables

+
Variable dt
+
Variable +dxes
+
Variable +epsilon
+
Variable es
+
Variable hs
+
Variable +j_distribution
+
Variable +j_steps
+
Variable js
+
Variable +shape
+
+

Module meanas.test.utils

+

Functions

+

Function +assert_close

+
+

def assert_close(x: numpy.ndarray[typing.Any, numpy.dtype[+_ScalarType_co]], y: numpy.ndarray[typing.Any, numpy.dtype[+_ScalarType_co]], *args, **kwargs) -> None

+
+

Function +assert_fields_close

+
+

def assert_fields_close(x: numpy.ndarray[typing.Any, numpy.dtype[+_ScalarType_co]], y: numpy.ndarray[typing.Any, numpy.dtype[+_ScalarType_co]], *args, **kwargs) -> None

+
+
+

Generated by pdoc 0.11.1 (https://pdoc3.github.io).

+ + diff --git a/doc.md b/doc.md new file mode 100644 index 0000000..3192c7a --- /dev/null +++ b/doc.md @@ -0,0 +1,6494 @@ +--- +description: | + API documentation for modules: meanas, meanas.eigensolvers, meanas.fdfd, meanas.fdfd.bloch, meanas.fdfd.farfield, meanas.fdfd.functional, meanas.fdfd.operators, meanas.fdfd.scpml, meanas.fdfd.solvers, meanas.fdfd.waveguide_2d, meanas.fdfd.waveguide_3d, meanas.fdfd.waveguide_cyl, meanas.fdmath, meanas.fdmath.functional, meanas.fdmath.operators, meanas.fdmath.types, meanas.fdmath.vectorization, meanas.fdtd, meanas.fdtd.base, meanas.fdtd.boundaries, meanas.fdtd.energy, meanas.fdtd.pml, meanas.test, meanas.test.conftest, meanas.test.test_fdfd, meanas.test.test_fdfd_pml, meanas.test.test_fdtd, meanas.test.utils. + +lang: en + +classoption: oneside +geometry: margin=1in +papersize: a4 + +linkcolor: blue +links-as-notes: true +... + + +------------------------------------------- + + + +# Module `meanas` {#meanas} + +# meanas + +**meanas** is a python package for electromagnetic simulations + +** UNSTABLE / WORK IN PROGRESS ** + +Formerly known as [fdfd_tools](https://mpxd.net/code/jan/fdfd_tools). + +This package is intended for building simulation inputs, analyzing +simulation outputs, and running short simulations on unspecialized hardware. +It is designed to provide tooling and a baseline for other, high-performance +purpose- and hardware-specific solvers. + + +**Contents** + +- Finite difference frequency domain (FDFD) + * Library of sparse matrices for representing the electromagnetic wave + equation in 3D, as well as auxiliary matrices for conversion between fields + * Waveguide mode operators + * Waveguide mode eigensolver + * Stretched-coordinate PML boundaries (SCPML) + * Functional versions of most operators + * Anisotropic media (limited to diagonal elements eps_xx, eps_yy, eps_zz, mu_xx, ...) + * Arbitrary distributions of perfect electric and magnetic conductors (PEC / PMC) +- Finite difference time domain (FDTD) + * Basic Maxwell time-steps + * Poynting vector and energy calculation + * Convolutional PMLs + +This package does *not* provide a fast matrix solver, though by default +[generic()](#meanas.fdfd.solvers.generic)(...) will call +scipy.sparse.linalg.qmr(...) to perform a solve. +For 2D FDFD problems this should be fine; likewise, the waveguide mode +solver uses scipy's eigenvalue solver, with reasonable results. + +For solving large (or 3D) FDFD problems, I recommend a GPU-based iterative +solver, such as [opencl_fdfd](https://mpxd.net/code/jan/opencl_fdfd) or +those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html). Your +solver will need the ability to solve complex symmetric (non-Hermitian) +linear systems, ideally with double precision. + +- [Source repository](https://mpxd.net/code/jan/meanas) +- [PyPI](https://pypi.org/project/meanas) +- [Github mirror](https://github.com/anewusername/meanas) + + +## Installation + +**Requirements:** + +* python >=3.11 +* numpy +* scipy + + +Install from PyPI with pip: +```bash +pip3 install 'meanas[dev]' +``` + +### Development install +Install python3 and git: +```bash +# This is for Debian/Ubuntu/other-apt-based systems; you may need an alternative command +sudo apt install python3 build-essential python3-dev git +``` + +In-place development install: +```bash +# Download using git +git clone https://mpxd.net/code/jan/meanas.git + +# If you'd like to create a virtualenv, do so: +python3 -m venv my_venv + +# If you are using a virtualenv, activate it +source my_venv/bin/activate + +# Install in-place (-e, editable) from ./meanas, including development dependencies ([dev]) +pip3 install --user -e './meanas[dev]' + +# Run tests +cd meanas +python3 -m pytest -rsxX | tee test_results.txt +``` + +#### See also: +- [git book](https://git-scm.com/book/en/v2) +- [venv documentation](https://docs.python.org/3/tutorial/venv.html) +- [python language reference](https://docs.python.org/3/reference/index.html) +- [python standard library](https://docs.python.org/3/library/index.html) + + +## Use + +See `examples/` for some simple examples; you may need additional +packages such as [gridlock](https://mpxd.net/code/jan/gridlock) +to run the examples. + + + +## Sub-modules + +* [meanas.eigensolvers](#meanas.eigensolvers) +* [meanas.fdfd](#meanas.fdfd) +* [meanas.fdmath](#meanas.fdmath) +* [meanas.fdtd](#meanas.fdtd) +* [meanas.test](#meanas.test) + + + + + + +------------------------------------------- + + + +# Module `meanas.eigensolvers` {#meanas.eigensolvers} + +Solvers for eigenvalue / eigenvector problems + + + + + +## Functions + + + +### Function `power_iteration` {#meanas.eigensolvers.power_iteration} + + + + + + +> `def power_iteration(operator: scipy.sparse._matrix.spmatrix, guess_vector: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]] | None = None, iterations: int = 20) -> tuple[complex, numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]` + + +Use power iteration to estimate the dominant eigenvector of a matrix. + + +Args +-----= +**```operator```** +: Matrix to analyze. + + +**```guess_vector```** +: Starting point for the eigenvector. Default is a randomly chosen vector. + + +**```iterations```** +: Number of iterations to perform. Default 20. + + + +Returns +-----= +(Largest-magnitude eigenvalue, Corresponding eigenvector estimate) + + +### Function `rayleigh_quotient_iteration` {#meanas.eigensolvers.rayleigh_quotient_iteration} + + + + + + +> `def rayleigh_quotient_iteration(operator: scipy.sparse._matrix.spmatrix | scipy.sparse.linalg._interface.LinearOperator, guess_vector: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], iterations: int = 40, tolerance: float = 1e-13, solver: collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]] | None = None) -> tuple[complex, numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]` + + +Use Rayleigh quotient iteration to refine an eigenvector guess. + + +Args +-----= +**```operator```** +: Matrix to analyze. + + +**```guess_vector```** +: Eigenvector to refine. + + +**```iterations```** +: Maximum number of iterations to perform. Default 40. + + +**```tolerance```** +: Stop iteration if `(A - I*eigenvalue) @ v < num_vectors * tolerance`, + Default 1e-13. + + +**```solver```** +: Solver function of the form `x = solver(A, b)`. + By default, use scipy.sparse.spsolve for sparse matrices and + scipy.sparse.bicgstab for general LinearOperator instances. + + + +Returns +-----= +(eigenvalues, eigenvectors) + + +### Function `signed_eigensolve` {#meanas.eigensolvers.signed_eigensolve} + + + + + + +> `def signed_eigensolve(operator: scipy.sparse._matrix.spmatrix | scipy.sparse.linalg._interface.LinearOperator, how_many: int, negative: bool = False) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]` + + +Find the largest-magnitude positive-only (or negative-only) eigenvalues and + eigenvectors of the provided matrix. + + +Args +-----= +**```operator```** +: Matrix to analyze. + + +**```how_many```** +: How many eigenvalues to find. + + +**```negative```** +: Whether to find negative-only eigenvalues. + Default False (positive only). + + + +Returns +-----= +(sorted list of eigenvalues, 2D ndarray of corresponding eigenvectors) +`eigenvectors[:, k]` corresponds to the k-th eigenvalue + + + + +------------------------------------------- + + + +# Module `meanas.fdfd` {#meanas.fdfd} + +Tools for finite difference frequency-domain (FDFD) simulations and calculations. + +These mostly involve picking a single frequency, then setting up and solving a +matrix equation (Ax=b) or eigenvalue problem. + + +Submodules: + +- [meanas.fdfd.operators](#meanas.fdfd.operators), [meanas.fdfd.functional](#meanas.fdfd.functional): General FDFD problem setup. +- [meanas.fdfd.solvers](#meanas.fdfd.solvers): Solver interface and reference implementation. +- [meanas.fdfd.scpml](#meanas.fdfd.scpml): Stretched-coordinate perfectly matched layer (scpml) boundary conditions +- [meanas.fdfd.waveguide\_2d](#meanas.fdfd.waveguide\_2d): Operators and mode-solver for waveguides with constant cross-section. +- [meanas.fdfd.waveguide\_3d](#meanas.fdfd.waveguide\_3d): Functions for transforming [meanas.fdfd.waveguide\_2d](#meanas.fdfd.waveguide\_2d) results into 3D. + + +================================================================ + +From the "Frequency domain" section of [meanas.fdmath](#meanas.fdmath), we have + +$$ + \begin{aligned} + \tilde{E}_{l, \vec{r}} &= \tilde{E}_{\vec{r}} e^{-\imath \omega l \Delta_t} \\ + \tilde{H}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &= \tilde{H}_{\vec{r} + \frac{1}{2}} e^{-\imath \omega (l - \frac{1}{2}) \Delta_t} \\ + \tilde{J}_{l, \vec{r}} &= \tilde{J}_{\vec{r}} e^{-\imath \omega (l - \frac{1}{2}) \Delta_t} \\ + \tilde{M}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &= \tilde{M}_{\vec{r} + \frac{1}{2}} e^{-\imath \omega l \Delta_t} \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{\vec{r}}) + -\Omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} &= -\imath \Omega \tilde{J}_{\vec{r}} e^{\imath \omega \Delta_t / 2} \\ + \Omega &= 2 \sin(\omega \Delta_t / 2) / \Delta_t + \end{aligned} +$$ + +resulting in + +$$ + \begin{aligned} + \tilde{\partial}_t &\Rightarrow -\imath \Omega e^{-\imath \omega \Delta_t / 2}\\ + \hat{\partial}_t &\Rightarrow -\imath \Omega e^{ \imath \omega \Delta_t / 2}\\ + \end{aligned} +$$ + +Maxwell's equations are then + +$$ + \begin{aligned} + \tilde{\nabla} \times \tilde{E}_{\vec{r}} &= + \imath \Omega e^{-\imath \omega \Delta_t / 2} \hat{B}_{\vec{r} + \frac{1}{2}} + - \hat{M}_{\vec{r} + \frac{1}{2}} \\ + \hat{\nabla} \times \hat{H}_{\vec{r} + \frac{1}{2}} &= + -\imath \Omega e^{ \imath \omega \Delta_t / 2} \tilde{D}_{\vec{r}} + + \tilde{J}_{\vec{r}} \\ + \tilde{\nabla} \cdot \hat{B}_{\vec{r} + \frac{1}{2}} &= 0 \\ + \hat{\nabla} \cdot \tilde{D}_{\vec{r}} &= \rho_{\vec{r}} + \end{aligned} +$$ + +With $\Delta_t \to 0$, this simplifies to + +$$ + \begin{aligned} + \tilde{E}_{l, \vec{r}} &\to \tilde{E}_{\vec{r}} \\ + \tilde{H}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &\to \tilde{H}_{\vec{r} + \frac{1}{2}} \\ + \tilde{J}_{l, \vec{r}} &\to \tilde{J}_{\vec{r}} \\ + \tilde{M}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &\to \tilde{M}_{\vec{r} + \frac{1}{2}} \\ + \Omega &\to \omega \\ + \tilde{\partial}_t &\to -\imath \omega \\ + \hat{\partial}_t &\to -\imath \omega \\ + \end{aligned} +$$ + +and then + +$$ + \begin{aligned} + \tilde{\nabla} \times \tilde{E}_{\vec{r}} &= + \imath \omega \hat{B}_{\vec{r} + \frac{1}{2}} + - \hat{M}_{\vec{r} + \frac{1}{2}} \\ + \hat{\nabla} \times \hat{H}_{\vec{r} + \frac{1}{2}} &= + -\imath \omega \tilde{D}_{\vec{r}} + + \tilde{J}_{\vec{r}} \\ + \end{aligned} +$$ + +$$ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{\vec{r}}) + -\omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} = -\imath \omega \tilde{J}_{\vec{r}} \\ +$$ + +# TODO FDFD? +# TODO PML + + + +## Sub-modules + +* [meanas.fdfd.bloch](#meanas.fdfd.bloch) +* [meanas.fdfd.farfield](#meanas.fdfd.farfield) +* [meanas.fdfd.functional](#meanas.fdfd.functional) +* [meanas.fdfd.operators](#meanas.fdfd.operators) +* [meanas.fdfd.scpml](#meanas.fdfd.scpml) +* [meanas.fdfd.solvers](#meanas.fdfd.solvers) +* [meanas.fdfd.waveguide_2d](#meanas.fdfd.waveguide_2d) +* [meanas.fdfd.waveguide_3d](#meanas.fdfd.waveguide_3d) +* [meanas.fdfd.waveguide_cyl](#meanas.fdfd.waveguide_cyl) + + + + + + +------------------------------------------- + + + +# Module `meanas.fdfd.bloch` {#meanas.fdfd.bloch} + +Bloch eigenmode solver/operators + +This module contains functions for generating and solving the + 3D Bloch eigenproblem. The approach is to transform the problem + into the (spatial) fourier domain, transforming the equation + + 1/mu * curl(1/eps * curl(H_eigenmode)) = (w/c)^2 H_eigenmode + + into + + conv(1/mu_k, ik x conv(1/eps_k, ik x H_k)) = (w/c)^2 H_k + + where: + + - the \_k subscript denotes a 3D fourier transformed field + - each component of H\_k corresponds to a plane wave with wavevector k + - x is the cross product + - conv() denotes convolution + + Since k and H are orthogonal for each plane wave, we can use each + k to create an orthogonal basis (k, m, n), with `k x m = n`, and + `|m| = |n| = 1`. The cross products are then simplified as follows: + + - h is shorthand for H\_k + - (...)\_xyz denotes the (x, y, z) basis + - (...)\_kmn denotes the (k, m, n) basis + - hm is the component of h in the m direction, etc. + + We know + + k @ h = kx hx + ky hy + kz hz = 0 = hk + h = hk + hm + hn = hm + hn + k = kk + km + kn = kk = |k| + + We can write + + k x h = (ky hz - kz hy, + kz hx - kx hz, + kx hy - ky hx)_xyz + = ((k x h) @ k, (k x h) @ m, (k x h) @ n)_kmn + = (0, (m x k) @ h, (n x k) @ h)_kmn # triple product ordering + = (0, kk (-n @ h), kk (m @ h))_kmn # (m x k) = -|k| n, etc. + = |k| (0, -h @ n, h @ m)_kmn + + which gives us a straightforward way to perform the cross product + while simultaneously transforming into the \_kmn basis. + We can also write + + k x h = (km hn - kn hm, + kn hk - kk hn, + kk hm - km hk)_kmn + = (0, -kk hn, kk hm)_kmn + = (-kk hn)(mx, my, mz)_xyz + (kk hm)(nx, ny, nz)_xyz + = |k| (hm * (nx, ny, nz)_xyz + - hn * (mx, my, mz)_xyz) + + which gives us a way to perform the cross product while simultaneously + trasnforming back into the \_xyz basis. + + We can also simplify conv(X\_k, Y\_k) as `fftn(X * ifftn(Y_k))`. + + Using these results and storing H\_k as `h = (hm, hn)`, we have + + e_xyz = fftn(1/eps * ifftn(|k| (hm * n - hn * m))) + b_mn = |k| (-e_xyz @ n, e_xyz @ m) + h_mn = fftn(1/mu * ifftn(b_m * m + b_n * n)) + + which forms the operator from the left side of the equation. + + We can then use a preconditioned block Rayleigh iteration algorithm, as in + SG Johnson and JD Joannopoulos, Block-iterative frequency-domain methods + for Maxwell's equations in a planewave basis, Optics Express 8, 3, 173-190 (2001) + (similar to that used in MPB) to find the eigenvectors for this operator. + + === + + Typically you will want to do something like + + recip_lattice = numpy.diag(1/numpy.array(epsilon[0].shape * dx)) + n, v = bloch.eigsolve(5, k0, recip_lattice, epsilon) + f = numpy.sqrt(-numpy.real(n[0])) + n_eff = norm(recip_lattice @ k0) / f + + v2e = bloch.hmn_2_exyz(k0, recip_lattice, epsilon) + e_field = v2e(v[0]) + + k, f = find_k(frequency=1/1550, + tolerance=(1/1550 - 1/1551), + direction=[1, 0, 0], + G_matrix=recip_lattice, + epsilon=epsilon, + band=0) + + + + + +## Functions + + + +### Function `eigsolve` {#meanas.fdfd.bloch.eigsolve} + + + + + + +> `def eigsolve(num_modes: int, k0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, tolerance: float = 1e-07, max_iters: int = 10000, reset_iters: int = 100, y0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]], ForwardRef(None)] = None, callback: collections.abc.Callable[..., None] | None = None) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]` + + +Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector + k0 of the specified structure. + + +Args +-----= +**```k0```** +: Bloch wavevector, \[k0x, k0y, k0z]. + + +**```G_matrix```** +: 3x3 matrix, with reciprocal lattice vectors as columns. + + +**```epsilon```** +: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + + +**```mu```** +: Magnetic permability distribution for the simulation. + Default None (1 everywhere). + + +**```tolerance```** +: Solver stops when fractional change in the objective + `trace(Z.H @ A @ Z @ inv(Z Z.H))` is smaller than the tolerance + + +**```max_iters```** +: TODO + + +**```reset_iters```** +: TODO + + +**```callback```** +: TODO + + +**```y0```** +: TODO, initial guess + + + +Returns +-----= +(eigenvalues, eigenvectors) where eigenvalues\[i] corresponds to the +vector `eigenvectors[i, :]` + + +### Function `fftn` {#meanas.fdfd.bloch.fftn} + + + + + + +> `def fftn(*args: Any, **kwargs: Any) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]` + + + + + +### Function `find_k` {#meanas.fdfd.bloch.find_k} + + + + + + +> `def find_k(frequency: float, tolerance: float, direction: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, band: int = 0, k_bounds: tuple[float, float] = (0, 0.5), k_guess: float | None = None, solve_callback: collections.abc.Callable[..., None] | None = None, iter_callback: collections.abc.Callable[..., None] | None = None, v0: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]] | None = None) -> tuple[float, float, numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]` + + +Search for a bloch vector that has a given frequency. + + +Args +-----= +**```frequency```** +: Target frequency. + + +**```tolerance```** +: Target frequency tolerance. + + +**```direction```** +: k-vector direction to search along. + + +**```G_matrix```** +: 3x3 matrix, with reciprocal lattice vectors as columns. + + +**```epsilon```** +: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + + +**```mu```** +: Magnetic permability distribution for the simulation. + Default None (1 everywhere). + + +**```band```** +: Which band to search in. Default 0 (lowest frequency). + + +**```k_bounds```** +: Minimum and maximum values for k. Default (0, 0.5). + + +**```k_guess```** +: Initial value for k. + + +**```solve_callback```** +: TODO + + +**```iter_callback```** +: TODO + + + +Returns +-----= +(k, actual\_frequency, eigenvalues, eigenvectors) +The found k-vector and its frequency, along with all eigenvalues and eigenvectors. + + +### Function `generate_kmn` {#meanas.fdfd.bloch.generate_kmn} + + + + + + +> `def generate_kmn(k0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], shape: collections.abc.Sequence[int]) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]` + + +Generate a (k, m, n) orthogonal basis for each k-vector in the simulation grid. + + +Args +-----= +**```k0```** +: [k0x, k0y, k0z], Bloch wavevector, in G basis. + + +**```G_matrix```** +: 3x3 matrix, with reciprocal lattice vectors as columns. + + +**```shape```** +: [nx, ny, nz] shape of the simulation grid. + + + +Returns +-----= +`(|k|, m, n)` where `|k|` has shape `tuple(shape) + (1,)` + and m, n have shape `tuple(shape) + (3,)`. + All are given in the xyz basis (e.g. `|k|[0,0,0] = norm(G_matrix @ k0)`). + + +### Function `hmn_2_exyz` {#meanas.fdfd.bloch.hmn_2_exyz} + + + + + + +> `def hmn_2_exyz(k0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]` + + +Generate an operator which converts a vectorized spatial-frequency-space + h\_mn into an E-field distribution, i.e. + + ifft(conv(1/eps_k, ik x h_mn)) + +The operator is a function that acts on a vector h\_mn of size `2 * epsilon[0].size`. + +See the [meanas.fdfd.bloch](#meanas.fdfd.bloch) docstring for more information. + + +Args +-----= +**```k0```** +: Bloch wavevector, \[k0x, k0y, k0z]. + + +**```G_matrix```** +: 3x3 matrix, with reciprocal lattice vectors as columns. + + +**```epsilon```** +: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + + + +Returns +-----= +Function for converting h\_mn into E\_xyz + + +### Function `hmn_2_hxyz` {#meanas.fdfd.bloch.hmn_2_hxyz} + + + + + + +> `def hmn_2_hxyz(k0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]` + + +Generate an operator which converts a vectorized spatial-frequency-space + h\_mn into an H-field distribution, i.e. + + ifft(h_mn) + +The operator is a function that acts on a vector h\_mn of size `2 * epsilon[0].size`. + +See the [meanas.fdfd.bloch](#meanas.fdfd.bloch) docstring for more information. + + +Args +-----= +**```k0```** +: Bloch wavevector, \[k0x, k0y, k0z]. + + +**```G_matrix```** +: 3x3 matrix, with reciprocal lattice vectors as columns. + + +**```epsilon```** +: Dielectric constant distribution for the simulation. + Only epsilon\[0].shape is used. + + + +Returns +-----= +Function for converting h\_mn into H\_xyz + + +### Function `ifftn` {#meanas.fdfd.bloch.ifftn} + + + + + + +> `def ifftn(*args: Any, **kwargs: Any) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]` + + + + + +### Function `inner_product` {#meanas.fdfd.bloch.inner_product} + + + + + + +> `def inner_product(eL, hL, eR, hR) -> complex` + + + + + +### Function `inverse_maxwell_operator_approx` {#meanas.fdfd.bloch.inverse_maxwell_operator_approx} + + + + + + +> `def inverse_maxwell_operator_approx(k0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]` + + +Generate an approximate inverse of the Maxwell operator, + + ik x conv(eps_k, ik x conv(mu_k, ___)) + + which can be used to improve the speed of ARPACK in shift-invert mode. + +See the [meanas.fdfd.bloch](#meanas.fdfd.bloch) docstring for more information. + + +Args +-----= +**```k0```** +: Bloch wavevector, \[k0x, k0y, k0z]. + + +**```G_matrix```** +: 3x3 matrix, with reciprocal lattice vectors as columns. + + +**```epsilon```** +: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + + +**```mu```** +: Magnetic permability distribution for the simulation. + Default None (1 everywhere). + + + +Returns +-----= +Function which applies the approximate inverse of the maxwell operator to h\_mn. + + +### Function `maxwell_operator` {#meanas.fdfd.bloch.maxwell_operator} + + + + + + +> `def maxwell_operator(k0: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], G_matrix: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]` + + +Generate the Maxwell operator + + conv(1/mu_k, ik x conv(1/eps_k, ik x ___)) + +which is the spatial-frequency-space representation of + + 1/mu * curl(1/eps * curl(___)) + +The operator is a function that acts on a vector h_mn of size `2 * epsilon[0].size` + +See the [meanas.fdfd.bloch](#meanas.fdfd.bloch) docstring for more information. + + +Args +-----= +**```k0```** +: Bloch wavevector, \[k0x, k0y, k0z]. + + +**```G_matrix```** +: 3x3 matrix, with reciprocal lattice vectors as columns. + + +**```epsilon```** +: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + + +**```mu```** +: Magnetic permability distribution for the simulation. + Default None (1 everywhere). + + + +Returns +-----= +Function which applies the maxwell operator to h_mn. + + +### Function `trq` {#meanas.fdfd.bloch.trq} + + + + + + +> `def trq(eI, hI, eO, hO) -> tuple[complex, complex]` + + + + + + + +------------------------------------------- + + + +# Module `meanas.fdfd.farfield` {#meanas.fdfd.farfield} + +Functions for performing near-to-farfield transformation (and the reverse). + + + + + +## Functions + + + +### Function `far_to_nearfield` {#meanas.fdfd.farfield.far_to_nearfield} + + + + + + +> `def far_to_nearfield(E_far: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], H_far: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], dkx: float, dky: float, padded_size: list[int] | int | None = None) -> dict[str, typing.Any]` + + +Compute the farfield, i.e. the distribution of the fields after propagation + through several wavelengths of uniform medium. + +The input fields should be complex phasors. + + +Args +-----= +**```E_far```** +: List of 2 ndarrays containing the 2D phasor field slices for the transverse + E fields (e.g. [Ex, Ey] for calculating the nearfield toward the z-direction). + Fields should be normalized so that + E_far = E_far_actual / (i k exp(-i k r) / (4 pi r)) + + +**```H_far```** +: List of 2 ndarrays containing the 2D phasor field slices for the transverse + H fields (e.g. [Hx, hy] for calculating the nearfield toward the z-direction). + Fields should be normalized so that + H_far = H_far_actual / (i k exp(-i k r) / (4 pi r)) + + +**```dkx```** +: kx discretization, in units of wavelength. + + +**```dky```** +: ky discretization, in units of wavelength. + + +**```padded_size```** +: Shape of the output. A single integer n will be expanded to (n, n). + Powers of 2 are most efficient for FFT computation. + Default is the smallest power of 2 larger than the input, for each axis. + + + +Returns +-----= +Dict with keys + +- E: E-field nearfield +- H: H-field nearfield +- dx, dy: spatial discretization, normalized to wavelength (dimensionless) + + +### Function `near_to_farfield` {#meanas.fdfd.farfield.near_to_farfield} + + + + + + +> `def near_to_farfield(E_near: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], H_near: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], dx: float, dy: float, padded_size: list[int] | int | None = None) -> dict[str, typing.Any]` + + +Compute the farfield, i.e. the distribution of the fields after propagation + through several wavelengths of uniform medium. + +The input fields should be complex phasors. + + +Args +-----= +**```E_near```** +: List of 2 ndarrays containing the 2D phasor field slices for the transverse + E fields (e.g. [Ex, Ey] for calculating the farfield toward the z-direction). + + +**```H_near```** +: List of 2 ndarrays containing the 2D phasor field slices for the transverse + H fields (e.g. [Hx, hy] for calculating the farfield towrad the z-direction). + + +**```dx```** +: Cell size along x-dimension, in units of wavelength. + + +**```dy```** +: Cell size along y-dimension, in units of wavelength. + + +**```padded_size```** +: Shape of the output. A single integer n will be expanded to (n, n). + Powers of 2 are most efficient for FFT computation. + Default is the smallest power of 2 larger than the input, for each axis. + + + +Returns +-----= +Dict with keys + +- E\_far: Normalized E-field farfield; multiply by + (i k exp(-i k r) / (4 pi r)) to get the actual field value. +- H\_far: Normalized H-field farfield; multiply by + (i k exp(-i k r) / (4 pi r)) to get the actual field value. +- kx, ky: Wavevector values corresponding to the x- and y- axes in E_far and H_far, + normalized to wavelength (dimensionless). +- dkx, dky: step size for kx and ky, normalized to wavelength. +- theta: arctan2(ky, kx) corresponding to each (kx, ky). + This is the angle in the x-y plane, counterclockwise from above, starting from +x. +- phi: arccos(kz / k) corresponding to each (kx, ky). + This is the angle away from +z. + + + + +------------------------------------------- + + + +# Module `meanas.fdfd.functional` {#meanas.fdfd.functional} + +Functional versions of many FDFD operators. These can be useful for performing + FDFD calculations without needing to construct large matrices in memory. + +The functions generated here expect cfdfield\_t inputs with shape (3, X, Y, Z), +e.g. E = [E_x, E_y, E_z] where each (complex) component has shape (X, Y, Z) + + + + + +## Functions + + + +### Function `e2h` {#meanas.fdfd.functional.e2h} + + + + + + +> `def e2h(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]` + + +Utility operator for converting the E field into the H field. +For use with [e\_full()](#meanas.fdfd.functional.e\_full) -- assumes that there is no magnetic current M. + + +Args +-----= +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```mu```** +: Magnetic permeability (default 1 everywhere) + + + +Returns +-----= +Function f for converting E to H, +f(E) -> H + + +### Function `e_full` {#meanas.fdfd.functional.e_full} + + + + + + +> `def e_full(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]` + + +Wave operator for use with E-field. See operators.e\_full for details. + + +Args +-----= +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```epsilon```** +: Dielectric constant + + +**```mu```** +: Magnetic permeability (default 1 everywhere) + + + +Returns +-----= +Function f implementing the wave operator +f(E) -> `-i * omega * J` + + +### Function `e_tfsf_source` {#meanas.fdfd.functional.e_tfsf_source} + + + + + + +> `def e_tfsf_source(TF_region: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]` + + +Operator that turns an E-field distribution into a total-field/scattered-field +(TFSF) source. + + +Args +-----= +**```TF_region```** +: mask which is set to 1 in the total-field region, and 0 elsewhere + (i.e. in the scattered-field region). + Should have the same shape as the simulation grid, e.g. epsilon\[0].shape. + + +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```epsilon```** +: Dielectric constant distribution + + +**```mu```** +: Magnetic permeability (default 1 everywhere) + + + +Returns +-----= +Function f which takes an E field and returns a current distribution, +f(E) -> J + + +### Function `eh_full` {#meanas.fdfd.functional.eh_full} + + + + + + +> `def eh_full(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]], tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]]` + + +Wave operator for full (both E and H) field representation. +See operators.eh\_full. + + +Args +-----= +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```epsilon```** +: Dielectric constant + + +**```mu```** +: Magnetic permeability (default 1 everywhere) + + + +Returns +-----= +Function f implementing the wave operator +f(E, H) -> `(J, -M)` + + +### Function `m2j` {#meanas.fdfd.functional.m2j} + + + + + + +> `def m2j(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]` + + +Utility operator for converting magnetic current M distribution +into equivalent electric current distribution J. +For use with e.g. [e\_full()](#meanas.fdfd.functional.e\_full). + + +Args +-----= +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```mu```** +: Magnetic permeability (default 1 everywhere) + + + +Returns +-----= +Function f for converting M to J, +f(M) -> J + + +### Function `poynting_e_cross_h` {#meanas.fdfd.functional.poynting_e_cross_h} + + + + + + +> `def poynting_e_cross_h(dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]]) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]` + + +Generates a function that takes the single-frequency E and H fields +and calculates the cross product E x H = $E \times H$ as required +for the Poynting vector, $S = E \times H$ + + +Note +-----= +This function also shifts the input E field by one cell as required +for computing the Poynting cross product (see [meanas.fdfd](#meanas.fdfd) module docs). + + +Note +-----= +If E and H are peak amplitudes as assumed elsewhere in this code, +the time-average of the poynting vector is ` = Re(S)/2 = Re(E x H*) / 2`. +The factor of `1/2` can be omitted if root-mean-square quantities are used +instead. + + +Args +-----= +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + + +Returns +-----= +Function f that returns E x H as required for the poynting vector. + + + + +------------------------------------------- + + + +# Module `meanas.fdfd.operators` {#meanas.fdfd.operators} + +Sparse matrix operators for use with electromagnetic wave equations. + +These functions return sparse-matrix (scipy.sparse.spmatrix) representations of + a variety of operators, intended for use with E and H fields vectorized using the + [vec()](#meanas.fdmath.vectorization.vec) and [unvec()](#meanas.fdmath.vectorization.unvec) functions. + +E- and H-field values are defined on a Yee cell; epsilon values should be calculated for + cells centered at each E component (mu at each H component). + +Many of these functions require a dxes parameter, of type dx\_lists\_t; see +the [meanas.fdmath.types](#meanas.fdmath.types) submodule for details. + + +The following operators are included: + +- E-only wave operator +- H-only wave operator +- EH wave operator +- Curl for use with E, H fields +- E to H conversion +- M to J conversion +- Poynting cross products +- Circular shifts +- Discrete derivatives +- Averaging operators +- Cross product matrices + + + + + +## Functions + + + +### Function `e2h` {#meanas.fdfd.operators.e2h} + + + + + + +> `def e2h(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Utility operator for converting the E field into the H field. +For use with [e\_full()](#meanas.fdfd.operators.e\_full) -- assumes that there is no magnetic current M. + + +Args +-----= +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```mu```** +: Vectorized magnetic permeability (default 1 everywhere) + + +**```pmc```** +: Vectorized mask specifying PMC cells. Any cells where `pmc != 0` are interpreted + as containing a perfect magnetic conductor (PMC). + The PMC is applied per-field-component (i.e. `pmc.size == epsilon.size`) + + + +Returns +-----= +Sparse matrix for converting E to H. + + +### Function `e_boundary_source` {#meanas.fdfd.operators.e_boundary_source} + + + + + + +> `def e_boundary_source(mask: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, periodic_mask_edges: bool = False) -> scipy.sparse._matrix.spmatrix` + + +Operator that turns an E-field distrubtion into a current (J) distribution + along the edges (external and internal) of the provided mask. This is just an + [e\_tfsf\_source()](#meanas.fdfd.operators.e\_tfsf\_source) with an additional masking step. + + +Args +-----= +**```mask```** +: The current distribution is generated at the edges of the mask, + i.e. any points where shifting the mask by one cell in any direction + would change its value. + + +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```epsilon```** +: Vectorized dielectric constant + + +**```mu```** +: Vectorized magnetic permeability (default 1 everywhere). + + + +Returns +-----= +Sparse matrix that turns an E-field into a current (J) distribution. + + +### Function `e_full` {#meanas.fdfd.operators.e_full} + + + + + + +> `def e_full(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Wave operator + $$ \nabla \times (\frac{1}{\mu} \nabla \times) - \Omega^2 \epsilon $$ + + del x (1/mu * del x) - omega**2 * epsilon + + for use with the E-field, with wave equation + $$ (\nabla \times (\frac{1}{\mu} \nabla \times) - \Omega^2 \epsilon) E = -\imath \omega J $$ + + (del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J + +To make this matrix symmetric, use the preconditioners from [e\_full\_preconditioners()](#meanas.fdfd.operators.e\_full\_preconditioners). + + +Args +-----= +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```epsilon```** +: Vectorized dielectric constant + + +**```mu```** +: Vectorized magnetic permeability (default 1 everywhere). + + +**```pec```** +: Vectorized mask specifying PEC cells. Any cells where `pec != 0` are interpreted + as containing a perfect electrical conductor (PEC). + The PEC is applied per-field-component (i.e. `pec.size == epsilon.size`) + + +**```pmc```** +: Vectorized mask specifying PMC cells. Any cells where `pmc != 0` are interpreted + as containing a perfect magnetic conductor (PMC). + The PMC is applied per-field-component (i.e. `pmc.size == epsilon.size`) + + + +Returns +-----= +Sparse matrix containing the wave operator. + + +### Function `e_full_preconditioners` {#meanas.fdfd.operators.e_full_preconditioners} + + + + + + +> `def e_full_preconditioners(dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]]) -> tuple[scipy.sparse._matrix.spmatrix, scipy.sparse._matrix.spmatrix]` + + +Left and right preconditioners (Pl, Pr) for symmetrizing the [e\_full()](#meanas.fdfd.operators.e\_full) wave operator. + +The preconditioned matrix `A_symm = (Pl @ A @ Pr)` is complex-symmetric + (non-Hermitian unless there is no loss or PMLs). + +The preconditioner matrices are diagonal and complex, with `Pr = 1 / Pl` + + +Args +-----= +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + + +Returns +-----= +Preconditioner matrices (Pl, Pr). + + +### Function `e_tfsf_source` {#meanas.fdfd.operators.e_tfsf_source} + + + + + + +> `def e_tfsf_source(TF_region: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Operator that turns a desired E-field distribution into a + total-field/scattered-field (TFSF) source. + +TODO: Reference Rumpf paper + + +Args +-----= +**```TF_region```** +: Mask, which is set to 1 inside the total-field region and 0 in the + scattered-field region + + +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```epsilon```** +: Vectorized dielectric constant + + +**```mu```** +: Vectorized magnetic permeability (default 1 everywhere). + + + +Returns +-----= +Sparse matrix that turns an E-field into a current (J) distribution. + + +### Function `eh_full` {#meanas.fdfd.operators.eh_full} + + + + + + +> `def eh_full(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Wave operator for \[E, H] field representation. This operator implements Maxwell's + equations without cancelling out either E or H. The operator is +$$ \begin{bmatrix} + -\imath \omega \epsilon & \nabla \times \\ + \nabla \times & \imath \omega \mu + \end{bmatrix} $$ + + [[-i * omega * epsilon, del x ], + [del x, i * omega * mu]] + +for use with a field vector of the form cat(vec(E), vec(H)): +$$ \begin{bmatrix} + -\imath \omega \epsilon & \nabla \times \\ + \nabla \times & \imath \omega \mu + \end{bmatrix} + \begin{bmatrix} E \\ + H + \end{bmatrix} + = \begin{bmatrix} J \\ + -M + \end{bmatrix} $$ + + +Args +-----= +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```epsilon```** +: Vectorized dielectric constant + + +**```mu```** +: Vectorized magnetic permeability (default 1 everywhere) + + +**```pec```** +: Vectorized mask specifying PEC cells. Any cells where `pec != 0` are interpreted + as containing a perfect electrical conductor (PEC). + The PEC is applied per-field-component (i.e. `pec.size == epsilon.size`) + + +**```pmc```** +: Vectorized mask specifying PMC cells. Any cells where `pmc != 0` are interpreted + as containing a perfect magnetic conductor (PMC). + The PMC is applied per-field-component (i.e. `pmc.size == epsilon.size`) + + + +Returns +-----= +Sparse matrix containing the wave operator. + + +### Function `h_full` {#meanas.fdfd.operators.h_full} + + + + + + +> `def h_full(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Wave operator + $$ \nabla \times (\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu $$ + + del x (1/epsilon * del x) - omega**2 * mu + + for use with the H-field, with wave equation + $$ (\nabla \times (\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu) E = \imath \omega M $$ + + (del x (1/epsilon * del x) - omega**2 * mu) E = i * omega * M + + +Args +-----= +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```epsilon```** +: Vectorized dielectric constant + + +**```mu```** +: Vectorized magnetic permeability (default 1 everywhere) + + +**```pec```** +: Vectorized mask specifying PEC cells. Any cells where `pec != 0` are interpreted + as containing a perfect electrical conductor (PEC). + The PEC is applied per-field-component (i.e. `pec.size == epsilon.size`) + + +**```pmc```** +: Vectorized mask specifying PMC cells. Any cells where `pmc != 0` are interpreted + as containing a perfect magnetic conductor (PMC). + The PMC is applied per-field-component (i.e. `pmc.size == epsilon.size`) + + + +Returns +-----= +Sparse matrix containing the wave operator. + + +### Function `m2j` {#meanas.fdfd.operators.m2j} + + + + + + +> `def m2j(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Operator for converting a magnetic current M into an electric current J. +For use with eg. [e\_full()](#meanas.fdfd.operators.e\_full). + + +Args +-----= +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```mu```** +: Vectorized magnetic permeability (default 1 everywhere) + + + +Returns +-----= +Sparse matrix for converting M to J. + + +### Function `poynting_e_cross` {#meanas.fdfd.operators.poynting_e_cross} + + + + + + +> `def poynting_e_cross(e: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]]) -> scipy.sparse._matrix.spmatrix` + + +Operator for computing the Poynting vector, containing the +(E x) portion of the Poynting vector. + + +Args +-----= +**```e```** +: Vectorized E-field for the ExH cross product + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + + +Returns +-----= +Sparse matrix containing (E x) portion of Poynting cross product. + + +### Function `poynting_h_cross` {#meanas.fdfd.operators.poynting_h_cross} + + + + + + +> `def poynting_h_cross(h: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]]) -> scipy.sparse._matrix.spmatrix` + + +Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector. + + +Args +-----= +**```h```** +: Vectorized H-field for the HxE cross product + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + + +Returns +-----= +Sparse matrix containing (H x) portion of Poynting cross product. + + + + +------------------------------------------- + + + +# Module `meanas.fdfd.scpml` {#meanas.fdfd.scpml} + +Functions for creating stretched coordinate perfectly matched layer (PML) absorbers. + + + + +## Variables + + + +### Variable `s_function_t` {#meanas.fdfd.scpml.s_function_t} + + + +Typedef for s-functions, see [prepare\_s\_function()](#meanas.fdfd.scpml.prepare\_s\_function) + + + +## Functions + + + +### Function `prepare_s_function` {#meanas.fdfd.scpml.prepare_s_function} + + + + + + +> `def prepare_s_function(ln_R: float = -16, m: float = 4) -> collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]` + + +Create an s_function to pass to the SCPML functions. This is used when you would like to +customize the PML parameters. + + +Args +-----= +**```ln_R```** +: Natural logarithm of the desired reflectance + + +**```m```** +: Polynomial order for the PML (imaginary part increases as distance ** m) + + + +Returns +-----= +An s_function, which takes an ndarray (distances) and returns an ndarray (complex part +of the cell width; needs to be divided by `sqrt(epilon_effective) * real(omega))` +before use. + + +### Function `stretch_with_scpml` {#meanas.fdfd.scpml.stretch_with_scpml} + + + + + + +> `def stretch_with_scpml(dxes: list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], axis: int, polarity: int, omega: float, epsilon_effective: float = 1.0, thickness: int = 10, s_function: collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] | None = None) -> list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]]` + + +Stretch dxes to contain a stretched-coordinate PML (SCPML) in one direction along one axis. + + +Args +-----= +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```axis```** +: axis to stretch (0=x, 1=y, 2=z) + + +**```polarity```** +: direction to stretch (-1 for -ve, +1 for +ve) + + +**```omega```** +: Angular frequency for the simulation + + +**```epsilon_effective```** +: Effective epsilon of the PML. Match this to the material at the + edge of your grid. Default 1. + + +**```thickness```** +: number of cells to use for pml (default 10) + + +**```s_function```** +: Created by [prepare\_s\_function()](#meanas.fdfd.scpml.prepare\_s\_function)(...), allowing customization + of pml parameters. Default uses [prepare\_s\_function()](#meanas.fdfd.scpml.prepare\_s\_function) with no parameters. + + + +Returns +-----= +Complex cell widths (dx_lists_mut) as discussed in [meanas.fdmath.types](#meanas.fdmath.types). +Multiple calls to this function may be necessary if multiple absorpbing boundaries are needed. + + +### Function `uniform_grid_scpml` {#meanas.fdfd.scpml.uniform_grid_scpml} + + + + + + +> `def uniform_grid_scpml(shape: collections.abc.Sequence[int], thicknesses: collections.abc.Sequence[int], omega: float, epsilon_effective: float = 1.0, s_function: collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]], numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] | None = None) -> list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]]` + + +Create dx arrays for a uniform grid with a cell width of 1 and a pml. + +If you want something more fine-grained, check out [stretch\_with\_scpml()](#meanas.fdfd.scpml.stretch\_with\_scpml)(...). + + +Args +-----= +**```shape```** +: Shape of the grid, including the PMLs (which are 2*thicknesses thick) + + +**```thicknesses```** +: \[th\_x, th\_y, th\_z] + Thickness of the PML in each direction. + Both polarities are added. + Each th_ of pml is applied twice, once on each edge of the grid along the given axis. + `th_*` may be zero, in which case no pml is added. + + +**```omega```** +: Angular frequency for the simulation + + +**```epsilon_effective```** +: Effective epsilon of the PML. Match this to the material + at the edge of your grid. + Default 1. + + +**```s_function```** +: created by [prepare\_s\_function()](#meanas.fdfd.scpml.prepare\_s\_function)(...), allowing customization of pml parameters. + Default uses [prepare\_s\_function()](#meanas.fdfd.scpml.prepare\_s\_function) with no parameters. + + + +Returns +-----= +Complex cell widths (dx_lists_mut) as discussed in [meanas.fdmath.types](#meanas.fdmath.types). + + + + +------------------------------------------- + + + +# Module `meanas.fdfd.solvers` {#meanas.fdfd.solvers} + +Solvers and solver interface for FDFD problems. + + + + + +## Functions + + + +### Function `generic` {#meanas.fdfd.solvers.generic} + + + + + + +> `def generic(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], J: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, adjoint: bool = False, matrix_solver: collections.abc.Callable[..., typing.Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[typing.Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[typing.Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[typing.Union[bool, int, float, complex, str, bytes]]]] = , matrix_solver_opts: dict[str, typing.Any] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]` + + +Conjugate gradient FDFD solver using CSR sparse matrices. + +All ndarray arguments should be 1D arrays, as returned by [vec()](#meanas.fdmath.vectorization.vec). + + +Args +-----= +**```omega```** +: Complex frequency to solve at. + + +**```dxes```** +: \[\[dx\_e, dy\_e, dz\_e], \[dx\_h, dy\_h, dz\_h]] (complex cell sizes) as + discussed in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```J```** +: Electric current distribution (at E-field locations) + + +**```epsilon```** +: Dielectric constant distribution (at E-field locations) + + +**```mu```** +: Magnetic permeability distribution (at H-field locations) + + +**```pec```** +: Perfect electric conductor distribution + (at E-field locations; non-zero value indicates PEC is present) + + +**```pmc```** +: Perfect magnetic conductor distribution + (at H-field locations; non-zero value indicates PMC is present) + + +**```adjoint```** +: If true, solves the adjoint problem. + + +**```matrix_solver```** +: Called as `matrix_solver(A, b, **matrix_solver_opts) -> x`, + where A: scipy.sparse.csr\_matrix; + b: ArrayLike; + x: ArrayLike; + Default is a wrapped version of scipy.sparse.linalg.qmr() + which doesn't return convergence info and logs the residual + every 100 iterations. + + +**```matrix_solver_opts```** +: Passed as kwargs to matrix\_solver(...) + + + +Returns +-----= +E-field which solves the system. + + + + +------------------------------------------- + + + +# Module `meanas.fdfd.waveguide_2d` {#meanas.fdfd.waveguide_2d} + +Operators and helper functions for waveguides with unchanging cross-section. + +The propagation direction is chosen to be along the z axis, and all fields +are given an implicit z-dependence of the form `exp(-1 * wavenumber * z)`. + +As the z-dependence is known, all the functions in this file assume a 2D grid + (i.e. `dxes = [[[dx_e[0], dx_e[1], ...], [dy_e[0], ...]], [[dx_h[0], ...], [dy_h[0], ...]]]`). + + +=============== + +Consider Maxwell's equations in continuous space, in the frequency domain. Assuming +a structure with some (x, y) cross-section extending uniformly into the z dimension, +with a diagonal $\epsilon$ tensor, we have + +$$ +\begin{aligned} +\nabla \times \vec{E}(x, y, z) &= -\imath \omega \mu \vec{H} \\ +\nabla \times \vec{H}(x, y, z) &= \imath \omega \epsilon \vec{E} \\ +\vec{E}(x,y,z) &= (\vec{E}_t(x, y) + E_z(x, y)\vec{z}) e^{-\imath \beta z} \\ +\vec{H}(x,y,z) &= (\vec{H}_t(x, y) + H_z(x, y)\vec{z}) e^{-\imath \beta z} \\ +\end{aligned} +$$ + +Expanding the first two equations into vector components, we get + +$$ +\begin{aligned} +-\imath \omega \mu_{xx} H_x &= \partial_y E_z - \partial_z E_y \\ +-\imath \omega \mu_{yy} H_y &= \partial_z E_x - \partial_x E_z \\ +-\imath \omega \mu_{zz} H_z &= \partial_x E_y - \partial_y E_x \\ +\imath \omega \epsilon_{xx} E_x &= \partial_y H_z - \partial_z H_y \\ +\imath \omega \epsilon_{yy} E_y &= \partial_z H_x - \partial_x H_z \\ +\imath \omega \epsilon_{zz} E_z &= \partial_x H_y - \partial_y H_x \\ +\end{aligned} +$$ + +Substituting in our expressions for $\vec{E}$, $\vec{H}$ and discretizing: + +$$ +\begin{aligned} +-\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \imath \beta E_y \\ +-\imath \omega \mu_{yy} H_y &= -\imath \beta E_x - \tilde{\partial}_x E_z \\ +-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_x E_y - \tilde{\partial}_y E_x \\ +\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath \beta H_y \\ +\imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - \hat{\partial}_x H_z \\ +\imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\ +\end{aligned} +$$ + +Rewrite the last three equations as + +$$ +\begin{aligned} +\imath \beta H_y &= \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\ +\imath \beta H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\ +\imath \omega E_z &= \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y - \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\ +\end{aligned} +$$ + +Now apply $\imath \beta \tilde{\partial}_x$ to the last equation, +then substitute in for $\imath \beta H_x$ and $\imath \beta H_y$: + +$$ +\begin{aligned} +\imath \beta \tilde{\partial}_x \imath \omega E_z &= \imath \beta \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y + - \imath \beta \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\ + &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z) + - \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\ + &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x) + - \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y) \\ +\imath \beta \tilde{\partial}_x E_z &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\ +\end{aligned} +$$ + +With a similar approach (but using $\imath \beta \tilde{\partial}_y$ instead), we can get + +$$ +\begin{aligned} +\imath \beta \tilde{\partial}_y E_z &= \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\ +\end{aligned} +$$ + +We can combine this equation for $\imath \beta \tilde{\partial}_y E_z$ with +the unused $\imath \omega \mu_{xx} H_x$ and $\imath \omega \mu_{yy} H_y$ equations to get + +$$ +\begin{aligned} +-\imath \omega \mu_{xx} \imath \beta H_x &= -\beta^2 E_y + \imath \beta \tilde{\partial}_y E_z \\ +-\imath \omega \mu_{xx} \imath \beta H_x &= -\beta^2 E_y + \tilde{\partial}_y ( + \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + )\\ +\end{aligned} +$$ + +and + +$$ +\begin{aligned} +-\imath \omega \mu_{yy} \imath \beta H_y &= \beta^2 E_x - \imath \beta \tilde{\partial}_x E_z \\ +-\imath \omega \mu_{yy} \imath \beta H_y &= \beta^2 E_x - \tilde{\partial}_x ( + \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + )\\ +\end{aligned} +$$ + +However, based on our rewritten equation for $\imath \beta H_x$ and the so-far unused +equation for $\imath \omega \mu_{zz} H_z$ we can also write + +$$ +\begin{aligned} +-\imath \omega \mu_{xx} (\imath \beta H_x) &= -\imath \omega \mu_{xx} (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\ + &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y + \imath \omega \mu_{xx} \hat{\partial}_x ( + \frac{1}{-\imath \omega \mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x)) \\ + &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y + -\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +\end{aligned} +$$ + +and, similarly, + +$$ +\begin{aligned} +-\imath \omega \mu_{yy} (\imath \beta H_y) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x + +\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +\end{aligned} +$$ + +By combining both pairs of expressions, we get + +$$ +\begin{aligned} +\beta^2 E_x - \tilde{\partial}_x ( + \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + ) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x + +\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +-\beta^2 E_y + \tilde{\partial}_y ( + \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + ) &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y + -\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +\end{aligned} +$$ + +Using these, we can construct the eigenvalue problem + +$$ +\beta^2 \begin{bmatrix} E_x \\ + E_y \end{bmatrix} = + (\omega^2 \begin{bmatrix} \mu_{yy} \epsilon_{xx} & 0 \\ + 0 & \mu_{xx} \epsilon_{yy} \end{bmatrix} + + \begin{bmatrix} -\mu_{yy} \hat{\partial}_y \\ + \mu_{xx} \hat{\partial}_x \end{bmatrix} \mu_{zz}^{-1} + \begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} + + \begin{bmatrix} \tilde{\partial}_x \\ + \tilde{\partial}_y \end{bmatrix} \epsilon_{zz}^{-1} + \begin{bmatrix} \hat{\partial}_x \epsilon_{xx} & \hat{\partial}_y \epsilon_{yy} \end{bmatrix}) + \begin{bmatrix} E_x \\ + E_y \end{bmatrix} +$$ + +In the literature, $\beta$ is usually used to denote the lossless/real part of the propagation constant, +but in [meanas](#meanas) it is allowed to be complex. + +An equivalent eigenvalue problem can be formed using the $H_x$ and $H_y$ fields, if those are more convenient. + +Note that $E_z$ was never discretized, so $\beta$ will need adjustment to account for numerical dispersion +if the result is introduced into a space with a discretized z-axis. + + + + + +## Functions + + + +### Function `curl_e` {#meanas.fdfd.waveguide_2d.curl_e} + + + + + + +> `def curl_e(wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]]) -> scipy.sparse._matrix.spmatrix` + + +Discretized curl operator for use with the waveguide E field. + + +Args +-----= +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + + +Returns +-----= +Sparse matrix representation of the operator. + + +### Function `curl_h` {#meanas.fdfd.waveguide_2d.curl_h} + + + + + + +> `def curl_h(wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]]) -> scipy.sparse._matrix.spmatrix` + + +Discretized curl operator for use with the waveguide H field. + + +Args +-----= +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + + +Returns +-----= +Sparse matrix representation of the operator. + + +### Function `e2h` {#meanas.fdfd.waveguide_2d.e2h} + + + + + + +> `def e2h(wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Returns an operator which, when applied to a vectorized E eigenfield, produces + the vectorized H eigenfield. + + +Args +-----= +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + + +**```omega```** +: The angular frequency of the system + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```mu```** +: Vectorized magnetic permeability grid (default 1 everywhere) + + + +Returns +-----= +Sparse matrix representation of the operator. + + +### Function `e_err` {#meanas.fdfd.waveguide_2d.e_err} + + + + + + +> `def e_err(e: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> float` + + +Calculates the relative error in the E field + + +Args +-----= +**```e```** +: Vectorized E field + + +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + + +**```omega```** +: The angular frequency of the system + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + +**```mu```** +: Vectorized magnetic permeability grid (default 1 everywhere) + + + +Returns +-----= +Relative error `norm(A_e @ e) / norm(e)`. + + +### Function `exy2e` {#meanas.fdfd.waveguide_2d.exy2e} + + + + + + +> `def exy2e(wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]) -> scipy.sparse._matrix.spmatrix` + + +Operator which transforms the vector e\_xy containing the vectorized E_x and E_y fields, + into a vectorized E containing all three E components + +From the operator derivation (see module docs), we have + +$$ +\imath \omega \epsilon_{zz} E_z = \hat{\partial}_x H_y - \hat{\partial}_y H_x \\ +$$ + +as well as the intermediate equations + +$$ +\begin{aligned} +\imath \beta H_y &= \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\ +\imath \beta H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\ +\end{aligned} +$$ + +Combining these, we get + +$$ +\begin{aligned} +E_z &= \frac{1}{- \omega \beta \epsilon_{zz}} (( + \hat{\partial}_y \hat{\partial}_x H_z + -\hat{\partial}_x \hat{\partial}_y H_z) + + \imath \omega (\hat{\partial}_x \epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y)) + &= \frac{1}{\imath \beta \epsilon_{zz}} (\hat{\partial}_x \epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y) +\end{aligned} +$$ + + +Args +-----= +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy` + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + + +Returns +-----= +Sparse matrix representing the operator. + + +### Function `exy2h` {#meanas.fdfd.waveguide_2d.exy2h} + + + + + + +> `def exy2h(wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Operator which transforms the vector e\_xy containing the vectorized E_x and E_y fields, + into a vectorized H containing all three H components + + +Args +-----= +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. + It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy` + + +**```omega```** +: The angular frequency of the system + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + +**```mu```** +: Vectorized magnetic permeability grid (default 1 everywhere) + + + +Returns +-----= +Sparse matrix representing the operator. + + +### Function `get_abcd` {#meanas.fdfd.waveguide_2d.get_abcd} + + + + + + +> `def get_abcd(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, **kwargs)` + + + + + +### Function `get_s` {#meanas.fdfd.waveguide_2d.get_s} + + + + + + +> `def get_s(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, force_nogain: bool = False, force_reciprocal: bool = False, **kwargs)` + + + + + +### Function `get_tr` {#meanas.fdfd.waveguide_2d.get_tr} + + + + + + +> `def get_tr(ehL, wavenumbers_L, ehR, wavenumbers_R, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]])` + + + + + +### Function `h2e` {#meanas.fdfd.waveguide_2d.h2e} + + + + + + +> `def h2e(wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]) -> scipy.sparse._matrix.spmatrix` + + +Returns an operator which, when applied to a vectorized H eigenfield, produces + the vectorized E eigenfield. + + +Args +-----= +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + + +**```omega```** +: The angular frequency of the system + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + + +Returns +-----= +Sparse matrix representation of the operator. + + +### Function `h_err` {#meanas.fdfd.waveguide_2d.h_err} + + + + + + +> `def h_err(h: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> float` + + +Calculates the relative error in the H field + + +Args +-----= +**```h```** +: Vectorized H field + + +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + + +**```omega```** +: The angular frequency of the system + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + +**```mu```** +: Vectorized magnetic permeability grid (default 1 everywhere) + + + +Returns +-----= +Relative error `norm(A_h @ h) / norm(h)`. + + +### Function `hxy2e` {#meanas.fdfd.waveguide_2d.hxy2e} + + + + + + +> `def hxy2e(wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Operator which transforms the vector h\_xy containing the vectorized H_x and H_y fields, + into a vectorized E containing all three E components + + +Args +-----= +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. + It should satisfy `operator_h() @ h_xy == wavenumber**2 * h_xy` + + +**```omega```** +: The angular frequency of the system + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + +**```mu```** +: Vectorized magnetic permeability grid (default 1 everywhere) + + + +Returns +-----= +Sparse matrix representing the operator. + + +### Function `hxy2h` {#meanas.fdfd.waveguide_2d.hxy2h} + + + + + + +> `def hxy2h(wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Operator which transforms the vector h\_xy containing the vectorized H_x and H_y fields, + into a vectorized H containing all three H components + + +Args +-----= +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. + It should satisfy `operator_h() @ h_xy == wavenumber**2 * h_xy` + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```mu```** +: Vectorized magnetic permeability grid (default 1 everywhere) + + + +Returns +-----= +Sparse matrix representing the operator. + + +### Function `inner_product` {#meanas.fdfd.waveguide_2d.inner_product} + + + + + + +> `def inner_product(e1: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], h2: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], prop_phase: float = 0, conj_h: bool = False, trapezoid: bool = False) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]` + + + + + +### Function `normalized_fields_e` {#meanas.fdfd.waveguide_2d.normalized_fields_e} + + + + + + +> `def normalized_fields_e(e_xy: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, prop_phase: float = 0) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]` + + +Given a vector e\_xy containing the vectorized E_x and E_y fields, + returns normalized, vectorized E and H fields for the system. + + +Args +-----= +**```e_xy```** +: Vector containing E_x and E_y fields + + +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. + It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy` + + +**```omega```** +: The angular frequency of the system + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + +**```mu```** +: Vectorized magnetic permeability grid (default 1 everywhere) + + +**```prop_phase```** +: Phase shift `(dz * corrected_wavenumber)` over 1 cell in propagation direction. + Default 0 (continuous propagation direction, i.e. dz->0). + + + +Returns +-----= +(e, h), where each field is vectorized, normalized, +and contains all three vector components. + + +### Function `normalized_fields_h` {#meanas.fdfd.waveguide_2d.normalized_fields_h} + + + + + + +> `def normalized_fields_h(h_xy: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, prop_phase: float = 0) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]` + + +Given a vector h\_xy containing the vectorized H_x and H_y fields, + returns normalized, vectorized E and H fields for the system. + + +Args +-----= +**```h_xy```** +: Vector containing H_x and H_y fields + + +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. + It should satisfy `operator_h() @ h_xy == wavenumber**2 * h_xy` + + +**```omega```** +: The angular frequency of the system + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + +**```mu```** +: Vectorized magnetic permeability grid (default 1 everywhere) + + +**```prop_phase```** +: Phase shift `(dz * corrected_wavenumber)` over 1 cell in propagation direction. + Default 0 (continuous propagation direction, i.e. dz->0). + + + +Returns +-----= +(e, h), where each field is vectorized, normalized, +and contains all three vector components. + + +### Function `operator_e` {#meanas.fdfd.waveguide_2d.operator_e} + + + + + + +> `def operator_e(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Waveguide operator of the form + + omega**2 * mu * epsilon + + mu * [[-Dy], [Dx]] / mu * [-Dy, Dx] + + [[Dx], [Dy]] / epsilon * [Dx, Dy] * epsilon + +for use with a field vector of the form cat(\[E\_x, E\_y]). + +More precisely, the operator is + +$$ +\omega^2 \begin{bmatrix} \mu_{yy} \epsilon_{xx} & 0 \\ + 0 & \mu_{xx} \epsilon_{yy} \end{bmatrix} + + \begin{bmatrix} -\mu_{yy} \hat{\partial}_y \\ + \mu_{xx} \hat{\partial}_x \end{bmatrix} \mu_{zz}^{-1} + \begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} + + \begin{bmatrix} \tilde{\partial}_x \\ + \tilde{\partial}_y \end{bmatrix} \epsilon_{zz}^{-1} + \begin{bmatrix} \hat{\partial}_x \epsilon_{xx} & \hat{\partial}_y \epsilon_{yy} \end{bmatrix} +$$ + +$\tilde{\partial}_x$ and $\hat{\partial}_x$ are the forward and backward derivatives along x, +and each $\epsilon_{xx}$, $\mu_{yy}$, etc. is a diagonal matrix containing the vectorized material +property distribution. + +This operator can be used to form an eigenvalue problem of the form +`operator_e(...) @ [E_x, E_y] = wavenumber**2 * [E_x, E_y]` + +which can then be solved for the eigenmodes of the system (an `exp(-i * wavenumber * z)` +z-dependence is assumed for the fields). + + +Args +-----= +**```omega```** +: The angular frequency of the system. + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + +**```mu```** +: Vectorized magnetic permeability grid (default 1 everywhere) + + + +Returns +-----= +Sparse matrix representation of the operator. + + +### Function `operator_h` {#meanas.fdfd.waveguide_2d.operator_h} + + + + + + +> `def operator_h(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Waveguide operator of the form + + omega**2 * epsilon * mu + + epsilon * [[-Dy], [Dx]] / epsilon * [-Dy, Dx] + + [[Dx], [Dy]] / mu * [Dx, Dy] * mu + +for use with a field vector of the form cat(\[H\_x, H\_y]). + +More precisely, the operator is + +$$ +\omega^2 \begin{bmatrix} \epsilon_{yy} \mu_{xx} & 0 \\ + 0 & \epsilon_{xx} \mu_{yy} \end{bmatrix} + + \begin{bmatrix} -\epsilon_{yy} \tilde{\partial}_y \\ + \epsilon_{xx} \tilde{\partial}_x \end{bmatrix} \epsilon_{zz}^{-1} + \begin{bmatrix} -\hat{\partial}_y & \hat{\partial}_x \end{bmatrix} + + \begin{bmatrix} \hat{\partial}_x \\ + \hat{\partial}_y \end{bmatrix} \mu_{zz}^{-1} + \begin{bmatrix} \tilde{\partial}_x \mu_{xx} & \tilde{\partial}_y \mu_{yy} \end{bmatrix} +$$ + +$\tilde{\partial}_x$ and $\hat{\partial}_x$ are the forward and backward derivatives along x, +and each $\epsilon_{xx}$, $\mu_{yy}$, etc. is a diagonal matrix containing the vectorized material +property distribution. + +This operator can be used to form an eigenvalue problem of the form +`operator_h(...) @ [H_x, H_y] = wavenumber**2 * [H_x, H_y]` + +which can then be solved for the eigenmodes of the system (an `exp(-i * wavenumber * z)` +z-dependence is assumed for the fields). + + +Args +-----= +**```omega```** +: The angular frequency of the system. + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + +**```mu```** +: Vectorized magnetic permeability grid (default 1 everywhere) + + + +Returns +-----= +Sparse matrix representation of the operator. + + +### Function `sensitivity` {#meanas.fdfd.waveguide_2d.sensitivity} + + + + + + +> `def sensitivity(e_norm: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], h_norm: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]` + + +Given a waveguide structure (dxes, epsilon, mu) and mode fields +(e\_norm, h\_norm, wavenumber, omega), calculates the sensitivity of the wavenumber +$\beta$ to changes in the dielectric structure $\epsilon$. + +The output is a vector of the same size as vec(epsilon), with each element specifying the +sensitivity of wavenumber to changes in the corresponding element in vec(epsilon), i.e. + +$$sens_{i} = \frac{\partial\beta}{\partial\epsilon_i}$$ + +An adjoint approach is used to calculate the sensitivity; the derivation is provided here: + +Starting with the eigenvalue equation + +$$\beta^2 E_{xy} = A_E E_{xy}$$ + +where $A_E$ is the waveguide operator from [operator\_e()](#meanas.fdfd.waveguide\_2d.operator\_e), and $E_{xy} = \begin{bmatrix} E_x \\ + E_y \end{bmatrix}$, +we can differentiate with respect to one of the $\epsilon$ elements (i.e. at one Yee grid point), $\epsilon_i$: + +$$ +(2 \beta) \partial_{\epsilon_i}(\beta) E_{xy} + \beta^2 \partial_{\epsilon_i} E_{xy} + = \partial_{\epsilon_i}(A_E) E_{xy} + A_E \partial_{\epsilon_i} E_{xy} +$$ + +We then multiply by $H_{yx}^\star = \begin{bmatrix}H_y^\star \\ -H_x^\star \end{bmatrix}$ from the left: + +$$ +(2 \beta) \partial_{\epsilon_i}(\beta) H_{yx}^\star E_{xy} + \beta^2 H_{yx}^\star \partial_{\epsilon_i} E_{xy} + = H_{yx}^\star \partial_{\epsilon_i}(A_E) E_{xy} + H_{yx}^\star A_E \partial_{\epsilon_i} E_{xy} +$$ + +However, $H_{yx}^\star$ is actually a left-eigenvector of $A_E$. This can be verified by inspecting +the form of [operator\_h()](#meanas.fdfd.waveguide\_2d.operator\_h) ($A_H$) and comparing its conjugate transpose to [operator\_e()](#meanas.fdfd.waveguide\_2d.operator\_e) ($A_E$). Also, note +$H_{yx}^\star \cdot E_{xy} = H^\star \times E$ recalls the mode orthogonality relation. See doi:10.5194/ars-9-85-201 +for a similar approach. Therefore, + +$$ +H_{yx}^\star A_E \partial_{\epsilon_i} E_{xy} = \beta^2 H_{yx}^\star \partial_{\epsilon_i} E_{xy} +$$ + +and we can simplify to + +$$ +\partial_{\epsilon_i}(\beta) + = \frac{1}{2 \beta} \frac{H_{yx}^\star \partial_{\epsilon_i}(A_E) E_{xy} }{H_{yx}^\star E_{xy}} +$$ + +This expression can be quickly calculated for all $i$ by writing out the various terms of +$\partial_{\epsilon_i} A_E$ and recognizing that the vector-matrix-vector products (i.e. scalars) +$sens_i = \vec{v}_{left} \partial_{\epsilon_i} (\epsilon_{xyz}) \vec{v}_{right}$, indexed by $i$, can be expressed as +elementwise multiplications $\vec{sens} = \vec{v}_{left} \star \vec{v}_{right}$ + + + +Args +-----= +**```e_norm```** +: Normalized, vectorized E_xyz field for the mode. E.g. as returned by [normalized\_fields\_e()](#meanas.fdfd.waveguide\_2d.normalized\_fields\_e). + + +**```h_norm```** +: Normalized, vectorized H_xyz field for the mode. E.g. as returned by [normalized\_fields\_e()](#meanas.fdfd.waveguide\_2d.normalized\_fields\_e). + + +**```wavenumber```** +: Propagation constant for the mode. The z-axis is assumed to be continuous (i.e. without numerical dispersion). + + +**```omega```** +: The angular frequency of the system. + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + +**```mu```** +: Vectorized magnetic permeability grid (default 1 everywhere) + + + +Returns +-----= +Sparse matrix representation of the operator. + + +### Function `solve_mode` {#meanas.fdfd.waveguide_2d.solve_mode} + + + + + + +> `def solve_mode(mode_number: int, *args: Any, **kwargs: Any) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], complex]` + + +Wrapper around [solve\_modes()](#meanas.fdfd.waveguide\_2d.solve\_modes) that solves for a single mode. + + +Args +-----= +**```mode_number```** +: 0-indexed mode number to solve for + + +**```*args```** +: passed to [solve\_modes()](#meanas.fdfd.waveguide\_2d.solve\_modes) + + +**```**kwargs```** +: passed to [solve\_modes()](#meanas.fdfd.waveguide\_2d.solve\_modes) + + + +Returns +-----= +(e_xy, wavenumber) + + +### Function `solve_modes` {#meanas.fdfd.waveguide_2d.solve_modes} + + + + + + +> `def solve_modes(mode_numbers: collections.abc.Sequence[int], omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, mode_margin: int = 2) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]` + + +Given a 2D region, attempts to solve for the eigenmode with the specified mode numbers. + + +Args +-----= +**```mode_numbers```** +: List of 0-indexed mode numbers to solve for + + +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```epsilon```** +: Dielectric constant + + +**```mu```** +: Magnetic permeability (default 1 everywhere) + + +**```mode_margin```** +: The eigensolver will actually solve for `(max(mode_number) + mode_margin)` + modes, but only return the target mode. Increasing this value can improve the solver's + ability to find the correct mode. Default 2. + + + +Returns +-----= +e\_xys +: NDArray of vfdfield_t specifying fields. First dimension is mode number. + + +wavenumbers +: list of wavenumbers + + + + + + +------------------------------------------- + + + +# Module `meanas.fdfd.waveguide_3d` {#meanas.fdfd.waveguide_3d} + +Tools for working with waveguide modes in 3D domains. + +This module relies heavily on waveguide\_2d and mostly just transforms +its parameters into 2D equivalents and expands the results back into 3D. + + + + + +## Functions + + + +### Function `compute_overlap_e` {#meanas.fdfd.waveguide_3d.compute_overlap_e} + + + + + + +> `def compute_overlap_e(E: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], axis: int, polarity: int, slices: collections.abc.Sequence[slice]) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]` + + +Given an eigenmode obtained by [solve\_mode()](#meanas.fdfd.waveguide\_3d.solve\_mode), calculates an overlap_e for the +mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn) +[assumes reflection symmetry]. + +TODO: add reference + + +Args +-----= +**```E```** +: E-field of the mode + + +**```H```** +: H-field of the mode (advanced by half of a Yee cell from E) + + +**```wavenumber```** +: Wavenumber of the mode + + +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```axis```** +: Propagation axis (0=x, 1=y, 2=z) + + +**```polarity```** +: Propagation direction (+1 for +ve, -1 for -ve) + + +**```slices```** +: epsilon\[tuple(slices)] is used to select the portion of the grid to use + as the waveguide cross-section. slices[axis] should select only one item. + + +**```mu```** +: Magnetic permeability (default 1 everywhere) + + + +Returns +-----= +overlap_e such that `numpy.sum(overlap_e * other_e.conj())` computes the overlap integral + + +### Function `compute_source` {#meanas.fdfd.waveguide_3d.compute_source} + + + + + + +> `def compute_source(E: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], axis: int, polarity: int, slices: collections.abc.Sequence[slice], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]` + + +Given an eigenmode obtained by [solve\_mode()](#meanas.fdfd.waveguide\_3d.solve\_mode), returns the current source distribution +necessary to position a unidirectional source at the slice location. + + +Args +-----= +**```E```** +: E-field of the mode + + +**```wavenumber```** +: Wavenumber of the mode + + +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```axis```** +: Propagation axis (0=x, 1=y, 2=z) + + +**```polarity```** +: Propagation direction (+1 for +ve, -1 for -ve) + + +**```slices```** +: epsilon\[tuple(slices)] is used to select the portion of the grid to use + as the waveguide cross-section. slices\[axis] should select only one item. + + +**```mu```** +: Magnetic permeability (default 1 everywhere) + + + +Returns +-----= +J distribution for the unidirectional source + + +### Function `expand_e` {#meanas.fdfd.waveguide_3d.expand_e} + + + + + + +> `def expand_e(E: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], axis: int, polarity: int, slices: collections.abc.Sequence[slice]) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]` + + +Given an eigenmode obtained by [solve\_mode()](#meanas.fdfd.waveguide\_3d.solve\_mode), expands the E-field from the 2D +slice where the mode was calculated to the entire domain (along the propagation +axis). This assumes the epsilon cross-section remains constant throughout the +entire domain; it is up to the caller to truncate the expansion to any regions +where it is valid. + + +Args +-----= +**```E```** +: E-field of the mode + + +**```wavenumber```** +: Wavenumber of the mode + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```axis```** +: Propagation axis (0=x, 1=y, 2=z) + + +**```polarity```** +: Propagation direction (+1 for +ve, -1 for -ve) + + +**```slices```** +: epsilon\[tuple(slices)] is used to select the portion of the grid to use + as the waveguide cross-section. slices[axis] should select only one item. + + + +Returns +-----= +E, with the original field expanded along the specified axis. + + +### Function `solve_mode` {#meanas.fdfd.waveguide_3d.solve_mode} + + + + + + +> `def solve_mode(mode_number: int, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], axis: int, polarity: int, slices: collections.abc.Sequence[slice], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> dict[str, complex | numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]]]` + + +Given a 3D grid, selects a slice from the grid and attempts to + solve for an eigenmode propagating through that slice. + + +Args +-----= +**```mode_number```** +: Number of the mode, 0-indexed + + +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) + + +**```axis```** +: Propagation axis (0=x, 1=y, 2=z) + + +**```polarity```** +: Propagation direction (+1 for +ve, -1 for -ve) + + +**```slices```** +: epsilon\[tuple(slices)] is used to select the portion of the grid to use + as the waveguide cross-section. slices\[axis] should select only one item. + + +**```epsilon```** +: Dielectric constant + + +**```mu```** +: Magnetic permeability (default 1 everywhere) + + + +Returns +-----= +``` +{ + 'E': NDArray[complexfloating], + 'H': NDArray[complexfloating], + 'wavenumber': complex, +} +``` + + + + +------------------------------------------- + + + +# Module `meanas.fdfd.waveguide_cyl` {#meanas.fdfd.waveguide_cyl} + +Operators and helper functions for cylindrical waveguides with unchanging cross-section. + +WORK IN PROGRESS, CURRENTLY BROKEN + +As the z-dependence is known, all the functions in this file assume a 2D grid + (i.e. `dxes = [[[dr_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dr_h_0, ...], [dy_h_0, ...]]]`). + + + + + +## Functions + + + +### Function `cylindrical_operator` {#meanas.fdfd.waveguide_cyl.cylindrical_operator} + + + + + + +> `def cylindrical_operator(omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], rmin: float) -> scipy.sparse._matrix.spmatrix` + + +Cylindrical coordinate waveguide operator of the form + +(NOTE: See 10.1364/OL.33.001848) +TODO: consider 10.1364/OE.20.021583 + +TODO + +for use with a field vector of the form \[E\_r, E\_y]. + +This operator can be used to form an eigenvalue problem of the form + A @ [E_r, E_y] = wavenumber**2 * [E_r, E_y] + +which can then be solved for the eigenmodes of the system +(an `exp(-i * wavenumber * theta)` theta-dependence is assumed for the fields). + + +Args +-----= +**```omega```** +: The angular frequency of the system + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + +**```rmin```** +: Radius at the left edge of the simulation domain (minimum 'x') + + + +Returns +-----= +Sparse matrix representation of the operator + + +### Function `dxes2T` {#meanas.fdfd.waveguide_cyl.dxes2T} + + + + + + +> `def dxes2T(dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], rmin=builtins.float) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]` + + + + + +### Function `e2h` {#meanas.fdfd.waveguide_cyl.e2h} + + + + + + +> `def e2h(wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Returns an operator which, when applied to a vectorized E eigenfield, produces + the vectorized H eigenfield. + + +Args +-----= +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + + +**```omega```** +: The angular frequency of the system + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```mu```** +: Vectorized magnetic permeability grid (default 1 everywhere) + + + +Returns +-----= +Sparse matrix representation of the operator. + + +### Function `exy2e` {#meanas.fdfd.waveguide_cyl.exy2e} + + + + + + +> `def exy2e(wavenumber: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]) -> scipy.sparse._matrix.spmatrix` + + +Operator which transforms the vector e\_xy containing the vectorized E_x and E_y fields, + into a vectorized E containing all three E components + + +Args +-----= +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy` + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + + +Returns +-----= +Sparse matrix representing the operator. + + +### Function `exy2h` {#meanas.fdfd.waveguide_cyl.exy2h} + + + + + + +> `def exy2h(wavenumber: complex, omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None) -> scipy.sparse._matrix.spmatrix` + + +Operator which transforms the vector e\_xy containing the vectorized E_x and E_y fields, + into a vectorized H containing all three H components + + +Args +-----= +**```wavenumber```** +: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. + It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy` + + +**```omega```** +: The angular frequency of the system + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```epsilon```** +: Vectorized dielectric constant grid + + +**```mu```** +: Vectorized magnetic permeability grid (default 1 everywhere) + + + +Returns +-----= +Sparse matrix representing the operator. + + +### Function `linear_wavenumbers` {#meanas.fdfd.waveguide_cyl.linear_wavenumbers} + + + + + + +> `def linear_wavenumbers(e_xys: numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], angular_wavenumbers: Union[collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], rmin: float) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]` + + +Calculate linear wavenumbers (1/distance) based on angular wavenumbers (1/rad) + and the mode's energy distribution. + + +Args +-----= +**```e_xys```** +: Vectorized mode fields with shape [num_modes, 2 * x *y) + + +**```angular_wavenumbers```** +: Angular wavenumbers corresponding to the fields in e\_xys + + +**```epsilon```** +: Vectorized dielectric constant grid with shape (3, x, y) + + +**```dxes```** +: Grid parameters \[dx\_e, dx\_h] as described in [meanas.fdmath.types](#meanas.fdmath.types) (2D) + + +**```rmin```** +: Radius at the left edge of the simulation domain (minimum 'x') + + + +Returns +-----= +NDArray containing the calculated linear (1/distance) wavenumbers + + +### Function `solve_mode` {#meanas.fdfd.waveguide_cyl.solve_mode} + + + + + + +> `def solve_mode(mode_number: int, *args: Any, **kwargs: Any) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], complex]` + + +Wrapper around [solve\_modes()](#meanas.fdfd.waveguide\_cyl.solve\_modes) that solves for a single mode. + + +Args +-----= +**```mode_number```** +: 0-indexed mode number to solve for + + +**```*args```** +: passed to [solve\_modes()](#meanas.fdfd.waveguide\_cyl.solve\_modes) + + +**```**kwargs```** +: passed to [solve\_modes()](#meanas.fdfd.waveguide\_cyl.solve\_modes) + + + +Returns +-----= +(e_xy, angular_wavenumber) + + +### Function `solve_modes` {#meanas.fdfd.waveguide_cyl.solve_modes} + + + + + + +> `def solve_modes(mode_numbers: collections.abc.Sequence[int], omega: complex, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], rmin: float, mode_margin: int = 2) -> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]]` + + +TODO: fixup +Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode + of the bent waveguide with the specified mode number. + + +Args +-----= +**```mode_number```** +: Number of the mode, 0-indexed + + +**```omega```** +: Angular frequency of the simulation + + +**```dxes```** +: Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types. + The first coordinate is assumed to be r, the second is y. + + +**```epsilon```** +: Dielectric constant + + +**```rmin```** +: Radius of curvature for the simulation. This should be the minimum value of + r within the simulation domain. + + + +Returns +-----= +e\_xys +: NDArray of vfdfield_t specifying fields. First dimension is mode number. + + +angular\_wavenumbers +: list of wavenumbers in 1/rad units. + + + + + + +------------------------------------------- + + + +# Module `meanas.fdmath` {#meanas.fdmath} + +Basic discrete calculus for finite difference (fd) simulations. + + +Fields, Functions, and Operators +================================ + +Discrete fields are stored in one of two forms: + +- The fdfield\_t form is a multidimensional numpy.NDArray + + For a scalar field, this is just U\[m, n, p], where m, n, and p are + discrete indices referring to positions on the x, y, and z axes respectively. + + For a vector field, the first index specifies which vector component is accessed: + `E[:, m, n, p] = [Ex[m, n, p], Ey[m, n, p], Ez[m, n, p]]`. +- The vfdfield\_t form is simply a vectorzied (i.e. 1D) version of the fdfield\_t, + as obtained by [vec()](#meanas.fdmath.vectorization.vec) (effectively just numpy.ravel) + +Operators which act on fields also come in two forms: + + Python functions, created by the functions in [meanas.fdmath.functional](#meanas.fdmath.functional). + The generated functions act on fields in the fdfield\_t form. + + Linear operators, usually 2D sparse matrices using scipy.sparse, created + by [meanas.fdmath.operators](#meanas.fdmath.operators). These operators act on vectorized fields in the + vfdfield\_t form. + +The operations performed should be equivalent: `functional.op(*args)(E)` should be +equivalent to `unvec(operators.op(*args) @ vec(E), E.shape[1:])`. + +Generally speaking the field\_t form is easier to work with, but can be harder or less +efficient to compose (e.g. it is easy to generate a single matrix by multiplying a +series of other matrices). + + +Discrete calculus +================= + +This documentation and approach is roughly based on W.C. Chew's excellent +"Electromagnetic Theory on a Lattice" (doi:10.1063/1.355770), +which covers a superset of this material with similar notation and more detail. + + +## Scalar Derivatives And Cell Shifts + + +Define the discrete forward derivative as + $$ [\tilde{\partial}_x f]_{m + \frac{1}{2}} = \frac{1}{\Delta_{x, m}} (f_{m + 1} - f_m) $$ + where $f$ is a function defined at discrete locations on the x-axis (labeled using $m$). + The value at $m$ occupies a length $\Delta_{x, m}$ along the x-axis. Note that $m$ + is an index along the x-axis, _not_ necessarily an x-coordinate, since each length + $\Delta_{x, m}, \Delta_{x, m+1}, ...$ is independently chosen. + +If we treat f as a 1D array of values, with the i-th value f\[i] taking up a length dx\[i] +along the x-axis, the forward derivative is + + deriv_forward(f)[i] = (f[i + 1] - f[i]) / dx[i] + + +Likewise, discrete reverse derivative is + $$ [\hat{\partial}_x f ]_{m - \frac{1}{2}} = \frac{1}{\Delta_{x, m}} (f_{m} - f_{m - 1}) $$ + or + + deriv_back(f)[i] = (f[i] - f[i - 1]) / dx[i] + +The derivatives' values are shifted by a half-cell relative to the original function, and +will have different cell widths if all the dx\[i] ( $\Delta_{x, m}$ ) are not +identical: + + [figure: derivatives and cell sizes] + dx0 dx1 dx2 dx3 cell sizes for function + ----- ----- ----------- ----- + ______________________________ + | | | | + f0 | f1 | f2 | f3 | function + _____|_____|___________|_____| + | | | | + | Df0 | Df1 | Df2 | Df3 forward derivative (periodic boundary) + __|_____|________|________|___ + + dx'3] dx'0 dx'1 dx'2 [dx'3 cell sizes for forward derivative + -- ----- -------- -------- --- + dx'0] dx'1 dx'2 dx'3 [dx'0 cell sizes for reverse derivative + ______________________________ + | | | | + | df1 | df2 | df3 | df0 reverse derivative (periodic boundary) + __|_____|________|________|___ + + Periodic boundaries are used here and elsewhere unless otherwise noted. + +In the above figure, + `f0 =` $f_0$, `f1 =` $f_1$ + `Df0 =` $[\tilde{\partial}f]_{0 + \frac{1}{2}}$ + `Df1 =` $[\tilde{\partial}f]_{1 + \frac{1}{2}}$ + `df0 =` $[\hat{\partial}f]_{0 - \frac{1}{2}}$ + etc. + +The fractional subscript $m + \frac{1}{2}$ is used to indicate values defined + at shifted locations relative to the original $m$, with corresponding lengths + $$ \Delta_{x, m + \frac{1}{2}} = \frac{1}{2} * (\Delta_{x, m} + \Delta_{x, m + 1}) $$ + +Just as $m$ is not itself an x-coordinate, neither is $m + \frac{1}{2}$; +carefully note the positions of the various cells in the above figure vs their labels. +If the positions labeled with $m$ are considered the "base" or "original" grid, +the positions labeled with $m + \frac{1}{2}$ are said to lie on a "dual" or +"derived" grid. + +For the remainder of the Discrete calculus section, all figures will show +constant-length cells in order to focus on the vector derivatives themselves. +See the Grid description section below for additional information on this topic +and generalization to three dimensions. + + +## Gradients and fore-vectors + + +Expanding to three dimensions, we can define two gradients + $$ [\tilde{\nabla} f]_{m,n,p} = \vec{x} [\tilde{\partial}_x f]_{m + \frac{1}{2},n,p} + + \vec{y} [\tilde{\partial}_y f]_{m,n + \frac{1}{2},p} + + \vec{z} [\tilde{\partial}_z f]_{m,n,p + \frac{1}{2}} $$ + $$ [\hat{\nabla} f]_{m,n,p} = \vec{x} [\hat{\partial}_x f]_{m + \frac{1}{2},n,p} + + \vec{y} [\hat{\partial}_y f]_{m,n + \frac{1}{2},p} + + \vec{z} [\hat{\partial}_z f]_{m,n,p + \frac{1}{2}} $$ + + or + + [code: gradients] + grad_forward(f)[i,j,k] = [Dx_forward(f)[i, j, k], + Dy_forward(f)[i, j, k], + Dz_forward(f)[i, j, k]] + = [(f[i + 1, j, k] - f[i, j, k]) / dx[i], + (f[i, j + 1, k] - f[i, j, k]) / dy[i], + (f[i, j, k + 1] - f[i, j, k]) / dz[i]] + + grad_back(f)[i,j,k] = [Dx_back(f)[i, j, k], + Dy_back(f)[i, j, k], + Dz_back(f)[i, j, k]] + = [(f[i, j, k] - f[i - 1, j, k]) / dx[i], + (f[i, j, k] - f[i, j - 1, k]) / dy[i], + (f[i, j, k] - f[i, j, k - 1]) / dz[i]] + +The three derivatives in the gradient cause shifts in different +directions, so the x/y/z components of the resulting "vector" are defined +at different points: the x-component is shifted in the x-direction, +y in y, and z in z. + +We call the resulting object a "fore-vector" or "back-vector", depending +on the direction of the shift. We write it as + $$ \tilde{g}_{m,n,p} = \vec{x} g^x_{m + \frac{1}{2},n,p} + + \vec{y} g^y_{m,n + \frac{1}{2},p} + + \vec{z} g^z_{m,n,p + \frac{1}{2}} $$ + $$ \hat{g}_{m,n,p} = \vec{x} g^x_{m - \frac{1}{2},n,p} + + \vec{y} g^y_{m,n - \frac{1}{2},p} + + \vec{z} g^z_{m,n,p - \frac{1}{2}} $$ + + + [figure: gradient / fore-vector] + (m, n+1, p+1) ______________ (m+1, n+1, p+1) + /: /| + / : / | + / : / | + (m, n, p+1)/_____________/ | The forward derivatives are defined + | : | | at the Dx, Dy, Dz points, + | :.........|...| but the forward-gradient fore-vector + z y Dz / | / is the set of all three + |/_x | Dy | / and is said to be "located" at (m,n,p) + |/ |/ + (m, n, p)|_____Dx______| (m+1, n, p) + + + +## Divergences + + +There are also two divergences, + + $$ d_{n,m,p} = [\tilde{\nabla} \cdot \hat{g}]_{n,m,p} + = [\tilde{\partial}_x g^x]_{m,n,p} + + [\tilde{\partial}_y g^y]_{m,n,p} + + [\tilde{\partial}_z g^z]_{m,n,p} $$ + + $$ d_{n,m,p} = [\hat{\nabla} \cdot \tilde{g}]_{n,m,p} + = [\hat{\partial}_x g^x]_{m,n,p} + + [\hat{\partial}_y g^y]_{m,n,p} + + [\hat{\partial}_z g^z]_{m,n,p} $$ + + or + + [code: divergences] + div_forward(g)[i,j,k] = Dx_forward(gx)[i, j, k] + + Dy_forward(gy)[i, j, k] + + Dz_forward(gz)[i, j, k] + = (gx[i + 1, j, k] - gx[i, j, k]) / dx[i] + + (gy[i, j + 1, k] - gy[i, j, k]) / dy[i] + + (gz[i, j, k + 1] - gz[i, j, k]) / dz[i] + + div_back(g)[i,j,k] = Dx_back(gx)[i, j, k] + + Dy_back(gy)[i, j, k] + + Dz_back(gz)[i, j, k] + = (gx[i, j, k] - gx[i - 1, j, k]) / dx[i] + + (gy[i, j, k] - gy[i, j - 1, k]) / dy[i] + + (gz[i, j, k] - gz[i, j, k - 1]) / dz[i] + +where `g = [gx, gy, gz]` is a fore- or back-vector field. + +Since we applied the forward divergence to the back-vector (and vice-versa), the resulting scalar value +is defined at the back-vector's (fore-vector's) location $(m,n,p)$ and not at the locations of its components +$(m \pm \frac{1}{2},n,p)$ etc. + + [figure: divergence] + ^^ + (m-1/2, n+1/2, p+1/2) _____||_______ (m+1/2, n+1/2, p+1/2) + /: || ,, /| + / : || // / | The divergence at (m, n, p) (the center + / : // / | of this cube) of a fore-vector field + (m-1/2, n-1/2, p+1/2)/_____________/ | is the sum of the outward-pointing + | : | | fore-vector components, which are + z y <==|== :.........|.====> located at the face centers. + |/_x | / | / + | / // | / Note that in a nonuniform grid, each + |/ // || |/ dimension is normalized by the cell width. + (m-1/2, n-1/2, p-1/2)|____//_______| (m+1/2, n-1/2, p-1/2) + '' || + VV + + +## Curls + + +The two curls are then + + $$ \begin{aligned} + \hat{h}_{m + \frac{1}{2}, n + \frac{1}{2}, p + \frac{1}{2}} &= \\ + [\tilde{\nabla} \times \tilde{g}]_{m + \frac{1}{2}, n + \frac{1}{2}, p + \frac{1}{2}} &= + \vec{x} (\tilde{\partial}_y g^z_{m,n,p + \frac{1}{2}} - \tilde{\partial}_z g^y_{m,n + \frac{1}{2},p}) \\ + &+ \vec{y} (\tilde{\partial}_z g^x_{m + \frac{1}{2},n,p} - \tilde{\partial}_x g^z_{m,n,p + \frac{1}{2}}) \\ + &+ \vec{z} (\tilde{\partial}_x g^y_{m,n + \frac{1}{2},p} - \tilde{\partial}_y g^z_{m + \frac{1}{2},n,p}) + \end{aligned} $$ + + and + + $$ \tilde{h}_{m - \frac{1}{2}, n - \frac{1}{2}, p - \frac{1}{2}} = + [\hat{\nabla} \times \hat{g}]_{m - \frac{1}{2}, n - \frac{1}{2}, p - \frac{1}{2}} $$ + + where $\hat{g}$ and $\tilde{g}$ are located at $(m,n,p)$ + with components at $(m \pm \frac{1}{2},n,p)$ etc., + while $\hat{h}$ and $\tilde{h}$ are located at $(m \pm \frac{1}{2}, n \pm \frac{1}{2}, p \pm \frac{1}{2})$ + with components at $(m, n \pm \frac{1}{2}, p \pm \frac{1}{2})$ etc. + + + [code: curls] + curl_forward(g)[i,j,k] = [Dy_forward(gz)[i, j, k] - Dz_forward(gy)[i, j, k], + Dz_forward(gx)[i, j, k] - Dx_forward(gz)[i, j, k], + Dx_forward(gy)[i, j, k] - Dy_forward(gx)[i, j, k]] + + curl_back(g)[i,j,k] = [Dy_back(gz)[i, j, k] - Dz_back(gy)[i, j, k], + Dz_back(gx)[i, j, k] - Dx_back(gz)[i, j, k], + Dx_back(gy)[i, j, k] - Dy_back(gx)[i, j, k]] + + +For example, consider the forward curl, at (m, n, p), of a back-vector field g, defined + on a grid containing (m + 1/2, n + 1/2, p + 1/2). + The curl will be a fore-vector, so its z-component will be defined at (m, n, p + 1/2). + Take the nearest x- and y-components of g in the xy plane where the curl's z-component + is located; these are + + [curl components] + (m, n + 1/2, p + 1/2) : x-component of back-vector at (m + 1/2, n + 1/2, p + 1/2) + (m + 1, n + 1/2, p + 1/2) : x-component of back-vector at (m + 3/2, n + 1/2, p + 1/2) + (m + 1/2, n , p + 1/2) : y-component of back-vector at (m + 1/2, n + 1/2, p + 1/2) + (m + 1/2, n + 1 , p + 1/2) : y-component of back-vector at (m + 1/2, n + 3/2, p + 1/2) + + These four xy-components can be used to form a loop around the curl's z-component; its magnitude and sign + is set by their loop-oriented sum (i.e. two have their signs flipped to complete the loop). + + [figure: z-component of curl] + : | + z y : ^^ | + |/_x :....||.<.....| (m+1, n+1, p+1/2) + / || / + | v || | ^ + |/ |/ + (m, n, p+1/2) |_____>______| (m+1, n, p+1/2) + + + +Maxwell's Equations +=================== + +If we discretize both space (m,n,p) and time (l), Maxwell's equations become + + $$ \begin{aligned} + \tilde{\nabla} \times \tilde{E}_{l,\vec{r}} &= -\tilde{\partial}_t \hat{B}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}} + - \hat{M}_{l, \vec{r} + \frac{1}{2}} \\ + \hat{\nabla} \times \hat{H}_{l-\frac{1}{2},\vec{r} + \frac{1}{2}} &= \hat{\partial}_t \tilde{D}_{l, \vec{r}} + + \tilde{J}_{l-\frac{1}{2},\vec{r}} \\ + \tilde{\nabla} \cdot \hat{B}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}} &= 0 \\ + \hat{\nabla} \cdot \tilde{D}_{l,\vec{r}} &= \rho_{l,\vec{r}} + \end{aligned} $$ + + with + + $$ \begin{aligned} + \hat{B}_{\vec{r}} &= \mu_{\vec{r} + \frac{1}{2}} \cdot \hat{H}_{\vec{r} + \frac{1}{2}} \\ + \tilde{D}_{\vec{r}} &= \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} + \end{aligned} $$ + +where the spatial subscripts are abbreviated as $\vec{r} = (m, n, p)$ and +$\vec{r} + \frac{1}{2} = (m + \frac{1}{2}, n + \frac{1}{2}, p + \frac{1}{2})$, +$\tilde{E}$ and $\hat{H}$ are the electric and magnetic fields, +$\tilde{J}$ and $\hat{M}$ are the electric and magnetic current distributions, +and $\epsilon$ and $\mu$ are the dielectric permittivity and magnetic permeability. + +The above is Yee's algorithm, written in a form analogous to Maxwell's equations. +The time derivatives can be expanded to form the update equations: + + [code: Maxwell's equations updates] + H[i, j, k] -= dt * (curl_forward(E)[i, j, k] + M[t, i, j, k]) / mu[i, j, k] + E[i, j, k] += dt * (curl_back( H)[i, j, k] + J[t, i, j, k]) / epsilon[i, j, k] + +Note that the E-field fore-vector and H-field back-vector are offset by a half-cell, resulting +in distinct locations for all six E- and H-field components: + + [figure: Field components] + + (m - 1/2,=> ____________Hx__________[H] <= r + 1/2 = (m + 1/2, + n + 1/2, /: /: /| n + 1/2, + z y p + 1/2) / : / : / | p + 1/2) + |/_x / : / : / | + / : Ez__________Hy | Locations of the E- and + / : : : /| | H-field components for the + (m - 1/2, / : : Ey...../.|..Hz [E] fore-vector at r = (m,n,p) + n - 1/2, =>/________________________/ | /| (the large cube's center) + p + 1/2) | : : / | | / | and [H] back-vector at r + 1/2 + | : :/ | |/ | (the top right corner) + | : [E].......|.Ex | + | :.................|......| <= (m + 1/2, n + 1/2, p + 1/2) + | / | / + | / | / + | / | / This is the Yee discretization + | / | / scheme ("Yee cell"). + r - 1/2 = | / | / + (m - 1/2, |/ |/ + n - 1/2,=> |________________________| <= (m + 1/2, n - 1/2, p - 1/2) + p - 1/2) + +Each component forms its own grid, offset from the others: + + [figure: E-fields for adjacent cells] + + H1__________Hx0_________H0 + z y /: /| + |/_x / : / | This figure shows H back-vector locations + / : / | H0, H1, etc. and their associated components + Hy1 : Hy0 | H0 = (Hx0, Hy0, Hz0) etc. + / : / | + / Hz1 / Hz0 + H2___________Hx3_________H3 | The equivalent drawing for E would have + | : | | fore-vectors located at the cube's + | : | | center (and the centers of adjacent cubes), + | : | | with components on the cube's faces. + | H5..........Hx4...|......H4 + | / | / + Hz2 / Hz2 / + | / | / + | Hy6 | Hy4 + | / | / + |/ |/ + H6__________Hx7__________H7 + + +The divergence equations can be derived by taking the divergence of the curl equations +and combining them with charge continuity, + $$ \hat{\nabla} \cdot \tilde{J} + \hat{\partial}_t \rho = 0 $$ + implying that the discrete Maxwell's equations do not produce spurious charges. + + +## Wave Equation + + +Taking the backward curl of the $\tilde{\nabla} \times \tilde{E}$ equation and +replacing the resulting $\hat{\nabla} \times \hat{H}$ term using its respective equation, +and setting $\hat{M}$ to zero, we can form the discrete wave equation: + +$$ + \begin{aligned} + \tilde{\nabla} \times \tilde{E}_{l,\vec{r}} &= + -\tilde{\partial}_t \hat{B}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}} + - \hat{M}_{l-1, \vec{r} + \frac{1}{2}} \\ + \mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}} &= + -\tilde{\partial}_t \hat{H}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}} \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) &= + \hat{\nabla} \times (-\tilde{\partial}_t \hat{H}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}}) \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) &= + -\tilde{\partial}_t \hat{\nabla} \times \hat{H}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}} \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) &= + -\tilde{\partial}_t \hat{\partial}_t \epsilon_{\vec{r}} \tilde{E}_{l, \vec{r}} + \hat{\partial}_t \tilde{J}_{l-\frac{1}{2},\vec{r}} \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) + + \tilde{\partial}_t \hat{\partial}_t \epsilon_{\vec{r}} \cdot \tilde{E}_{l, \vec{r}} + &= \tilde{\partial}_t \tilde{J}_{l - \frac{1}{2}, \vec{r}} + \end{aligned} +$$ + + +## Frequency Domain + + +We can substitute in a time-harmonic fields + +$$ + \begin{aligned} + \tilde{E}_{l, \vec{r}} &= \tilde{E}_{\vec{r}} e^{-\imath \omega l \Delta_t} \\ + \tilde{J}_{l, \vec{r}} &= \tilde{J}_{\vec{r}} e^{-\imath \omega (l - \frac{1}{2}) \Delta_t} + \end{aligned} +$$ + +resulting in + +$$ + \begin{aligned} + \tilde{\partial}_t &\Rightarrow (e^{ \imath \omega \Delta_t} - 1) / \Delta_t = \frac{-2 \imath}{\Delta_t} \sin(\omega \Delta_t / 2) e^{-\imath \omega \Delta_t / 2} = -\imath \Omega e^{-\imath \omega \Delta_t / 2}\\ + \hat{\partial}_t &\Rightarrow (1 - e^{-\imath \omega \Delta_t}) / \Delta_t = \frac{-2 \imath}{\Delta_t} \sin(\omega \Delta_t / 2) e^{ \imath \omega \Delta_t / 2} = -\imath \Omega e^{ \imath \omega \Delta_t / 2}\\ + \Omega &= 2 \sin(\omega \Delta_t / 2) / \Delta_t + \end{aligned} +$$ + +This gives the frequency-domain wave equation, + +$$ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{\vec{r}}) + -\Omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} = -\imath \Omega \tilde{J}_{\vec{r}} e^{\imath \omega \Delta_t / 2} \\ +$$ + + +## Plane Waves And Dispersion Relation + + +With uniform material distribution and no sources + +$$ + \begin{aligned} + \mu_{\vec{r} + \frac{1}{2}} &= \mu \\ + \epsilon_{\vec{r}} &= \epsilon \\ + \tilde{J}_{\vec{r}} &= 0 \\ + \end{aligned} +$$ + +the frequency domain wave equation simplifies to + +$$ \hat{\nabla} \times \tilde{\nabla} \times \tilde{E}_{\vec{r}} - \Omega^2 \epsilon \mu \tilde{E}_{\vec{r}} = 0 $$ + +Since $\hat{\nabla} \cdot \tilde{E}_{\vec{r}} = 0$, we can simplify + +$$ + \begin{aligned} + \hat{\nabla} \times \tilde{\nabla} \times \tilde{E}_{\vec{r}} + &= \tilde{\nabla}(\hat{\nabla} \cdot \tilde{E}_{\vec{r}}) - \hat{\nabla} \cdot \tilde{\nabla} \tilde{E}_{\vec{r}} \\ + &= - \hat{\nabla} \cdot \tilde{\nabla} \tilde{E}_{\vec{r}} \\ + &= - \tilde{\nabla}^2 \tilde{E}_{\vec{r}} + \end{aligned} +$$ + +and we get + +$$ \tilde{\nabla}^2 \tilde{E}_{\vec{r}} + \Omega^2 \epsilon \mu \tilde{E}_{\vec{r}} = 0 $$ + +We can convert this to three scalar-wave equations of the form + +$$ (\tilde{\nabla}^2 + K^2) \phi_{\vec{r}} = 0 $$ + +with $K^2 = \Omega^2 \mu \epsilon$. Now we let + +$$ \phi_{\vec{r}} = A e^{\imath (k_x m \Delta_x + k_y n \Delta_y + k_z p \Delta_z)} $$ + +resulting in + +$$ + \begin{aligned} + \tilde{\partial}_x &\Rightarrow (e^{ \imath k_x \Delta_x} - 1) / \Delta_t = \frac{-2 \imath}{\Delta_x} \sin(k_x \Delta_x / 2) e^{ \imath k_x \Delta_x / 2} = \imath K_x e^{ \imath k_x \Delta_x / 2}\\ + \hat{\partial}_x &\Rightarrow (1 - e^{-\imath k_x \Delta_x}) / \Delta_t = \frac{-2 \imath}{\Delta_x} \sin(k_x \Delta_x / 2) e^{-\imath k_x \Delta_x / 2} = \imath K_x e^{-\imath k_x \Delta_x / 2}\\ + K_x &= 2 \sin(k_x \Delta_x / 2) / \Delta_x \\ + \end{aligned} +$$ + +with similar expressions for the y and z dimnsions (and $K_y, K_z$). + +This implies + +$$ + \tilde{\nabla}^2 = -(K_x^2 + K_y^2 + K_z^2) \phi_{\vec{r}} \\ + K_x^2 + K_y^2 + K_z^2 = \Omega^2 \mu \epsilon = \Omega^2 / c^2 +$$ + +where $c = \sqrt{\mu \epsilon}$. + +Assuming real $(k_x, k_y, k_z), \omega$ will be real only if + +$$ c^2 \Delta_t^2 = \frac{\Delta_t^2}{\mu \epsilon} < 1/(\frac{1}{\Delta_x^2} + \frac{1}{\Delta_y^2} + \frac{1}{\Delta_z^2}) $$ + +If $\Delta_x = \Delta_y = \Delta_z$, this simplifies to $c \Delta_t < \Delta_x / \sqrt{3}$. +This last form can be interpreted as enforcing causality; the distance that light +travels in one timestep (i.e., $c \Delta_t$) must be less than the diagonal +of the smallest cell ( $\Delta_x / \sqrt{3}$ when on a uniform cubic grid). + + +Grid description +================ + +As described in the section on scalar discrete derivatives above, cell widths +(dx\[i], dy\[j], dz\[k]) along each axis can be arbitrary and independently +defined. Moreover, all field components are actually defined at "derived" or "dual" +positions, in-between the "base" grid points on one or more axes. + +To get a better sense of how this works, let's start by drawing a grid with uniform +dy and dz and nonuniform dx. We will only draw one cell in the y and z dimensions +to make the illustration simpler; we need at least two cells in the x dimension to +demonstrate how nonuniform dx affects the various components. + +Place the E fore-vectors at integer indices $r = (m, n, p)$ and the H back-vectors +at fractional indices $r + \frac{1}{2} = (m + \frac{1}{2}, n + \frac{1}{2}, +p + \frac{1}{2})$. Remember that these are indices and not coordinates; they can +correspond to arbitrary (monotonically increasing) coordinates depending on the cell widths. + +Draw lines to denote the planes on which the H components and back-vectors are defined. +For simplicity, don't draw the equivalent planes for the E components and fore-vectors, +except as necessary to show their locations -- it's easiest to just connect them to their +associated H-equivalents. + +The result looks something like this: + + [figure: Component centers] + p= + [H]__________Hx___________[H]_____Hx______[H] __ +1/2 + z y /: /: /: /: /| | | + |/_x / : / : / : / : / | | | + / : / : / : / : / | | | + Hy : Ez...........Hy : Ez......Hy | | | + /: : : : /: : : : /| | | | + / : Hz : Ey....../.:..Hz : Ey./.|..Hz __ 0 | dz[0] + / : /: : / / : /: : / / | /| | | + /_________________________/_______________/ | / | | | + | :/ : :/ | :/ : :/ | |/ | | | + | Ex : [E].......|..Ex : [E]..|..Ex | | | + | : | : | | | | + | [H]..........Hx....|......[H].....H|x.....[H] __ --------- (n=+1/2, p=-1/2) + | / | / | / / / + Hz / Hz / Hz / / / + | / | / | / / / + | Hy | Hy | Hy __ 0 / dy[0] + | / | / | / / / + | / | / | / / / + |/ |/ |/ / / + [H]__________Hx___________[H]_____Hx______[H] __ -1/2 / + =n + |------------|------------|-------|-------| + -1/2 0 +1/2 +1 +3/2 = m + + ------------------------- ---------------- + dx[0] dx[1] + + Part of a nonuniform "base grid", with labels specifying + positions of the various field components. [E] fore-vectors + are at the cell centers, and [H] back-vectors are at the + vertices. H components along the near (-y) top (+z) edge + have been omitted to make the insides of the cubes easier + to visualize. + +The above figure shows where all the components are located; however, it is also useful to show +what volumes those components correspond to. Consider the Ex component at `m = +1/2`: it is +shifted in the x-direction by a half-cell from the E fore-vector at `m = 0` (labeled \[E] +in the figure). It corresponds to a volume between `m = 0` and `m = +1` (the other +dimensions are not shifted, i.e. they are still bounded by `n, p = +-1/2`). (See figure +below). Since m is an index and not an x-coordinate, the Ex component is not necessarily +at the center of the volume it represents, and the x-length of its volume is the derived +quantity `dx'[0] = (dx[0] + dx[1]) / 2` rather than the base dx. +(See also Scalar derivatives and cell shifts). + + [figure: Ex volumes] + p= + <_________________________________________> __ +1/2 + z y << /: / /: >> | | + |/_x < < / : / / : > > | | + < < / : / / : > > | | + < < / : / / : > > | | + <: < / : : / : >: > | | + < : < / : : / : > : > __ 0 | dz[0] + < : < / : : / :> : > | | + <____________/____________________/_______> : > | | + < : < | : : | > : > | | + < Ex < | : Ex | > Ex > | | + < : < | : : | > : > | | + < : <....|.......:........:...|.......>...:...> __ --------- (n=+1/2, p=-1/2) + < : < | / : /| /> : > / / + < : < | / : / | / > : > / / + < :< | / :/ | / > :> / / + < < | / : | / > > _ 0 / dy[0] + < < | / | / > > / / + < < | / | / > > / / + << |/ |/ >> / / + <____________|____________________|_______> __ -1/2 / + =n + |------------|------------|-------|-------| + -1/2 0 +1/2 +1 +3/2 = m + + ~------------ -------------------- -------~ + dx'[-1] dx'[0] dx'[1] + + The Ex values are positioned on the x-faces of the base + grid. They represent the Ex field in volumes shifted by + a half-cell in the x-dimension, as shown here. Only the + center cell (with width dx'[0]) is fully shown; the + other two are truncated (shown using >< markers). + + Note that the Ex positions are the in the same positions + as the previous figure; only the cell boundaries have moved. + Also note that the points at which Ex is defined are not + necessarily centered in the volumes they represent; non- + uniform cell sizes result in off-center volumes like the + center cell here. + +The next figure shows the volumes corresponding to the Hy components, which +are shifted in two dimensions (x and z) compared to the base grid. + + [figure: Hy volumes] + p= + z y mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm __ +1/2 s + |/_x << m: m: >> | | + < < m : m : > > | | dz'[1] + < < m : m : > > | | + Hy........... m........Hy...........m......Hy > | | + < < m : m : > > | | + < ______ m_____:_______________m_____:_>______ __ 0 + < < m /: m / > > | | + mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm > | | + < < | / : | / > > | | dz'[0] + < < | / : | / > > | | + < < | / : | / > > | | + < wwwww|w/wwwwwwwwwwwwwwwwwww|w/wwwww>wwwwwwww __ s + < < |/ w |/ w> > / / + _____________|_____________________|________ > / / + < < | w | w > > / / + < Hy........|...w........Hy.......|...w...>..Hy _ 0 / dy[0] + < < | w | w > > / / + << | w | w > > / / + < |w |w >> / / + wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww __ -1/2 / + + |------------|------------|--------|-------| + -1/2 0 +1/2 +1 +3/2 = m + + ~------------ --------------------- -------~ + dx'[-1] dx'[0] dx'[1] + + The Hy values are positioned on the y-edges of the base + grid. Again here, the 'Hy' labels represent the same points + as in the basic grid figure above; the edges have shifted + by a half-cell along the x- and z-axes. + + The grid lines _|:/ are edges of the area represented by + each Hy value, and the lines drawn using .w represent + edges where a cell's faces extend beyond the drawn area + (i.e. where the drawing is truncated in the x- or z- + directions). + + +## Datastructure: dx_lists_t + + +In this documentation, the E fore-vectors are placed on the base grid. An +equivalent formulation could place the H back-vectors on the base grid instead. +However, in the case of a non-uniform grid, the operation to get from the "base" +cell widths to "derived" ones is not its own inverse. + +The base grid's cell sizes could be fully described by a list of three 1D arrays, +specifying the cell widths along all three axes: + + [dx, dy, dz] = [[dx[0], dx[1], ...], [dy[0], ...], [dz[0], ...]] + +Note that this is a list-of-arrays rather than a 2D array, as the simulation domain +may have a different number of cells along each axis. + +Knowing the base grid's cell widths and the boundary conditions (periodic unless +otherwise noted) is enough information to calculate the cell widths `dx'`, `dy'`, +and `dz'` for the derived grids. + +However, since most operations are trivially generalized to allow either E or H +to be defined on the base grid, they are written to take the a full set of base +and derived cell widths, distinguished by which field they apply to rather than +their "base" or "derived" status. This removes the need for each function to +generate the derived widths, and makes the "base" vs "derived" distinction +unnecessary in the code. + +The resulting data structure containing all the cell widths takes the form of a +list-of-lists-of-arrays. The first list-of-arrays provides the cell widths for +the E-field fore-vectors, while the second list-of-arrays does the same for the +H-field back-vectors: + + [[[dx_e[0], dx_e[1], ...], [dy_e[0], ...], [dz_e[0], ...]], + [[dx_h[0], dx_h[1], ...], [dy_h[0], ...], [dz_h[0], ...]]] + + where dx\_e\[0] is the x-width of the `m=0` cells, as used when calculating dE/dx, + and dy\_h\[0] is the y-width of the `n=0` cells, as used when calculating dH/dy, etc. + + +Permittivity and Permeability +============================= + +Since each vector component of E and H is defined in a different location and represents +a different volume, the value of the spatially-discrete epsilon and mu can also be +different for all three field components, even when representing a simple planar interface +between two isotropic materials. + +As a result, epsilon and mu are taken to have the same dimensions as the field, and +composed of the three diagonal tensor components: + + [equations: epsilon_and_mu] + epsilon = [epsilon_xx, epsilon_yy, epsilon_zz] + mu = [mu_xx, mu_yy, mu_zz] + +or + +$$ + \epsilon = \begin{bmatrix} \epsilon_{xx} & 0 & 0 \\ + 0 & \epsilon_{yy} & 0 \\ + 0 & 0 & \epsilon_{zz} \end{bmatrix} +$$ +$$ + \mu = \begin{bmatrix} \mu_{xx} & 0 & 0 \\ + 0 & \mu_{yy} & 0 \\ + 0 & 0 & \mu_{zz} \end{bmatrix} +$$ + +where the off-diagonal terms (e.g. epsilon\_xy) are assumed to be zero. + +High-accuracy volumetric integration of shapes on multiple grids can be performed +by the [gridlock](https://mpxd.net/code/jan/gridlock) module. + +The values of the vacuum permittivity and permability effectively become scaling +factors that appear in several locations (e.g. between the E and H fields). In +order to limit floating-point inaccuracy and simplify calculations, they are often +set to 1 and relative permittivities and permeabilities are used in their places; +the true values can be multiplied back in after the simulation is complete if non- +normalized results are needed. + + + +## Sub-modules + +* [meanas.fdmath.functional](#meanas.fdmath.functional) +* [meanas.fdmath.operators](#meanas.fdmath.operators) +* [meanas.fdmath.types](#meanas.fdmath.types) +* [meanas.fdmath.vectorization](#meanas.fdmath.vectorization) + + + + + + +------------------------------------------- + + + +# Module `meanas.fdmath.functional` {#meanas.fdmath.functional} + +Math functions for finite difference simulations + +Basic discrete calculus etc. + + + + + +## Functions + + + +### Function `curl_back` {#meanas.fdmath.functional.curl_back} + + + + + + +> `def curl_back(dx_h: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]] | None = None) -> collections.abc.Callable[[~TT], ~TT]` + + +Create a function which takes the backward curl of a field. + + +Args +-----= +**```dx_h```** +: Lists of cell sizes for all axes + \[\[dx\_0, dx\_1, ...], \[dy\_0, dy\_1, ...], ...]. + + + +Returns +-----= +Function f for taking the discrete backward curl of a field, +f(H) -> curlH $= \nabla_b \times H$ + + +### Function `curl_back_parts` {#meanas.fdmath.functional.curl_back_parts} + + + + + + +> `def curl_back_parts(dx_h: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]] | None = None) -> collections.abc.Callable` + + + + + +### Function `curl_forward` {#meanas.fdmath.functional.curl_forward} + + + + + + +> `def curl_forward(dx_e: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]] | None = None) -> collections.abc.Callable[[~TT], ~TT]` + + +Curl operator for use with the E field. + + +Args +-----= +**```dx_e```** +: Lists of cell sizes for all axes + \[\[dx\_0, dx\_1, ...], \[dy\_0, dy\_1, ...], ...]. + + + +Returns +-----= +Function f for taking the discrete forward curl of a field, +f(E) -> curlE $= \nabla_f \times E$ + + +### Function `curl_forward_parts` {#meanas.fdmath.functional.curl_forward_parts} + + + + + + +> `def curl_forward_parts(dx_e: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]] | None = None) -> collections.abc.Callable` + + + + + +### Function `deriv_back` {#meanas.fdmath.functional.deriv_back} + + + + + + +> `def deriv_back(dx_h: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]] | None = None) -> tuple[collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]]` + + +Utility operators for taking discretized derivatives (forward variant). + + +Args +-----= +**```dx_h```** +: Lists of cell sizes for all axes + \[\[dx\_0, dx\_1, ...], \[dy\_0, dy\_1, ...], ...]. + + + +Returns +-----= +List of functions for taking forward derivatives along each axis. + + +### Function `deriv_forward` {#meanas.fdmath.functional.deriv_forward} + + + + + + +> `def deriv_forward(dx_e: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]] | None = None) -> tuple[collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]]` + + +Utility operators for taking discretized derivatives (backward variant). + + +Args +-----= +**```dx_e```** +: Lists of cell sizes for all axes + \[\[dx\_0, dx\_1, ...], \[dy\_0, dy\_1, ...], ...]. + + + +Returns +-----= +List of functions for taking forward derivatives along each axis. + + + + +------------------------------------------- + + + +# Module `meanas.fdmath.operators` {#meanas.fdmath.operators} + +Matrix operators for finite difference simulations + +Basic discrete calculus etc. + + + + + +## Functions + + + +### Function `avg_back` {#meanas.fdmath.operators.avg_back} + + + + + + +> `def avg_back(axis: int, shape: collections.abc.Sequence[int]) -> scipy.sparse._matrix.spmatrix` + + +Backward average operator `(x4 = (x4 + x3) / 2)` + + +Args +-----= +**```axis```** +: Axis to average along (x=0, y=1, z=2) + + +**```shape```** +: Shape of the grid to average + + + +Returns +-----= +Sparse matrix for backward average operation. + + +### Function `avg_forward` {#meanas.fdmath.operators.avg_forward} + + + + + + +> `def avg_forward(axis: int, shape: collections.abc.Sequence[int]) -> scipy.sparse._matrix.spmatrix` + + +Forward average operator `(x4 = (x4 + x5) / 2)` + + +Args +-----= +**```axis```** +: Axis to average along (x=0, y=1, z=2) + + +**```shape```** +: Shape of the grid to average + + + +Returns +-----= +Sparse matrix for forward average operation. + + +### Function `cross` {#meanas.fdmath.operators.cross} + + + + + + +> `def cross(B: collections.abc.Sequence[scipy.sparse._matrix.spmatrix]) -> scipy.sparse._matrix.spmatrix` + + +Cross product operator + + +Args +-----= +**```B```** +: List \[Bx, By, Bz] of sparse matrices corresponding to the x, y, z + portions of the operator on the left side of the cross product. + + + +Returns +-----= +Sparse matrix corresponding to (B x), where x is the cross product. + + +### Function `curl_back` {#meanas.fdmath.operators.curl_back} + + + + + + +> `def curl_back(dx_h: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]) -> scipy.sparse._matrix.spmatrix` + + +Curl operator for use with the H field. + + +Args +-----= +**```dx_h```** +: Lists of cell sizes for all axes + \[\[dx\_0, dx\_1, ...], \[dy\_0, dy\_1, ...], ...]. + + + +Returns +-----= +Sparse matrix for taking the discretized curl of the H-field + + +### Function `curl_forward` {#meanas.fdmath.operators.curl_forward} + + + + + + +> `def curl_forward(dx_e: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]) -> scipy.sparse._matrix.spmatrix` + + +Curl operator for use with the E field. + + +Args +-----= +**```dx_e```** +: Lists of cell sizes for all axes + \[\[dx\_0, dx\_1, ...], \[dy\_0, dy\_1, ...], ...]. + + + +Returns +-----= +Sparse matrix for taking the discretized curl of the E-field + + +### Function `deriv_back` {#meanas.fdmath.operators.deriv_back} + + + + + + +> `def deriv_back(dx_h: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]) -> list[scipy.sparse._matrix.spmatrix]` + + +Utility operators for taking discretized derivatives (backward variant). + + +Args +-----= +**```dx_h```** +: Lists of cell sizes for all axes + \[\[dx\_0, dx\_1, ...], \[dy\_0, dy\_1, ...], ...]. + + + +Returns +-----= +List of operators for taking forward derivatives along each axis. + + +### Function `deriv_forward` {#meanas.fdmath.operators.deriv_forward} + + + + + + +> `def deriv_forward(dx_e: collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]) -> list[scipy.sparse._matrix.spmatrix]` + + +Utility operators for taking discretized derivatives (forward variant). + + +Args +-----= +**```dx_e```** +: Lists of cell sizes for all axes + \[\[dx\_0, dx\_1, ...], \[dy\_0, dy\_1, ...], ...]. + + + +Returns +-----= +List of operators for taking forward derivatives along each axis. + + +### Function `shift_circ` {#meanas.fdmath.operators.shift_circ} + + + + + + +> `def shift_circ(axis: int, shape: collections.abc.Sequence[int], shift_distance: int = 1) -> scipy.sparse._matrix.spmatrix` + + +Utility operator for performing a circular shift along a specified axis by a + specified number of elements. + + +Args +-----= +**```axis```** +: Axis to shift along. x=0, y=1, z=2 + + +**```shape```** +: Shape of the grid being shifted + + +**```shift_distance```** +: Number of cells to shift by. May be negative. Default 1. + + + +Returns +-----= +Sparse matrix for performing the circular shift. + + +### Function `shift_with_mirror` {#meanas.fdmath.operators.shift_with_mirror} + + + + + + +> `def shift_with_mirror(axis: int, shape: collections.abc.Sequence[int], shift_distance: int = 1) -> scipy.sparse._matrix.spmatrix` + + +Utility operator for performing an n-element shift along a specified axis, with mirror +boundary conditions applied to the cells beyond the receding edge. + + +Args +-----= +**```axis```** +: Axis to shift along. x=0, y=1, z=2 + + +**```shape```** +: Shape of the grid being shifted + + +**```shift_distance```** +: Number of cells to shift by. May be negative. Default 1. + + + +Returns +-----= +Sparse matrix for performing the shift-with-mirror. + + +### Function `vec_cross` {#meanas.fdmath.operators.vec_cross} + + + + + + +> `def vec_cross(b: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]) -> scipy.sparse._matrix.spmatrix` + + +Vector cross product operator + + +Args +-----= +**```b```** +: Vector on the left side of the cross product. + + + +Returns +-----= +Sparse matrix corresponding to (b x), where x is the cross product. + + + + +------------------------------------------- + + + +# Module `meanas.fdmath.types` {#meanas.fdmath.types} + +Types shared across multiple submodules + + + + +## Variables + + + +### Variable `cfdfield_t` {#meanas.fdmath.types.cfdfield_t} + + + +Complex vector field with shape (3, X, Y, Z) (e.g. \[E\_x, E\_y, E\_z]) + + +### Variable `cfdfield_updater_t` {#meanas.fdmath.types.cfdfield_updater_t} + + + +Convenience type for functions which take and return an cfdfield_t + + +### Variable `dx_lists_mut` {#meanas.fdmath.types.dx_lists_mut} + + + +Mutable version of [dx\_lists\_t](#meanas.fdmath.types.dx\_lists\_t) + + +### Variable `dx_lists_t` {#meanas.fdmath.types.dx_lists_t} + + + +'dxes' datastructure which contains grid cell width information in the following format: + + [[[dx_e[0], dx_e[1], ...], [dy_e[0], ...], [dz_e[0], ...]], + [[dx_h[0], dx_h[1], ...], [dy_h[0], ...], [dz_h[0], ...]]] + + where dx\_e\[0] is the x-width of the `x=0` cells, as used when calculating dE/dx, + and dy\_h\[0] is the y-width of the `y=0` cells, as used when calculating dH/dy, etc. + + +### Variable `fdfield_t` {#meanas.fdmath.types.fdfield_t} + + + +Vector field with shape (3, X, Y, Z) (e.g. \[E\_x, E\_y, E\_z]) + + +### Variable `fdfield_updater_t` {#meanas.fdmath.types.fdfield_updater_t} + + + +Convenience type for functions which take and return an fdfield_t + + +### Variable `vcfdfield_t` {#meanas.fdmath.types.vcfdfield_t} + + + +Linearized complex vector field (single vector of length 3*X*Y*Z) + + +### Variable `vfdfield_t` {#meanas.fdmath.types.vfdfield_t} + + + +Linearized vector field (single vector of length 3*X*Y*Z) + + + + + +------------------------------------------- + + + +# Module `meanas.fdmath.vectorization` {#meanas.fdmath.vectorization} + +Functions for moving between a vector field (list of 3 ndarrays, \[f\_x, f\_y, f\_z]) +and a 1D array representation of that field \[f\_x0, f\_x1, f\_x2,... f\_y0,... f\_z0,...]. +Vectorized versions of the field use row-major (ie., C-style) ordering. + + + + + +## Functions + + + +### Function `unvec` {#meanas.fdmath.vectorization.unvec} + + + + + + +> `def unvec(v: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]] | None, shape: collections.abc.Sequence[int], nvdim: int = 3) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]] | None` + + +Perform the inverse of vec(): take a 1D ndarray and output an nvdim-component field + of form e.g. \[f\_x, f\_y, f\_z] (`nvdim=3`) where each of `f_*` is a len(shape)-dimensional + ndarray. + +Returns None if called with `v=None`. + + +Args +-----= +**```v```** +: 1D ndarray representing a vector field of shape shape (or None) + + +**```shape```** +: shape of the vector field + + +**```nvdim```** +: Number of components in each vector + + + +Returns +-----= +\[f\_x, f\_y, f\_z] where each f\_ is a len(shape) dimensional ndarray (or None) + + +### Function `vec` {#meanas.fdmath.vectorization.vec} + + + + + + +> `def vec(f: Union[numpy.ndarray[Any, numpy.dtype[numpy.floating]], numpy.ndarray[Any, numpy.dtype[numpy.complexfloating]], collections.abc.Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]], ForwardRef(None)]) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | numpy.ndarray[typing.Any, numpy.dtype[numpy.complexfloating]] | None` + + +Create a 1D ndarray from a vector field which spans a 1-3D region. + +Returns None if called with `f=None`. + + +Args +-----= +**```f```** +: A vector field, e.g. \[f\_x, f\_y, f\_z] where each f\_ component is a 1- to + 3-D ndarray (`f_*` should all be the same size). Doesn't fail with `f=None`. + + + +Returns +-----= +1D ndarray containing the linearized field (or None) + + + + +------------------------------------------- + + + +# Module `meanas.fdtd` {#meanas.fdtd} + +Utilities for running finite-difference time-domain (FDTD) simulations + +See the discussion of `Maxwell's Equations` in [meanas.fdmath](#meanas.fdmath) for basic +mathematical background. + + +Timestep +======== + +From the discussion of "Plane waves and the Dispersion relation" in [meanas.fdmath](#meanas.fdmath), +we have + +$$ c^2 \Delta_t^2 = \frac{\Delta_t^2}{\mu \epsilon} < 1/(\frac{1}{\Delta_x^2} + \frac{1}{\Delta_y^2} + \frac{1}{\Delta_z^2}) $$ + +or, if $\Delta_x = \Delta_y = \Delta_z$, then $c \Delta_t < \frac{\Delta_x}{\sqrt{3}}$. + +Based on this, we can set + + dt = sqrt(mu.min() * epsilon.min()) / sqrt(1/dx_min**2 + 1/dy_min**2 + 1/dz_min**2) + +The dx\_min, dy\_min, dz\_min should be the minimum value across both the base and derived grids. + + +Poynting Vector and Energy Conservation +======================================= + +Let + +$$ \begin{aligned} + \tilde{S}_{l, l', \vec{r}} &=& &\tilde{E}_{l, \vec{r}} \otimes \hat{H}_{l', \vec{r} + \frac{1}{2}} \\ + &=& &\vec{x} (\tilde{E}^y_{l,m+1,n,p} \hat{H}^z_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^z_{l,m+1,n,p} \hat{H}^y_{l', \vec{r} + \frac{1}{2}}) \\ + & &+ &\vec{y} (\tilde{E}^z_{l,m,n+1,p} \hat{H}^x_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^x_{l,m,n+1,p} \hat{H}^z_{l', \vec{r} + \frac{1}{2}}) \\ + & &+ &\vec{z} (\tilde{E}^x_{l,m,n,p+1} \hat{H}^y_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^y_{l,m,n,p+1} \hat{H}^z_{l', \vec{r} + \frac{1}{2}}) + \end{aligned} +$$ + +where $\vec{r} = (m, n, p)$ and $\otimes$ is a modified cross product +in which the $\tilde{E}$ terms are shifted as indicated. + +By taking the divergence and rearranging terms, we can show that + +$$ + \begin{aligned} + \hat{\nabla} \cdot \tilde{S}_{l, l', \vec{r}} + &= \hat{\nabla} \cdot (\tilde{E}_{l, \vec{r}} \otimes \hat{H}_{l', \vec{r} + \frac{1}{2}}) \\ + &= \hat{H}_{l', \vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l, \vec{r}} - + \tilde{E}_{l, \vec{r}} \cdot \hat{\nabla} \times \hat{H}_{l', \vec{r} + \frac{1}{2}} \\ + &= \hat{H}_{l', \vec{r} + \frac{1}{2}} \cdot + (-\tilde{\partial}_t \mu_{\vec{r} + \frac{1}{2}} \hat{H}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} - + \hat{M}_{l, \vec{r} + \frac{1}{2}}) - + \tilde{E}_{l, \vec{r}} \cdot (\hat{\partial}_t \tilde{\epsilon}_{\vec{r}} \tilde{E}_{l'+\frac{1}{2}, \vec{r}} + + \tilde{J}_{l', \vec{r}}) \\ + &= \hat{H}_{l'} \cdot (-\mu / \Delta_t)(\hat{H}_{l + \frac{1}{2}} - \hat{H}_{l - \frac{1}{2}}) - + \tilde{E}_l \cdot (\epsilon / \Delta_t )(\tilde{E}_{l'+\frac{1}{2}} - \tilde{E}_{l'-\frac{1}{2}}) + - \hat{H}_{l'} \cdot \hat{M}_{l} - \tilde{E}_l \cdot \tilde{J}_{l'} \\ + \end{aligned} +$$ + +where in the last line the spatial subscripts have been dropped to emphasize +the time subscripts $l, l'$, i.e. + +$$ + \begin{aligned} + \tilde{E}_l &= \tilde{E}_{l, \vec{r}} \\ + \hat{H}_l &= \tilde{H}_{l, \vec{r} + \frac{1}{2}} \\ + \tilde{\epsilon} &= \tilde{\epsilon}_{\vec{r}} \\ + \end{aligned} +$$ + +etc. +For $l' = l + \frac{1}{2}$ we get + +$$ + \begin{aligned} + \hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}} + &= \hat{H}_{l + \frac{1}{2}} \cdot + (-\mu / \Delta_t)(\hat{H}_{l + \frac{1}{2}} - \hat{H}_{l - \frac{1}{2}}) - + \tilde{E}_l \cdot (\epsilon / \Delta_t)(\tilde{E}_{l+1} - \tilde{E}_l) + - \hat{H}_{l'} \cdot \hat{M}_l - \tilde{E}_l \cdot \tilde{J}_{l + \frac{1}{2}} \\ + &= (-\mu / \Delta_t)(\hat{H}^2_{l + \frac{1}{2}} - \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}}) - + (\epsilon / \Delta_t)(\tilde{E}_{l+1} \cdot \tilde{E}_l - \tilde{E}^2_l) + - \hat{H}_{l'} \cdot \hat{M}_l - \tilde{E}_l \cdot \tilde{J}_{l + \frac{1}{2}} \\ + &= -(\mu \hat{H}^2_{l + \frac{1}{2}} + +\epsilon \tilde{E}_{l+1} \cdot \tilde{E}_l) / \Delta_t \\ + +(\mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}} + +\epsilon \tilde{E}^2_l) / \Delta_t \\ + - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\ + \end{aligned} +$$ + +and for $l' = l - \frac{1}{2}$, + +$$ + \begin{aligned} + \hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}} + &= (\mu \hat{H}^2_{l - \frac{1}{2}} + +\epsilon \tilde{E}_{l-1} \cdot \tilde{E}_l) / \Delta_t \\ + -(\mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}} + +\epsilon \tilde{E}^2_l) / \Delta_t \\ + - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\ + \end{aligned} +$$ + +These two results form the discrete time-domain analogue to Poynting's theorem. +They hint at the expressions for the energy, which can be calculated at the same +time-index as either the E or H field: + +$$ + \begin{aligned} + U_l &= \epsilon \tilde{E}^2_l + \mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}} \\ + U_{l + \frac{1}{2}} &= \epsilon \tilde{E}_l \cdot \tilde{E}_{l + 1} + \mu \hat{H}^2_{l + \frac{1}{2}} \\ + \end{aligned} +$$ + +Rewriting the Poynting theorem in terms of the energy expressions, + +$$ + \begin{aligned} + (U_{l+\frac{1}{2}} - U_l) / \Delta_t + &= -\hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}} \\ + - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\ + (U_l - U_{l-\frac{1}{2}}) / \Delta_t + &= -\hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}} \\ + - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\ + \end{aligned} +$$ + +This result is exact and should practically hold to within numerical precision. No time- +or spatial-averaging is necessary. + +Note that each value of $J$ contributes to the energy twice (i.e. once per field update) +despite only causing the value of $E$ to change once (same for $M$ and $H$). + + +Sources +============= + +It is often useful to excite the simulation with an arbitrary broadband pulse and then +extract the frequency-domain response by performing an on-the-fly Fourier transform +of the time-domain fields. + +The Ricker wavelet (normalized second derivative of a Gaussian) is commonly used for the pulse +shape. It can be written + +$$ f_r(t) = (1 - \frac{1}{2} (\omega (t - \tau))^2) e^{-(\frac{\omega (t - \tau)}{2})^2} $$ + +with $\tau > \frac{2 * \pi}{\omega}$ as a minimum delay to avoid a discontinuity at +t=0 (assuming the source is off for t<0 this gives $\sim 10^{-3}$ error at t=0). + + + +Boundary conditions +=================== +# TODO notes about boundaries / PMLs + + + +## Sub-modules + +* [meanas.fdtd.base](#meanas.fdtd.base) +* [meanas.fdtd.boundaries](#meanas.fdtd.boundaries) +* [meanas.fdtd.energy](#meanas.fdtd.energy) +* [meanas.fdtd.pml](#meanas.fdtd.pml) + + + + + + +------------------------------------------- + + + +# Module `meanas.fdtd.base` {#meanas.fdtd.base} + +Basic FDTD field updates + + + + + +## Functions + + + +### Function `maxwell_e` {#meanas.fdtd.base.maxwell_e} + + + + + + +> `def maxwell_e(dt: float, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]` + + +Build a function which performs a portion the time-domain E-field update, + + E += curl_back(H[t]) / epsilon + +The full update should be + + E += (curl_back(H[t]) + J) / epsilon + +which requires an additional step of `E += J / epsilon` which is not performed +by the generated function. + +See [meanas.fdmath](#meanas.fdmath) for descriptions of + +- This update step: "Maxwell's equations" section +- dxes: "Datastructure: dx_lists_t" section +- epsilon: "Permittivity and Permeability" section + +Also see the "Timestep" section of [meanas.fdtd](#meanas.fdtd) for a discussion of +the dt parameter. + + +Args +-----= +**```dt```** +: Timestep. See [meanas.fdtd](#meanas.fdtd) for details. + + +**```dxes```** +: Grid description; see [meanas.fdmath](#meanas.fdmath). + + + +Returns +-----= +Function `f(E_old, H_old, epsilon) -> E_new`. + + +### Function `maxwell_h` {#meanas.fdtd.base.maxwell_h} + + + + + + +> `def maxwell_h(dt: float, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]` + + +Build a function which performs part of the time-domain H-field update, + + H -= curl_forward(E[t]) / mu + +The full update should be + + H -= (curl_forward(E[t]) + M) / mu + +which requires an additional step of `H -= M / mu` which is not performed +by the generated function; this step can be omitted if there is no magnetic +current M. + +See [meanas.fdmath](#meanas.fdmath) for descriptions of + +- This update step: "Maxwell's equations" section +- dxes: "Datastructure: dx_lists_t" section +- mu: "Permittivity and Permeability" section + +Also see the "Timestep" section of [meanas.fdtd](#meanas.fdtd) for a discussion of +the dt parameter. + + +Args +-----= +**```dt```** +: Timestep. See [meanas.fdtd](#meanas.fdtd) for details. + + +**```dxes```** +: Grid description; see [meanas.fdmath](#meanas.fdmath). + + + +Returns +-----= +Function `f(E_old, H_old, epsilon) -> E_new`. + + + + +------------------------------------------- + + + +# Module `meanas.fdtd.boundaries` {#meanas.fdtd.boundaries} + +Boundary conditions + +#TODO conducting boundary documentation + + + + + +## Functions + + + +### Function `conducting_boundary` {#meanas.fdtd.boundaries.conducting_boundary} + + + + + + +> `def conducting_boundary(direction: int, polarity: int) -> tuple[collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], collections.abc.Callable[..., numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]]]` + + + + + + + +------------------------------------------- + + + +# Module `meanas.fdtd.energy` {#meanas.fdtd.energy} + + + + + + + +## Functions + + + +### Function `delta_energy_e2h` {#meanas.fdtd.energy.delta_energy_e2h} + + + + + + +> `def delta_energy_e2h(dt: float, h0: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], e1: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], h2: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], e3: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]` + + +Change in energy during the half-step from e1 to h2. + +This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2) + + +Args +-----= +**```h0```** +: E-field one half-timestep before the start of the energy delta. + + +**```e1```** +: H-field at the start of the energy delta. + + +**```h2```** +: E-field at the end of the energy delta (one half-timestep after e1). + + +**```e3```** +: H-field one half-timestep after the end of the energy delta. + + +**```epsilon```** +: Dielectric constant distribution. + + +**```mu```** +: Magnetic permeability distribution. + + +**```dxes```** +: Grid description; see [meanas.fdmath](#meanas.fdmath). + + + +Returns +-----= +Change in energy from the time of e1 to the time of h2. + + +### Function `delta_energy_h2e` {#meanas.fdtd.energy.delta_energy_h2e} + + + + + + +> `def delta_energy_h2e(dt: float, e0: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], h1: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], e2: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], h3: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]` + + +Change in energy during the half-step from h1 to e2. + +This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2) + + +Args +-----= +**```e0```** +: E-field one half-timestep before the start of the energy delta. + + +**```h1```** +: H-field at the start of the energy delta. + + +**```e2```** +: E-field at the end of the energy delta (one half-timestep after h1). + + +**```h3```** +: H-field one half-timestep after the end of the energy delta. + + +**```epsilon```** +: Dielectric constant distribution. + + +**```mu```** +: Magnetic permeability distribution. + + +**```dxes```** +: Grid description; see [meanas.fdmath](#meanas.fdmath). + + + +Returns +-----= +Change in energy from the time of h1 to the time of e2. + + +### Function `delta_energy_j` {#meanas.fdtd.energy.delta_energy_j} + + + + + + +> `def delta_energy_j(j0: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], e1: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]` + + +Calculate + +Note that each value of $J$ contributes to the energy twice (i.e. once per field update) +despite only causing the value of $E$ to change once (same for $M$ and $H$). + + +### Function `dxmul` {#meanas.fdtd.energy.dxmul} + + + + + + +> `def dxmul(ee: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], hh: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | float | None = None, mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | float | None = None, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]` + + + + + +### Function `energy_estep` {#meanas.fdtd.energy.energy_estep} + + + + + + +> `def energy_estep(h0: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], e1: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], h2: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]` + + +Calculate energy U at the time of the provided E-field e1. + +TODO: Figure out what this means spatially. + + +Args +-----= +**```h0```** +: H-field one half-timestep before the energy. + + +**```e1```** +: E-field (at the same timestep as the energy). + + +**```h2```** +: H-field one half-timestep after the energy. + + +**```epsilon```** +: Dielectric constant distribution. + + +**```mu```** +: Magnetic permeability distribution. + + +**```dxes```** +: Grid description; see [meanas.fdmath](#meanas.fdmath). + + + +Returns +-----= +Energy, at the time of the E-field e1. + + +### Function `energy_hstep` {#meanas.fdtd.energy.energy_hstep} + + + + + + +> `def energy_hstep(e0: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], h1: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], e2: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, mu: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]` + + +Calculate energy U at the time of the provided H-field h1. + +TODO: Figure out what this means spatially. + + +Args +-----= +**```e0```** +: E-field one half-timestep before the energy. + + +**```h1```** +: H-field (at the same timestep as the energy). + + +**```e2```** +: E-field one half-timestep after the energy. + + +**```epsilon```** +: Dielectric constant distribution. + + +**```mu```** +: Magnetic permeability distribution. + + +**```dxes```** +: Grid description; see [meanas.fdmath](#meanas.fdmath). + + + +Returns +-----= +Energy, at the time of the H-field h1. + + +### Function `poynting` {#meanas.fdtd.energy.poynting} + + + + + + +> `def poynting(e: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], h: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]` + + +Calculate the poynting vector S ($S$). + +This is the energy transfer rate (amount of energy U per dt transferred +between adjacent cells) in each direction that happens during the half-step +bounded by the two provided fields. + +The returned vector field S is the energy flow across +x, +y, and +z +boundaries of the corresponding U cell. For example, + +``` + mx = numpy.roll(mask, -1, axis=0) + my = numpy.roll(mask, -1, axis=1) + mz = numpy.roll(mask, -1, axis=2) + + u_hstep = fdtd.energy_hstep(e0=es[ii - 1], h1=hs[ii], e2=es[ii], **args) + u_estep = fdtd.energy_estep(h0=hs[ii], e1=es[ii], h2=hs[ii + 1], **args) + delta_j_B = fdtd.delta_energy_j(j0=js[ii], e1=es[ii], dxes=dxes) + du_half_h2e = u_estep - u_hstep - delta_j_B + + s_h2e = -fdtd.poynting(e=es[ii], h=hs[ii], dxes=dxes) * dt + planes = [s_h2e[0, mask].sum(), -s_h2e[0, mx].sum(), + s_h2e[1, mask].sum(), -s_h2e[1, my].sum(), + s_h2e[2, mask].sum(), -s_h2e[2, mz].sum()] + + assert_close(sum(planes), du_half_h2e[mask]) +``` + +(see meanas.tests.test\_fdtd.test\_poynting\_planes) + +The full relationship is +$$ + \begin{aligned} + (U_{l+\frac{1}{2}} - U_l) / \Delta_t + &= -\hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}} \\ + - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\ + (U_l - U_{l-\frac{1}{2}}) / \Delta_t + &= -\hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}} \\ + - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\ + \end{aligned} +$$ + +These equalities are exact and should practically hold to within numerical precision. +No time- or spatial-averaging is necessary. (See [meanas.fdtd](#meanas.fdtd) docs for derivation.) + + +Args +-----= +**```e```** +: E-field + + +**```h```** +: H-field (one half-timestep before or after e) + + +**```dxes```** +: Grid description; see [meanas.fdmath](#meanas.fdmath). + + + +Returns +-----= +s +: Vector field. Components indicate the energy transfer rate from the + corresponding energy cell into its +x, +y, and +z neighbors during + the half-step from the time of the earlier input field until the + time of later input field. + + + + +### Function `poynting_divergence` {#meanas.fdtd.energy.poynting_divergence} + + + + + + +> `def poynting_divergence(s: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, *, e: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, h: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]] | None = None, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]] | None = None) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]` + + +Calculate the divergence of the poynting vector. + +This is the net energy flow for each cell, i.e. the change in energy U +per dt caused by transfer of energy to nearby cells (rather than +absorption/emission by currents J or M). + +See [poynting()](#meanas.fdtd.energy.poynting) and [meanas.fdtd](#meanas.fdtd) for more details. + +Args +-----= +**```s```** +: Poynting vector, as calculated with [poynting()](#meanas.fdtd.energy.poynting). Optional; caller + can provide e and h instead. + + +**```e```** +: E-field (optional; need either s or both e and h) + + +**```h```** +: H-field (optional; need either s or both e and h) + + +**```dxes```** +: Grid description; see [meanas.fdmath](#meanas.fdmath). + + + +Returns +-----= +ds +: Divergence of the poynting vector. + Entries indicate the net energy flow out of the corresponding + energy cell. + + + + + + +------------------------------------------- + + + +# Module `meanas.fdtd.pml` {#meanas.fdtd.pml} + +PML implementations + +#TODO discussion of PMLs +#TODO cpml documentation + + + + + +## Functions + + + +### Function `cpml_params` {#meanas.fdtd.pml.cpml_params} + + + + + + +> `def cpml_params(axis: int, polarity: int, dt: float, thickness: int = 8, ln_R_per_layer: float = -1.6, epsilon_eff: float = 1, mu_eff: float = 1, m: float = 3.5, ma: float = 1, cfs_alpha: float = 0) -> dict[str, typing.Any]` + + + + + +### Function `updates_with_cpml` {#meanas.fdtd.pml.updates_with_cpml} + + + + + + +> `def updates_with_cpml(cpml_params: collections.abc.Sequence[collections.abc.Sequence[dict[str, typing.Any] | None]], dt: float, dxes: collections.abc.Sequence[collections.abc.Sequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], *, dtype: Union[numpy.dtype[Any], ForwardRef(None), type[Any], numpy._typing._dtype_like._SupportsDType[numpy.dtype[Any]], str, tuple[Any, int], tuple[Any, Union[SupportsIndex, collections.abc.Sequence[SupportsIndex]]], list[Any], numpy._typing._dtype_like._DTypeDict, tuple[Any, Any]] = numpy.float32) -> tuple[collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], None], collections.abc.Callable[[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]], numpy.ndarray[typing.Any, numpy.dtype[numpy.floating]]], None]]` + + + + + + + +------------------------------------------- + + + +# Module `meanas.test` {#meanas.test} + +Tests (run with `python3 -m pytest -rxPXs | tee results.txt`) + + + +## Sub-modules + +* [meanas.test.conftest](#meanas.test.conftest) +* [meanas.test.test_fdfd](#meanas.test.test_fdfd) +* [meanas.test.test_fdfd_pml](#meanas.test.test_fdfd_pml) +* [meanas.test.test_fdtd](#meanas.test.test_fdtd) +* [meanas.test.utils](#meanas.test.utils) + + + + + + +------------------------------------------- + + + +# Module `meanas.test.conftest` {#meanas.test.conftest} + +Test fixtures + + + + + +## Functions + + + +### Function `dx` {#meanas.test.conftest.dx} + + + + + + +> `def dx(request: Any) -> float` + + + + + +### Function `dxes` {#meanas.test.conftest.dxes} + + + + + + +> `def dxes(request: Any, shape: tuple[int, ...], dx: float) -> list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]]` + + + + + +### Function `epsilon` {#meanas.test.conftest.epsilon} + + + + + + +> `def epsilon(request: Any, shape: tuple[int, ...], epsilon_bg: float, epsilon_fg: float) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]` + + + + + +### Function `epsilon_bg` {#meanas.test.conftest.epsilon_bg} + + + + + + +> `def epsilon_bg(request: Any) -> float` + + + + + +### Function `epsilon_fg` {#meanas.test.conftest.epsilon_fg} + + + + + + +> `def epsilon_fg(request: Any) -> float` + + + + + +### Function `j_mag` {#meanas.test.conftest.j_mag} + + + + + + +> `def j_mag(request: Any) -> float` + + + + + +### Function `shape` {#meanas.test.conftest.shape} + + + + + + +> `def shape(request: Any) -> tuple[int, ...]` + + + + + + + +------------------------------------------- + + + +# Module `meanas.test.test_fdfd` {#meanas.test.test_fdfd} + + + + + + + +## Functions + + + +### Function `j_distribution` {#meanas.test.test_fdfd.j_distribution} + + + + + + +> `def j_distribution(request: Any, shape: tuple[int, ...], j_mag: float) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]` + + + + + +### Function `omega` {#meanas.test.test_fdfd.omega} + + + + + + +> `def omega(request: Any) -> float` + + + + + +### Function `pec` {#meanas.test.test_fdfd.pec} + + + + + + +> `def pec(request: Any) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None` + + + + + +### Function `pmc` {#meanas.test.test_fdfd.pmc} + + + + + + +> `def pmc(request: Any) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None` + + + + + +### Function `sim` {#meanas.test.test_fdfd.sim} + + + + + + +> `def sim(request: Any, shape: tuple[int, ...], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], dxes: list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], j_distribution: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], omega: float, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None) -> meanas.test.test_fdfd.FDResult` + + +Build simulation from parts + + +### Function `test_poynting_planes` {#meanas.test.test_fdfd.test_poynting_planes} + + + + + + +> `def test_poynting_planes(sim: FDResult) -> None` + + + + + +### Function `test_residual` {#meanas.test.test_fdfd.test_residual} + + + + + + +> `def test_residual(sim: FDResult) -> None` + + + + + + +## Classes + + + +### Class `FDResult` {#meanas.test.test_fdfd.FDResult} + + + +[[view code]](https://mpxd.net/code/jan/meanas/src/commit/651e255704ecd14e72a49f0a5662cc304accfd9f/meanas/test/test_fdfd.py#L102-L111) + + + +> `class FDResult(shape: tuple[int, ...], dxes: list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], omega: complex, j: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], e: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None)` + + +FDResult(shape: tuple[int, ...], dxes: list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], omega: complex, j: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], e: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None) + + + + + +#### Class variables + + + +##### Variable `dxes` {#meanas.test.test_fdfd.FDResult.dxes} + + + + +##### Variable `e` {#meanas.test.test_fdfd.FDResult.e} + + + + +##### Variable `epsilon` {#meanas.test.test_fdfd.FDResult.epsilon} + + + + +##### Variable `j` {#meanas.test.test_fdfd.FDResult.j} + + + + +##### Variable `omega` {#meanas.test.test_fdfd.FDResult.omega} + + + + +##### Variable `pec` {#meanas.test.test_fdfd.FDResult.pec} + + + + +##### Variable `pmc` {#meanas.test.test_fdfd.FDResult.pmc} + + + + +##### Variable `shape` {#meanas.test.test_fdfd.FDResult.shape} + + + + + + + + +------------------------------------------- + + + +# Module `meanas.test.test_fdfd_pml` {#meanas.test.test_fdfd_pml} + + + + + + + +## Functions + + + +### Function `dxes` {#meanas.test.test_fdfd_pml.dxes} + + + + + + +> `def dxes(request: Any, shape: tuple[int, ...], dx: float, omega: float, epsilon_fg: float) -> list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]]` + + + + + +### Function `epsilon` {#meanas.test.test_fdfd_pml.epsilon} + + + + + + +> `def epsilon(request: Any, shape: tuple[int, ...], epsilon_bg: float, epsilon_fg: float) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]` + + + + + +### Function `j_distribution` {#meanas.test.test_fdfd_pml.j_distribution} + + + + + + +> `def j_distribution(request: Any, shape: tuple[int, ...], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], dxes: collections.abc.MutableSequence[collections.abc.MutableSequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], omega: float, src_polarity: int) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]]` + + + + + +### Function `omega` {#meanas.test.test_fdfd_pml.omega} + + + + + + +> `def omega(request: Any) -> float` + + + + + +### Function `pec` {#meanas.test.test_fdfd_pml.pec} + + + + + + +> `def pec(request: Any) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None` + + + + + +### Function `pmc` {#meanas.test.test_fdfd_pml.pmc} + + + + + + +> `def pmc(request: Any) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None` + + + + + +### Function `shape` {#meanas.test.test_fdfd_pml.shape} + + + + + + +> `def shape(request: Any) -> tuple[int, int, int]` + + + + + +### Function `sim` {#meanas.test.test_fdfd_pml.sim} + + + + + + +> `def sim(request: Any, shape: tuple[int, ...], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], dxes: collections.abc.MutableSequence[collections.abc.MutableSequence[numpy.ndarray[typing.Any, numpy.dtype[numpy.floating | numpy.complexfloating]]]], j_distribution: numpy.ndarray[typing.Any, numpy.dtype[numpy.complex128]], omega: float, pec: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None, pmc: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] | None) -> meanas.test.test_fdfd.FDResult` + + + + + +### Function `src_polarity` {#meanas.test.test_fdfd_pml.src_polarity} + + + + + + +> `def src_polarity(request: Any) -> int` + + + + + +### Function `test_pml` {#meanas.test.test_fdfd_pml.test_pml} + + + + + + +> `def test_pml(sim: meanas.test.test_fdfd.FDResult, src_polarity: int) -> None` + + + + + + + +------------------------------------------- + + + +# Module `meanas.test.test_fdtd` {#meanas.test.test_fdtd} + + + + + + + +## Functions + + + +### Function `dt` {#meanas.test.test_fdtd.dt} + + + + + + +> `def dt(request: Any) -> float` + + + + + +### Function `j_distribution` {#meanas.test.test_fdtd.j_distribution} + + + + + + +> `def j_distribution(request: Any, shape: tuple[int, ...], j_mag: float) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]` + + + + + +### Function `j_steps` {#meanas.test.test_fdtd.j_steps} + + + + + + +> `def j_steps(request: Any) -> tuple[int, ...]` + + + + + +### Function `sim` {#meanas.test.test_fdtd.sim} + + + + + + +> `def sim(request: Any, shape: tuple[int, ...], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], dxes: list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], dt: float, j_distribution: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], j_steps: tuple[int, ...]) -> meanas.test.test_fdtd.TDResult` + + + + + +### Function `test_energy_conservation` {#meanas.test.test_fdtd.test_energy_conservation} + + + + + + +> `def test_energy_conservation(sim: TDResult) -> None` + + +Assumes fields start at 0 before J0 is added + + +### Function `test_initial_energy` {#meanas.test.test_fdtd.test_initial_energy} + + + + + + +> `def test_initial_energy(sim: TDResult) -> None` + + +Assumes fields start at 0 before J0 is added + + +### Function `test_initial_fields` {#meanas.test.test_fdtd.test_initial_fields} + + + + + + +> `def test_initial_fields(sim: TDResult) -> None` + + + + + +### Function `test_poynting_divergence` {#meanas.test.test_fdtd.test_poynting_divergence} + + + + + + +> `def test_poynting_divergence(sim: TDResult) -> None` + + + + + +### Function `test_poynting_planes` {#meanas.test.test_fdtd.test_poynting_planes} + + + + + + +> `def test_poynting_planes(sim: TDResult) -> None` + + + + + + +## Classes + + + +### Class `TDResult` {#meanas.test.test_fdtd.TDResult} + + + +[[view code]](https://mpxd.net/code/jan/meanas/src/commit/651e255704ecd14e72a49f0a5662cc304accfd9f/meanas/test/test_fdtd.py#L158-L168) + + + +> `class TDResult(shape: tuple[int, ...], dt: float, dxes: list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], j_distribution: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], j_steps: tuple[int, ...], es: list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] = , hs: list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] = , js: list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] = )` + + +TDResult(shape: tuple[int, ...], dt: float, dxes: list[list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]], epsilon: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], j_distribution: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], j_steps: tuple[int, ...], es: list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] = , hs: list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] = , js: list[numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] = ) + + + + + +#### Class variables + + + +##### Variable `dt` {#meanas.test.test_fdtd.TDResult.dt} + + + + +##### Variable `dxes` {#meanas.test.test_fdtd.TDResult.dxes} + + + + +##### Variable `epsilon` {#meanas.test.test_fdtd.TDResult.epsilon} + + + + +##### Variable `es` {#meanas.test.test_fdtd.TDResult.es} + + + + +##### Variable `hs` {#meanas.test.test_fdtd.TDResult.hs} + + + + +##### Variable `j_distribution` {#meanas.test.test_fdtd.TDResult.j_distribution} + + + + +##### Variable `j_steps` {#meanas.test.test_fdtd.TDResult.j_steps} + + + + +##### Variable `js` {#meanas.test.test_fdtd.TDResult.js} + + + + +##### Variable `shape` {#meanas.test.test_fdtd.TDResult.shape} + + + + + + + + +------------------------------------------- + + + +# Module `meanas.test.utils` {#meanas.test.utils} + + + + + + + +## Functions + + + +### Function `assert_close` {#meanas.test.utils.assert_close} + + + + + + +> `def assert_close(x: numpy.ndarray[typing.Any, numpy.dtype[+_ScalarType_co]], y: numpy.ndarray[typing.Any, numpy.dtype[+_ScalarType_co]], *args, **kwargs) -> None` + + + + + +### Function `assert_fields_close` {#meanas.test.utils.assert_fields_close} + + + + + + +> `def assert_fields_close(x: numpy.ndarray[typing.Any, numpy.dtype[+_ScalarType_co]], y: numpy.ndarray[typing.Any, numpy.dtype[+_ScalarType_co]], *args, **kwargs) -> None` + + + + + + +----- +Generated by *pdoc* 0.11.1 (). diff --git a/examples/bend.py b/examples/bend.py new file mode 100644 index 0000000..2af58ff --- /dev/null +++ b/examples/bend.py @@ -0,0 +1,61 @@ +import numpy +from numpy import pi +import gridlock +from meanas.fdfd import waveguide_2d, waveguide_cyl +from matplotlib import pyplot + + +wl = 1310 +dx = 10 +radius = 25e3 +width = 400 +thf = 161 +thp = 77 +eps_si = 3.51 ** 2 +eps_ox = 1.453 ** 2 + + + +x0 = (width / 2) % dx +omega = 2 * pi / wl + +grid = gridlock.Grid([ + numpy.arange(-3000, 3000 + dx, dx), + numpy.arange(-1500, 1500 + dx, dx), + numpy.arange(-1 * dx, 1 * dx + dx, dx)], + periodic=True, + ) +epsilon = grid.allocate(eps_ox) + +grid.draw_cuboid(epsilon, center=[x0, thf / 2, 0], dimensions=[width, thf, 1e6], foreground=eps_si) +grid.draw_cuboid(epsilon, center=[x0, thp / 2, 0], dimensions=[width + 3e3, thp, 1e6], foreground=eps_si) +grid.draw_cuboid(epsilon, center=[x0 + width / 2 + 2e3, thf / 2, 0], dimensions=[1e3, thf, 1e6], foreground=eps_si) +grid.draw_cuboid(epsilon, center=[x0 - width / 2 - 2e3, thf / 2, 0], dimensions=[1e3, thf, 1e6], foreground=eps_si) + + +tilt = (1 + grid.xyz[0] / radius) +se = tilt[None, :, None, None] * epsilon +#print(tilt) + + +dxes = [grid.dxyz, grid.autoshifted_dxyz()] +dxes_2d = [[d[0], d[1]] for d in dxes] +mode_numbers = numpy.arange(6) +args = dict(dxes=dxes_2d, omega=omega, mode_numbers=numpy.arange(6)) + +e_xys, wavenumbers = waveguide_2d.solve_modes(epsilon=se[:, :, :, 1].ravel(), **args) +ee, hh = waveguide_2d.normalized_fields_e(e_xys[0], wavenumber=wavenumbers[0], dxes=dxes_2d, omega=omega, epsilon=se[:, :, :, 1].ravel()) + +#print('tilted baseline:' wavenumbers * wl / pi / 2) + + +rmin = radius + grid.xyz[0].min() +epsv = epsilon[:, :, :, 1].ravel() +e2, angular_wavenumbers2 = waveguide_cyl.solve_modes(epsilon=epsv, rmin=rmin, **args) +print('cylindrical:', angular_wavenumbers2 * wl / pi / 2 / radius) + + +wavenumbers_2 = waveguide_cyl.linear_wavenumbers(e_xys=e2, angular_wavenumbers=angular_wavenumbers2, rmin=rmin, epsilon=epsv, dxes=dxes_2d) +print('cyl_auto:', wavenumbers_2 * wl / pi / 2) + + diff --git a/examples/eme.py b/examples/eme.py new file mode 100644 index 0000000..8c761cc --- /dev/null +++ b/examples/eme.py @@ -0,0 +1,65 @@ +import numpy +from numpy import pi +import gridlock +from gridlock import XYZExtent +from meanas.fdfd import waveguide_2d, waveguide_cyl +from matplotlib import pyplot + + +wl = 1310 +dx = 10 +radius = 25e3 +width = 400 +thf = 161 +thp = 77 +eps_si = 3.51 ** 2 +eps_ox = 1.453 ** 2 + + + +x0 = (width / 2) % dx +omega = 2 * pi / wl + +grid = gridlock.Grid([ + numpy.arange(-3000, 3000 + dx, dx), + numpy.arange(-1500, 1500 + dx, dx), + numpy.arange(-5 * dx, 5 * dx + dx, dx)], + periodic=True, + ) +epsilon = grid.allocate(eps_ox) + +grid.draw_cuboid(epsilon, extent=XYZExtent(xctr=x0, lx=width + 5e3, ymin=0, ymax=thf, zmin=-1e6, zmax=0), foreground=eps_si) +grid.draw_cuboid(epsilon, extent=XYZExtent(xmax=-width / 2, lx=1.5e3, ymin=thp, ymax=1e6, zmin=-1e6, zmax=0), foreground=eps_ox) +grid.draw_cuboid(epsilon, extent=XYZExtent(xmin= width / 2, lx=1.5e3, ymin=thp, ymax=1e6, zmin=-1e6, zmax=0), foreground=eps_ox) + +grid.draw_cuboid(epsilon, extent=XYZExtent(xmax=-(width / 2 + 2.5e3), lx=1e3, ymin=0, ymax=thf, zmin=0, zmax=1e6), foreground=eps_si) +grid.draw_cuboid(epsilon, extent=XYZExtent(xmax= width / 2 + 2.5e3, lx=1e3, ymin=0, ymax=thf, zmin=0, zmax=1e6), foreground=eps_si) + + +dxes = [grid.dxyz, grid.autoshifted_dxyz()] +dxes_2d = [[d[0], d[1]] for d in dxes] +mode_numbers = numpy.arange(20) +args = dict(dxes=dxes_2d, omega=omega, mode_numbers=mode_numbers) + +eps1 = epsilon[:, :, :, 1].ravel() +eps2 = epsilon[:, :, :, -2].ravel() +eL_xys, wavenumbers_L = waveguide_2d.solve_modes(epsilon=eps1, **args) +eR_xys, wavenumbers_R = waveguide_2d.solve_modes(epsilon=eps2, **args) +eh_L = [ + waveguide_2d.normalized_fields_e(e_xy, wavenumber=wavenumber, dxes=dxes_2d, omega=omega, epsilon=eps1) + for e_xy, wavenumber in zip(eL_xys, wavenumbers_L)] +eh_R = [ + waveguide_2d.normalized_fields_e(e_xy, wavenumber=wavenumber, dxes=dxes_2d, omega=omega, epsilon=eps2) + for e_xy, wavenumber in zip(eR_xys, wavenumbers_R)] + + +eh_R = [ + waveguide_2d.normalized_fields_e(e_xy, wavenumber=wavenumber, dxes=dxes_2d, omega=omega, epsilon=eps2) + for e_xy, wavenumber in zip(eR_xys, wavenumbers_R)] + + +ss = waveguide_2d.get_s(eh_L, wavenumbers_L, eh_R, wavenumbers_R, dxes=dxes_2d) + +ss11 = waveguide_2d.get_s(eh_L, wavenumbers_L, eh_L, wavenumbers_L, dxes=dxes_2d) +ss22 = waveguide_2d.get_s(eh_R, wavenumbers_R, eh_R, wavenumbers_R, dxes=dxes_2d) + diff --git a/examples/eme_bend.py b/examples/eme_bend.py new file mode 100644 index 0000000..3bd76c9 --- /dev/null +++ b/examples/eme_bend.py @@ -0,0 +1,105 @@ +import numpy +from numpy import pi +import gridlock +from gridlock import XYZExtent +from meanas.fdfd import waveguide_2d, waveguide_cyl +from meanas.fdmath import vec, unvec +from matplotlib import pyplot, colors +from scipy import sparse +import skrf +from skrf import Network + + +wl = 1310 +dx = 10 +radius = 25e3 +width = 400 +thf = 161 +thp = 77 +eps_si = 3.51 ** 2 +eps_ox = 1.453 ** 2 + + + +x0 = (width / 2) % dx +omega = 2 * pi / wl + +grid = gridlock.Grid([ + numpy.arange(-3000, 3000 + dx, dx), + numpy.arange(-1500, 1500 + dx, dx), + numpy.arange(-5 * dx, 5 * dx + dx, dx)], + periodic=True, + ) +epsilon = grid.allocate(eps_ox) + +grid.draw_cuboid(epsilon, extent=XYZExtent(xctr=x0, lx=width + 5e3, ymin=0, ymax=thf, zmin=-1e6, zmax=0), foreground=eps_si) +grid.draw_cuboid(epsilon, extent=XYZExtent(xmax=-width / 2, lx=1.5e3, ymin=thp, ymax=1e6, zmin=-1e6, zctr=0), foreground=eps_ox) +grid.draw_cuboid(epsilon, extent=XYZExtent(xmin= width / 2, lx=1.5e3, ymin=thp, ymax=1e6, zmin=-1e6, zctr=0), foreground=eps_ox) + + +dxes = [grid.dxyz, grid.autoshifted_dxyz()] +dxes_2d = [[d[0], d[1]] for d in dxes] +mode_numbers = numpy.arange(20) +args = dict(dxes=dxes_2d, omega=omega, mode_numbers=mode_numbers) + +eps = epsilon[:, :, :, 2].ravel() +rmin = radius + grid.xyz[0].min() +eL_xys, wavenumbers_L = waveguide_2d.solve_modes(epsilon=eps, **args) +eR_xys, ang_wavenumbers_R = waveguide_cyl.solve_modes(epsilon=eps, **args, rmin=rmin) +linear_wavenumbers_R = waveguide_cyl.linear_wavenumbers(e_xys=eR_xys, angular_wavenumbers=ang_wavenumbers_R, rmin=rmin, epsilon=eps, dxes=dxes_2d) + +eh_L = [ + waveguide_2d.normalized_fields_e(e_xy, wavenumber=wavenumber, dxes=dxes_2d, omega=omega, epsilon=eps) + for e_xy, wavenumber in zip(eL_xys, wavenumbers_L)] +eh_R = [ + waveguide_cyl.normalized_fields_e(e_xy, angular_wavenumber=ang_wavenumber, dxes=dxes_2d, omega=omega, epsilon=eps, rmin=rmin) + for e_xy, ang_wavenumber in zip(eR_xys, ang_wavenumbers_R)] + + +ss = waveguide_2d.get_s(eh_L, wavenumbers_L, eh_R, linear_wavenumbers_R, dxes=dxes_2d) + +ss11 = waveguide_2d.get_s(eh_L, wavenumbers_L, eh_L, wavenumbers_L, dxes=dxes_2d) +ss22 = waveguide_2d.get_s(eh_R, linear_wavenumbers_R, eh_R, linear_wavenumbers_R, dxes=dxes_2d) + + +fig, axes = pyplot.subplots(2, 2) +mb0 = axes[0, 0].pcolormesh(numpy.abs(ss[::-1])**2, cmap='hot', vmin=0) +fig.colorbar(mb0) +axes[1, 0].set_title('S Abs^2') +mb2 = axes[1, 0].pcolormesh(ss[::-1].real, cmap='bwr', norm=colors.CenteredNorm()) +fig.colorbar(mb2) +axes[1, 0].set_title('S Real') +mb3 = axes[1, 1].pcolormesh(ss[::-1].imag, cmap='bwr', norm=colors.CenteredNorm()) +fig.colorbar(mb3) +axes[1, 1].set_title('S Imag') +pyplot.show(block=False) + +e1, h1 = eh_L[2] +e2, h2 = eh_R[2] + +figE, axesE = pyplot.subplots(3, 2) +figH, axesH = pyplot.subplots(3, 2) +esqmax = max(numpy.abs(e1).max(), numpy.abs(e2).max()) ** 2 +hsqmax = max(numpy.abs(h1).max(), numpy.abs(h2).max()) ** 2 +for mm, (ee, hh) in enumerate(zip((e1, e2), (h1, h2))): + E = unvec(ee, grid.shape[:2]) + H = unvec(hh, grid.shape[:2]) + for aa in range(3): + axesE[aa, mm].pcolormesh((numpy.abs(E[aa]) ** 2).T, cmap='bwr', norm=colors.CenteredNorm(halfrange=esqmax)) + axesH[aa, mm].pcolormesh((numpy.abs(H[aa]) ** 2).T, cmap='bwr', norm=colors.CenteredNorm(halfrange=hsqmax)) +pyplot.show(block=False) + + + +net_wb = Network(f=[1 / wl], s = ss) +net_bw = net_wb.copy() +net_bw.renumber(numpy.arange(40), numpy.roll(numpy.arange(40), 20)) + +wg_phase = sparse.diags_array(numpy.exp(-1j * wavenumbers_L * 100e3)) +bend_phase = sparse.diags_array(numpy.exp(-1j * ang_wavenumbers_R * pi / 2)) +net_propwg = Network(f=[1 / wl], s = sparse.block_array(([None, wg_phase], [wg_phase, None])).toarray()[None, ...]) +net_propbend = Network(f=[1 / wl], s = sparse.block_array(([None, bend_phase], [bend_phase, None])).toarray()[None, ...]) + + +cir = skrf.network.cascade_list([net_propwg, net_wb, net_propbend, net_bw, net_propwg]) + diff --git a/examples/fdtd.py b/examples/fdtd.py index 8378b34..1b8827f 100644 --- a/examples/fdtd.py +++ b/examples/fdtd.py @@ -6,13 +6,20 @@ See main() for simulation setup. import sys import time +import copy import numpy import h5py +from numpy.linalg import norm from meanas import fdtd from meanas.fdtd import cpml_params, updates_with_cpml -from masque import Pattern, shapes +from meanas.fdtd.misc import gaussian_packet + +from meanas.fdfd.operators import e_full +from meanas.fdfd.scpml import stretch_with_scpml +from meanas.fdmath import vec +from masque import Pattern, Circle, Polygon import gridlock import pcgen @@ -41,50 +48,51 @@ def perturbed_l3(a: float, radius: float, **kwargs) -> Pattern: `masque.Pattern` object containing the L3 design """ - default_args = {'hole_dose': 1, - 'trench_dose': 1, - 'hole_layer': 0, - 'trench_layer': 1, - 'shifts_a': (0.15, 0, 0.075), - 'shifts_r': (1.0, 1.0, 1.0), - 'xy_size': (10, 10), - 'perturbed_radius': 1.1, - 'trench_width': 1.2e3, - } + default_args = { + 'hole_layer': 0, + 'trench_layer': 1, + 'shifts_a': (0.15, 0, 0.075), + 'shifts_r': (1.0, 1.0, 1.0), + 'xy_size': (10, 10), + 'perturbed_radius': 1.1, + 'trench_width': 1.2e3, + } kwargs = {**default_args, **kwargs} - xyr = pcgen.l3_shift_perturbed_defect(mirror_dims=kwargs['xy_size'], - perturbed_radius=kwargs['perturbed_radius'], - shifts_a=kwargs['shifts_a'], - shifts_r=kwargs['shifts_r']) + xyr = pcgen.l3_shift_perturbed_defect( + mirror_dims=kwargs['xy_size'], + perturbed_radius=kwargs['perturbed_radius'], + shifts_a=kwargs['shifts_a'], + shifts_r=kwargs['shifts_r'], + ) xyr *= a xyr[:, 2] *= radius pat = Pattern() - pat.name = f'L3p-a{a:g}r{radius:g}rp{kwargs["perturbed_radius"]:g}' - pat.shapes += [shapes.Circle(radius=r, offset=(x, y), - dose=kwargs['hole_dose'], - layer=kwargs['hole_layer']) - for x, y, r in xyr] + #pat.name = f'L3p-a{a:g}r{radius:g}rp{kwargs["perturbed_radius"]:g}' + pat.shapes[(kwargs['hole_layer'], 0)] += [ + Circle(radius=r, offset=(x, y)) + for x, y, r in xyr] maxes = numpy.max(numpy.fabs(xyr), axis=0) - pat.shapes += [shapes.Polygon.rectangle( - lx=(2 * maxes[0]), ly=kwargs['trench_width'], - offset=(0, s * (maxes[1] + a + kwargs['trench_width'] / 2)), - dose=kwargs['trench_dose'], layer=kwargs['trench_layer']) - for s in (-1, 1)] + pat.shapes[(kwargs['trench_layer'], 0)] += [ + Polygon.rectangle( + lx=(2 * maxes[0]), ly=kwargs['trench_width'], + offset=(0, s * (maxes[1] + a + kwargs['trench_width'] / 2)) + ) + for s in (-1, 1)] return pat def main(): dtype = numpy.float32 - max_t = 8000 # number of timesteps + max_t = 3600 # number of timesteps dx = 40 # discretization (nm/cell) pml_thickness = 8 # (number of cells) wl = 1550 # Excitation wavelength and fwhm - dwl = 200 + dwl = 100 # Device design parameters xy_size = numpy.array([10, 10]) @@ -107,69 +115,89 @@ def main(): # #### Create the grid, mask, and draw the device #### grid = gridlock.Grid(edge_coords) - epsilon = grid.allocate(n_air**2, dtype=dtype) - grid.draw_slab(epsilon, - surface_normal=2, - center=[0, 0, 0], - thickness=th, - eps=n_slab**2) + epsilon = grid.allocate(n_air ** 2, dtype=dtype) + grid.draw_slab( + epsilon, + slab = dict(axis='z', center=0, span=th), + foreground = n_slab ** 2, + ) + mask = perturbed_l3(a, r) + grid.draw_polygons( + epsilon, + slab = dict(axis='z', center=0, span=2 * th), + foreground = n_air ** 2, + offset2d = (0, 0), + polygons = mask.as_polygons(library=None), + ) - grid.draw_polygons(epsilon, - surface_normal=2, - center=[0, 0, 0], - thickness=2 * th, - eps=n_air**2, - polygons=mask.as_polygons()) + print(f'{grid.shape=}') - print(grid.shape) - - dt = .99/numpy.sqrt(3) - e = [numpy.zeros_like(epsilon[0], dtype=dtype) for _ in range(3)] - h = [numpy.zeros_like(epsilon[0], dtype=dtype) for _ in range(3)] + dt = dx * 0.99 / numpy.sqrt(3) + ee = numpy.zeros_like(epsilon, dtype=dtype) + hh = numpy.zeros_like(epsilon, dtype=dtype) dxes = [grid.dxyz, grid.autoshifted_dxyz()] # PMLs in every direction - pml_params = [[cpml_params(axis=dd, polarity=pp, dt=dt, - thickness=pml_thickness, epsilon_eff=1.0**2) - for pp in (-1, +1)] - for dd in range(3)] - update_E, update_H = updates_with_cpml(cpml_params=pml_params, dt=dt, - dxes=dxes, epsilon=epsilon) + pml_params = [ + [cpml_params(axis=dd, polarity=pp, dt=dt, thickness=pml_thickness, epsilon_eff=n_air ** 2) + for pp in (-1, +1)] + for dd in range(3)] + update_E, update_H = updates_with_cpml(cpml_params=pml_params, dt=dt, dxes=dxes, epsilon=epsilon) + + # sample_interval = numpy.floor(1 / (2 * 1 / wl * dt)).astype(int) + # print(f'Save time interval would be {sample_interval} * dt = {sample_interval * dt:3g}') + # Source parameters and function - w = 2 * numpy.pi * dx / wl - fwhm = dwl * w * w / (2 * numpy.pi * dx) - alpha = (fwhm ** 2) / 8 * numpy.log(2) - delay = 7/numpy.sqrt(2 * alpha) + source_phasor, _delay = gaussian_packet(wl=wl, dwl=100, dt=dt, turn_on=1e-5) + aa, cc, ss = source_phasor(numpy.arange(max_t)) + srca_real = aa * cc + src_maxt = numpy.argwhere(numpy.diff(aa < 1e-5))[-1] + assert aa[src_maxt - 1] >= 1e-5 + phasor_norm = dt / (aa * cc * cc).sum() - def field_source(i): - t0 = i * dt - delay - return numpy.sin(w * t0) * numpy.exp(-alpha * t0**2) + Jph = numpy.zeros_like(epsilon, dtype=complex) + Jph[1, *(grid.shape // 2)] = epsilon[1, *(grid.shape // 2)] + Eph = numpy.zeros_like(Jph) # #### Run a bunch of iterations #### output_file = h5py.File('simulation_output.h5', 'w') start = time.perf_counter() - for t in range(max_t): - update_E(e, h, epsilon) + for tt in range(max_t): + update_E(ee, hh, epsilon) - e[1][tuple(grid.shape//2)] += field_source(t) - update_H(e, h) + if tt < src_maxt: + ee[1, *(grid.shape // 2)] -= srca_real[tt] + update_H(ee, hh) - avg_rate = (t + 1)/(time.perf_counter() - start)) - print(f'iteration {t}: average {avg_rate} iterations per sec') + avg_rate = (tt + 1) / (time.perf_counter() - start) sys.stdout.flush() - if t % 20 == 0: - r = sum([(f * f * e).sum() for f, e in zip(e, epsilon)]) - print('E sum', r) + if tt % 200 == 0: + print(f'iteration {tt}: average {avg_rate} iterations per sec') + E_energy_sum = (ee * ee * epsilon).sum() + print(f'{E_energy_sum=}') # Save field slices - if (t % 20 == 0 and (max_t - t <= 1000 or t <= 2000)) or t == max_t-1: - print('saving E-field') - for j, f in enumerate(e): - output_file['/E{}_t{}'.format('xyz'[j], t)] = f[:, :, round(f.shape[2]/2)] + if (tt % 20 == 0 and (max_t - tt <= 1000 or tt <= 2000)) or tt == max_t - 1: + print(f'saving E-field at iteration {tt}') + output_file[f'/E_t{tt}'] = ee[:, :, :, ee.shape[3] // 2] + + Eph += (cc[tt] - 1j * ss[tt]) * phasor_norm * ee + + omega = 2 * pi / wl + Eph *= numpy.exp(-1j * dt / 2 * omega) + b = -1j * omega * Jph + dxes_fdfd = copy.deepcopy(dxes) + for pp in (-1, +1): + for dd in range(3): + stretch_with_scpml(dxes_fdfd, axis=dd, polarity=pp, omega=omega, epsilon_effective=n_air ** 2, thickness=pml_thickness) + A = e_full(omega=omega, dxes=dxes, epsilon=epsilon) + residual = norm(A @ vec(ee) - vec(b)) / norm(vec(b)) + print(f'FDFD residual is {residual}') + if __name__ == '__main__': main() diff --git a/examples/nom.py b/examples/nom.py new file mode 100644 index 0000000..aa43898 --- /dev/null +++ b/examples/nom.py @@ -0,0 +1,284 @@ + +from simphony.elements import Model +from simphony.netlist import Subcircuit +from simphony.simulation import SweepSimulation + +from matplotlib import pyplot as plt + + +class PeriodicLayer(Model): + def __init__(self, left_modes, right_modes, s_params): + self.left_modes = left_modes + self.right_modes = right_modes + self.left_ports = len(self.left_modes) + self.right_ports = len(self.right_modes) + self.normalize_fields() + self.s_params = s_params + + def normalize_fields(self): + for mode in range(len(self.left_modes)): + self.left_modes[mode].normalize() + for mode in range(len(self.right_modes)): + self.right_modes[mode].normalize() + + +class PeriodicEME: + def __init__(self, layers=[], num_periods=1): + self.layers = layers + self.num_periods = num_periods + self.wavelength = wavelength + + def propagate(self): + wl = self.wavelength + if not len(self.layers): + raise Exception("Must place layers before propagating") + + num_modes = max([l.num_modes for l in self.layers]) + iface = InterfaceSingleMode if num_modes == 1 else InterfaceMultiMode + + eme = EME(layers=self.layers) + left, right = eme.propagate() + self.single_period = eme.s_matrix + + period_layer = PeriodicLayer(left.modes, right.modes, self.single_period) + current_layer = PeriodicLayer(left.modes, right.modes, self.single_period) + interface = iface(right, left) + + for _ in range(self.num_periods - 1): + current_layer.s_params = cascade(current_layer, interface, wl) + current_layer.s_params = cascade(current_layer, period_layer, wl) + + self.s_params = current_layer.s_params + + +class EME: + def __init__(self, layers=[]): + self.layers = layers + self.wavelength = None + + def propagate(self): + layers = self.layers + wl = layers[0].wavelength if self.wavelength is None else self.wavelength + if not len(layers): + raise Exception("Must place layers before propagating") + + num_modes = max([l.num_modes for l in layers]) + iface = InterfaceSingleMode if num_modes == 1 else InterfaceMultiMode + + first_layer = layers[0] + current = Current(wl, first_layer) + interface = iface(first_layer, layers[1]) + + current.s = cascade(current, interface, wl) + current.right_pins = interface.right_pins + + for index in range(1, len(layers) - 1): + layer1 = layers[index] + layer2 = layers[index + 1] + interface = iface(layer1, layer2) + + current.s = cascade(current, layer1, wl) + current.right_pins = layer1.right_pins + + current.s = cascade(current, interface, wl) + current.right_pins = interface.right_pins + + last_layer = layers[-1] + current.s = cascade(current, last_layer, wl) + current.right_pins = last_layer.right_pins + + self.s_matrix = current.s + return first_layer, last_layer + + +def stack(sa, sb): + qab = numpy.eye() - sa.r11 @ sb.r11 + qba = numpy.eye() - sa.r11 @ sb.r11 + #s.t12 = sa.t12 @ numpy.pinv(qab) @ sb.t12 + #s.r21 = sa.t12 @ numpy.pinv(qab) @ sb.r22 @ sa.t21 + sa.r22 + #s.r12 = sb.t21 @ numpy.pinv(qba) @ sa.r11 @ sb.t12 + sb.r11 + #s.t21 = sb.t21 @ numpy.pinv(qba) @ sa.t21 + s.t12 = sa.t12 @ numpy.linalg.solve(qab, sb.t12) + s.r21 = sa.t12 @ numpy.linalg.solve(qab, sb.r22 @ sa.t21) + sa.r22 + s.r12 = sb.t21 @ numpy.linalg.solve(qba, sa.r11 @ sb.t12) + sb.r11 + s.t21 = sb.t21 @ numpy.linalg.solve(qba, sa.t21) + return s + + +def cascade(first, second, wavelength): + circuit = Subcircuit("Device") + + circuit.add([(first, "first"), (second, "second")]) + for port in range(first.right_ports): + circuit.connect("first", "right" + str(port), "second", "left" + str(port)) + + simulation = SweepSimulation(circuit, wavelength, wavelength, num=1) + result = simulation.simulate() + return result.s + + +class InterfaceSingleMode(Model): + def __init__(self, layer1, layer2, num_modes=1): + self.num_modes = num_modes + self.num_ports = 2 * num_modes + self.s = self.solve(layer1, layer2, num_modes) + + def solve(self, layer1, layer2, num_modes): + nm = num_modes + s = numpy.zeros((2 * nm, 2 * nm), dtype=complex) + + for ii, left_mode in enumerate(layer1.modes): + for oo, right_mode in enumerate(layer2.modes): + r, t = get_rt(left_mode, right_mode) + s[ oo, ii] = r + s[nm + oo, ii] = t + + for ii, right_mode in enumerate(layer2.modes): + for oo, left_mode in enumerate(layer1.modes): + r, t = get_rt(right_mode, left_mode) + s[ oo, nm + ii] = t + s[nm + oo, nm + ii] = r + return s + + +class InterfaceMultiMode(Model): + def __init__(self, layer1, layer2): + self.s = self.solve(layer1, layer2) + + def solve(self, layer1, layer2): + n1p = layer1.num_modes + n2p = layer2.num_modes + num_ports = n1p + n2p + s = numpy.zeros((num_ports, num_ports), dtype=complex) + + for l1p in range(n1p): + ts = get_t(l1p, layer1, layer2) + rs = get_r(l1p, layer1, layer2, ts) + s[n1p:, l1p] = ts + s[:n1p, l1p] = rs + + for l2p in range(n2p): + ts = get_t(l2p, layer2, layer1) + rs = get_r(l2p, layer2, layer1, ts) + s[:n1p, n1p + l2p] = ts + s[n1p:, n1p + l2p] = rs + + return s + + +def get_t(p, left, right): + A = numpy.empty(left.shape[0], right.shape[0], dtype=complex) + for ll in range(left.shape[0]): + for rr in range(right.shape[0]): + # TODO optimize loop? + A[i, k] = inner_product(right[rr], left[ll]) + inner_product(left[ll], right[rr]) + + b = numpy.zeros(left.shape[0i]) + b[p] = 2 * inner_product(left[p], left[p]) + + x = numpy.linalg.solve(A, b) + # NOTE: `A` does not depend on `p`, so it might make sense to partially precompute + # the solution (pinv(A), or LU decomposition?) + # Actually solve() can take multiple vectors, so just pass it something with the full diagonal? + + xx = numpy.matmul(numpy.linalg.pinv(A), b) #TODO verify + assert(numpy.allclose(xx, x)) + return x + + +def get_r(p, left, right, t): + r = numpy.empty(left.num_modes, dtype=complex) + for ii in range(left.num_modes): + r[ii] = sum((inner_product(right[kk], left[ii]) - inner_product(left[ii], right[kk])) * t[kk] + for kk in range(right.num_modes) + ) / (2 * inner_product(left[ii], left[ii])) + return r + + +def get_rt(left, right): + s = 0.5 * (inner_product(left, right) + inner_product(right, left)) + d = 0.5 * (inner_product(left, right) - inner_product(right, left)) + t = (s * s - d * d) / s + r = 1 - t / (s + d) + return -r, t + + +def inner_product(left_E, right_H, dxes): + cross_z = left_E[0] * right_H.conj()[1] - left_E[1] * right_H[0].conj() +# cross_z = numpy.cross(left_E, numpy.conj(right_H), axisa=0, axisb=0, axisc=0)[2] + return numpy.trapz(numpy.trapz(cross_z, dxes[0][0]), dxes[0][1]) / 2 # TODO might need cumsum on dxes + + +def propagation_matrix(self, modes, wavelength, distance): + eigenv = numpy.array([mode.neff for mode in modes]) * 2 * numpy.pi / wavelength + prop_diag = numpy.diag(numpy.exp(distance * 1j * numpy.hstack((eigenv, eigenv)))) + prop_matrix = numpy.roll(prop_diag, len(eigenv), axis=0) + return prop_matrix + + +def connect_s(A: numpy.ndarray, k: int, B: numpy.ndarray, l: int): + """ + TODO + connect two n-port networks' s-matrices together. + specifically, connect port `k` on network `A` to port `l` on network + `B`. The resultant network has nports = (A.rank + B.rank-2). + + Args: + A: S-parameter matrix of `A`, shape is fxnxn + k: port index on `A` (port indices start from 0) + B: S-parameter matrix of `B`, shape is fxnxn + l: port index on `B` + + Returns: + C: new S-parameter matrix + """ + if k > A.shape[-1] - 1 or l > B.shape[-1] - 1: + raise (ValueError("port indices are out of range")) + + C = scipy.sparse.block_diag((A, B), dtype=complex) + return innerconnect_s(C, k, A.shape[0] + l) + + +def innerconnect_s(A, k, l): + """ + TODO + n x n x freq + connect two ports of a single n-port network's s-matrix. + Specifically, connect port `k` to port `l` on `A`. This results in + a (n-2)-port network. + + Args: + A: S-parameter matrix of `A`, shape is fxnxn + k: port index on `A` (port indices start from 0) + l: port index on `A` + + Returns: + C: new S-parameter matrix + + Notes: + Relevant papers: + - Compton, R.C.; , "Perspectives in microwave circuit analysis," Circuits and Systems, 1989., Proceedings of the 32nd Midwest Symposium on , vol., no., pp.716-718 vol.2, 14-16 Aug 1989. URL: http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=101955&isnumber=3167 + - Filipsson, Gunnar; , "A New General Computer Algorithm for S-Matrix Calculation of Interconnected Multiports," Microwave Conference, 1981. 11th European , vol., no., pp.700-704, 7-11 Sept. 1981. URL: http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=4131699&isnumber=4131585 + """ + if k > A.shape[-1] - 1 or l > A.shape[-1] - 1: + raise (ValueError("port indices are out of range")) + + l = [l] + k = [k] + + mkl = 1 - A[k, l] + mlk = 1 - A[l, k] + C = A + (A[k, :] * A[:, l] * mlk + + A[l, :] * A[:, k] * mkk + + A[k, :] * A[l, l] * A[:, k] + + A[l, :] * A[k, k] * A[:, l] + ) / ( + mlk * mkl - A[k, k] * A[l, l] + ) + + # remove connected ports + C = npy.delete(C, (k, l), 1) + C = npy.delete(C, (k, l), 2) + + return C + diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index 5fda683..0ab295b 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -433,8 +433,11 @@ def _normalized_fields( norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle) + print('\nAAA\n', inner_product(e, h, dxes, prop_phase=prop_phase)) e *= norm_factor h *= norm_factor + print(f'{sign=} {norm_amplitude=} {norm_angle=} {prop_phase=}') + print(inner_product(e, h, dxes, prop_phase=prop_phase)) return e, h @@ -954,5 +957,3 @@ def inner_product( # TODO documentation Sz_b = E1[1] * H2[0] * dxes_real[0][0][:, None] * dxes_real[1][1][None, :] Sz = 0.5 * (Sz_a.sum() - Sz_b.sum()) return Sz - - diff --git a/meanas/fdtd/misc.py b/meanas/fdtd/misc.py new file mode 100644 index 0000000..74d0149 --- /dev/null +++ b/meanas/fdtd/misc.py @@ -0,0 +1,130 @@ +from typing import Callable +from collections.abc import Sequence +import logging + +import numpy +from numpy.typing import NDArray, ArrayLike +from numpy import pi + + +logger = logging.getLogger(__name__) + + +pulse_fn_t = Callable[[int | NDArray], tuple[float, float, float]] + + +def gaussian_packet( + wl: float, + dwl: float, + dt: float, + turn_on: float = 1e-10, + one_sided: bool = False, + ) -> tuple[pulse_fn_t, float]: + """ + Gaussian pulse (or gaussian ramp) for FDTD excitation + + exp(-a*t*t) ==> exp(-omega * omega / (4 * a)) [fourier, ignoring leading const.] + + FWHM_time is 2 * sqrt(2 * log(2)) * sqrt(2 / a) + FWHM_omega is 2 * sqrt(2 * log(2)) * sqrt(2 * a) = 4 * sqrt(log(2) * a) + + """ + # dt * dw = 4 * ln(2) + + omega = 2 * pi / wl + freq = 1 / wl + fwhm_omega = dwl * omega * omega / (2 * pi) # dwl -> d_omega (approx) + alpha = (fwhm_omega * fwhm_omega) * numpy.log(2) / 8 + delay = numpy.sqrt(-numpy.log(turn_on) / alpha) + delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase + logger.info(f'src_time {2 * delay / dt}') + + def source_phasor(ii: int | NDArray) -> tuple[float, float, float]: + t0 = ii * dt - delay + envelope = numpy.sqrt(numpy.sqrt(2 * alpha / pi)) * numpy.exp(-alpha * t0 * t0) + + if one_sided and t0 > 0: + envelope = 1 + + cc = numpy.cos(omega * t0) + ss = numpy.sin(omega * t0) + + return envelope, cc, ss + + # nrm = numpy.exp(-omega * omega / alpha) / 2 + + return source_phasor, delay + + +def ricker_pulse( + wl: float, + dt: float, + turn_on: float = 1e-10, + ) -> tuple[pulse_fn_t, float]: + """ + Ricker wavelet (second derivative of a gaussian pulse) + + t0 = ii * dt - delay + R = w_peak * t0 / 2 + f(t) = (1 - 2 * (pi * f_peak * t0) ** 2) * exp(-(pi * f_peak * t0)**2 + = (1 - (w_peak * t0)**2 / 2 exp(-(w_peak * t0 / 2) **2) + = (1 - 2 * R * R) * exp(-R * R) + + # NOTE: don't use cosine/sine for J, just for phasor readout + """ + omega = 2 * pi / wl + freq = 1 / wl + r0 = omega / 2 + + from scipy.optimize import root_scalar + delay_results = root_scalar(lambda xx: (1 - omega * omega * tt * tt / 2) * numpy.exp(-omega * omega / 4 * tt * tt) - turn_on, x0=0, x1=-2 / omega) + delay = delay_results.root + delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase + + def source_phasor(ii: int | NDArray) -> tuple[float, float, float]: + t0 = ii * dt - delay + rr = omega * t0 / 2 + ff = (1 - 2 * rr * rr) * numpy.exp(-rr * rr) + + cc = numpy.cos(omega * t0) + ss = numpy.sin(omega * t0) + + return ff, cc, ss + + return source_phasor, delay + + +def gaussian_beam( + xyz: list[NDArray], + center: ArrayLike, + w0: float, + tilt: float, + wl: float, + ) -> NDArray[numpy.complex128]: + grids = numpy.asarray(numpy.meshgrid(*xyz, indexing='ij')) + grids -= numpy.asarray(center)[:, None, None, None] + + rot = numpy.array([ + [ numpy.cos(tilt), 0, numpy.sin(tilt)], + [ 0, 1, 0], + [-numpy.sin(tilt), 0, numpy.cos(tilt)], + ]) + + xx, yy, zz = numpy.einsum('ij,jxyz->ixyz', rot, grids) + r2 = xx * xx + yy * yy + z2 = zz * zz + + zr = pi * w0 * w0 / wl + zr2 = zr * zr + wz2 = w0 * w0 * (1 + z2 / zr2) + wz = numpy.sqrt(wz2) # == fwhm(z) / sqrt(2 * ln(2)) + + kk = 2 * pi / wl + Rz = zz * (1 + zr2 / z2) + gouy = numpy.arctan(zz / zr) + + gaussian = w0 / wz * numpy.exp(-r2 / wz2) * numpy.exp(1j * (kk * zz + kk * r2 / 2 / Rz - gouy)) + + row = gaussian[:, :, gaussian.shape[2] // 2] + norm = numpy.sqrt((row * row.conj()).sum()) + return gaussian / norm diff --git a/pcgen.py b/pcgen.py new file mode 100644 index 0000000..58facaa --- /dev/null +++ b/pcgen.py @@ -0,0 +1,230 @@ +""" +Routines for creating normalized 2D lattices and common photonic crystal + cavity designs. +""" + +from typing import Sequence + +import numpy +from numpy.typing import NDArray + +def triangular_lattice( + dims: Sequence[int], + asymmetrical: bool = False + ) -> NDArray[numpy.float64]: + """ + Return an ndarray of `[[x0, y0], [x1, y1], ...]` denoting lattice sites for + a triangular lattice in 2D. The lattice will be centered around `(0, 0)`, + unless `asymmetrical=True` in which case there will be extra holes in the +x + direction. + + Args: + dims: Number of lattice sites in the [x, y] directions. + asymmetrical: If `True`, each row in x will contain the same number of + lattice sites. If `False`, the structure is symmetrical + around `(0, 0)`. + + Returns: + `[[x0, y0], [x1, 1], ...]` denoting lattice sites. + """ + dims = numpy.asarray(dims, dtype=int) + + if asymmetrical: + k = 0 + else: + k = 1 + + positions = [] + ymax = (dims[1] - 1)/2 + for j in numpy.linspace(-ymax, ymax, dims[0]): + j_odd = numpy.floor(j).astype(int) % 2 + + x_offset = j_odd * 0.5 + y_offset = j * numpy.sqrt(3)/2 + + num_x = dims[0] - k * j_odd + xmax = (dims[0] - 1)/2 + xs = numpy.linspace(-xmax, xmax - k * j_odd, num_x) + x_offset + ys = numpy.full_like(xs, y_offset) + + positions += [numpy.vstack((xs, ys)).T] + + xy = numpy.vstack(tuple(positions)) + return xy[xy[:, 0].argsort(), ] + + +def square_lattice(dims: Sequence[int]) -> NDArray[numpy.float64]: + """ + Return an ndarray of `[[x0, y0], [x1, y1], ...]` denoting lattice sites for + a square lattice in 2D. The lattice will be centered around `(0, 0)`. + + Args: + dims: Number of lattice sites in the [x, y] directions. + + Returns: + `[[x0, y0], [x1, 1], ...]` denoting lattice sites. + """ + xs, ys = numpy.meshgrid(range(dims[0]), range(dims[1]), 'xy') + xs -= dims[0]/2 + ys -= dims[1]/2 + xy = numpy.vstack((xs.flatten(), ys.flatten())).T + return xy[xy[:, 0].argsort(), ] + +# ### Photonic crystal functions ### + + +def nanobeam_holes( + a_defect: float, + num_defect_holes: int, + num_mirror_holes: int + ) -> NDArray[numpy.float64]: + """ + Returns a list of `[[x0, r0], [x1, r1], ...]` of nanobeam hole positions and radii. + Creates a region in which the lattice constant and radius are progressively + (linearly) altered over `num_defect_holes` holes until they reach the value + specified by `a_defect`, then symmetrically returned to a lattice constant and + radius of 1, which is repeated `num_mirror_holes` times on each side. + + Args: + a_defect: Minimum lattice constant for the defect, as a fraction of the + mirror lattice constant (ie., for no defect, `a_defect = 1`). + num_defect_holes: How many holes form the defect (per-side) + num_mirror_holes: How many holes form the mirror (per-side) + + Returns: + ndarray `[[x0, r0], [x1, r1], ...]` of nanobeam hole positions and radii. + """ + a_values = numpy.linspace(a_defect, 1, num_defect_holes, endpoint=False) + xs = a_values.cumsum() - (a_values[0] / 2) # Later mirroring makes center distance 2x as long + mirror_xs = numpy.arange(1, num_mirror_holes + 1) + xs[-1] + mirror_rs = numpy.ones_like(mirror_xs) + return numpy.vstack((numpy.hstack((-mirror_xs[::-1], -xs[::-1], xs, mirror_xs)), + numpy.hstack((mirror_rs[::-1], a_values[::-1], a_values, mirror_rs)))).T + + +def ln_defect( + mirror_dims: Sequence[int], + defect_length: int, + ) -> NDArray[numpy.float64]: + """ + N-hole defect in a triangular lattice. + + Args: + mirror_dims: `[x, y]` mirror lengths (number of holes). Total number of holes + is `2 * n + 1` in each direction. + defect_length: Length of defect. Should be an odd number. + + Returns: + `[[x0, y0], [x1, y1], ...]` for all the holes + """ + if defect_length % 2 != 1: + raise Exception('defect_length must be odd!') + p = triangular_lattice([2 * d + 1 for d in mirror_dims]) + half_length = numpy.floor(defect_length / 2) + hole_nums = numpy.arange(-half_length, half_length + 1) + holes_to_keep = numpy.in1d(p[:, 0], hole_nums, invert=True) + return p[numpy.logical_or(holes_to_keep, p[:, 1] != 0), ] + + +def ln_shift_defect( + mirror_dims: Sequence[int], + defect_length: int, + shifts_a: Sequence[float] = (0.15, 0, 0.075), + shifts_r: Sequence[float] = (1, 1, 1) + ) -> NDArray[numpy.float64]: + """ + N-hole defect with shifted holes (intended to give the mode a gaussian profile + in real- and k-space so as to improve both Q and confinement). Holes along the + defect line are shifted and altered according to the `shifts_*` parameters. + + Args: + mirror_dims: [x, y] mirror lengths (number of holes). Total number of holes + is `2 * n + 1` in each direction. + defect_length: Length of defect. Should be an odd number. + shifts_a: Percentage of a to shift (1st, 2nd, 3rd,...) holes along the defect line + shifts_r: Factor to multiply the radius by. Should match length of `shifts_a`. + + Returns: + `[[x0, y0, r0], [x1, y1, r1], ...]` for all the holes + """ + xy = ln_defect(mirror_dims, defect_length) + + # Add column for radius + xyr = numpy.hstack((xy, numpy.ones((xy.shape[0], 1)))) + + # Shift holes + assert len(shifts_a) == len(shifts_r) + + x_removed = numpy.floor(defect_length / 2) + + for ind in range(len(shifts_a)): + for sign in (-1, 1): + x_val = sign * (x_removed + ind + 1) + which = numpy.logical_and(xyr[:, 0] == x_val, xyr[:, 1] == 0) + xyr[which, ] = (x_val + numpy.sign(x_val) * shifts_a[ind], 0, shifts_r[ind]) + + return xyr + + +def r6_defect( + mirror_dims: Sequence[int], + ) -> NDArray[numpy.float64]: + """ + R6 defect in a triangular lattice. + + Args: + mirror_dims: [x, y] mirror lengths (number of holes). Total number of holes + is `2 * n + 1` in each direction. + + Returns: + `[[x0, y0], [x1, y1], ...]` specifying hole centers. + """ + xy = triangular_lattice([2 * d + 1 for d in mirror_dims]) + + rem_holes_plus = numpy.array([[1, 0], + [0.5, +numpy.sqrt(3)/2], + [0.5, -numpy.sqrt(3)/2]]) + rem_holes = numpy.vstack((rem_holes_plus, -rem_holes_plus)) + + for rem_xy in rem_holes: + xy = xy[(xy != rem_xy).any(axis=1), ] + + return xy + + +def l3_shift_perturbed_defect( + mirror_dims: Sequence[int], + perturbed_radius: float = 1.1, + shifts_a: Sequence[float] = (), + shifts_r: Sequence[float] = () + ) -> NDArray[numpy.float64]: + """ + 3-hole defect with perturbed hole sizes intended to form an upwards-directed + beam. Can also include shifted holes along the defect line, intended + to give the mode a more gaussian profile to improve Q. + + Args: + mirror_dims: [x, y] mirror lengths (number of holes). Total number of holes + is `2 * n + 1` in each direction. + perturbed_radius: Amount to perturb the radius of the holes used for beam-forming + shifts_a: Percentage of a to shift (1st, 2nd, 3rd,...) holes along the defect line + shifts_r: Factor to multiply the radius by. Should match length of `shifts_a` + + Returns: + `[[x0, y0, r0], [x1, y1, r1], ...]` for all the holes + """ + xyr = ln_shift_defect(mirror_dims, 3, shifts_a, shifts_r) + + abs_x, abs_y = (numpy.fabs(xyr[:, i]) for i in (0, 1)) + + # Sorted unique xs and ys + # Ignore row y=0 because it might have shifted holes + xs = numpy.unique(abs_x[abs_x != 0]) + ys = numpy.unique(abs_y) + + # which holes should be perturbed? (xs[[3, 7]], ys[1]) and (xs[[2, 6]], ys[2]) + perturbed_holes = numpy.array([(xs[a], ys[b]) for a, b in ((3, 1), (7, 1), (2, 2), (6, 2))]) + for row in xyr: + if (numpy.fabs(row[:2])[None, :] == perturbed_holes).all(axis=1).any(): + row[2] = perturbed_radius + return xyr diff --git a/pdoc__init__.py b/pdoc__init__.py new file mode 100644 index 0000000..8be8fd3 --- /dev/null +++ b/pdoc__init__.py @@ -0,0 +1,1664 @@ +""" +Python package `pdoc` provides types, functions, and a command-line +interface for accessing public documentation of Python modules, and +for presenting it in a user-friendly, industry-standard open format. +It is best suited for small- to medium-sized projects with tidy, +hierarchical APIs. + +.. include:: ./documentation.md +""" +import types +import ast +import enum +import importlib.machinery +import importlib.util +import inspect +import os +import os.path as path +import re +import sys +import typing +from contextlib import contextmanager +from copy import copy +from functools import lru_cache, reduce, partial, wraps +from itertools import tee, groupby +from types import ModuleType +from typing import ( # noqa: F401 + cast, Any, Callable, Dict, Generator, Iterable, List, Mapping, NewType, + Optional, Set, Tuple, Type, TypeVar, Union, +) +from warnings import warn + +from mako.lookup import TemplateLookup +from mako.exceptions import TopLevelLookupException +from mako.template import Template + +try: + from pdoc._version import version as __version__ # noqa: F401 +except ImportError: + __version__ = '???' # Package not installed + + +def _isclass(obj): + return inspect.isclass(obj) and not isinstance(obj, types.GenericAlias) + + +_get_type_hints = lru_cache()(typing.get_type_hints) + +_URL_MODULE_SUFFIX = '.html' +_URL_INDEX_MODULE_SUFFIX = '.m.html' # For modules named literal 'index' +_URL_PACKAGE_SUFFIX = '/index.html' + +# type.__module__ can be None by the Python spec. In those cases, use this value +_UNKNOWN_MODULE = '?' + +T = TypeVar('T', 'Module', 'Class', 'Function', 'Variable') + +__pdoc__: Dict[str, Union[bool, str]] = {} + +tpl_lookup = TemplateLookup( + cache_args=dict(cached=True, + cache_type='memory'), + input_encoding='utf-8', + directories=[path.join(path.dirname(__file__), "templates")], +) +""" +A `mako.lookup.TemplateLookup` object that knows how to load templates +from the file system. You may add additional paths by modifying the +object's `directories` attribute. +""" +if os.getenv("XDG_CONFIG_HOME"): + tpl_lookup.directories.insert(0, path.join(os.getenv("XDG_CONFIG_HOME", ''), "pdoc")) + + +class Context(dict): + """ + The context object that maps all documented identifiers + (`pdoc.Doc.refname`) to their respective `pdoc.Doc` objects. + + You can pass an instance of `pdoc.Context` to `pdoc.Module` constructor. + All `pdoc.Module` objects that share the same `pdoc.Context` will see + (and be able to link in HTML to) each other's identifiers. + + If you don't pass your own `Context` instance to `Module` constructor, + a global context object will be used. + """ + __pdoc__['Context.__init__'] = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # A surrogate so that the check in Module._link_inheritance() + # "__pdoc__-overriden key {!r} does not exist" can see the object + # (and not warn). + self.blacklisted = getattr(args[0], 'blacklisted', set()) if args else set() + + +_global_context = Context() + + +def reset(): + """Resets the global `pdoc.Context` to the initial (empty) state.""" + global _global_context + _global_context.clear() + + # Clear LRU caches + for func in (_get_type_hints, + _is_blacklisted, + _is_whitelisted): + func.cache_clear() + for cls in (Doc, Module, Class, Function, Variable, External): + for _, method in inspect.getmembers(cls): + if isinstance(method, property): + method = method.fget + if hasattr(method, 'cache_clear'): + method.cache_clear() + + +def _get_config(**kwargs): + # Apply config.mako configuration + MAKO_INTERNALS = Template('').module.__dict__.keys() + DEFAULT_CONFIG = path.join(path.dirname(__file__), 'templates', 'config.mako') + config = {} + for config_module in (Template(filename=DEFAULT_CONFIG).module, + tpl_lookup.get_template('/config.mako').module): + config.update((var, getattr(config_module, var, None)) + for var in config_module.__dict__ + if var not in MAKO_INTERNALS) + + known_keys = (set(config) + | {'docformat'} # Feature. https://github.com/pdoc3/pdoc/issues/169 + # deprecated + | {'module', 'modules', 'http_server', 'external_links', 'search_query'}) + invalid_keys = {k: v for k, v in kwargs.items() if k not in known_keys} + if invalid_keys: + warn(f'Unknown configuration variables (not in config.mako): {invalid_keys}') + config.update(kwargs) + + if 'search_query' in config: + warn('Option `search_query` has been deprecated. Use `google_search_query` instead', + DeprecationWarning, stacklevel=2) + config['google_search_query'] = config['search_query'] + del config['search_query'] + + return config + + +def _render_template(template_name, **kwargs): + """ + Returns the Mako template with the given name. If the template + cannot be found, a nicer error message is displayed. + """ + config = _get_config(**kwargs) + + try: + t = tpl_lookup.get_template(template_name) + except TopLevelLookupException: + paths = [path.join(p, template_name.lstrip('/')) for p in tpl_lookup.directories] + raise OSError(f"No template found at any of: {', '.join(paths)}") + + try: + return t.render(**config).strip() + except Exception: + from mako import exceptions + print(exceptions.text_error_template().render(), + file=sys.stderr) + raise + + +def html(module_name, docfilter=None, reload=False, skip_errors=False, **kwargs) -> str: + """ + Returns the documentation for the module `module_name` in HTML + format. The module must be a module or an importable string. + + `docfilter` is an optional predicate that controls which + documentation objects are shown in the output. It is a function + that takes a single argument (a documentation object) and returns + `True` or `False`. If `False`, that object will not be documented. + """ + mod = Module(import_module(module_name, reload=reload), + docfilter=docfilter, skip_errors=skip_errors) + link_inheritance() + return mod.html(**kwargs) + + +def text(module_name, docfilter=None, reload=False, skip_errors=False, **kwargs) -> str: + """ + Returns the documentation for the module `module_name` in plain + text format suitable for viewing on a terminal. + The module must be a module or an importable string. + + `docfilter` is an optional predicate that controls which + documentation objects are shown in the output. It is a function + that takes a single argument (a documentation object) and returns + `True` or `False`. If `False`, that object will not be documented. + """ + mod = Module(import_module(module_name, reload=reload), + docfilter=docfilter, skip_errors=skip_errors) + link_inheritance() + return mod.text(**kwargs) + + +def import_module(module: Union[str, ModuleType], + *, reload: bool = False) -> ModuleType: + """ + Return module object matching `module` specification (either a python + module path or a filesystem path to file/directory). + """ + @contextmanager + def _module_path(module): + from os.path import abspath, dirname, isfile, isdir, split + path = '_pdoc_dummy_nonexistent' + module_name = inspect.getmodulename(module) + if isdir(module): + path, module = split(abspath(module)) + elif isfile(module) and module_name: + path, module = dirname(abspath(module)), module_name + try: + sys.path.insert(0, path) + yield module + finally: + sys.path.remove(path) + + if isinstance(module, Module): + module = module.obj + if isinstance(module, str): + with _module_path(module) as module_path: + try: + module = importlib.import_module(module_path) + except Exception as e: + raise ImportError(f'Error importing {module!r}: {e.__class__.__name__}: {e}') + + assert inspect.ismodule(module) + # If this is pdoc itself, return without reloading. Otherwise later + # `isinstance(..., pdoc.Doc)` calls won't work correctly. + if reload and not module.__name__.startswith(__name__): + module = importlib.reload(module) + # We recursively reload all submodules, in case __all_ is used - cf. issue #264 + for mod_key, mod in list(sys.modules.items()): + if mod_key.startswith(module.__name__): + importlib.reload(mod) + return module + + +def _pairwise(iterable): + """s -> (s0,s1), (s1,s2), (s2, s3), ...""" + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + +def _pep224_docstrings(doc_obj: Union['Module', 'Class'], *, + _init_tree=None) -> Tuple[Dict[str, str], + Dict[str, str]]: + """ + Extracts PEP-224 docstrings and doc-comments (`#: ...`) for variables of `doc_obj` + (either a `pdoc.Module` or `pdoc.Class`). + + Returns a tuple of two dicts mapping variable names to their docstrings. + The second dict contains instance variables and is non-empty only in case + `doc_obj` is a `pdoc.Class` which has `__init__` method. + """ + # No variables in namespace packages + if isinstance(doc_obj, Module) and doc_obj.is_namespace: + return {}, {} + + vars: Dict[str, str] = {} + instance_vars: Dict[str, str] = {} + + if _init_tree: + tree = _init_tree + else: + try: + # Maybe raise exceptions with appropriate message + # before using cleaned doc_obj.source + _ = inspect.findsource(doc_obj.obj) + tree = ast.parse(doc_obj.source) # type: ignore + except (OSError, TypeError, SyntaxError) as exc: + # Don't emit a warning for builtins that don't have source available + is_builtin = getattr(doc_obj.obj, '__module__', None) == 'builtins' + if not is_builtin: + warn(f"Couldn't read PEP-224 variable docstrings from {doc_obj!r}: {exc}", + stacklevel=3 + int(isinstance(doc_obj, Class))) + return {}, {} + + if isinstance(doc_obj, Class): + tree = tree.body[0] # ast.parse creates a dummy ast.Module wrapper + + # For classes, maybe add instance variables defined in __init__ + # Get the *last* __init__ node in case it is preceded by @overloads. + for node in reversed(tree.body): + if isinstance(node, ast.FunctionDef) and node.name == '__init__': + instance_vars, _ = _pep224_docstrings(doc_obj, _init_tree=node) + break + + def get_name(assign_node): + if isinstance(assign_node, ast.Assign) and len(assign_node.targets) == 1: + target = assign_node.targets[0] + elif isinstance(assign_node, ast.AnnAssign): + target = assign_node.target + # Skip the annotation. PEP 526 says: + # > Putting the instance variable annotations together in the class + # > makes it easier to find them, and helps a first-time reader of the code. + else: + return None + + if not _init_tree and isinstance(target, ast.Name): + name = target.id + elif (_init_tree and + isinstance(target, ast.Attribute) and + isinstance(target.value, ast.Name) and + target.value.id == 'self'): + name = target.attr + else: + return None + + if not _is_public(name) and not _is_whitelisted(name, doc_obj): + return None + + return name + + # For handling PEP-224 docstrings for variables + for assign_node, str_node in _pairwise(ast.iter_child_nodes(tree)): + if not (isinstance(assign_node, (ast.Assign, ast.AnnAssign)) and + isinstance(str_node, ast.Expr) and + isinstance(str_node.value, ast.Str)): + continue + + name = get_name(assign_node) + if not name: + continue + + docstring = inspect.cleandoc(str_node.value.s).strip() + if not docstring: + continue + + vars[name] = docstring + + # For handling '#:' docstrings for variables + for assign_node in ast.iter_child_nodes(tree): + if not isinstance(assign_node, (ast.Assign, ast.AnnAssign)): + continue + + name = get_name(assign_node) + if not name: + continue + + # Already documented. PEP-224 method above takes precedence. + if name in vars: + continue + + def get_indent(line): + return len(line) - len(line.lstrip()) + + source_lines = doc_obj.source.splitlines() # type: ignore + assign_line = source_lines[assign_node.lineno - 1] + assign_indent = get_indent(assign_line) + comment_lines = [] + MARKER = '#: ' + for line in reversed(source_lines[:assign_node.lineno - 1]): + if get_indent(line) == assign_indent and line.lstrip().startswith(MARKER): + comment_lines.append(line.split(MARKER, maxsplit=1)[1]) + else: + break + + # Since we went 'up' need to reverse lines to be in correct order + comment_lines = comment_lines[::-1] + + # Finally: check for a '#: ' comment at the end of the assignment line itself. + if MARKER in assign_line: + comment_lines.append(assign_line.rsplit(MARKER, maxsplit=1)[1]) + + if comment_lines: + vars[name] = '\n'.join(comment_lines) + + return vars, instance_vars + + +@lru_cache() +def _is_whitelisted(name: str, doc_obj: Union['Module', 'Class']): + """ + Returns `True` if `name` (relative or absolute refname) is + contained in some module's __pdoc__ with a truish value. + """ + refname = f'{doc_obj.refname}.{name}' + module: Optional[Module] = doc_obj.module + while module: + qualname = refname[len(module.refname) + 1:] + if module.__pdoc__.get(qualname) or module.__pdoc__.get(refname): + return True + module = module.supermodule + return False + + +@lru_cache() +def _is_blacklisted(name: str, doc_obj: Union['Module', 'Class']): + """ + Returns `True` if `name` (relative or absolute refname) is + contained in some module's __pdoc__ with value False. + """ + refname = f'{doc_obj.refname}.{name}' + module: Optional[Module] = doc_obj.module + while module: + qualname = refname[len(module.refname) + 1:] + if module.__pdoc__.get(qualname) is False or module.__pdoc__.get(refname) is False: + return True + module = module.supermodule + return False + + +def _is_public(ident_name): + """ + Returns `True` if `ident_name` matches the export criteria for an + identifier name. + """ + return not ident_name.startswith("_") + + +def _is_function(obj): + return inspect.isroutine(obj) and callable(obj) + + +def _is_descriptor(obj): + return (inspect.isdatadescriptor(obj) or + inspect.ismethoddescriptor(obj) or + inspect.isgetsetdescriptor(obj) or + inspect.ismemberdescriptor(obj)) + + +def _filter_type(type: Type[T], + values: Union[Iterable['Doc'], Mapping[str, 'Doc']]) -> List[T]: + """ + Return a list of values from `values` of type `type`. + """ + if isinstance(values, dict): + values = values.values() + return [i for i in values if isinstance(i, type)] + + +def _toposort(graph: Mapping[T, Set[T]]) -> Generator[T, None, None]: + """ + Return items of `graph` sorted in topological order. + Source: https://rosettacode.org/wiki/Topological_sort#Python + """ + items_without_deps = reduce(set.union, graph.values(), set()) - set(graph.keys()) # type: ignore # noqa: E501 + yield from items_without_deps + ordered = items_without_deps + while True: + graph = {item: (deps - ordered) + for item, deps in graph.items() + if item not in ordered} + ordered = {item + for item, deps in graph.items() + if not deps} + yield from ordered + if not ordered: + break + assert not graph, f"A cyclic dependency exists amongst {graph!r}" + + +def link_inheritance(context: Context = None): + """ + Link inheritance relationsships between `pdoc.Class` objects + (and between their members) of all `pdoc.Module` objects that + share the provided `context` (`pdoc.Context`). + + You need to call this if you expect `pdoc.Doc.inherits` and + inherited `pdoc.Doc.docstring` to be set correctly. + """ + if context is None: + context = _global_context + + graph = {cls: set(cls.mro(only_documented=True)) + for cls in _filter_type(Class, context)} + + for cls in _toposort(graph): + cls._fill_inheritance() + + for module in _filter_type(Module, context): + module._link_inheritance() + + +class Doc: + """ + A base class for all documentation objects. + + A documentation object corresponds to *something* in a Python module + that has a docstring associated with it. Typically, this includes + modules, classes, functions, and methods. However, `pdoc` adds support + for extracting some docstrings from abstract syntax trees, making + (module, class or instance) variables supported too. + + A special type of documentation object `pdoc.External` is used to + represent identifiers that are not part of the public interface of + a module. (The name "External" is a bit of a misnomer, since it can + also correspond to unexported members of the module, particularly in + a class's ancestor list.) + """ + __slots__ = ('module', 'name', 'obj', 'docstring', 'inherits') + + def __init__(self, name: str, module, obj, docstring: str = None): + """ + Initializes a documentation object, where `name` is the public + identifier name, `module` is a `pdoc.Module` object where raw + Python object `obj` is defined, and `docstring` is its + documentation string. If `docstring` is left empty, it will be + read with `inspect.getdoc()`. + """ + self.module = module + """ + The module documentation object that this object is defined in. + """ + + self.name = name + """ + The identifier name for this object. + """ + + self.obj = obj + """ + The raw python object. + """ + + docstring = (docstring or inspect.getdoc(obj) or '').strip() + if '.. include::' in docstring: + from pdoc.html_helpers import _ToMarkdown + docstring = _ToMarkdown.admonitions(docstring, self.module, ('include',)) + self.docstring = docstring + """ + The cleaned docstring for this object with any `.. include::` + directives resolved (i.e. content included). + """ + + self.inherits: Optional[Union[Class, Function, Variable]] = None + """ + The Doc object (Class, Function, or Variable) this object inherits from, + if any. + """ + + def __repr__(self): + return f'<{self.__class__.__name__} {self.refname!r}>' + + @property # type: ignore + @lru_cache() + def source(self) -> str: + """ + Cleaned (dedented) source code of the Python object. If not + available, an empty string. + """ + try: + lines, _ = inspect.getsourcelines(self.obj) + except (ValueError, TypeError, OSError): + return '' + return inspect.cleandoc(''.join(['\n'] + lines)) + + @property + def refname(self) -> str: + """ + Reference name of this documentation + object, usually its fully qualified path + (e.g. pdoc.Doc.refname). Every + documentation object provides this property. + """ + # Ok for Module and External, the rest need it overriden + return self.name + + @property + def qualname(self) -> str: + """ + Module-relative "qualified" name of this documentation + object, used for show (e.g. Doc.qualname). + """ + return getattr(self.obj, '__qualname__', self.name) + + @lru_cache() + def url(self, relative_to: 'Module' = None, *, link_prefix: str = '', + top_ancestor: bool = False) -> str: + """ + Canonical relative URL (including page fragment) for this + documentation object. + + Specify `relative_to` (a `pdoc.Module` object) to obtain a + relative URL. + + For usage of `link_prefix` see `pdoc.html()`. + + If `top_ancestor` is `True`, the returned URL instead points to + the top ancestor in the object's `pdoc.Doc.inherits` chain. + """ + if top_ancestor: + self = self._inherits_top() + + if relative_to is None or link_prefix: + return link_prefix + self._url() + + if self.module.name == relative_to.name: + return f'#{self.refname}' + + # Otherwise, compute relative path from current module to link target + url = os.path.relpath(self._url(), relative_to.url()).replace(path.sep, '/') + # We have one set of '..' too many + if url.startswith('../'): + url = url[3:] + return url + + def _url(self): + return f'{self.module._url()}#{self.refname}' + + def _inherits_top(self): + """ + Follow the `pdoc.Doc.inherits` chain and return the top object. + """ + top = self + while top.inherits: + top = top.inherits + return top + + def __lt__(self, other): + return self.refname < other.refname + + +class Module(Doc): + """ + Representation of a module's documentation. + """ + __pdoc__["Module.name"] = """ + The name of this module with respect to the context/path in which + it was imported from. It is always an absolute import path. + """ + + __slots__ = ('supermodule', 'doc', '_context', '_is_inheritance_linked', + '_skipped_submodules') + + def __init__(self, module: Union[ModuleType, str], *, docfilter: Callable[[Doc], bool] = None, + supermodule: 'Module' = None, context: Context = None, + skip_errors: bool = False): + """ + Creates a `Module` documentation object given the actual + module Python object. + + `docfilter` is an optional predicate that controls which + sub-objects are documentated (see also: `pdoc.html()`). + + `supermodule` is the parent `pdoc.Module` this module is + a submodule of. + + `context` is an instance of `pdoc.Context`. If `None` a + global context object will be used. + + If `skip_errors` is `True` and an unimportable, erroneous + submodule is encountered, a warning will be issued instead + of raising an exception. + """ + if isinstance(module, str): + module = import_module(module) + + super().__init__(module.__name__, self, module) + if self.name.endswith('.__init__') and not self.is_package: + self.name = self.name[:-len('.__init__')] + + self._context = _global_context if context is None else context + """ + A lookup table for ALL doc objects of all modules that share this context, + mainly used in `Module.find_ident()`. + """ + assert isinstance(self._context, Context), \ + 'pdoc.Module(context=) should be a pdoc.Context instance' + + self.supermodule = supermodule + """ + The parent `pdoc.Module` this module is a submodule of, or `None`. + """ + + self.doc: Dict[str, Union[Module, Class, Function, Variable]] = {} + """A mapping from identifier name to a documentation object.""" + + self._is_inheritance_linked = False + """Re-entry guard for `pdoc.Module._link_inheritance()`.""" + + self._skipped_submodules = set() + + var_docstrings, _ = _pep224_docstrings(self) + + # Populate self.doc with this module's public members + public_objs = [] + if hasattr(self.obj, '__all__'): + for name in self.obj.__all__: + try: + obj = getattr(self.obj, name) + except AttributeError: + warn(f"Module {self.module!r} doesn't contain identifier `{name}` " + "exported in `__all__`") + if not _is_blacklisted(name, self): + obj = inspect.unwrap(obj) + public_objs.append((name, obj)) + else: + def is_from_this_module(obj): + mod = inspect.getmodule(inspect.unwrap(obj)) + return mod is None or mod.__name__ == self.obj.__name__ + + for name, obj in inspect.getmembers(self.obj): + if ((_is_public(name) or + _is_whitelisted(name, self)) and + (_is_blacklisted(name, self) or # skips unwrapping that follows + is_from_this_module(obj) or + name in var_docstrings)): + + if _is_blacklisted(name, self): + self._context.blacklisted.add(f'{self.refname}.{name}') + continue + + obj = inspect.unwrap(obj) + public_objs.append((name, obj)) + + index = list(self.obj.__dict__).index + public_objs.sort(key=lambda i: index(i[0])) + + for name, obj in public_objs: + if _is_function(obj): + self.doc[name] = Function(name, self, obj) + elif _isclass(obj): + self.doc[name] = Class(name, self, obj) + elif name in var_docstrings: + self.doc[name] = Variable(name, self, var_docstrings[name], obj=obj) + + # If the module is a package, scan the directory for submodules + if self.is_package: + + def iter_modules(paths): + """ + Custom implementation of `pkgutil.iter_modules()` + because that one doesn't play well with namespace packages. + See: https://github.com/pypa/setuptools/issues/83 + """ + from os.path import isdir, join + for pth in paths: + for file in os.listdir(pth): + if file.startswith(('.', '__pycache__', '__init__.py')): + continue + module_name = inspect.getmodulename(file) + if module_name: + yield module_name + if isdir(join(pth, file)) and '.' not in file: + yield file + + for root in iter_modules(self.obj.__path__): + # Ignore if this module was already doc'd. + if root in self.doc: + continue + + # Ignore if it isn't exported + if not _is_public(root) and not _is_whitelisted(root, self): + continue + if _is_blacklisted(root, self): + self._skipped_submodules.add(root) + continue + + assert self.refname == self.name + fullname = f"{self.name}.{root}" + try: + m = Module(import_module(fullname), + docfilter=docfilter, supermodule=self, + context=self._context, skip_errors=skip_errors) + except Exception as ex: + if skip_errors: + warn(str(ex), Module.ImportWarning) + continue + raise + + self.doc[root] = m + # Skip empty namespace packages because they may + # as well be other auxiliary directories + if m.is_namespace and not m.doc: + del self.doc[root] + self._context.pop(m.refname) + + # Apply docfilter + if docfilter: + for name, dobj in self.doc.copy().items(): + if not docfilter(dobj): + self.doc.pop(name) + self._context.pop(dobj.refname, None) + + # Build the reference name dictionary of the module + self._context[self.refname] = self + for docobj in self.doc.values(): + self._context[docobj.refname] = docobj + if isinstance(docobj, Class): + self._context.update((obj.refname, obj) + for obj in docobj.doc.values()) + + class ImportWarning(UserWarning): + """ + Our custom import warning because the builtin is ignored by default. + https://docs.python.org/3/library/warnings.html#default-warning-filter + """ + + __pdoc__['Module.ImportWarning'] = False + + @property + def __pdoc__(self) -> dict: + """This module's __pdoc__ dict, or an empty dict if none.""" + return getattr(self.obj, '__pdoc__', {}) + + def _link_inheritance(self): + # Inherited members are already in place since + # `Class._fill_inheritance()` has been called from + # `pdoc.fill_inheritance()`. + # Now look for docstrings in the module's __pdoc__ override. + + if self._is_inheritance_linked: + # Prevent re-linking inheritance for modules which have already + # had done so. Otherwise, this would raise "does not exist" + # errors if `pdoc.link_inheritance()` is called multiple times. + return + + # Apply __pdoc__ overrides + for name, docstring in self.__pdoc__.items(): + # In case of whitelisting with "True", there's nothing to do + if docstring is True: + continue + + refname = f"{self.refname}.{name}" + if docstring in (False, None): + if docstring is None: + warn('Setting `__pdoc__[key] = None` is deprecated; ' + 'use `__pdoc__[key] = False` ' + f'(key: {name!r}, module: {self.name!r}).') + + if name in self._skipped_submodules: + continue + + if (not name.endswith('.__init__') and + name not in self.doc and + refname not in self._context and + refname not in self._context.blacklisted): + warn(f'__pdoc__-overriden key {name!r} does not exist ' + f'in module {self.name!r}') + + obj = self.find_ident(name) + cls = getattr(obj, 'cls', None) + if cls: + del cls.doc[obj.name] + self.doc.pop(name, None) + self._context.pop(refname, None) + + # Pop also all that startwith refname + for key in list(self._context.keys()): + if key.startswith(refname + '.'): + del self._context[key] + + continue + + dobj = self.find_ident(refname) + if isinstance(dobj, External): + continue + if not isinstance(docstring, str): + raise ValueError('__pdoc__ dict values must be strings; ' + f'__pdoc__[{name!r}] is of type {type(docstring)}') + dobj.docstring = inspect.cleandoc(docstring) + + # Now after docstrings are set correctly, continue the + # inheritance routine, marking members inherited or not + for c in _filter_type(Class, self.doc): + c._link_inheritance() + + self._is_inheritance_linked = True + + def text(self, **kwargs) -> str: + """ + Returns the documentation for this module as plain text. + """ + txt = _render_template('/text.mako', module=self, **kwargs) + return re.sub("\n\n\n+", "\n\n", txt) + + def html(self, minify=True, **kwargs) -> str: + """ + Returns the documentation for this module as + self-contained HTML. + + If `minify` is `True`, the resulting HTML is minified. + + For explanation of other arguments, see `pdoc.html()`. + + `kwargs` is passed to the `mako` render function. + """ + html = _render_template('/html.mako', module=self, **kwargs) + if minify: + from pdoc.html_helpers import minify_html + html = minify_html(html) + return html + + @property + def is_package(self) -> bool: + """ + `True` if this module is a package. + + Works by checking whether the module has a `__path__` attribute. + """ + return hasattr(self.obj, "__path__") + + @property + def is_namespace(self) -> bool: + """ + `True` if this module is a namespace package. + """ + try: + return self.obj.__spec__.origin in (None, 'namespace') # None in Py3.7+ + except AttributeError: + return False + + def find_class(self, cls: type) -> Doc: + """ + Given a Python `cls` object, try to find it in this module + or in any of the exported identifiers of the submodules. + """ + # XXX: Is this corrent? Does it always match + # `Class.module.name + Class.qualname`?. Especially now? + # If not, see what was here before. + return self.find_ident(f'{cls.__module__ or _UNKNOWN_MODULE}.{cls.__qualname__}') + + def find_ident(self, name: str) -> Doc: + """ + Searches this module and **all** other public modules + for an identifier with name `name` in its list of + exported identifiers. + + The documentation object corresponding to the identifier is + returned. If one cannot be found, then an instance of + `External` is returned populated with the given identifier. + """ + _name = name.rstrip('()') # Function specified with parentheses + + if _name.endswith('.__init__'): # Ref to class' init is ref to class itself + _name = _name[:-len('.__init__')] + + return (self.doc.get(_name) or + self._context.get(_name) or + self._context.get(f'{self.name}.{_name}') or + External(name)) + + def _filter_doc_objs(self, type: Type[T], sort=True) -> List[T]: + result = _filter_type(type, self.doc) + return sorted(result) if sort else result + + def variables(self, sort=True) -> List['Variable']: + """ + Returns all documented module-level variables in the module, + optionally sorted alphabetically, as a list of `pdoc.Variable`. + """ + return self._filter_doc_objs(Variable, sort) + + def classes(self, sort=True) -> List['Class']: + """ + Returns all documented module-level classes in the module, + optionally sorted alphabetically, as a list of `pdoc.Class`. + """ + return self._filter_doc_objs(Class, sort) + + def functions(self, sort=True) -> List['Function']: + """ + Returns all documented module-level functions in the module, + optionally sorted alphabetically, as a list of `pdoc.Function`. + """ + return self._filter_doc_objs(Function, sort) + + def submodules(self) -> List['Module']: + """ + Returns all documented sub-modules of the module sorted + alphabetically as a list of `pdoc.Module`. + """ + return self._filter_doc_objs(Module) + + def _url(self): + url = self.module.name.replace('.', '/') + if self.is_package: + return url + _URL_PACKAGE_SUFFIX + elif url.endswith('/index'): + return url + _URL_INDEX_MODULE_SUFFIX + return url + _URL_MODULE_SUFFIX + + +def _getmembers_all(obj: type) -> List[Tuple[str, Any]]: + # The following code based on inspect.getmembers() @ 5b23f7618d43 + mro = obj.__mro__[:-1] # Skip object + names = set(dir(obj)) + # Add keys from bases + for base in mro: + names.update(base.__dict__.keys()) + # Add members for which type annotations exist + names.update(getattr(obj, '__annotations__', {}).keys()) + + results = [] + for name in names: + try: + value = getattr(obj, name) + except AttributeError: + for base in mro: + if name in base.__dict__: + value = base.__dict__[name] + break + else: + # Missing slot member or a buggy __dir__; + # In out case likely a type-annotated member + # which we'll interpret as a variable + value = None + results.append((name, value)) + return results + + +class Class(Doc): + """ + Representation of a class' documentation. + """ + __slots__ = ('doc', '_super_members') + + def __init__(self, name: str, module: Module, obj, *, docstring: str = None): + assert inspect.isclass(obj) + + if docstring is None: + init_doc = inspect.getdoc(obj.__init__) or '' + if init_doc == object.__init__.__doc__: + init_doc = '' + docstring = f'{inspect.getdoc(obj) or ""}\n\n{init_doc}'.strip() + + super().__init__(name, module, obj, docstring=docstring) + + self.doc: Dict[str, Union[Function, Variable]] = {} + """A mapping from identifier name to a `pdoc.Doc` objects.""" + + # Annotations for filtering. + # Use only own, non-inherited annotations (the rest will be inherited) + annotations = getattr(self.obj, '__annotations__', {}) + + public_objs = [] + for _name, obj in _getmembers_all(self.obj): + # Filter only *own* members. The rest are inherited + # in Class._fill_inheritance() + if ((_name in self.obj.__dict__ or + _name in annotations) and + (_is_public(_name) or + _is_whitelisted(_name, self))): + + if _is_blacklisted(_name, self): + self.module._context.blacklisted.add(f'{self.refname}.{_name}') + continue + + obj = inspect.unwrap(obj) + public_objs.append((_name, obj)) + + def definition_order_index( + name, + _annot_index=list(annotations).index, + _dict_index=list(self.obj.__dict__).index): + try: + return _dict_index(name) + except ValueError: + pass + try: + return _annot_index(name) - len(annotations) # sort annotated first + except ValueError: + return 9e9 + + public_objs.sort(key=lambda i: definition_order_index(i[0])) + + var_docstrings, instance_var_docstrings = _pep224_docstrings(self) + + # Convert the public Python objects to documentation objects. + for name, obj in public_objs: + if _is_function(obj): + self.doc[name] = Function( + name, self.module, obj, cls=self) + else: + self.doc[name] = Variable( + name, self.module, + docstring=( + var_docstrings.get(name) or + (_isclass(obj) or _is_descriptor(obj)) and inspect.getdoc(obj)), + cls=self, + obj=getattr(obj, 'fget', getattr(obj, '__get__', None)), + instance_var=(_is_descriptor(obj) or + name in getattr(self.obj, '__slots__', ()))) + + for name, docstring in instance_var_docstrings.items(): + self.doc[name] = Variable( + name, self.module, docstring, cls=self, + obj=getattr(self.obj, name, None), + instance_var=True) + + @staticmethod + def _method_type(cls: type, name: str): + """ + Returns `None` if the method `name` of class `cls` + is a regular method. Otherwise, it returns + `classmethod` or `staticmethod`, as appropriate. + """ + func = getattr(cls, name, None) + if inspect.ismethod(func): + # If the function is already bound, it's a classmethod. + # Regular methods are not bound before initialization. + return classmethod + for c in inspect.getmro(cls): + if name in c.__dict__: + if isinstance(c.__dict__[name], staticmethod): + return staticmethod + return None + raise RuntimeError(f"{cls}.{name} not found") + + @property + def refname(self) -> str: + return f'{self.module.name}.{self.qualname}' + + def mro(self, only_documented=False) -> List['Class']: + """ + Returns a list of ancestor (superclass) documentation objects + in method resolution order. + + The list will contain objects of type `pdoc.Class` + if the types are documented, and `pdoc.External` otherwise. + """ + classes = [cast(Class, self.module.find_class(c)) + for c in inspect.getmro(self.obj) + if c not in (self.obj, object)] + if self in classes: + # This can contain self in case of a class inheriting from + # a class with (previously) the same name. E.g. + # + # class Loc(namedtuple('Loc', 'lat lon')): ... + # + # We remove it from ancestors so that toposort doesn't break. + classes.remove(self) + if only_documented: + classes = _filter_type(Class, classes) + return classes + + def subclasses(self) -> List['Class']: + """ + Returns a list of subclasses of this class that are visible to the + Python interpreter (obtained from `type.__subclasses__()`). + + The objects in the list are of type `pdoc.Class` if available, + and `pdoc.External` otherwise. + """ + return sorted(cast(Class, self.module.find_class(c)) + for c in type.__subclasses__(self.obj)) + + def params(self, *, annotate=False, link=None) -> List[str]: + """ + Return a list of formatted parameters accepted by the + class constructor (method `__init__`). See `pdoc.Function.params`. + """ + name = self.name + '.__init__' + qualname = self.qualname + '.__init__' + refname = self.refname + '.__init__' + exclusions = self.module.__pdoc__ + if name in exclusions or qualname in exclusions or refname in exclusions: + return [] + + return Function._params(self, annotate=annotate, link=link, module=self.module) + + def _filter_doc_objs(self, type: Type[T], include_inherited=True, + filter_func: Callable[[T], bool] = lambda x: True, + sort=True) -> List[T]: + result = [obj for obj in _filter_type(type, self.doc) + if (include_inherited or not obj.inherits) and filter_func(obj)] + return sorted(result) if sort else result + + def class_variables(self, include_inherited=True, sort=True) -> List['Variable']: + """ + Returns an optionally-sorted list of `pdoc.Variable` objects that + represent this class' class variables. + """ + return self._filter_doc_objs( + Variable, include_inherited, lambda dobj: not dobj.instance_var, + sort) + + def instance_variables(self, include_inherited=True, sort=True) -> List['Variable']: + """ + Returns an optionally-sorted list of `pdoc.Variable` objects that + represent this class' instance variables. Instance variables + are those defined in a class's `__init__` as `self.variable = ...`. + """ + return self._filter_doc_objs( + Variable, include_inherited, lambda dobj: dobj.instance_var, + sort) + + def methods(self, include_inherited=True, sort=True) -> List['Function']: + """ + Returns an optionally-sorted list of `pdoc.Function` objects that + represent this class' methods. + """ + return self._filter_doc_objs( + Function, include_inherited, lambda dobj: dobj.is_method, + sort) + + def functions(self, include_inherited=True, sort=True) -> List['Function']: + """ + Returns an optionally-sorted list of `pdoc.Function` objects that + represent this class' static functions. + """ + return self._filter_doc_objs( + Function, include_inherited, lambda dobj: not dobj.is_method, + sort) + + def inherited_members(self) -> List[Tuple['Class', List[Doc]]]: + """ + Returns all inherited members as a list of tuples + (ancestor class, list of ancestor class' members sorted by name), + sorted by MRO. + """ + return sorted(((cast(Class, k), sorted(g)) + for k, g in groupby((i.inherits + for i in self.doc.values() if i.inherits), + key=lambda i: i.cls)), # type: ignore + key=lambda x, _mro_index=self.mro().index: _mro_index(x[0])) # type: ignore + + def _fill_inheritance(self): + """ + Traverses this class's ancestor list and attempts to fill in + missing documentation objects from its ancestors. + + Afterwards, call to `pdoc.Class._link_inheritance()` to also + set `pdoc.Doc.inherits` pointers. + """ + super_members = self._super_members = {} + for cls in self.mro(only_documented=True): + for name, dobj in cls.doc.items(): + if name not in super_members and dobj.docstring: + super_members[name] = dobj + if name not in self.doc: + dobj = copy(dobj) + dobj.cls = self + + self.doc[name] = dobj + self.module._context[dobj.refname] = dobj + + def _link_inheritance(self): + """ + Set `pdoc.Doc.inherits` pointers to inherited ancestors' members, + as appropriate. This must be called after + `pdoc.Class._fill_inheritance()`. + + The reason this is split in two parts is that in-between + the `__pdoc__` overrides are applied. + """ + if not hasattr(self, '_super_members'): + return + + for name, parent_dobj in self._super_members.items(): + try: + dobj = self.doc[name] + except KeyError: + # There is a key in some __pdoc__ dict blocking this member + continue + if (dobj.obj is parent_dobj.obj or + (dobj.docstring or parent_dobj.docstring) == parent_dobj.docstring): + dobj.inherits = parent_dobj + dobj.docstring = parent_dobj.docstring + del self._super_members + + +def maybe_lru_cache(func): + cached_func = lru_cache()(func) + + @wraps(func) + def wrapper(*args): + try: + return cached_func(*args) + except TypeError: + return func(*args) + + return wrapper + + +@maybe_lru_cache +def _formatannotation(annot): + """ + Format typing annotation with better handling of `typing.NewType`, + `typing.Optional`, `nptyping.NDArray` and other types. + + >>> _formatannotation(NewType('MyType', str)) + 'MyType' + >>> _formatannotation(Optional[Tuple[Optional[int], None]]) + 'Optional[Tuple[Optional[int], None]]' + """ + class force_repr(str): + __repr__ = str.__str__ + + def maybe_replace_reprs(a): + # NoneType -> None + if a is type(None): # noqa: E721 + return force_repr('None') + # Union[T, None] -> Optional[T] + if (getattr(a, '__origin__', None) is typing.Union and + len(a.__args__) == 2 and + type(None) in a.__args__): + t = inspect.formatannotation( + maybe_replace_reprs(next(filter(None, a.__args__)))) + return force_repr(f'Optional[{t}]') + # typing.NewType('T', foo) -> T + module = getattr(a, '__module__', '') + if module == 'typing' and getattr(a, '__qualname__', '').startswith('NewType.'): + return force_repr(a.__name__) + # nptyping.types._ndarray.NDArray -> NDArray[(Any,), Int[64]] # GH-231 + if module.startswith('nptyping.'): + return force_repr(repr(a)) + # Recurse into typing.Callable/etc. args + if hasattr(a, 'copy_with') and hasattr(a, '__args__'): + if a is typing.Callable: + # Bug on Python < 3.9, https://bugs.python.org/issue42195 + return a + a = a.copy_with(tuple([maybe_replace_reprs(arg) for arg in a.__args__])) + return a + + return str(inspect.formatannotation(maybe_replace_reprs(annot))) + + +class Function(Doc): + """ + Representation of documentation for a function or method. + """ + __slots__ = ('cls',) + + def __init__(self, name: str, module: Module, obj, *, cls: Class = None): + """ + Same as `pdoc.Doc`, except `obj` must be a + Python function object. The docstring is gathered automatically. + + `cls` should be set when this is a method or a static function + beloing to a class. `cls` should be a `pdoc.Class` object. + + `method` should be `True` when the function is a method. In + all other cases, it should be `False`. + """ + assert callable(obj), (name, module, obj) + super().__init__(name, module, obj) + + self.cls = cls + """ + The `pdoc.Class` documentation object if the function is a method. + If not, this is None. + """ + + @property + def is_method(self) -> bool: + """ + Whether this function is a normal bound method. + + In particular, static and class methods have this set to False. + """ + assert self.cls + return not Class._method_type(self.cls.obj, self.name) + + @property + def method(self): + warn('`Function.method` is deprecated. Use: `Function.is_method`', DeprecationWarning, + stacklevel=2) + return self.is_method + + __pdoc__['Function.method'] = False + + def funcdef(self) -> str: + """ + Generates the string of keywords used to define the function, + for example `def` or `async def`. + """ + return 'async def' if self._is_async else 'def' + + @property + def _is_async(self): + """ + Returns whether is function is asynchronous, either as a coroutine or an async + generator. + """ + try: + # Both of these are required because coroutines aren't classified as async + # generators and vice versa. + obj = inspect.unwrap(self.obj) + return (inspect.iscoroutinefunction(obj) or + inspect.isasyncgenfunction(obj)) + except AttributeError: + return False + + def return_annotation(self, *, link=None) -> str: + """Formatted function return type annotation or empty string if none.""" + annot = '' + for method in ( + lambda: _get_type_hints(self.obj)['return'], + # Mainly for non-property variables + lambda: _get_type_hints(cast(Class, self.cls).obj)[self.name], + # global variables + lambda: _get_type_hints(not self.cls and self.module.obj)[self.name], + lambda: inspect.signature(self.obj).return_annotation, + # Use raw annotation strings in unmatched forward declarations + lambda: cast(Class, self.cls).obj.__annotations__[self.name], + # Extract annotation from the docstring for C builtin function + lambda: Function._signature_from_string(self).return_annotation, + ): + try: + annot = method() + except Exception: + continue + else: + break + else: + # Don't warn on variables. The annotation just isn't available. + if not isinstance(self, Variable): + warn(f"Error handling return annotation for {self!r}", stacklevel=3) + + if annot is inspect.Parameter.empty or not annot: + return '' + + if isinstance(annot, str): + s = annot + else: + s = _formatannotation(annot) + s = re.sub(r'\bForwardRef\((?P[\"\'])(?P.*?)(?P=quot)\)', + r'\g', s) + s = s.replace(' ', '\N{NBSP}') # Better line breaks in html signatures + + if link: + from pdoc.html_helpers import _linkify + s = re.sub(r'[\w\.]+', partial(_linkify, link=link, module=self.module), s) + return s + + def params(self, *, annotate: bool = False, link: Callable[[Doc], str] = None) -> List[str]: + """ + Returns a list where each element is a nicely formatted + parameter of this function. This includes argument lists, + keyword arguments and default values, and it doesn't include any + optional arguments whose names begin with an underscore. + + If `annotate` is True, the parameter strings include [PEP 484] + type hint annotations. + + [PEP 484]: https://www.python.org/dev/peps/pep-0484/ + """ + return self._params(self, annotate=annotate, link=link, module=self.module) + + @staticmethod + def _params(doc_obj, annotate=False, link=None, module=None): + try: + # We want __init__ to actually be implemented somewhere in the + # MRO to still satisfy https://github.com/pdoc3/pdoc/issues/124 + if ( + _isclass(doc_obj.obj) + and doc_obj.obj.__init__ is not object.__init__ + ): + # Remove the first argument (self) from __init__ signature + init_sig = inspect.signature(doc_obj.obj.__init__) + init_params = list(init_sig.parameters.values()) + signature = init_sig.replace(parameters=init_params[1:]) + else: + signature = inspect.signature(doc_obj.obj) + except ValueError: + signature = Function._signature_from_string(doc_obj) + if not signature: + return ['...'] + + def safe_default_value(p: inspect.Parameter): + value = p.default + if value is inspect.Parameter.empty: + return p + + replacement = next((i for i in ('os.environ', + 'sys.stdin', + 'sys.stdout', + 'sys.stderr',) + if value is eval(i)), None) + if not replacement: + if isinstance(value, enum.Enum): + replacement = str(value) + elif _isclass(value): + replacement = f'{value.__module__ or _UNKNOWN_MODULE}.{value.__qualname__}' + elif ' at 0x' in repr(value): + replacement = re.sub(r' at 0x\w+', '', repr(value)) + + nonlocal link + if link and ('<' in repr(value) or '>' in repr(value)): + import html + replacement = html.escape(replacement or repr(value)) + + if replacement: + class mock: + def __repr__(self): + return replacement + return p.replace(default=mock()) + return p + + params = [] + kw_only = False + pos_only = False + EMPTY = inspect.Parameter.empty + + if link: + from pdoc.html_helpers import _linkify + _linkify = partial(_linkify, link=link, module=module) + + for p in signature.parameters.values(): # type: inspect.Parameter + if not _is_public(p.name) and p.default is not EMPTY: + continue + + if p.kind == p.POSITIONAL_ONLY: + pos_only = True + elif pos_only: + params.append("/") + pos_only = False + + if p.kind == p.VAR_POSITIONAL: + kw_only = True + if p.kind == p.KEYWORD_ONLY and not kw_only: + kw_only = True + params.append('*') + + p = safe_default_value(p) + + if not annotate: + p = p.replace(annotation=EMPTY) + + formatted = p.name + if p.annotation is not EMPTY: + annotation = _formatannotation(p.annotation).replace(' ', '\N{NBSP}') + # "Eval" forward-declarations (typing string literals) + if isinstance(p.annotation, str): + annotation = annotation.strip("'") + if link: + annotation = re.sub(r'[\w\.]+', _linkify, annotation) + formatted += f':\N{NBSP}{annotation}' + if p.default is not EMPTY: + if p.annotation is not EMPTY: + formatted += f'\N{NBSP}=\N{NBSP}{repr(p.default)}' + else: + formatted += f'={repr(p.default)}' + if p.kind == p.VAR_POSITIONAL: + formatted = f'*{formatted}' + elif p.kind == p.VAR_KEYWORD: + formatted = f'**{formatted}' + + params.append(formatted) + + if pos_only: + params.append("/") + + return params + + @staticmethod + @lru_cache() + def _signature_from_string(self): + signature = None + for expr, cleanup_docstring, filter in ( + # Full proper typed signature, such as one from pybind11 + (r'^{}\(.*\)(?: -> .*)?$', True, lambda s: s), + # Human-readable, usage-like signature from some Python builtins + # (e.g. `range` or `slice` or `itertools.repeat` or `numpy.arange`) + (r'^{}\(.*\)(?= -|$)', False, lambda s: s.replace('[', '').replace(']', '')), + ): + strings = sorted(re.findall(expr.format(self.name), + self.docstring, re.MULTILINE), + key=len, reverse=True) + if strings: + string = filter(strings[0]) + _locals, _globals = {}, {} + _globals.update({'capsule': None}) # pybind11 capsule data type + _globals.update(typing.__dict__) + _globals.update(self.module.obj.__dict__) + # Trim binding module basename from type annotations + # See: https://github.com/pdoc3/pdoc/pull/148#discussion_r407114141 + module_basename = self.module.name.rsplit('.', maxsplit=1)[-1] + if module_basename in string and module_basename not in _globals: + string = re.sub(fr'(? str: + return f'{self.cls.refname if self.cls else self.module.refname}.{self.name}' + + +class Variable(Doc): + """ + Representation of a variable's documentation. This includes + module, class, and instance variables. + """ + __slots__ = ('cls', 'instance_var') + + def __init__(self, name: str, module: Module, docstring, *, + obj=None, cls: Class = None, instance_var: bool = False): + """ + Same as `pdoc.Doc`, except `cls` should be provided + as a `pdoc.Class` object when this is a class or instance + variable. + """ + super().__init__(name, module, obj, docstring) + + self.cls = cls + """ + The `pdoc.Class` object if this is a class or instance + variable. If not (i.e. it is a global variable), this is None. + """ + + self.instance_var = instance_var + """ + True if variable is some class' instance variable (as + opposed to class variable). + """ + + @property + def qualname(self) -> str: + if self.cls: + return f'{self.cls.qualname}.{self.name}' + return self.name + + @property + def refname(self) -> str: + return f'{self.cls.refname if self.cls else self.module.refname}.{self.name}' + + def type_annotation(self, *, link=None) -> str: + """Formatted variable type annotation or empty string if none.""" + return Function.return_annotation(cast(Function, self), link=link) + + +class External(Doc): + """ + A representation of an external identifier. The textual + representation is the same as an internal identifier. + + External identifiers are also used to represent something that is + not documented but appears somewhere in the public interface (like + the ancestor list of a class). + """ + + __pdoc__["External.docstring"] = """ + An empty string. External identifiers do not have + docstrings. + """ + __pdoc__["External.module"] = """ + Always `None`. External identifiers have no associated + `pdoc.Module`. + """ + __pdoc__["External.name"] = """ + Always equivalent to `pdoc.External.refname` since external + identifiers are always expressed in their fully qualified + form. + """ + + def __init__(self, name: str): + """ + Initializes an external identifier with `name`, where `name` + should be a fully qualified name. + """ + super().__init__(name, None, None) + + def url(self, *args, **kwargs): + """ + `External` objects return absolute urls matching `/{name}.ext`. + """ + return f'/{self.name}.ext' diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..d87ed75 --- /dev/null +++ b/pylintrc @@ -0,0 +1,569 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS,.git + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape, + invalid-name, + missing-function-docstring, + trailing-newlines, + too-many-locals, + too-many-arguments, + too-many-statements, + too-many-instance-attributes, + no-else-return, + pointless-string-statement, + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata, + nom + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=e, h, s, j, x, y, a, b + ex, + Run, + _ + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=any + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=160 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, while `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package.. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement. +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception". +overgeneral-exceptions=Exception diff --git a/pyproject.toml b/pyproject.toml index a6d31bd..2af4e57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ include = [ ] dynamic = ["version"] dependencies = [ - "numpy>=1.26", + "numpy>=2.0", "scipy~=1.14", ] diff --git a/simulation_output.h5 b/simulation_output.h5 new file mode 100644 index 0000000..1e40d35 Binary files /dev/null and b/simulation_output.h5 differ diff --git a/test.py b/test.py new file mode 100644 index 0000000..0d2bf35 --- /dev/null +++ b/test.py @@ -0,0 +1,429 @@ +from typing import Tuple +import multiprocessing +import logging +import copy +from itertools import chain + +import pyopencl, meanas, gridlock +import numpy +from numpy import pi, sin, cos, exp +from numpy.linalg import norm + +from meanas import fdtd, fdfd +from meanas.fdmath import vec +from meanas.fdfd.waveguide_3d import compute_source, compute_overlap_e +from meanas.fdfd import operators +from meanas.fdtd import maxwell_e, maxwell_h, cpml_params, updates_with_cpml, poynting + + +numpy.set_printoptions(linewidth=int(1e10)) + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) +logging.getLogger('matplotlib').setLevel(logging.WARNING) +logging.getLogger('pyopencl').setLevel(logging.WARNING) +logging.getLogger('pytools').setLevel(logging.WARNING) + + +fh = logging.FileHandler('opt.log') +fh.setLevel(logging.INFO) +logger.addHandler(fh) + + +def saveplot_2d_mp(*args): + multiprocessing.Process(target=saveplot_2d, args=args).start() + + +def saveplot_2d(val, name, xz_coords): + val = numpy.squeeze(val) + pyplot.figure(figsize=(8, 6)) + xz_grids = numpy.meshgrid(*xz_coords, indexing='ij') + vmax = numpy.abs(val).max() + if (val < 0).any(): + args = {'vmin': -vmax, 'vmax': vmax, 'cmap': 'seismic'} + else: + args = {'vmin': 0, 'vmax': vmax, 'cmap': 'hot'} + + pyplot.pcolormesh(*xz_grids, val, **args) + pyplot.colorbar(orientation='horizontal') + pyplot.title(f'{name}') + pyplot.gca().set_aspect('equal', adjustable='box') + pyplot.savefig(f'{name}.png', dpi=240) + pyplot.close() + + +def pulse(wl, dwl, dt, turn_on=1e-10): + # dt * dw = 4 * ln(2) + w = 2 * pi / wl + freq = 1 / wl + fwhm = dwl * w * w / (2 * pi) + alpha = (fwhm * fwhm) / 8 * numpy.log(2) + delay = numpy.sqrt(-numpy.log(turn_on) / alpha) + delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase + logger.info(f'src_time {2 * delay / dt}') + + n = numpy.floor(pi / (w * dt)) + logger.info(f'save timestep would be {n} * dt = {n * dt}') + + # nrm = numpy.exp(-w * w / alpha) / 2 + + def source_phasor(i): + t0 = i * dt - delay + envelope = numpy.sqrt(numpy.sqrt(2 * alpha / pi)) * numpy.exp(-alpha * t0**2) + # if t0 < 0: + # envelope = numpy.exp(-alpha * t0**2) + # else: + # envelope = 1 + return envelope, numpy.cos(w * t0), numpy.sin(w * t0) + return source_phasor, delay, n #, nrm + + +def get_wgmode_xp(half_dims, polarity, grid, epsilon, wl, dxes): + dims = [-half_dims, half_dims] + dims[0][0] = dims[1][0] + ind_dims = (grid.pos2ind(dims[0], which_shifts=None).astype(int), + grid.pos2ind(dims[1], which_shifts=None).astype(int)) + wg_slices = tuple(slice(i, f+1) for i, f in zip(*ind_dims)) + wg_args = { + 'omega': 2 * pi / wl, + 'slices': wg_slices, + 'dxes': dxes, + 'axis': 0, + 'polarity': polarity, + } + + wg_results = fdfd.waveguide_3d.solve_mode(mode_number=0, **wg_args, epsilon=epsilon) + return wg_args, wg_results + + +def get_gaussian(m, grid, dxes, wl): + def grid2gaussian(xyz, center, w0=4600, tilt=numpy.deg2rad(-8)): + xs, ys, zs = xyz + xs -= center[0] + ys -= center[1] + zs -= center[2] + xg, yg, zg = numpy.meshgrid(xs, ys, zs, indexing='ij') + + rot = numpy.array([[ cos(tilt), 0, sin(tilt)], + [ 0, 1, 0], + [-sin(tilt), 0, cos(tilt)]]) + + x, y, z = (rot @ numpy.stack((xg.ravel(), yg.ravel(), zg.ravel()))).reshape(3, *grid.shape) + r2 = x * x + y * y # sq. distance from beam center along tilted plane + z2 = z * z # sq. distance from waist along centerline + + zr = pi * w0 * w0 / wl + zr2 = zr * zr + wz2 = w0 * w0 * (1 + z2 / zr2) + wz = numpy.sqrt(wz2) + + k = 2 * pi / wl + Rz = z * (1 + zr2 / z2) + gouy = numpy.arctan(z / zr) + + gaussian = w0 / wz * exp(-r2 / wz2) * exp(1j * (k * z + k * r2 / 2 / Rz - gouy)) + # window_x = scipy.signal.windows.kaiser(xs.size, 14) + # gaussian *= window_x[:, None, None] + return gaussian + + zsEy = grid.shifted_xyz(1)[2] + gaussianEy = grid2gaussian(grid.shifted_xyz(1), [0, 0, zsEy[m[2]]]) + + normEy = gaussianEy[m[0]:-m[0], :, m[2]] + gaussianEy /= numpy.sqrt((normEy[1].conj() * normEy[1]).sum()) + + return gaussianEy + + +def run(pml=(10, 0, 10), dx=20, wl=1310, dwl=130, wg_zh=400, wg_x=-7500, fiber_z=1000, max_t=int(10e3)): + omega = 2 * pi / wl + + x_min = -10e3 - pml[0] * dx + x_max = 10e3 + pml[0] * dx + z_min = -600 - pml[2] * dx + z_max = 1400 + pml[2] * dx + + ex = numpy.arange(x_min, x_max + dx / 2, dx) + ez = numpy.arange(z_min, z_max + dx / 2, dx) + exyz = [ex, [-dx / 2, dx / 2], ez] + grid = gridlock.Grid(exyz, periodic=True) + epsilon = grid.allocate(1.45**2) + + def unvec(f): + return meanas.fdmath.unvec(f, grid.shape) + +# grid.draw_slab(epsilon, surface_normal=2, center=[0, 0, 0], thickness=160, eps=3.5**2) + + + e = numpy.zeros_like(epsilon, dtype=numpy.float32) + h = numpy.zeros_like(epsilon, dtype=numpy.float32) + + dxes = [grid.dxyz, grid.autoshifted_dxyz()] + min_dx = min(min(dxn) for dxn in chain(*dxes)) + dt = min_dx * .99 / numpy.sqrt(3) + + source_phasor, delay, n_fft = pulse(wl, dwl, dt) + if 2 * delay / dt > max_t: + raise Exception('Source extends beyond end of simulation') + + + m = numpy.array(pml) + 10 + m[2] = grid.pos2ind([0, 0, fiber_z], which_shifts=0)[2] - grid.shape[2] + + ey_gauss = numpy.zeros_like(epsilon, dtype=complex) + ey_gauss = get_gaussian(m, grid, dxes, wl / 1.45) + e_gauss = numpy.zeros_like(epsilon, dtype=numpy.complex64) + e_gauss[1] = ey_gauss + mask = numpy.zeros_like(epsilon, dtype=int) + mask[..., :m[2]] = 1 + src_op = operators.e_boundary_source(mask=vec(mask), omega=omega, dxes=dxes, epsilon=vec(epsilon)) + + def zero_pmls(c): + for a in range(3): + c[a][:pml[0]+1, :, :] = 0 + c[a][-pml[0]-1:, :, :] = 0 + c[a][:, :, :pml[2]+1] = 0 + c[a][:, :, -pml[2]-1:] = 0 + return c + +# J = unvec(src_op @ vec(e_gauss)) +# J[:, :12, :, :] = 0 +# J[:, -12:, :, :] = 0 +# J[:, :, :, :12] = 0 +# J[:, :, :, -12:] = 0 +# zero_pmls(J) + + J = numpy.zeros_like(epsilon, dtype=complex) + J[1, 500, 0, 60] = 1 + zero_pmls(J) + + half_dims = numpy.array([wg_x, dx, wg_zh]) + wg_args, wg_results = get_wgmode_xp(half_dims, -1, grid, wl, dxes) + + E_out = compute_overlap_e(E=wg_results['E'], wavenumber=wg_results['wavenumber'], + dxes=dxes, axis=0, polarity=+1, slices=wg_args['slices']) + + + jr = (J.real / epsilon).astype(numpy.float32) + ji = (J.imag / epsilon).astype(numpy.float32) + + + eph = numpy.zeros_like(e, dtype=numpy.complex64) + ephm = numpy.zeros_like(e, dtype=numpy.complex64) +# powers = numpy.zeros((max_t, 5)) + p_ph = 0 + + pml_params = [[cpml_params(axis=dd, polarity=pp, dt=dt, + thickness=pml[dd], epsilon_eff=1.0**2) + if pml[dd] > 0 else None + for pp in (-1, +1)] + for dd in range(3)] + update_E, update_H = updates_with_cpml(cpml_params=pml_params, dt=dt, + dxes=dxes, epsilon=epsilon) + + mov_interval = 10 + mov = numpy.empty((max_t // mov_interval, e.shape[1], e.shape[3]), dtype=numpy.float32) + + for t in range(max_t): + update_E(e, h, epsilon) + _, cm5, sm5 = source_phasor(t - 0.5) + ephm += (cm5 - 1j * sm5) * e + + a, c, s = source_phasor(t) + p_ph += a * c * c + e -= (a * c) * jr - (a * s) * ji + + update_H(e, h) + + _, cp5, sp5 = source_phasor(t + 0.5) + eph += (cp5 - 1j * sp5) * e + +# S = poynting(e, h, epsilon) +# +# powers[t, :] = ( +# numpy.sum(S[2, m[0]+3:-m[0]-2, :, m[2]-6]), # below src +# numpy.sum(S[2, m[0]+3:-m[0]-2, :, m[2]+4]), # above src +# numpy.sum(S[2, m[0]+3:-m[0]-2, :, pml[2]+2]), # bottom +# numpy.sum(S[0, +m[0]+2, :, pml[2]+3:m[2]+4]), # left +# numpy.sum(S[0, -m[0]-2, :, pml[2]+3:m[2]+4]), # right +# ) + + if t % mov_interval == 0: + mov[t // mov_interval] = e[1, :, 0, :].real + + eph *= dt / p_ph + ephm *= dt / p_ph + + src_power = -(J * eph).real.sum() / 2 * dx ** 3 + + hph = meanas.fdfd.functional.e2h(omega=omega, dxes=dxes)(eph) + sph = meanas.fdtd.poynting(e=eph, h=hph.conj(), dxes=dxes) + planes_powers = numpy.array(( + -sph[0, 11, :, 11:-12].sum(), + +sph[0, -12, :, 11:-12].sum(), + -sph[2, 11:-12, :, 11].sum(), + +sph[2, 11:-12, :, -12].sum(), + )).real / 2 + planes_power = planes_powers.sum() + + print(f'{src_power=}, {planes_power=}') + + + # Verify + A = meanas.fdfd.operators.e_full(omega=omega, dxes=dxes, epsilon=vec(epsilon)) + b = -1j * omega * vec(J) #* numpy.exp(1j * dt / 2 * omega) + c = A @ vec(eph) + logger.info('FWD inaccuracy: |Ax-b|/|b| = {}'.format(norm(c-b) / norm(b))) + normdiv = norm(b) / norm(c) + logger.info(f'{normdiv=}') + logger.info('FWD renormed inaccuracy: |Ax-b|/|b| = {}'.format(norm(c * normdiv - b) / norm(b))) + + b = -1j * omega * vec(J) + logger.info('FWD base inaccuracy: |Ax-b|/|b| = {}'.format(norm(c-b) / norm(b))) + + from scipy.optimize import minimize + def resid(x): + b = -1j * omega * vec(J) * numpy.exp(1j * dt * x * omega) + return norm(c - b) / norm(b) + print('min', minimize(resid, 0.25, options={'xatol': 1e-14, 'fatol': 1e-14})) + +# fig, ax, anim = plot_movie(mov, balanced=True, interval=300) +# anim.save('output.mp4') + + print('solving...') + cdxes = copy.deepcopy(dxes) + for axis in range(3): + thickness = pml[axis] + if not thickness: + continue + for pp, polarity in enumerate((-1, 1)): + print(axis, polarity, thickness) + cdxes = fdfd.scpml.stretch_with_scpml(cdxes, axis=axis, polarity=polarity, + omega=omega, epsilon_effective=1.0**2, + thickness=thickness) + eph2v = meanas.fdfd.solvers.generic( + omega=omega, dxes=cdxes, J=vec(J), epsilon=vec(epsilon), + matrix_solver_opts={'atol': 1e-3, 'tol': 1e-3, 'x0': vec(eph)}) + eph2 = unvec(eph2v) + + pyplot.figure() + pyplot.pcolormesh(numpy.abs(eph/eph2)[1, 11:-11, 0, 11:-11].real.T) + pyplot.colorbar() + pyplot.title('mag') + pyplot.figure() + pyplot.pcolormesh(numpy.angle(eph/eph2)[1, 11:-11, 0, 11:-11].real.T) + pyplot.colorbar() + pyplot.title('angle') + pyplot.show() + breakpoint() + + +import matplotlib +from matplotlib import cycler, animation, colors, ticker, pyplot + +def set_pyplot_cycle() -> None: + pyplot.rc('lines', linewidth=2.5) + pyplot.rc('axes', prop_cycle( + cycler('color', 'krbgcm') + * cycler('linestyle', ['-', '--', ':', '-.']) + )) + + +def pcm(x, y, z, pca={}, cba={}, bare=False, eq=True) -> Tuple: + z = numpy.array(z) + + if numpy.any(z < 0): + vmax = numpy.abs(z).max() + pcolor_args = {'vmin': -vmax, 'vmax': vmax, 'cmap': 'seismic', **pca} + else: + pcolor_args = {'cmap': 'seismic', **pca} + + xe = centers2edges(x) + ye = centers2edges(y) + + if bare: + fig = pyplot.gcf() + ax = pyplot.gca() + else: + fig, ax = pyplot.subplot() + + im = ax.pcolormesh(xe, ye, z.T, **pcolor_args) + if eq: + ax.set_aspect('equal', adjustable='box') + + if not bare: + ax.format_coord = lambda xx, yy: format_coord(xx, yy, xe, ye, z.T) + fig.colorbar(im, ax=ax, **cba) + + return fig, ax, im + + +def pcc(x, y, z, cfa={}, cba={}, n_levels: int = 15, bare: bool = False, eq: bool = True) -> Tuple: + z = numpy.array(z) + + if numpy.any(z < 0): + vmax = numpy.abs(z).max() + pcolor_args = {'vmin': -vmax, 'vmax': vmax, 'cmap': 'seismic', **cfa} + else: + pcolor_args = {'cmap': 'hot', **cfa} + + xe = centers2edges(x) + ye = centers2edges(y) + + if bare: + fig = pyplot.gcf() + ax = pyplot.gca() + else: + fig, ax = pyplot.subplot() + + levels = ticker.MaxNLocator(nbins=n_levels).tick_values(z.min(), z.max()) + cmap = pyplot.get_cmap(pcolor_args['cmap']) + norm = color.BoundaryNorm(levels, ncolors=cmap.N, clip=True) + + im = ax.contourf(x, y, z.T, levels=levels, **pcolor_args) + if eq: + ax.set_aspect('equal', adjustable='box') + + if not bare: + ax.format_coord = lambda xx, yy: format_coord(xx, yy, xe, ye, z.T) + fig.colorbar(im, ax=ax, **cba) + + return fig, ax, im + + +def centers2edges(centers): + d = numpy.diff(centers) / 2 + e = numpy.hstack((centers[0] - d[0], centers[:-1] + d, centers[-1] + d[-1])) + return e + + +def format_coord(x, y, xs, ys, vs): + col = numpy.digitize(x, xs) + row = numpy.digitize(y, ys) + if 0 < row <= vs.shape[0] and 0 < col <= vs.shape[1]: + z = vs[row - 1, col - 1] + return f'x={x:1.4g}, y={y:1.4g}, z={z:1.4g}' + else: + return f'x={x:1.4g}, y={y:1.4g}' + + +def plot_movie(arr, balanced=True, interval=300, pca={}): + if balanced: + vmax = numpy.abs(arr).max() + pcolor_args = {'vmin': -vmax, 'vmax': vmax, 'cmap': 'seismic', **pca} + else: + pcolor_args = {'cmap': 'seismic', **pca} + + fig, ax = pyplot.subplots() + im = ax.pcolormesh(arr[0, :, :].T, **pcolor_args) + ax.set_aspect('equal', adjustable='box') + + def animate(ii): + im.set_array(arr[ii, :, :].T.ravel()) + + anim = animation.FuncAnimation(fig, animate, frames=arr.shape[0], repeat=True, interval=interval) + return fig, im, anim + + +if __name__ == '__main__': + run() diff --git a/test_test.py b/test_test.py new file mode 100644 index 0000000..038fbb6 --- /dev/null +++ b/test_test.py @@ -0,0 +1,121 @@ +import logging +import meanas +from meanas.fdfd.scpml import stretch_with_scpml +from meanas.fdfd.solvers import generic +from meanas.fdtd.misc import gaussian_beam +from meanas.fdmath import vec +from gridlock import Grid +import numpy +from numpy import pi +from matplotlib import pyplot, colors + + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) +for mm in ('matplotlib', 'PIL'): + logging.getLogger(mm).setLevel(logging.WARNING) + + +wl = 1310 +omega = 2 * pi / wl +eps_bg = 1.45 + +grid = Grid([ + numpy.arange(-20e3, 20e3 + 1, 10), + [-1, 1], + numpy.arange(-1e3, 1e3 + 1, 10), + ]) + +logger.info(grid.shape) +def unvec(vv): + return meanas.fdmath.unvec(vv, grid.shape) + +eps = grid.allocate(eps_bg) +dxes = [grid.dxyz, grid.autoshifted_dxyz()] + +xx, yy, zz = grid.shifted_xyz(1) +print(zz.min(), zz.max(), zz[-25]) +gauss0 = gaussian_beam(xyz=[xx[12:-12], yy, zz], center=[0, 0, zz[-25]], w0=4600, tilt=numpy.deg2rad(-10), wl=wl / eps_bg) + +e_gauss = numpy.zeros_like(eps, dtype=complex) +e_gauss[1, 12:-12, :, :] = gauss0 +mask = numpy.zeros_like(eps) +mask[..., :-25] = 1 + +fig, ax = pyplot.subplots() +mb = ax.pcolormesh(mask[0, :, 0, :].T, cmap='hot') +fig.colorbar(mb) +ax.set_aspect('equal') +ax.set_title('mask') + +fig, ax = pyplot.subplots() +mb = ax.pcolormesh((e_gauss * mask)[1, :, 0, :].real.T, cmap='bwr', norm=colors.CenteredNorm()) +fig.colorbar(mb) +ax.set_aspect('equal') +ax.set_title('e_masked') + +pyplot.show() + +vecJ = meanas.fdfd.operators.e_boundary_source(mask=vec(mask), omega=omega, dxes=dxes, epsilon=vec(eps)) @ vec(e_gauss) +J = unvec(vecJ) + +for pp in (-1, +1): + for aa in (0, 2): + dxes = stretch_with_scpml( + dxes=dxes, + axis=aa, + polarity=pp, + omega=omega, + thickness=10, + ) + +vecE = generic(omega=omega, dxes=dxes, J=vec(J), epsilon=vec(eps)) +vecH = meanas.fdfd.operators.e2h(omega=omega, dxes=dxes) @ vecE +vecS = meanas.fdfd.operators.poynting_e_cross(e=vecE, dxes=dxes) @ vecH.conj() + +E = unvec(vecE) +H = unvec(vecH) +S = unvec(vecS) +dxs, dys, dzs = grid.dxyz +EJ = (-E * J.conj()).sum(axis=0) * dxs[:, None, None] * dys[None, : None] * dzs[None, None, :] +P_in = EJ.sum().real / 2 + +logger.info(f'P_in = {EJ.sum() / 2:3g}') + +planes = numpy.array([ + -S[0, 11, :, :].sum(), + S[0, -11, :, :].sum(), + -S[2, :, :, 11].sum(), + S[2, :, :, -11].sum(), + ]) / 2 / P_in + +logger.info(f'{planes=}') +logger.info(f'{planes.sum().real}') + + +fig, ax = pyplot.subplots() +e2 = (E * E.conj() * eps).real.sum(axis=0) +mb = ax.pcolormesh(e2[:, 0, :].T / P_in, cmap='hot', norm=colors.LogNorm(vmin=e2.max() / 1e10)) +fig.colorbar(mb) +ax.set_aspect('equal') +ax.set_title('E^2 * eps') + +fig, ax = pyplot.subplots() +mb = ax.pcolormesh(S[0, :, 0, :].real.T / 2 / P_in, cmap='bwr', norm=colors.CenteredNorm()) +fig.colorbar(mb) +ax.set_aspect('equal') +ax.set_title('Sx') + +fig, ax = pyplot.subplots() +mb = ax.pcolormesh(S[2, :, 0, :].real.T / 2 / P_in, cmap='bwr', norm=colors.CenteredNorm()) +fig.colorbar(mb) +ax.set_aspect('equal') +ax.set_title('Sz') + +fig, ax = pyplot.subplots() +mb = ax.pcolormesh(EJ[:, 0, :].real.T / 2 / P_in, cmap='bwr', norm=colors.CenteredNorm()) +fig.colorbar(mb) +ax.set_aspect('equal') +ax.set_title('-E.J') + +pyplot.show()