{"id":13736518,"url":"https://github.com/flaport/fdtd","last_synced_at":"2025-04-13T11:46:54.841Z","repository":{"id":41827254,"uuid":"151546733","full_name":"flaport/fdtd","owner":"flaport","description":"A 3D electromagnetic FDTD simulator written in Python with optional GPU support","archived":false,"fork":false,"pushed_at":"2024-09-22T12:20:11.000Z","size":378,"stargazers_count":559,"open_issues_count":29,"forks_count":129,"subscribers_count":25,"default_branch":"master","last_synced_at":"2025-04-06T08:11:13.114Z","etag":null,"topics":["3d-fdtd","electric-fields","fdtd","fdtd-simulator","magnetic-fields","numpy","optics","photonics","physics","physics-simulation","python","pytorch","simulation","simulation-framework"],"latest_commit_sha":null,"homepage":"https://fdtd.readthedocs.io","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/flaport.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-10-04T09:18:35.000Z","updated_at":"2025-04-04T14:31:39.000Z","dependencies_parsed_at":"2023-09-24T07:00:46.631Z","dependency_job_id":"a67b9508-29d4-4fc0-967f-cf4d4d6423b5","html_url":"https://github.com/flaport/fdtd","commit_stats":{"total_commits":211,"total_committers":10,"mean_commits":21.1,"dds":"0.26066350710900477","last_synced_commit":"56d3c439ef7bae3cbf14e2418418f62a0a22edad"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flaport%2Ffdtd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flaport%2Ffdtd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flaport%2Ffdtd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flaport%2Ffdtd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/flaport","download_url":"https://codeload.github.com/flaport/fdtd/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248710408,"owners_count":21149185,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["3d-fdtd","electric-fields","fdtd","fdtd-simulator","magnetic-fields","numpy","optics","photonics","physics","physics-simulation","python","pytorch","simulation","simulation-framework"],"created_at":"2024-08-03T03:01:23.267Z","updated_at":"2025-04-13T11:46:54.819Z","avatar_url":"https://github.com/flaport.png","language":"Python","readme":"# Python 3D FDTD Simulator\n\n![Docs](https://readthedocs.org/projects/fdtd/badge/?version=latest)\n\nA 3D electromagnetic FDTD simulator written in Python. The FDTD simulator has\nan optional PyTorch backend, enabling FDTD simulations on a GPU.\n\n## Installation\n\nThe `fdtd`-library can be installed with `pip`:\n\n```\npip install fdtd\n```\n\nThe development version can be installed by cloning the repository\n\n```\ngit clone http://github.com/flaport/fdtd\n```\n\nand linking it with pip\n\n```\npip install -e fdtd\n```\n\nDevelopment dependencies can be installed with\n\n```\npip install -e fdtd[dev]\n```\n\n## Dependencies\n\n- python 3.6+\n- numpy\n- scipy\n- matplotlib\n- tqdm\n- pytorch (optional)\n\n## Contributing\n\nAll improvements or additions (for example new objects, sources or detectors) are\nwelcome. Please make a pull-request 😊.\n\n## Documentation\n\nread the documentation here: [https://fdtd.readthedocs.org](https://fdtd.readthedocs.org)\n\n### Imports\n\nThe `fdtd` library is simply imported as follows:\n\n```python\nimport fdtd\n```\n\n### Setting the backend\n\nThe `fdtd` library allows to choose a backend. The `\"numpy\"` backend is the\ndefault one, but there are also several additional PyTorch backends:\n\n- `\"numpy\"` (defaults to float64 arrays)\n- `\"torch\"` (defaults to float64 tensors)\n- `\"torch.float32\"`\n- `\"torch.float64\"`\n- `\"torch.cuda\"` (defaults to float64 tensors)\n- `\"torch.cuda.float32\"`\n- `\"torch.cuda.float64\"`\n\nFor example, this is how to choose the `\"torch\"` backend:\n\n```python\nfdtd.set_backend(\"torch\")\n```\n\nIn general, the `\"numpy\"` backend is preferred for standard CPU calculations\nwith `\"float64\"` precision. In general, `\"float64\"` precision is always\npreferred over `\"float32\"` for FDTD simulations, however, `\"float32\"` might\ngive a significant performance boost.\n\nThe `\"cuda\"` backends are only available for computers with a GPU.\n\n### The FDTD-grid\n\nThe FDTD grid defines the simulation region.\n\n```python\n# signature\nfdtd.Grid(\n    shape: Tuple[Number, Number, Number],\n    grid_spacing: float = 155e-9,\n    permittivity: float = 1.0,\n    permeability: float = 1.0,\n    courant_number: float = None,\n)\n```\n\nA grid is defined by its `shape`, which is just a 3D tuple of `Number`-types\n(integers or floats). If the shape is given in floats, it denotes the width,\nheight and length of the grid in meters. If the shape is given in integers, it\ndenotes the width, height and length of the grid in terms of the\n`grid_spacing`. Internally, these numbers will be translated to three integers:\n`grid.Nx`, `grid.Ny` and `grid.Nz`.\n\nA `grid_spacing` can be given. For stability reasons, it is recommended to\nchoose a grid spacing that is at least 10 times smaller than the _smallest_\nwavelength in the grid. This means that for a grid containing a source with\nwavelength `1550nm` and a material with refractive index of `3.1`, the\nrecommended minimum `grid_spacing` turns out to be `50nm`\n\nFor the `permittivity` and `permeability` floats or arrays with the following\nshapes\n\n- `(grid.Nx, grid.Ny, grid.Nz)`\n- or `(grid.Nx, grid.Ny, grid.Nz, 1)`\n- or `(grid.Nx, grid.Ny, grid.Nz, 3)`\n\nare expected. In the last case, the shape implies the possibility for different\npermittivity for each of the major axes (so-called _uniaxial_ or _biaxial_\nmaterials). Internally, these variables will be converted (for performance\nreasons) to their inverses `grid.inverse_permittivity` array and a\n`grid.inverse_permeability` array of shape `(grid.Nx, grid.Ny, grid.Nz, 3)`. It\nis possible to change those arrays after making the grid.\n\nFinally, the `courant_number` of the grid determines the relation between the\n`time_step` of the simulation and the `grid_spacing` of the grid. If not given,\nit is chosen to be the maximum number allowed by the [Courant-Friedrichs-Lewy\nCondition](https://en.wikipedia.org/wiki/Courant–Friedrichs–Lewy_condition):\n`1` for `1D` simulations, `1/√2` for `2D` simulations and `1/√3` for `3D`\nsimulations (the dimensionality will be derived by the shape of the grid). For\nstability reasons, it is recommended not to change this value.\n\n```python\ngrid = fdtd.Grid(\n    shape = (25e-6, 15e-6, 1), # 25um x 15um x 1 (grid_spacing) --\u003e 2D FDTD\n)\nprint(grid)\n```\n\n    Grid(shape=(161,97,1), grid_spacing=1.55e-07, courant_number=0.70)\n\n### Adding an object to the grid\n\nAn other option to locally change the `permittivity` or `permeability` in the\ngrid is to add an `Object` to the grid.\n\n```python\n# signature\nfdtd.Object(\n    permittivity: Tensorlike,\n    name: str = None\n)\n```\n\nAn object defines a part of the grid with modified update equations, allowing\nto introduce for example absorbing materials or biaxial materials for which\nmixing between the axes are present through `Pockels coefficients` or many\nmore. In this case we'll make an object with a different `permittivity` than\nthe grid it is in.\n\nJust like for the grid, the `Object` expects a `permittivity` to be a floats or\nan array of the following possible shapes\n\n- `(obj.Nx, obj.Ny, obj.Nz)`\n- or `(obj.Nx, obj.Ny, obj.Nz, 1)`\n- or `(obj.Nx, obj.Ny, obj.Nz, 3)`\n\nNote that the values `obj.Nx`, `obj.Ny` and `obj.Nz` are not given to the\nobject constructor. They are in stead derived from its placing in the grid:\n\n```python\ngrid[11:32, 30:84, 0] = fdtd.Object(permittivity=1.7**2, name=\"object\")\n```\n\nSeveral things happen here. First of all, the object is given the space\n`[11:32, 30:84, 0]` in the grid. Because it is given this space, the object's\n`Nx`, `Ny` and `Nz` are automatically set. Furthermore, by supplying a name to\nthe object, this name will become available in the grid:\n\n```python\nprint(grid.object)\n```\n\n        Object(name='object')\n            @ x=11:32, y=30:84, z=0:1\n\nA second object can be added to the grid:\n\n```python\ngrid[13e-6:18e-6, 5e-6:8e-6, 0] = fdtd.Object(permittivity=1.5**2)\n```\n\nHere, a slice with floating point numbers was chosen. These floats will be\nreplaced by integer `Nx`, `Ny` and `Nz` during the registration of the object.\nSince the object did not receive a name, the object won't be available as an\nattribute of the grid. However, it is still available via the `grid.objects`\nlist:\n\n```python\nprint(grid.objects)\n```\n\n    [Object(name='object'), Object(name=None)]\n\nThis list stores all objects (i.e. of type `fdtd.Object`) in the order that\nthey were added to the grid.\n\n### Adding a source to the grid\n\nSimilarly as to adding an object to the grid, an `fdtd.LineSource` can also be\nadded:\n\n```python\n# signature\nfdtd.LineSource(\n    period: Number = 15, # timesteps or seconds\n    amplitude: float = 1.0,\n    phase_shift: float = 0.0,\n    name: str = None,\n)\n```\n\nAnd also just like an `fdtd.Object`, an `fdtd.LineSource` size is defined by its\nplacement on the grid:\n\n```python\ngrid[7.5e-6:8.0e-6, 11.8e-6:13.0e-6, 0] = fdtd.LineSource(\n    period = 1550e-9 / (3e8), name=\"source\"\n)\n```\n\nHowever, it is important to note that in this case a `LineSource` is added to\nthe grid, i.e. the source spans the diagonal of the cube defined by the slices.\nInternally, these slices will be converted into lists to ensure this behavior:\n\n```python\nprint(grid.source)\n```\n\n        LineSource(period=14, amplitude=1.0, phase_shift=0.0, name='source')\n            @ x=[48, ... , 51], y=[76, ... , 83], z=[0, ... , 0]\n\nNote that one could also have supplied lists to index the grid in the first\nplace. This feature could be useful to create a `LineSource` of arbitrary\nshape.\n\n### Adding a detector to the grid\n\n```python\n# signature\nfdtd.LineDetector(\n    name=None\n)\n```\n\nAdding a detector to the grid works the same as adding a source\n\n```python\ngrid[12e-6, :, 0] = fdtd.LineDetector(name=\"detector\")\n```\n\n```python\nprint(grid.detector)\n```\n\n        LineDetector(name='detector')\n            @ x=[77, ... , 77], y=[0, ... , 96], z=[0, ... , 0]\n\n### Adding grid boundaries\n\n```python\n# signature\nfdtd.PML(\n    a: float = 1e-8, # stability factor\n    name: str = None\n)\n```\n\nAlthough, having an object, source and detector to simulate is in principle\nenough to perform an FDTD simulation, One also needs to define a grid boundary\nto prevent the fields to be reflected. One of those boundaries that can be\nadded to the grid is a [Perfectly Matched\nLayer](https://en.wikipedia.org/wiki/Perfectly_matched_layer) or `PML`. These\nare basically absorbing boundaries.\n\n```python\n# x boundaries\ngrid[0:10, :, :] = fdtd.PML(name=\"pml_xlow\")\ngrid[-10:, :, :] = fdtd.PML(name=\"pml_xhigh\")\n\n# y boundaries\ngrid[:, 0:10, :] = fdtd.PML(name=\"pml_ylow\")\ngrid[:, -10:, :] = fdtd.PML(name=\"pml_yhigh\")\n```\n\n### Grid summary\n\nA simple summary of the grid can be shown by printing out the grid:\n\n```python\nprint(grid)\n```\n\n    Grid(shape=(161,97,1), grid_spacing=1.55e-07, courant_number=0.70)\n\n    sources:\n        LineSource(period=14, amplitude=1.0, phase_shift=0.0, name='source')\n            @ x=[48, ... , 51], y=[76, ... , 83], z=[0, ... , 0]\n\n    detectors:\n        LineDetector(name='detector')\n            @ x=[77, ... , 77], y=[0, ... , 96], z=[0, ... , 0]\n\n    boundaries:\n        PML(name='pml_xlow')\n            @ x=0:10, y=:, z=:\n        PML(name='pml_xhigh')\n            @ x=-10:, y=:, z=:\n        PML(name='pml_ylow')\n            @ x=:, y=0:10, z=:\n        PML(name='pml_yhigh')\n            @ x=:, y=-10:, z=:\n\n    objects:\n        Object(name='object')\n            @ x=11:32, y=30:84, z=0:1\n        Object(name=None)\n            @ x=84:116, y=32:52, z=0:1\n\n### Running a simulation\n\nRunning a simulation is as simple as using the `grid.run` method.\n\n```python\ngrid.run(\n    total_time: Number,\n    progress_bar: bool = True\n)\n```\n\nJust like for the lengths in the grid, the `total_time` of the simulation\ncan be specified as an integer (number of `time_steps`) or as a float (in\nseconds).\n\n```python\ngrid.run(total_time=100)\n```\n\n### Grid visualization\n\nLet's visualize the grid. This can be done with the `grid.visualize` method:\n\n```python\n# signature\ngrid.visualize(\n    grid,\n    x=None,\n    y=None,\n    z=None,\n    cmap=\"Blues\",\n    pbcolor=\"C3\",\n    pmlcolor=(0, 0, 0, 0.1),\n    objcolor=(1, 0, 0, 0.1),\n    srccolor=\"C0\",\n    detcolor=\"C2\",\n    show=True,\n)\n```\n\nThis method will by default visualize all objects in the grid, as well as the\nfield intensity at the current `time_step` at a certain `x`, `y` **OR** `z`-plane. By\nsetting `show=False`, one can disable the immediate visualization of the\nmatplotlib image.\n\n```python\ngrid.visualize(z=0)\n```\n\n![png](docs/_static/grid.png)\n\n## Background\n\nAn as quick as possible explanation of the FDTD discretization of the Maxwell\nequations.\n\n### Update Equations\n\nAn electromagnetic FDTD solver solves the time-dependent Maxwell Equations\n\n```python\n    curl(H) = ε*ε0*dE/dt\n    curl(E) = -µ*µ0*dH/dt\n```\n\nThese two equations are called _Ampere's Law_ and _Faraday's Law_ respectively.\n\nIn these equations, ε and µ are the relative permittivity and permeability\ntensors respectively. ε0 and µ0 are the vacuum permittivity and permeability\nand their square root can be absorbed into E and H respectively, such that `E := √ε0*E` and `H := √µ0*H`.\n\nDoing this, the Maxwell equations can be written as update equations:\n\n```python\n    E  += c*dt*inv(ε)*curl(H)\n    H  -= c*dt*inv(µ)*curl(E)\n```\n\nThe electric and magnetic field can then be discretized on a grid with\ninterlaced Yee-coordinates, which in 3D looks like this:\n\n![grid discretization in 3D](docs/_static/yee.svg)\n\nAccording to the Yee discretization algorithm, there are inherently two types\nof fields on the grid: `E`-type fields on integer grid locations and `H`-type\nfields on half-integer grid locations.\n\nThe beauty of these interlaced coordinates is that they enable a very natural\nway of writing the curl of the electric and magnetic fields: the curl of an\nH-type field will be an E-type field and vice versa.\n\nThis way, the curl of E can be written as\n\n```python\n    curl(E)[m,n,p] = (dEz/dy - dEy/dz, dEx/dz - dEz/dx, dEy/dx - dEx/dy)[m,n,p]\n                   =( ((Ez[m,n+1,p]-Ez[m,n,p])/dy - (Ey[m,n,p+1]-Ey[m,n,p])/dz),\n                      ((Ex[m,n,p+1]-Ex[m,n,p])/dz - (Ez[m+1,n,p]-Ez[m,n,p])/dx),\n                      ((Ey[m+1,n,p]-Ey[m,n,p])/dx - (Ex[m,n+1,p]-Ex[m,n,p])/dy) )\n                   =(1/du)*( ((Ez[m,n+1,p]-Ez[m,n,p]) - (Ey[m,n,p+1]-Ey[m,n,p])), [assume dx=dy=dz=du]\n                             ((Ex[m,n,p+1]-Ex[m,n,p]) - (Ez[m+1,n,p]-Ez[m,n,p])),\n                             ((Ey[m+1,n,p]-Ey[m,n,p]) - (Ex[m,n+1,p]-Ex[m,n,p])) )\n\n```\n\nthis can be written efficiently with array slices (note that the factor\n`(1/du)` was left out):\n\n```python\ndef curl_E(E):\n    curl_E = np.zeros(E.shape)\n    curl_E[:,:-1,:,0] += E[:,1:,:,2] - E[:,:-1,:,2]\n    curl_E[:,:,:-1,0] -= E[:,:,1:,1] - E[:,:,:-1,1]\n\n    curl_E[:,:,:-1,1] += E[:,:,1:,0] - E[:,:,:-1,0]\n    curl_E[:-1,:,:,1] -= E[1:,:,:,2] - E[:-1,:,:,2]\n\n    curl_E[:-1,:,:,2] += E[1:,:,:,1] - E[:-1,:,:,1]\n    curl_E[:,:-1,:,2] -= E[:,1:,:,0] - E[:,:-1,:,0]\n    return curl_E\n```\n\nThe curl for H can be obtained in a similar way (note again that the factor\n`(1/du)` was left out):\n\n```python\ndef curl_H(H):\n    curl_H = np.zeros(H.shape)\n\n    curl_H[:,1:,:,0] += H[:,1:,:,2] - H[:,:-1,:,2]\n    curl_H[:,:,1:,0] -= H[:,:,1:,1] - H[:,:,:-1,1]\n\n    curl_H[:,:,1:,1] += H[:,:,1:,0] - H[:,:,:-1,0]\n    curl_H[1:,:,:,1] -= H[1:,:,:,2] - H[:-1,:,:,2]\n\n    curl_H[1:,:,:,2] += H[1:,:,:,1] - H[:-1,:,:,1]\n    curl_H[:,1:,:,2] -= H[:,1:,:,0] - H[:,:-1,:,0]\n    return curl_H\n```\n\nThe update equations can now be rewritten as\n\n```python\n    E  += (c*dt/du)*inv(ε)*curl_H\n    H  -= (c*dt/du)*inv(µ)*curl_E\n```\n\nThe number `(c*dt/du)` is a dimensionless parameter called the _Courant number_\n`sc`. For stability reasons, the Courant number should always be smaller than\n`1/√D`, with `D` the dimension of the simulation. This can be intuitively be\nunderstood as the condition that information should always travel slower than\nthe speed of light through the grid. In the FDTD method described here,\ninformation can only travel to the neighboring grid cells (through application\nof the curl). It would therefore take `D` time steps to travel over the\ndiagonal of a `D`-dimensional cube (square in `2D`, cube in `3D`), the Courant\ncondition follows then automatically from the fact that the length of this\ndiagonal is `1/√D`.\n\nThis yields the final update equations for the FDTD algorithm:\n\n```python\n    E  += sc*inv(ε)*curl_H\n    H  -= sc*inv(µ)*curl_E\n```\n\nThis is also how it is implemented:\n\n```python\nclass Grid:\n    # ... [initialization]\n\n    def step(self):\n        self.update_E()\n        self.update_H()\n\n    def update_E(self):\n        self.E += self.courant_number * self.inverse_permittivity * curl_H(self.H)\n\n    def update_H(self):\n        self.H -= self.courant_number * self.inverse_permeability * curl_E(self.E)\n```\n\n### Sources\n\nAmpere's Law can be updated to incorporate a current density:\n\n```python\n    curl(H) = J + ε*ε0*dE/dt\n```\n\nMaking again the usual substitutions `sc := c*dt/du`, `E := √ε0*E` and `H := √µ0*H`, the update equations can be modified to include the current density:\n\n```python\n    E += sc*inv(ε)*curl_H - dt*inv(ε)*J/√ε0\n```\n\nMaking one final substitution `Es := -dt*inv(ε)*J/√ε0` allows us to write this\nin a very clean way:\n\n```python\n    E += sc*inv(ε)*curl_H + Es\n```\n\nWhere we defined Es as the _electric field source term_.\n\nIt is often useful to also define a _magnetic field source term_ `Hs`, which would be\nderived from the _magnetic current density_ if it were to exist. In the same way,\nFaraday's update equation can be rewritten as\n\n```python\n    H  -= sc*inv(µ)*curl_E + Hs\n```\n\n```python\nclass Source:\n    # ... [initialization]\n    def update_E(self):\n        # electric source function here\n\n    def update_H(self):\n        # magnetic source function here\n\nclass Grid:\n    # ... [initialization]\n    def update_E(self):\n        # ... [electric field update equation]\n        for source in self.sources:\n            source.update_E()\n\n    def update_H(self):\n        # ... [magnetic field update equation]\n        for source in self.sources:\n            source.update_H()\n```\n\n### Lossy Medium\n\nWhen a material has a _electric conductivity_ σ, a conduction-current will\nensure that the medium is lossy. Ampere's law with a conduction current becomes\n\n```python\n    curl(H) = σ*E + ε*ε0*dE/dt\n```\n\nMaking the usual substitutions, this becomes:\n\n```python\n    E(t+dt) - E(t) = sc*inv(ε)*curl_H(t+dt/2) - dt*inv(ε)*σ*E(t+dt/2)/ε0\n```\n\nThis update equation depends on the electric field on a half-integer time step (a\n_magnetic field time step_). We need to substitute `E(t+dt/2)=(E(t)+E(t+dt))/2` to\ninterpolate the electric field to the correct time step.\n\n```python\n    (1 + 0.5*dt*inv(ε)*σ/√ε0)*E(t+dt) = sc*inv(ε)*curl_H(t+dt/2) + (1 - 0.5*dt*inv(ε)*σ/ε0)*E(t)\n```\n\nWhich, yield the new update equations:\n\n```python\n    f = 0.5*inv(ε)*σ*sc*du/(ε0*c)\n    E *= inv(1 + f) * (1 - f)\n    E += inv(1 + f)*sc*inv(ε)*curl_H\n```\n\nNote that the more complicated the permittivity tensor ε is, the more time\nconsuming this algorithm will be. It is therefore sometimes a nice hack to\ntransfer the absorption to the magnetic domain by introducing a\n(_nonphysical_) magnetic conductivity, because the permeability tensor µ is\nusually just equal to one:\n\n```python\n    f = 0.5*inv(μ)*σm*sc*du/(μ0*c)\n    H *= inv(1 + f) * (1 - f)\n    H += inv(1 + f)*sc*inv(µ)*curl_E\n```\n\n### Energy Density and Poynting Vector\n\nThe electromagnetic energy density can be given by\n\n```python\n    e = (1/2)*ε*ε0*E**2 + (1/2)*µ*µ0*H**2\n```\n\nmaking the above substitutions, this becomes in simulation units:\n\n```python\n    e = (1/2)*ε*E**2 + (1/2)*µ*H**2\n```\n\nThe Poynting vector is given by\n\n```python\n    P = E×H\n```\n\nWhich in simulation units becomes\n\n```python\n    P = c*E×H\n```\n\nThe energy introduced by a source `Es` can be derived from tracking the change\nin energy density\n\n```python\n    de = ε*Es·E + (1/2)*ε*Es**2\n```\n\nThis could also be derived from Poyntings energy conservation law:\n\n```python\n    de/dt = -grad(S) - J·E\n```\n\nwhere the first term just describes the redistribution of energy in a volume\nand the second term describes the energy introduced by a current density.\n\nNote: although it is unphysical, one could also have introduced a magnetic\nsource. This source would have introduced the following energy:\n\n```python\n    de = ε*Hs·H + (1/2)*µ*Hs**2\n```\n\nSince the µ-tensor is usually just equal to one, using a magnetic source term\nis often more efficient.\n\nSimilarly, one can also keep track of the absorbed energy due to an electric\nconductivity in the following way:\n\n```python\n    f = 0.5*inv(ε)*σ*sc*du/(ε0*c)\n    Enoabs = E + sc*inv(ε)*curl_H\n    E *= inv(1 + f) * (1 - f)\n    E += inv(1 + f)*sc*inv(ε)*curl_H\n    dE = Enoabs - E\n    e_abs += ε*E*dE + 0.5*ε*dE**2\n```\n\nor if we want to keep track of the absorbed energy by magnetic a magnetic\nconductivity:\n\n```python\n    f = 0.5*inv(μ)*σm*sc*du/(μ0*c)\n    Hnoabs = E + sc*inv(µ)*curl_E\n    H *= inv(1 + f) * (1 - f)\n    H += inv(1 + f)*sc*inv(µ)*curl_E\n    dH = Hnoabs - H\n    e_abs += µ*H*dH + 0.5*µ*dH**2\n```\n\nThe electric term and magnetic term in the energy density are usually of the\nsame size. Therefore, the same amount of energy will be absorbed by introducing\na _magnetic conductivity_ σm as by introducing a _electric conductivity_ σ if:\n\n```python\n    inv(µ)*σm/µ0 = inv(ε)*σ/ε0\n```\n\n### Boundary Conditions\n\n#### Periodic Boundary Conditions\n\nAssuming we want periodic boundary conditions along the `X`-direction, then we\nhave to make sure that the fields at `Xlow` and `Xhigh` are the same. This has\nto be enforced after performing the update equations:\n\nNote that the electric field `E` is dependent on `curl_H`, which means that the\nfirst indices of `E` will not be updated through the update equations. It's\nthose indices that need to be set through the periodic boundary condition.\nConcretely: `E[0]` needs to be set to equal `E[-1]`. For the magnetic field,\nthe inverse is true: `H` is dependent on `curl_E`, which means that its last\nindices will not be set. This has to be done by the boundary condition: `H[-1]`\nneeds to be set equal to `H[0]`:\n\n```python\nclass PeriodicBoundaryX:\n    # ... [initialization]\n    def update_E(self):\n        self.grid.E[0, :, :, :] = self.grid.E[-1, :, :, :]\n\n    def update_H(self):\n        self.grid.H[-1, :, :, :] = self.grid.H[0, :, :, :]\n\nclass Grid:\n    # ... [initialization]\n    def update_E(self):\n        # ... [electric field update equation]\n        # ... [electric field source update equations]\n        for boundary in self.boundaries:\n            boundary.update_E()\n\n    def update_H(self):\n        # ... [magnetic field update equation]\n        # ... [magnetic field source update equations]\n        for boundary in self.boundaries:\n            boundary.update_H()\n```\n\n#### Perfectly Matched Layer\n\na Perfectly Matched Layer (PML) is the state of the art for\nintroducing absorbing boundary conditions in an FDTD grid.\nA PML is an impedance-matched absorbing area in the grid. It turns out that\nfor a impedance-matching condition to hold, the PML can only be absorbing in\na single direction. This is what makes a PML in fact a nonphysical material.\n\nConsider Ampere's law for the `Ez` component, where we use the following substitutions:\n`E := √ε0*E`, `H := √µ0*H` and `σ := inv(ε)*σ/ε0` are\nalready introduced:\n\n```python\n    ε*dEz/dt + ε*σ*Ez = c*dHy/dx - c*dHx/dy\n```\n\nThis becomes in the frequency domain:\n\n```python\n    iω*ε*Ez + ε*σ*Ez = c*dHy/dx - c*dHx/dy\n```\n\nWe can split this equation in a x-propagating wave and a y-propagating wave:\n\n```python\n    iω*ε*Ezx + ε*σx*Ezx = iω*ε*(1 + σx/iω)*Ezx = c*dHy/dx\n    iω*ε*Ezy + ε*σy*Ezy = iω*ε*(1 + σy/iω)*Ezy = -c*dHx/dy\n```\n\nWe can define the `S`-operators as follows\n\n```python\n    Su = 1 + σu/iω          with u in {x, y, z}\n```\n\nIn general, we prefer to add a stability factor `au` and a scaling factor `ku` to `Su`:\n\n```python\n    Su = ku + σu/(iω+au)    with u in {x, y, z}\n```\n\nSumming the two equations for `Ez` back together after dividing by the respective `S`-operator gives\n\n```python\n    iω*ε*Ez = (c/Sx)*dHy/dx - (c/Sy)*dHx/dy\n```\n\nConverting this back to the time domain gives\n\n```python\n    ε*dEz/dt = c*sx[*]dHy/dx - c*sx[*]dHx/dy\n```\n\nwhere `sx` denotes the inverse Fourier transform of `(1/Sx)` and `[*]` denotes a convolution.\nThe expression for `su` can be proven [after some derivation] to look as follows:\n\n```python\n    su = (1/ku)*δ(t) + Cu(t)    with u in {x, y, z}\n```\n\nwhere `δ(t)` denotes the Dirac delta function and `C(t)` an exponentially\ndecaying function given by:\n\n```python\n    Cu(t) = -(σu/ku**2)*exp(-(au+σu/ku)*t)     for all t \u003e 0 and u in {x, y, z}\n```\n\nPlugging this in gives:\n\n```python\n    dEz/dt = (c/kx)*inv(ε)*dHy/dx - (c/ky)*inv(ε)*dHx/dy + c*inv(ε)*Cx[*]dHy/dx - c*inv(ε)*Cx[*]dHx/dy\n           = (c/kx)*inv(ε)*dHy/dx - (c/ky)*inv(ε)*dHx/dy + c*inv(ε)*Фez/du      with du=dx=dy=dz\n```\n\nThis can be written as an update equation:\n\n```python\n    Ez += (1/kx)*sc*inv(ε)*dHy - (1/ky)*sc*inv(ε)*dHx + sc*inv(ε)*Фez\n```\n\nWhere we defined `Фeu` as\n\n```python\n    Фeu = Ψeuv - Ψezw           with u, v, w in {x, y, z}\n```\n\nand `Ψeuv` as the convolution updating the component `Eu` by taking the derivative of `Hw` in the `v` direction:\n\n```python\n    Ψeuv = dv*Cv[*]dHw/dv     with u, v, w in {x, y, z}\n```\n\nThis can be rewritten [after some derivation] as an update equation in itself:\n\n```python\n     Ψeuv = bv*Ψeuv + cv*dv*(dHw/dv)\n          = bv*Ψeuv + cv*dHw            with u, v, w in {x, y, z}\n```\n\nWhere the constants `bu` and `cu` are derived to be:\n\n```python\n    bu = exp(-(au + σu/ku)*dt)              with u in {x, y, z}\n    cu = σu*(bu - 1)/(σu*ku + au*ku**2)     with u in {x, y, z}\n```\n\nThe final PML algorithm for the electric field now becomes:\n\n1. Update `Фe=[Фex, Фey, Фez]` by using the update equation for the `Ψ`-components.\n2. Update the electric fields the normal way\n3. Add `Фe` to the electric fields.\n\nor as python code:\n\n```python\nclass PML(Boundary):\n    # ... [initialization]\n    def update_phi_E(self): # update convolution\n        self.psi_Ex *= self.bE\n        self.psi_Ey *= self.bE\n        self.psi_Ez *= self.bE\n\n        c = self.cE\n        Hx = self.grid.H[self.locx]\n        Hy = self.grid.H[self.locy]\n        Hz = self.grid.H[self.locz]\n\n        self.psi_Ex[:, 1:, :, 1] += (Hz[:, 1:, :] - Hz[:, :-1, :]) * c[:, 1:, :, 1]\n        self.psi_Ex[:, :, 1:, 2] += (Hy[:, :, 1:] - Hy[:, :, :-1]) * c[:, :, 1:, 2]\n\n        self.psi_Ey[:, :, 1:, 2] += (Hx[:, :, 1:] - Hx[:, :, :-1]) * c[:, :, 1:, 2]\n        self.psi_Ey[1:, :, :, 0] += (Hz[1:, :, :] - Hz[:-1, :, :]) * c[1:, :, :, 0]\n\n        self.psi_Ez[1:, :, :, 0] += (Hy[1:, :, :] - Hy[:-1, :, :]) * c[1:, :, :, 0]\n        self.psi_Ez[:, 1:, :, 1] += (Hx[:, 1:, :] - Hx[:, :-1, :]) * c[:, 1:, :, 1]\n\n        self.phi_E[..., 0] = self.psi_Ex[..., 1] - self.psi_Ex[..., 2]\n        self.phi_E[..., 1] = self.psi_Ey[..., 2] - self.psi_Ey[..., 0]\n        self.phi_E[..., 2] = self.psi_Ez[..., 0] - self.psi_Ez[..., 1]\n\n    def update_E(self): # update PML located at self.loc\n        self.grid.E[self.loc] += (\n            self.grid.courant_number\n            * self.grid.inverse_permittivity[self.loc]\n            * self.phi_E\n        )\n\nclass Grid:\n    # ... [initialization]\n    def update_E(self):\n        for boundary in self.boundaries:\n            boundary.update_phi_E()\n        # ... [electric field update equation]\n        # ... [electric field source update equations]\n        for boundary in self.boundaries:\n            boundary.update_E()\n```\n\nThe same has to be applied for the magnetic field.\n\nThese update equations for the PML were based on\n[Schneider, Chap. 11](https://www.eecs.wsu.edu/~schneidj/ufdtd).\n\n## Units\n\n\u003c!---\nflaport, if you have the time, I'd appreciate it if you could confirm\nthat I've understood this correctly.\nI'm adding this because I got pretty confused regarding the units;\nif you think it's self-evident, feel free to remove.\n\nIn particular, is the H := √µ0*H scaling really applied nowhere in the library?\nCan this be assumed?\n---\u003e\n\nAs a bare FDTD library, this is dimensionally agnostic for any unit system you may choose.\nNo conversion factors are applied within the library API; this is left to the user.\n(The code used to calculate the Courant limit may be a sticking point depending on the time scale involved).\n\nHowever, as noted above (`H := √µ0*H`), it is generally good numerical practice to scale all values to\nget the maximum precision from floating-point types.\n\nIn particular, a scaling scheme detailed in [\"Novel architectures for brain-inspired photonic computers\"](https://www.photonics.intec.ugent.be/download/phd_259.pdf), Chapters 4.1.2 and 4.1.6, is highly recommended.\n\nA set of conversion functions to and from reduced units are available for users in conversions.py.\n\n\u003c!---\nOn the other hand, use of this scaling scheme really makes most of the new functions less useful,\nbecause the results don't have physical dimensions by default and have to be scaled by weird\ncoefficients by the user (scale impedance?!?)\n\ngrid.H_scaling_factor = sqrt(mu0) ?\n---\u003e\n\n## Linter\n\nYou can run a linter in the root using `pylint fdtd`.\n\n## License\n\n© Floris laporte - [MIT License](license)\n","funding_links":[],"categories":["simulation"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflaport%2Ffdtd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fflaport%2Ffdtd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflaport%2Ffdtd/lists"}